Repository navigation
Implement in-app browser #1519
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement in-app browser #1519
Changes from all commits
b9b8630
e5c6475
a7a852e
aff1982
b663ab8
86b613c
2b37964
9ee6acc
6c42754
a01d70f
e3a6b59
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| import { browserHistory } from "@superset/local-db"; | ||
| import { like, or, sql } from "drizzle-orm"; | ||
| import { localDb } from "main/lib/local-db"; | ||
| import { z } from "zod"; | ||
| import { publicProcedure, router } from "../.."; | ||
|
|
||
| export const createBrowserHistoryRouter = () => { | ||
| return router({ | ||
| getAll: publicProcedure.query(() => { | ||
| return localDb | ||
| .select() | ||
| .from(browserHistory) | ||
| .orderBy(sql`${browserHistory.lastVisitedAt} desc`) | ||
| .limit(500) | ||
| .all(); | ||
| }), | ||
|
|
||
| search: publicProcedure | ||
| .input(z.object({ query: z.string() })) | ||
| .query(({ input }) => { | ||
| const pattern = `%${input.query}%`; | ||
| return localDb | ||
| .select() | ||
| .from(browserHistory) | ||
| .where( | ||
| or( | ||
| like(browserHistory.url, pattern), | ||
| like(browserHistory.title, pattern), | ||
| ), | ||
| ) | ||
| .orderBy(sql`${browserHistory.lastVisitedAt} desc`) | ||
| .limit(10) | ||
| .all(); | ||
| }), | ||
|
|
||
| upsert: publicProcedure | ||
| .input( | ||
| z.object({ | ||
| url: z.string(), | ||
| title: z.string(), | ||
| faviconUrl: z.string().nullable().optional(), | ||
| }), | ||
| ) | ||
| .mutation(({ input }) => { | ||
| localDb | ||
| .insert(browserHistory) | ||
| .values({ | ||
| url: input.url, | ||
| title: input.title, | ||
| faviconUrl: input.faviconUrl ?? null, | ||
| lastVisitedAt: Date.now(), | ||
| visitCount: 1, | ||
| }) | ||
| .onConflictDoUpdate({ | ||
| target: browserHistory.url, | ||
| set: { | ||
| title: input.title, | ||
| faviconUrl: input.faviconUrl ?? null, | ||
| lastVisitedAt: Date.now(), | ||
| visitCount: sql`${browserHistory.visitCount} + 1`, | ||
| }, | ||
| }) | ||
| .run(); | ||
| }), | ||
|
|
||
| clear: publicProcedure.mutation(() => { | ||
| localDb.delete(browserHistory).run(); | ||
| }), | ||
| }); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,176 @@ | ||
| import { observable } from "@trpc/server/observable"; | ||
| import { session } from "electron"; | ||
| import { browserManager } from "main/lib/browser/browser-manager"; | ||
| import { z } from "zod"; | ||
| import { publicProcedure, router } from "../.."; | ||
|
|
||
| export const createBrowserRouter = () => { | ||
| return router({ | ||
| register: publicProcedure | ||
| .input(z.object({ paneId: z.string(), webContentsId: z.number() })) | ||
| .mutation(({ input }) => { | ||
| browserManager.register(input.paneId, input.webContentsId); | ||
| return { success: true }; | ||
| }), | ||
|
|
||
| unregister: publicProcedure | ||
| .input(z.object({ paneId: z.string() })) | ||
| .mutation(({ input }) => { | ||
| browserManager.unregister(input.paneId); | ||
| return { success: true }; | ||
| }), | ||
|
|
||
| navigate: publicProcedure | ||
| .input(z.object({ paneId: z.string(), url: z.string() })) | ||
| .mutation(({ input }) => { | ||
| const wc = browserManager.getWebContents(input.paneId); | ||
| if (!wc) throw new Error(`No webContents for pane ${input.paneId}`); | ||
| wc.loadURL(input.url); | ||
| return { success: true }; | ||
| }), | ||
|
|
||
| goBack: publicProcedure | ||
| .input(z.object({ paneId: z.string() })) | ||
| .mutation(({ input }) => { | ||
| const wc = browserManager.getWebContents(input.paneId); | ||
| if (wc?.canGoBack()) wc.goBack(); | ||
| return { success: true }; | ||
| }), | ||
|
|
||
| goForward: publicProcedure | ||
| .input(z.object({ paneId: z.string() })) | ||
| .mutation(({ input }) => { | ||
| const wc = browserManager.getWebContents(input.paneId); | ||
| if (wc?.canGoForward()) wc.goForward(); | ||
| return { success: true }; | ||
| }), | ||
|
|
||
| reload: publicProcedure | ||
| .input(z.object({ paneId: z.string(), hard: z.boolean().optional() })) | ||
| .mutation(({ input }) => { | ||
| const wc = browserManager.getWebContents(input.paneId); | ||
| if (!wc) return { success: false }; | ||
| if (input.hard) { | ||
| wc.reloadIgnoringCache(); | ||
| } else { | ||
| wc.reload(); | ||
| } | ||
| return { success: true }; | ||
| }), | ||
|
|
||
| screenshot: publicProcedure | ||
| .input(z.object({ paneId: z.string() })) | ||
| .mutation(async ({ input }) => { | ||
| const base64 = await browserManager.screenshot(input.paneId); | ||
| return { base64 }; | ||
| }), | ||
|
|
||
| evaluateJS: publicProcedure | ||
| .input(z.object({ paneId: z.string(), code: z.string() })) | ||
| .mutation(async ({ input }) => { | ||
| const result = await browserManager.evaluateJS( | ||
| input.paneId, | ||
| input.code, | ||
| ); | ||
| return { result }; | ||
| }), | ||
|
|
||
| getConsoleLogs: publicProcedure | ||
| .input(z.object({ paneId: z.string() })) | ||
| .query(({ input }) => { | ||
| return browserManager.getConsoleLogs(input.paneId); | ||
| }), | ||
|
|
||
| consoleStream: publicProcedure | ||
| .input(z.object({ paneId: z.string() })) | ||
| .subscription(({ input }) => { | ||
| return observable<{ | ||
| level: string; | ||
| message: string; | ||
| timestamp: number; | ||
| }>((emit) => { | ||
| const handler = (entry: { | ||
| level: string; | ||
| message: string; | ||
| timestamp: number; | ||
| }) => { | ||
| emit.next(entry); | ||
| }; | ||
| browserManager.on(`console:${input.paneId}`, handler); | ||
| return () => { | ||
| browserManager.off(`console:${input.paneId}`, handler); | ||
| }; | ||
| }); | ||
| }), | ||
|
|
||
| onNewWindow: publicProcedure | ||
| .input(z.object({ paneId: z.string() })) | ||
| .subscription(({ input }) => { | ||
| return observable<{ url: string }>((emit) => { | ||
| const handler = (url: string) => { | ||
| emit.next({ url }); | ||
| }; | ||
| browserManager.on(`new-window:${input.paneId}`, handler); | ||
| return () => { | ||
| browserManager.off(`new-window:${input.paneId}`, handler); | ||
| }; | ||
| }); | ||
| }), | ||
|
|
||
| openDevTools: publicProcedure | ||
| .input(z.object({ paneId: z.string() })) | ||
| .mutation(({ input }) => { | ||
| browserManager.openDevTools(input.paneId); | ||
| return { success: true }; | ||
| }), | ||
|
|
||
| getDevToolsUrl: publicProcedure | ||
| .input(z.object({ browserPaneId: z.string() })) | ||
| .query(async ({ input }) => { | ||
| const url = await browserManager.getDevToolsUrl(input.browserPaneId); | ||
| return { url }; | ||
| }), | ||
|
|
||
| getPageInfo: publicProcedure | ||
| .input(z.object({ paneId: z.string() })) | ||
| .query(({ input }) => { | ||
| const wc = browserManager.getWebContents(input.paneId); | ||
| if (!wc) return null; | ||
| return { | ||
| url: wc.getURL(), | ||
| title: wc.getTitle(), | ||
| canGoBack: wc.canGoBack(), | ||
| canGoForward: wc.canGoForward(), | ||
| isLoading: wc.isLoading(), | ||
| }; | ||
| }), | ||
|
|
||
| clearBrowsingData: publicProcedure | ||
| .input( | ||
| z.object({ | ||
| type: z.enum(["cookies", "cache", "storage", "all"]), | ||
| }), | ||
| ) | ||
| .mutation(async ({ input }) => { | ||
| const ses = session.fromPartition("persist:superset"); | ||
| switch (input.type) { | ||
| case "cookies": | ||
| await ses.clearStorageData({ storages: ["cookies"] }); | ||
| break; | ||
| case "cache": | ||
| await ses.clearCache(); | ||
| break; | ||
| case "storage": | ||
| await ses.clearStorageData({ | ||
| storages: ["localstorage", "indexdb"], | ||
| }); | ||
| break; | ||
| case "all": | ||
| await ses.clearStorageData(); | ||
| await ses.clearCache(); | ||
| break; | ||
| } | ||
| return { success: true }; | ||
| }), | ||
| }); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -278,6 +278,19 @@ if (!gotTheLock) { | |
| initSentry(); | ||
| await initAppState(); | ||
|
|
||
| // Load browser extension for webview panes | ||
| try { | ||
| const extensionPath = app.isPackaged | ||
| ? path.join(process.resourcesPath, "browser-extension") | ||
| : path.join(__dirname, "../src/resources/browser-extension"); | ||
| await session | ||
| .fromPartition("persist:superset") | ||
| .loadExtension(extensionPath); | ||
| console.log("[main] Browser extension loaded"); | ||
| } catch (error) { | ||
| console.error("[main] Failed to load browser extension:", error); | ||
| } | ||
|
Comment on lines
+281
to
+292
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # Find electron-builder and electron.vite config files
fd -e "json" -e "config" | grep -E "(electron|build)" | head -20Repository: superset-sh/superset Length of output: 96 🏁 Script executed: # Also check the actual desktop app structure
find apps/desktop -type f -name "*.json" | head -20Repository: superset-sh/superset Length of output: 280 🏁 Script executed: # Look for the browser-extension directory
find . -type d -name "browser-extension" 2>/dev/nullRepository: superset-sh/superset Length of output: 110 🏁 Script executed: # Check for electron.vite config in apps/desktop
find apps/desktop -name "electron.vite*" -o -name "vite.config*" | head -10Repository: superset-sh/superset Length of output: 100 🏁 Script executed: # Read the package.json to see build configuration and electron-builder setup
cat apps/desktop/package.json | head -100Repository: superset-sh/superset Length of output: 4148 🏁 Script executed: # Read the electron.vite.config.ts to understand build output structure
cat apps/desktop/electron.vite.config.tsRepository: superset-sh/superset Length of output: 7208 🏁 Script executed: # Read the electron-builder.ts to check for extraResources configuration
cat apps/desktop/electron-builder.tsRepository: superset-sh/superset Length of output: 5025 🏁 Script executed: # Find and read the vite/helpers file
find apps/desktop/vite -type f -name "helpers*" -exec cat {} \;Repository: superset-sh/superset Length of output: 2307 Fix the dev extension path: browser-extension is not copied to the dist/ directory and the relative path is incorrect. The dev path 🤖 Prompt for AI Agents |
||
|
|
||
| // Must happen before renderer restore runs | ||
| await reconcileDaemonSessions(); | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
rg -n "DESKTOP_AUTOMATION_PORT" --type-all -C 3Repository: superset-sh/superset
Length of output: 502
🏁 Script executed:
Repository: superset-sh/superset
Length of output: 234
🏁 Script executed:
rg "DESKTOP_AUTOMATION_PORT" -C 3Repository: superset-sh/superset
Length of output: 2639
🏁 Script executed:
Repository: superset-sh/superset
Length of output: 46
🏁 Script executed:
Repository: superset-sh/superset
Length of output: 253
🏁 Script executed:
Repository: superset-sh/superset
Length of output: 187
🏁 Script executed:
Repository: superset-sh/superset
Length of output: 128
🏁 Script executed:
Repository: superset-sh/superset
Length of output: 408
🏁 Script executed:
Repository: superset-sh/superset
Length of output: 98
🏁 Script executed:
Repository: superset-sh/superset
Length of output: 1445
🏁 Script executed:
Repository: superset-sh/superset
Length of output: 1227
Add DESKTOP_AUTOMATION_PORT to ports.json.
The
DESKTOP_AUTOMATION_PORTis written to the.envfile at line 387, but it's missing from theports.jsonfile generated at lines 428-445. All other ports (includingCODE_INSPECTOR_PORT) are included in that file. Since this port is actively used for CDP/automation in the application, it should be documented inports.jsonfor consistency.Add the following entry:
Suggested diff
{ "port": $CODE_INSPECTOR_PORT, "label": "Code Inspector" } + { "port": $DESKTOP_AUTOMATION_PORT, "label": "Desktop Automation" } ]🤖 Prompt for AI Agents