gesso-electrobun 0.6.12 → 0.6.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # gesso-electrobun
2
2
 
3
+ ## 0.6.14
4
+
5
+ ### Patch Changes
6
+
7
+ - b9ddff5: A desktop window's screen tools can be offered to agents beside its channels: `relayScreenAgent` (`gesso-electrobun/view`) relays the render worker's agent port over the window's RPC, `createScreenAgent` (`gesso-electrobun/desktop`) holds the other end, and `serveDesktopAgent` takes it as `screen`. Only the screen tools cross. Meant for development, since pressing the screen's buttons is everything the person can do.
8
+ - gesso-framework@0.6.14
9
+
10
+ ## 0.6.13
11
+
12
+ ### Patch Changes
13
+
14
+ - Updated dependencies [0020afa]
15
+ - gesso-framework@0.6.13
16
+
3
17
  ## 0.6.12
4
18
 
5
19
  ### Patch Changes
package/dist/desktop.d.ts CHANGED
@@ -1,8 +1,28 @@
1
1
  import { r as GessoFrame } from "./frames-CsYW_e7F.js";
2
- import { n as withWindowRoute } from "./route-CNeMnj_6.js";
2
+ import { r as ScreenAgentMessage, s as withWindowRoute } from "./view-Cw282TuC.js";
3
+ import { AgentConfirmation, AgentSurface, AgentSurfaceLike, McpHandlerOptions } from "gesso-framework/agent";
3
4
  import { ChannelToken, ServedChannel } from "gesso-framework";
4
5
  import { Observable } from "rxjs";
5
- import { AgentConfirmation, AgentSurface, McpHandlerOptions } from "gesso-framework/agent";
6
+ //#region src/screen.d.ts
7
+ /** The main process's half. */
8
+ interface ScreenAgent {
9
+ /**
10
+ * The screen tools of the window attached last, or none while no
11
+ * window is. For `serveDesktopAgent`'s `screen`.
12
+ */
13
+ readonly surface: AgentSurfaceLike;
14
+ /**
15
+ * Attaches a window: `send` posts a message to its page's relay, and
16
+ * the window's messages are handed to `receive`. Detach it when the
17
+ * window closes. The window attached last is the one agents operate.
18
+ */
19
+ attach(send: (message: ScreenAgentMessage) => void): {
20
+ receive(message: ScreenAgentMessage): void;
21
+ detach(): void;
22
+ };
23
+ }
24
+ declare function createScreenAgent(): ScreenAgent;
25
+ //#endregion
6
26
  //#region src/agent.d.ts
7
27
  /**
8
28
  * A desktop application's channels, served to AI agents over MCP.
@@ -21,9 +41,10 @@ import { AgentConfirmation, AgentSurface, McpHandlerOptions } from "gesso-framew
21
41
  * `createDesktopApp` takes them per window, and the per-window ones
22
42
  * (`windowsChannel`) are about a window an agent does not have.
23
43
  *
24
- * What it offers is the channel tools only. The screen is in each
25
- * window's render worker, out of the main process's reach, so the
26
- * screen tools a web app offers are not here.
44
+ * What it offers is the channel tools, and the screen tools of a window
45
+ * when it is given `screen`: the screen is in each window's render
46
+ * worker, out of the main process's reach, so the window relays it
47
+ * (`createScreenAgent`).
27
48
  *
28
49
  * The descriptions come from `channelSchema`. A main process is bundled
29
50
  * by Electrobun's own build, which `gesso-vite-plugin` never sees, so
@@ -55,6 +76,12 @@ interface DesktopAgentOptions extends Omit<McpHandlerOptions, 'allowedOrigins'>
55
76
  confirm?: (request: AgentConfirmation) => boolean | Promise<boolean>;
56
77
  /** The server to start. Defaults to the runtime's `Bun.serve`. */
57
78
  serve?: DesktopServe;
79
+ /**
80
+ * A window's screen tools to offer beside the channels:
81
+ * `createScreenAgent().surface`. Pressing the screen's buttons is
82
+ * everything the person can do, so offer it in development.
83
+ */
84
+ screen?: AgentSurfaceLike;
58
85
  }
59
86
  interface DesktopAgent {
60
87
  /** Where an agent connects: `http://127.0.0.1:<port>/mcp`. */
@@ -231,5 +258,5 @@ declare const DesktopWindows: ChannelToken<DesktopWindowsView, DesktopWindowsCom
231
258
  /** Serves `DesktopWindows` to one window. Put it in `channels`. */
232
259
  declare function windowsChannel(app: DesktopApp, window: DesktopWindowHandle): ServedChannel;
233
260
  //#endregion
234
- export { type DesktopAgent, type DesktopAgentOptions, DesktopApp, DesktopAppOptions, type DesktopServe, DesktopWindowHandle, DesktopWindowOptions, DesktopWindowTransport, DesktopWindows, DesktopWindowsCommands, DesktopWindowsView, type ShowMessageBox, createDesktopApp, messageBoxConfirm, serveDesktopAgent, windowsChannel, withWindowRoute };
261
+ export { type DesktopAgent, type DesktopAgentOptions, DesktopApp, DesktopAppOptions, type DesktopServe, DesktopWindowHandle, DesktopWindowOptions, DesktopWindowTransport, DesktopWindows, DesktopWindowsCommands, DesktopWindowsView, type ScreenAgent, type ShowMessageBox, createDesktopApp, createScreenAgent, messageBoxConfirm, serveDesktopAgent, windowsChannel, withWindowRoute };
235
262
  //# sourceMappingURL=desktop.d.ts.map
package/dist/desktop.js CHANGED
@@ -1,8 +1,50 @@
1
1
  import { n as withWindowRoute } from "./route-B2qU38dY.js";
2
2
  import { serveChannelsToWindow } from "./main.js";
3
+ import { agentSurface, combineSurfaces, mcpHandler, remoteSurface } from "gesso-framework/agent";
3
4
  import { channel } from "gesso-framework";
4
5
  import { BehaviorSubject } from "rxjs";
5
- import { agentSurface, mcpHandler } from "gesso-framework/agent";
6
+ //#region src/screen.ts
7
+ /** The screen tools, by name; what crosses from the render worker. */
8
+ const SCREEN_TOOL = /^ui_/;
9
+ const NO_WINDOW = {
10
+ content: [{
11
+ type: "text",
12
+ text: "No window is open to operate."
13
+ }],
14
+ isError: true
15
+ };
16
+ function createScreenAgent() {
17
+ const windows = [];
18
+ const current = () => windows.at(-1)?.surface;
19
+ return {
20
+ surface: {
21
+ tools: async () => (await current()?.tools() ?? []).filter((tool) => SCREEN_TOOL.test(tool.name)),
22
+ resources: () => [],
23
+ call: async (name, args) => {
24
+ const window = current();
25
+ if (window === void 0 || !SCREEN_TOOL.test(name)) return NO_WINDOW;
26
+ return window.call(name, args);
27
+ },
28
+ read: () => void 0
29
+ },
30
+ attach: (send) => {
31
+ const port = {
32
+ postMessage: (message) => send(message),
33
+ onmessage: null
34
+ };
35
+ const entry = { surface: remoteSurface(port, { confirm: () => false }) };
36
+ windows.push(entry);
37
+ return {
38
+ receive: (message) => port.onmessage?.({ data: message }),
39
+ detach: () => {
40
+ const at = windows.indexOf(entry);
41
+ if (at !== -1) windows.splice(at, 1);
42
+ }
43
+ };
44
+ }
45
+ };
46
+ }
47
+ //#endregion
6
48
  //#region src/agent.ts
7
49
  const DEFAULT_PORT = 7310;
8
50
  const PORTS_TRIED = 10;
@@ -11,7 +53,7 @@ function serveDesktopAgent(channels, options = {}) {
11
53
  const hostname = options.hostname ?? "127.0.0.1";
12
54
  const first = options.port ?? DEFAULT_PORT;
13
55
  const surface = agentSurface(channels, options.confirm === void 0 ? {} : { confirm: options.confirm });
14
- const fetch = mcpHandler(surface, {
56
+ const fetch = mcpHandler(options.screen === void 0 ? surface : combineSurfaces([surface, options.screen]), {
15
57
  ...options,
16
58
  allowedOrigins: []
17
59
  });
@@ -242,6 +284,6 @@ function windowsChannel(app, window) {
242
284
  };
243
285
  }
244
286
  //#endregion
245
- export { DesktopWindows, createDesktopApp, messageBoxConfirm, serveDesktopAgent, windowsChannel, withWindowRoute };
287
+ export { DesktopWindows, createDesktopApp, createScreenAgent, messageBoxConfirm, serveDesktopAgent, windowsChannel, withWindowRoute };
246
288
 
247
289
  //# sourceMappingURL=desktop.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"desktop.js","names":[],"sources":["../src/agent.ts","../src/desktop.ts"],"sourcesContent":["import type { ServedChannel } from 'gesso-framework';\nimport {\n agentSurface,\n mcpHandler,\n type AgentConfirmation,\n type AgentSurface,\n type McpHandlerOptions\n} from 'gesso-framework/agent';\n\n/**\n * A desktop application's channels, served to AI agents over MCP.\n *\n * The main process already holds every channel's source: it is what\n * `createDesktopApp` serves to each window. So it is also where an\n * agent connects. This serves those same sources with MCP's HTTP\n * transport on the person's own machine, through `Bun.serve`, which\n * Cottontail provides as Bun does, and an agent such as Claude Code\n * connects by URL:\n *\n * const agent = serveDesktopAgent([counter], { name: 'my-app', confirm: messageBoxConfirm(Utils.showMessageBox) });\n * // claude mcp add --transport http my-app http://127.0.0.1:7310/mcp\n *\n * The channels are passed rather than read from the app, because\n * `createDesktopApp` takes them per window, and the per-window ones\n * (`windowsChannel`) are about a window an agent does not have.\n *\n * What it offers is the channel tools only. The screen is in each\n * window's render worker, out of the main process's reach, so the\n * screen tools a web app offers are not here.\n *\n * The descriptions come from `channelSchema`. A main process is bundled\n * by Electrobun's own build, which `gesso-vite-plugin` never sees, so\n * the template runs `gesso-channels` to write a module that describes\n * the contracts, and imports it.\n */\n\n/** A `Bun.serve`, as far as this uses one. */\nexport type DesktopServe = (options: {\n hostname: string;\n port: number;\n fetch: (request: Request) => Promise<Response>;\n}) => { stop(closeActiveConnections?: boolean): void };\n\nexport interface DesktopAgentOptions extends Omit<McpHandlerOptions, 'allowedOrigins'> {\n /**\n * The port to listen on (default 7310). When it is taken, the next\n * nine are tried in turn, so a second copy of the app still serves;\n * `url` says which one it got.\n */\n port?: number;\n /** Default `127.0.0.1`: reachable from this machine only. */\n hostname?: string;\n /**\n * Asks the person whether a `@confirm` command may be sent. Without\n * it such a command is refused. `messageBoxConfirm` makes one from\n * Electrobun's native dialog.\n */\n confirm?: (request: AgentConfirmation) => boolean | Promise<boolean>;\n /** The server to start. Defaults to the runtime's `Bun.serve`. */\n serve?: DesktopServe;\n}\n\nexport interface DesktopAgent {\n /** Where an agent connects: `http://127.0.0.1:<port>/mcp`. */\n readonly url: string;\n readonly surface: AgentSurface;\n /** Stops serving and stops following the channels. */\n stop(): void;\n}\n\nconst DEFAULT_PORT = 7310;\nconst PORTS_TRIED = 10;\n\nexport function serveDesktopAgent(channels: readonly ServedChannel[], options: DesktopAgentOptions = {}): DesktopAgent {\n const serve = options.serve ?? bunServe();\n const hostname = options.hostname ?? '127.0.0.1';\n const first = options.port ?? DEFAULT_PORT;\n const surface = agentSurface(channels, options.confirm === undefined ? {} : { confirm: options.confirm });\n // A desktop app has no browser pages of its own to let in, so every\n // request carrying an Origin is refused: that is a web page the person\n // has open, reaching for their machine.\n const fetch = mcpHandler(surface, { ...options, allowedOrigins: [] });\n\n let lastError: unknown;\n for (let port = first; port < first + PORTS_TRIED; port++) {\n try {\n const server = serve({ hostname, port, fetch });\n return {\n url: `http://${hostname}:${port}/mcp`,\n surface,\n stop: () => {\n server.stop(true);\n surface.dispose();\n }\n };\n } catch (error) {\n if (!isAddressInUse(error)) {\n surface.dispose();\n throw error;\n }\n lastError = error;\n }\n }\n surface.dispose();\n throw new Error(\n `Ports ${first} to ${first + PORTS_TRIED - 1} are all in use, so agents cannot be served. ` +\n `Pass another port to serveDesktopAgent. (${String(lastError)})`\n );\n}\n\n/** The options Electrobun's `Utils.showMessageBox` takes, as far as this uses them. */\nexport type ShowMessageBox = (options: {\n type?: 'info' | 'warning' | 'error' | 'question';\n title?: string;\n message?: string;\n detail?: string;\n buttons?: string[];\n defaultId?: number;\n cancelId?: number;\n}) => Promise<{ response: number }>;\n\n/**\n * A `confirm` that asks with the operating system's own dialog.\n *\n * Takes Electrobun's `Utils.showMessageBox` rather than importing it,\n * because Electrobun is a toolchain a project is projected into, not a\n * package this one can depend on.\n *\n * Everything the person needs to decide is in `message`, the command's\n * description and its arguments included, because macOS shows no\n * `detail`: a dialog that asked there showed only \"wants to\n * createBranch in workspace\", with nothing to say which branch.\n *\n * Decline is the first button. macOS makes the first button the\n * default, whatever `defaultId` says, so with Allow first, Enter\n * approved; first is also what `defaultId` and `cancelId` name, so\n * Enter, Escape and closing the dialog decline on every platform.\n */\nexport function messageBoxConfirm(showMessageBox: ShowMessageBox): (request: AgentConfirmation) => Promise<boolean> {\n return async request => {\n const argumentLines = Object.entries(request.arguments).map(([name, value]) => `${name}: ${JSON.stringify(value)}`);\n const message = [\n `An AI agent wants to ${request.command} in ${request.channel}.${request.destructive ? ' This cannot be undone.' : ''}`,\n request.description,\n argumentLines.join('\\n')\n ]\n .filter((part): part is string => part !== undefined && part !== '')\n .join('\\n\\n');\n const { response } = await showMessageBox({\n type: request.destructive ? 'warning' : 'question',\n title: 'An AI agent is asking',\n message,\n buttons: ['Decline', 'Allow'],\n defaultId: 0,\n cancelId: 0\n });\n return response === 1;\n };\n}\n\nfunction bunServe(): DesktopServe {\n const bun = (globalThis as { Bun?: { serve?: DesktopServe } }).Bun;\n if (typeof bun?.serve !== 'function') {\n throw new Error(\n 'serveDesktopAgent needs Bun.serve, which Cottontail and Bun provide. Pass serve to use another server.'\n );\n }\n return options => bun.serve!(options);\n}\n\nfunction isAddressInUse(error: unknown): boolean {\n const code = (error as { code?: unknown } | null)?.code;\n return code === 'EADDRINUSE' || /in use|EADDRINUSE/i.test(String((error as Error | null)?.message ?? error));\n}\n","/**\n * An application in the main process: windows, and the channels each\n * one is served.\n *\n * Nothing here imports Electrobun, and that is not fastidiousness. The\n * SDK is projected into a project by Hutch rather than installed from\n * a registry, so a package in\n * this workspace could not import it even if it wanted to. What the\n * application supplies instead is one function that opens a window,\n * which is the only Electrobun-shaped thing this needs, and which is\n * five lines at the call site:\n *\n * const app = createDesktopApp({\n * channels: window => [\n * { token: Catalogue, source: catalogue },\n * windowsChannel(app, window)\n * ],\n * open: (receive, handle) => {\n * const rpc = BrowserView.defineRPC<GessoWindowRPC>({\n * handlers: { requests: {}, messages: { gessoFrame: receive } }\n * });\n * const window = new BrowserWindow({\n * title: 'Notes',\n * url: withWindowRoute('views://mainview/index.html', handle.route),\n * rpc\n * });\n * return {\n * send: frame => window.webview.rpc.send.gessoFrame(frame),\n * close: () => window.close()\n * };\n * }\n * });\n * app.openWindow();\n *\n * The arrangement it buys: every\n * window is a replica of the same channels, so two windows agree by\n * construction rather than by synchronisation.\n */\nimport { channel, type ChannelToken, type ServedChannel } from 'gesso-framework';\nimport { BehaviorSubject, type Observable, type Subscription } from 'rxjs';\n\nimport type { GessoFrame } from './frames';\nimport { serveChannelsToWindow, type ChannelHost } from './main';\n\nexport { withWindowRoute } from './route';\n\n/** What the application does with the window it opened. */\nexport interface DesktopWindowTransport {\n /** Sends one frame to this window, over its own RPC. */\n send: (frame: GessoFrame) => void;\n /** Closes the native window. The adapter calls this; the platform may also close it on its own. */\n close: () => void;\n}\n\n/** A window this application opened. */\nexport interface DesktopWindowHandle {\n /** Stable for the life of the window, and never reused. */\n readonly id: number;\n /**\n * The application url the window was asked to open at, or null for\n * wherever the application starts.\n *\n * Set by `openWindow({ route })`, which is what a Cmd-click on an\n * in-app link comes to by default. The window cannot be told it\n * through a frame in time to start there, so `open` puts it on the\n * view's url, with `withWindowRoute`, and the window reads it back\n * with `windowRoute` before its shell starts.\n */\n readonly route: string | null;\n /** Closes the window and disposes the channels it was served. */\n close(): void;\n}\n\n/** What `openWindow` may be told about the window it opens. */\nexport interface DesktopWindowOptions {\n /**\n * The application url to open at, `/epic/BUD-12?story=BUD-13`. Carried\n * to `open` as `window.route`; see `DesktopWindowHandle.route`.\n */\n readonly route?: string;\n}\n\nexport interface DesktopAppOptions {\n /**\n * The channels each window is served.\n *\n * A function when a window needs a channel of its own, which is what\n * `windowsChannel` uses to give a window a way to close itself. It\n * is called once per window, and the observables it returns are\n * ordinarily the same ones every time: `provide` keeps a separate\n * record of what each client has seen, so sharing a source between\n * windows is what makes them agree.\n */\n channels: readonly ServedChannel[] | ((window: DesktopWindowHandle) => readonly ServedChannel[]);\n /**\n * Opens a native window.\n *\n * `receive` is what the window's frames must be fed into: wire it to\n * the RPC message the window sends frames on, before the window\n * opens, or the first handshake is lost.\n *\n * `window.route` is the application url the window should start at,\n * or null. Put it on the view's url with `withWindowRoute`, and read\n * it in the window with `windowRoute`; an `open` that ignores it\n * opens every window at the application's start, as before.\n */\n open: (receive: (frame: GessoFrame) => void, window: DesktopWindowHandle) => DesktopWindowTransport;\n /**\n * A url a window asked to have opened outside itself, with the\n * window that asked. `Utils.openExternal(url)` is what an Electrobun\n * application passes here.\n */\n onOpenUrl?: (url: string, window: DesktopWindowHandle) => void;\n /**\n * One of the application's own urls a window asked to have opened\n * somewhere new (a Cmd-click or a Ctrl-click on an in-app link), with\n * the window that asked.\n *\n * Defaults to `app.openWindow({ route: url })`: a new window of this\n * application, at that route, which is what a new tab is in an\n * application whose windows have none. An outbound url never comes\n * here; it is `onOpenUrl`'s, and goes to the person's browser.\n */\n onOpenRoute?: (url: string, window: DesktopWindowHandle) => void;\n /**\n * The appearance the platform is in, pushed to every window as it\n * changes and to a new window as it opens.\n *\n * The application supplies it because Electrobun does not: its SDK\n * has no appearance API at all, and the webview's own\n * `prefers-color-scheme` is wrong on WebKitGTK\n *. On a platform that has a\n * signal, this is where it goes; on one that does not, an\n * application setting is a perfectly good source.\n */\n colorScheme?: Observable<'light' | 'dark'>;\n /**\n * Called when the last window closes. A desktop application usually\n * stops here; one with a tray or a menu bar does not, which is why\n * this is a callback rather than an exit.\n */\n onLastWindowClosed?: () => void;\n /** Overrides the frame size messages are split at. Only a test should need to. */\n chunkBytes?: number;\n}\n\nexport interface DesktopApp {\n /**\n * Opens a window, serves it every channel, and returns its handle;\n * at `options.route` when one is given.\n */\n openWindow(options?: DesktopWindowOptions): DesktopWindowHandle;\n /** The windows open now, in the order they were opened. */\n readonly windows: readonly DesktopWindowHandle[];\n /** How many windows are open, as something a channel can publish. */\n readonly windowCount: Observable<number>;\n /** Closes every window and stops serving. */\n dispose(): void;\n}\n\nexport function createDesktopApp(options: DesktopAppOptions): DesktopApp {\n interface Entry {\n handle: DesktopWindowHandle;\n transport: DesktopWindowTransport;\n host: ChannelHost;\n }\n const entries = new Map<number, Entry>();\n /** One appearance subscription per window, ended when it closes. */\n const subscriptions = new Map<number, Subscription>();\n const order: DesktopWindowHandle[] = [];\n const count = new BehaviorSubject(0);\n let nextId = 1;\n let disposed = false;\n\n const forget = (id: number, closeNative: boolean): void => {\n const entry = entries.get(id);\n if (entry === undefined) {\n return;\n }\n entries.delete(id);\n subscriptions.get(id)?.unsubscribe();\n subscriptions.delete(id);\n const at = order.indexOf(entry.handle);\n if (at >= 0) {\n order.splice(at, 1);\n }\n entry.host.dispose();\n if (closeNative) {\n entry.transport.close();\n }\n count.next(order.length);\n if (order.length === 0 && !disposed) {\n options.onLastWindowClosed?.();\n }\n };\n\n const app: DesktopApp = {\n openWindow(windowOptions: DesktopWindowOptions = {}): DesktopWindowHandle {\n if (disposed) {\n throw new Error('This desktop application has been disposed; it cannot open a window.');\n }\n const id = nextId++;\n const handle: DesktopWindowHandle = {\n id,\n route: windowOptions.route ?? null,\n close: () => forget(id, true)\n };\n\n // The host exists before the window does, because `open` may\n // deliver a frame synchronously and a window whose first\n // handshake was dropped never replicates anything.\n let transport: DesktopWindowTransport | undefined;\n const host = serveChannelsToWindow(\n typeof options.channels === 'function' ? options.channels(handle) : options.channels,\n {\n send: frame => transport?.send(frame),\n chunkBytes: options.chunkBytes,\n onOpenUrl: url => options.onOpenUrl?.(url, handle),\n onOpenRoute: url => {\n if (options.onOpenRoute !== undefined) {\n options.onOpenRoute(url, handle);\n } else if (!disposed) {\n app.openWindow({ route: url });\n }\n }\n }\n );\n entries.set(id, { handle, transport: { send: () => {}, close: () => {} }, host });\n order.push(handle);\n\n // Anything pushed before the window has spoken is lost: a\n // webview's RPC is not listening until its page has loaded, and\n // the window opens well before that. Channels do not notice,\n // because a channel starts with the window asking. The\n // appearance is the one thing this side sends first, so it waits\n // for the window's first frame, whatever that frame is. A native\n // window found this; a spec with a transport that was live\n // immediately could not.\n let greeted = false;\n let latest: 'light' | 'dark' | undefined;\n transport = options.open(frame => {\n if (!greeted) {\n greeted = true;\n if (latest !== undefined) {\n host.setColorScheme(latest);\n }\n }\n host.receive(frame);\n }, handle);\n entries.set(id, { handle, transport, host });\n const appearance = options.colorScheme?.subscribe(scheme => {\n latest = scheme;\n if (greeted) {\n host.setColorScheme(scheme);\n }\n });\n if (appearance !== undefined) {\n subscriptions.set(id, appearance);\n }\n count.next(order.length);\n return handle;\n },\n get windows(): readonly DesktopWindowHandle[] {\n return [...order];\n },\n windowCount: count.asObservable(),\n dispose(): void {\n disposed = true;\n for (const id of new Set(entries.keys())) {\n forget(id, true);\n }\n count.complete();\n }\n };\n\n return app;\n}\n\n/** What a window can see and do about the windows of its application. */\nexport interface DesktopWindowsView {\n /** How many windows this application has open. */\n count: number;\n /** Which one this is, so a screen can say \"window 2 of 3\" without asking. */\n id: number;\n}\n\nexport interface DesktopWindowsCommands {\n open: () => void;\n closeThis: () => void;\n}\n\n/**\n * The channel a window opens another window through.\n *\n * It exists because opening a window is the one native act a screen\n * genuinely needs, and going through a channel keeps the view layer\n * from importing an adapter. the \"native menus bound to\n * store actions\" is the same idea from the other end, and on Linux it\n * is the only end: the runtime has no application menus there, so a\n * menu is a component and this is what it calls.\n */\nexport const DesktopWindows: ChannelToken<DesktopWindowsView, DesktopWindowsCommands> = channel<\n DesktopWindowsView,\n DesktopWindowsCommands\n>('gesso:windows', { count: 0, id: 0 });\n\n/** Serves `DesktopWindows` to one window. Put it in `channels`. */\nexport function windowsChannel(app: DesktopApp, window: DesktopWindowHandle): ServedChannel {\n return {\n token: DesktopWindows,\n source: {\n view: {\n count: app.windowCount,\n id: new BehaviorSubject(window.id)\n },\n commands: {\n open: () => {\n app.openWindow();\n },\n closeThis: () => {\n window.close();\n }\n }\n }\n };\n}\n\nexport {\n messageBoxConfirm,\n serveDesktopAgent,\n type DesktopAgent,\n type DesktopAgentOptions,\n type DesktopServe,\n type ShowMessageBox\n} from './agent';\n"],"mappings":"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"}
1
+ {"version":3,"file":"desktop.js","names":[],"sources":["../src/screen.ts","../src/agent.ts","../src/desktop.ts"],"sourcesContent":["import type { AgentSurfaceLike, AgentToolResult } from 'gesso-framework/agent';\nimport { remoteSurface } from 'gesso-framework/agent';\n\n/**\n * A desktop window's screen, offered to agents beside its channels.\n *\n * The screen tools (`ui_snapshot`, `ui_press`, `ui_type`, `ui_focus`,\n * `ui_key`) are served by each window's render worker, on its agent port,\n * and the main process cannot reach a worker. The window's own page can,\n * and it already talks to the main process over Electrobun's RPC. So\n * the page relays the agent port's messages over the RPC\n * (`relayScreenAgent`, in `gesso-electrobun/view`), and the main process holds the other end as a\n * surface (`createScreenAgent`) that `serveDesktopAgent` offers beside\n * the channel tools.\n *\n * Only the screen tools cross. The render worker's surface also lists\n * the channels it replicates, which the main process serves itself, so\n * they would only be listed twice.\n *\n * Pressing the screen's buttons is everything the person can do, so an\n * application offers this in development, not to every agent with the\n * token: a confirmation an agent could click through on the screen is\n * no confirmation.\n */\n\nimport type { ScreenAgentMessage } from './view';\n\n/** The screen tools, by name; what crosses from the render worker. */\nconst SCREEN_TOOL = /^ui_/;\n\n/** The main process's half. */\nexport interface ScreenAgent {\n /**\n * The screen tools of the window attached last, or none while no\n * window is. For `serveDesktopAgent`'s `screen`.\n */\n readonly surface: AgentSurfaceLike;\n /**\n * Attaches a window: `send` posts a message to its page's relay, and\n * the window's messages are handed to `receive`. Detach it when the\n * window closes. The window attached last is the one agents operate.\n */\n attach(send: (message: ScreenAgentMessage) => void): { receive(message: ScreenAgentMessage): void; detach(): void };\n}\n\nconst NO_WINDOW: AgentToolResult = {\n content: [{ type: 'text', text: 'No window is open to operate.' }],\n isError: true\n};\n\nexport function createScreenAgent(): ScreenAgent {\n const windows: { surface: AgentSurfaceLike }[] = [];\n const current = () => windows.at(-1)?.surface;\n const surface: AgentSurfaceLike = {\n tools: async () => ((await current()?.tools()) ?? []).filter(tool => SCREEN_TOOL.test(tool.name)),\n resources: () => [],\n call: async (name, args) => {\n const window = current();\n if (window === undefined || !SCREEN_TOOL.test(name)) return NO_WINDOW;\n return window.call(name, args);\n },\n read: () => undefined\n };\n return {\n surface,\n attach: send => {\n // A MessagePort's shape, over whatever carries the window's messages.\n const port = {\n postMessage: (message: unknown) => send(message),\n onmessage: null as ((event: MessageEvent) => void) | null\n };\n // The screen tools ask the person nothing; a channel command they\n // might reach is the main process's to confirm, not the window's.\n const entry = { surface: remoteSurface(port, { confirm: () => false }) };\n windows.push(entry);\n return {\n receive: message => port.onmessage?.({ data: message } as MessageEvent),\n detach: () => {\n const at = windows.indexOf(entry);\n if (at !== -1) windows.splice(at, 1);\n }\n };\n }\n };\n}\n","import type { ServedChannel } from 'gesso-framework';\nimport {\n agentSurface,\n combineSurfaces,\n mcpHandler,\n type AgentSurfaceLike,\n type AgentConfirmation,\n type AgentSurface,\n type McpHandlerOptions\n} from 'gesso-framework/agent';\n\n/**\n * A desktop application's channels, served to AI agents over MCP.\n *\n * The main process already holds every channel's source: it is what\n * `createDesktopApp` serves to each window. So it is also where an\n * agent connects. This serves those same sources with MCP's HTTP\n * transport on the person's own machine, through `Bun.serve`, which\n * Cottontail provides as Bun does, and an agent such as Claude Code\n * connects by URL:\n *\n * const agent = serveDesktopAgent([counter], { name: 'my-app', confirm: messageBoxConfirm(Utils.showMessageBox) });\n * // claude mcp add --transport http my-app http://127.0.0.1:7310/mcp\n *\n * The channels are passed rather than read from the app, because\n * `createDesktopApp` takes them per window, and the per-window ones\n * (`windowsChannel`) are about a window an agent does not have.\n *\n * What it offers is the channel tools, and the screen tools of a window\n * when it is given `screen`: the screen is in each window's render\n * worker, out of the main process's reach, so the window relays it\n * (`createScreenAgent`).\n *\n * The descriptions come from `channelSchema`. A main process is bundled\n * by Electrobun's own build, which `gesso-vite-plugin` never sees, so\n * the template runs `gesso-channels` to write a module that describes\n * the contracts, and imports it.\n */\n\n/** A `Bun.serve`, as far as this uses one. */\nexport type DesktopServe = (options: {\n hostname: string;\n port: number;\n fetch: (request: Request) => Promise<Response>;\n}) => { stop(closeActiveConnections?: boolean): void };\n\nexport interface DesktopAgentOptions extends Omit<McpHandlerOptions, 'allowedOrigins'> {\n /**\n * The port to listen on (default 7310). When it is taken, the next\n * nine are tried in turn, so a second copy of the app still serves;\n * `url` says which one it got.\n */\n port?: number;\n /** Default `127.0.0.1`: reachable from this machine only. */\n hostname?: string;\n /**\n * Asks the person whether a `@confirm` command may be sent. Without\n * it such a command is refused. `messageBoxConfirm` makes one from\n * Electrobun's native dialog.\n */\n confirm?: (request: AgentConfirmation) => boolean | Promise<boolean>;\n /** The server to start. Defaults to the runtime's `Bun.serve`. */\n serve?: DesktopServe;\n /**\n * A window's screen tools to offer beside the channels:\n * `createScreenAgent().surface`. Pressing the screen's buttons is\n * everything the person can do, so offer it in development.\n */\n screen?: AgentSurfaceLike;\n}\n\nexport interface DesktopAgent {\n /** Where an agent connects: `http://127.0.0.1:<port>/mcp`. */\n readonly url: string;\n readonly surface: AgentSurface;\n /** Stops serving and stops following the channels. */\n stop(): void;\n}\n\nconst DEFAULT_PORT = 7310;\nconst PORTS_TRIED = 10;\n\nexport function serveDesktopAgent(channels: readonly ServedChannel[], options: DesktopAgentOptions = {}): DesktopAgent {\n const serve = options.serve ?? bunServe();\n const hostname = options.hostname ?? '127.0.0.1';\n const first = options.port ?? DEFAULT_PORT;\n const surface = agentSurface(channels, options.confirm === undefined ? {} : { confirm: options.confirm });\n // A desktop app has no browser pages of its own to let in, so every\n // request carrying an Origin is refused: that is a web page the person\n // has open, reaching for their machine.\n const fetch = mcpHandler(options.screen === undefined ? surface : combineSurfaces([surface, options.screen]), {\n ...options,\n allowedOrigins: []\n });\n\n let lastError: unknown;\n for (let port = first; port < first + PORTS_TRIED; port++) {\n try {\n const server = serve({ hostname, port, fetch });\n return {\n url: `http://${hostname}:${port}/mcp`,\n surface,\n stop: () => {\n server.stop(true);\n surface.dispose();\n }\n };\n } catch (error) {\n if (!isAddressInUse(error)) {\n surface.dispose();\n throw error;\n }\n lastError = error;\n }\n }\n surface.dispose();\n throw new Error(\n `Ports ${first} to ${first + PORTS_TRIED - 1} are all in use, so agents cannot be served. ` +\n `Pass another port to serveDesktopAgent. (${String(lastError)})`\n );\n}\n\n/** The options Electrobun's `Utils.showMessageBox` takes, as far as this uses them. */\nexport type ShowMessageBox = (options: {\n type?: 'info' | 'warning' | 'error' | 'question';\n title?: string;\n message?: string;\n detail?: string;\n buttons?: string[];\n defaultId?: number;\n cancelId?: number;\n}) => Promise<{ response: number }>;\n\n/**\n * A `confirm` that asks with the operating system's own dialog.\n *\n * Takes Electrobun's `Utils.showMessageBox` rather than importing it,\n * because Electrobun is a toolchain a project is projected into, not a\n * package this one can depend on.\n *\n * Everything the person needs to decide is in `message`, the command's\n * description and its arguments included, because macOS shows no\n * `detail`: a dialog that asked there showed only \"wants to\n * createBranch in workspace\", with nothing to say which branch.\n *\n * Decline is the first button. macOS makes the first button the\n * default, whatever `defaultId` says, so with Allow first, Enter\n * approved; first is also what `defaultId` and `cancelId` name, so\n * Enter, Escape and closing the dialog decline on every platform.\n */\nexport function messageBoxConfirm(showMessageBox: ShowMessageBox): (request: AgentConfirmation) => Promise<boolean> {\n return async request => {\n const argumentLines = Object.entries(request.arguments).map(([name, value]) => `${name}: ${JSON.stringify(value)}`);\n const message = [\n `An AI agent wants to ${request.command} in ${request.channel}.${request.destructive ? ' This cannot be undone.' : ''}`,\n request.description,\n argumentLines.join('\\n')\n ]\n .filter((part): part is string => part !== undefined && part !== '')\n .join('\\n\\n');\n const { response } = await showMessageBox({\n type: request.destructive ? 'warning' : 'question',\n title: 'An AI agent is asking',\n message,\n buttons: ['Decline', 'Allow'],\n defaultId: 0,\n cancelId: 0\n });\n return response === 1;\n };\n}\n\nfunction bunServe(): DesktopServe {\n const bun = (globalThis as { Bun?: { serve?: DesktopServe } }).Bun;\n if (typeof bun?.serve !== 'function') {\n throw new Error(\n 'serveDesktopAgent needs Bun.serve, which Cottontail and Bun provide. Pass serve to use another server.'\n );\n }\n return options => bun.serve!(options);\n}\n\nfunction isAddressInUse(error: unknown): boolean {\n const code = (error as { code?: unknown } | null)?.code;\n return code === 'EADDRINUSE' || /in use|EADDRINUSE/i.test(String((error as Error | null)?.message ?? error));\n}\n","/**\n * An application in the main process: windows, and the channels each\n * one is served.\n *\n * Nothing here imports Electrobun, and that is not fastidiousness. The\n * SDK is projected into a project by Hutch rather than installed from\n * a registry, so a package in\n * this workspace could not import it even if it wanted to. What the\n * application supplies instead is one function that opens a window,\n * which is the only Electrobun-shaped thing this needs, and which is\n * five lines at the call site:\n *\n * const app = createDesktopApp({\n * channels: window => [\n * { token: Catalogue, source: catalogue },\n * windowsChannel(app, window)\n * ],\n * open: (receive, handle) => {\n * const rpc = BrowserView.defineRPC<GessoWindowRPC>({\n * handlers: { requests: {}, messages: { gessoFrame: receive } }\n * });\n * const window = new BrowserWindow({\n * title: 'Notes',\n * url: withWindowRoute('views://mainview/index.html', handle.route),\n * rpc\n * });\n * return {\n * send: frame => window.webview.rpc.send.gessoFrame(frame),\n * close: () => window.close()\n * };\n * }\n * });\n * app.openWindow();\n *\n * The arrangement it buys: every\n * window is a replica of the same channels, so two windows agree by\n * construction rather than by synchronisation.\n */\nimport { channel, type ChannelToken, type ServedChannel } from 'gesso-framework';\nimport { BehaviorSubject, type Observable, type Subscription } from 'rxjs';\n\nimport type { GessoFrame } from './frames';\nimport { serveChannelsToWindow, type ChannelHost } from './main';\n\nexport { withWindowRoute } from './route';\n\n/** What the application does with the window it opened. */\nexport interface DesktopWindowTransport {\n /** Sends one frame to this window, over its own RPC. */\n send: (frame: GessoFrame) => void;\n /** Closes the native window. The adapter calls this; the platform may also close it on its own. */\n close: () => void;\n}\n\n/** A window this application opened. */\nexport interface DesktopWindowHandle {\n /** Stable for the life of the window, and never reused. */\n readonly id: number;\n /**\n * The application url the window was asked to open at, or null for\n * wherever the application starts.\n *\n * Set by `openWindow({ route })`, which is what a Cmd-click on an\n * in-app link comes to by default. The window cannot be told it\n * through a frame in time to start there, so `open` puts it on the\n * view's url, with `withWindowRoute`, and the window reads it back\n * with `windowRoute` before its shell starts.\n */\n readonly route: string | null;\n /** Closes the window and disposes the channels it was served. */\n close(): void;\n}\n\n/** What `openWindow` may be told about the window it opens. */\nexport interface DesktopWindowOptions {\n /**\n * The application url to open at, `/epic/BUD-12?story=BUD-13`. Carried\n * to `open` as `window.route`; see `DesktopWindowHandle.route`.\n */\n readonly route?: string;\n}\n\nexport interface DesktopAppOptions {\n /**\n * The channels each window is served.\n *\n * A function when a window needs a channel of its own, which is what\n * `windowsChannel` uses to give a window a way to close itself. It\n * is called once per window, and the observables it returns are\n * ordinarily the same ones every time: `provide` keeps a separate\n * record of what each client has seen, so sharing a source between\n * windows is what makes them agree.\n */\n channels: readonly ServedChannel[] | ((window: DesktopWindowHandle) => readonly ServedChannel[]);\n /**\n * Opens a native window.\n *\n * `receive` is what the window's frames must be fed into: wire it to\n * the RPC message the window sends frames on, before the window\n * opens, or the first handshake is lost.\n *\n * `window.route` is the application url the window should start at,\n * or null. Put it on the view's url with `withWindowRoute`, and read\n * it in the window with `windowRoute`; an `open` that ignores it\n * opens every window at the application's start, as before.\n */\n open: (receive: (frame: GessoFrame) => void, window: DesktopWindowHandle) => DesktopWindowTransport;\n /**\n * A url a window asked to have opened outside itself, with the\n * window that asked. `Utils.openExternal(url)` is what an Electrobun\n * application passes here.\n */\n onOpenUrl?: (url: string, window: DesktopWindowHandle) => void;\n /**\n * One of the application's own urls a window asked to have opened\n * somewhere new (a Cmd-click or a Ctrl-click on an in-app link), with\n * the window that asked.\n *\n * Defaults to `app.openWindow({ route: url })`: a new window of this\n * application, at that route, which is what a new tab is in an\n * application whose windows have none. An outbound url never comes\n * here; it is `onOpenUrl`'s, and goes to the person's browser.\n */\n onOpenRoute?: (url: string, window: DesktopWindowHandle) => void;\n /**\n * The appearance the platform is in, pushed to every window as it\n * changes and to a new window as it opens.\n *\n * The application supplies it because Electrobun does not: its SDK\n * has no appearance API at all, and the webview's own\n * `prefers-color-scheme` is wrong on WebKitGTK\n *. On a platform that has a\n * signal, this is where it goes; on one that does not, an\n * application setting is a perfectly good source.\n */\n colorScheme?: Observable<'light' | 'dark'>;\n /**\n * Called when the last window closes. A desktop application usually\n * stops here; one with a tray or a menu bar does not, which is why\n * this is a callback rather than an exit.\n */\n onLastWindowClosed?: () => void;\n /** Overrides the frame size messages are split at. Only a test should need to. */\n chunkBytes?: number;\n}\n\nexport interface DesktopApp {\n /**\n * Opens a window, serves it every channel, and returns its handle;\n * at `options.route` when one is given.\n */\n openWindow(options?: DesktopWindowOptions): DesktopWindowHandle;\n /** The windows open now, in the order they were opened. */\n readonly windows: readonly DesktopWindowHandle[];\n /** How many windows are open, as something a channel can publish. */\n readonly windowCount: Observable<number>;\n /** Closes every window and stops serving. */\n dispose(): void;\n}\n\nexport function createDesktopApp(options: DesktopAppOptions): DesktopApp {\n interface Entry {\n handle: DesktopWindowHandle;\n transport: DesktopWindowTransport;\n host: ChannelHost;\n }\n const entries = new Map<number, Entry>();\n /** One appearance subscription per window, ended when it closes. */\n const subscriptions = new Map<number, Subscription>();\n const order: DesktopWindowHandle[] = [];\n const count = new BehaviorSubject(0);\n let nextId = 1;\n let disposed = false;\n\n const forget = (id: number, closeNative: boolean): void => {\n const entry = entries.get(id);\n if (entry === undefined) {\n return;\n }\n entries.delete(id);\n subscriptions.get(id)?.unsubscribe();\n subscriptions.delete(id);\n const at = order.indexOf(entry.handle);\n if (at >= 0) {\n order.splice(at, 1);\n }\n entry.host.dispose();\n if (closeNative) {\n entry.transport.close();\n }\n count.next(order.length);\n if (order.length === 0 && !disposed) {\n options.onLastWindowClosed?.();\n }\n };\n\n const app: DesktopApp = {\n openWindow(windowOptions: DesktopWindowOptions = {}): DesktopWindowHandle {\n if (disposed) {\n throw new Error('This desktop application has been disposed; it cannot open a window.');\n }\n const id = nextId++;\n const handle: DesktopWindowHandle = {\n id,\n route: windowOptions.route ?? null,\n close: () => forget(id, true)\n };\n\n // The host exists before the window does, because `open` may\n // deliver a frame synchronously and a window whose first\n // handshake was dropped never replicates anything.\n let transport: DesktopWindowTransport | undefined;\n const host = serveChannelsToWindow(\n typeof options.channels === 'function' ? options.channels(handle) : options.channels,\n {\n send: frame => transport?.send(frame),\n chunkBytes: options.chunkBytes,\n onOpenUrl: url => options.onOpenUrl?.(url, handle),\n onOpenRoute: url => {\n if (options.onOpenRoute !== undefined) {\n options.onOpenRoute(url, handle);\n } else if (!disposed) {\n app.openWindow({ route: url });\n }\n }\n }\n );\n entries.set(id, { handle, transport: { send: () => {}, close: () => {} }, host });\n order.push(handle);\n\n // Anything pushed before the window has spoken is lost: a\n // webview's RPC is not listening until its page has loaded, and\n // the window opens well before that. Channels do not notice,\n // because a channel starts with the window asking. The\n // appearance is the one thing this side sends first, so it waits\n // for the window's first frame, whatever that frame is. A native\n // window found this; a spec with a transport that was live\n // immediately could not.\n let greeted = false;\n let latest: 'light' | 'dark' | undefined;\n transport = options.open(frame => {\n if (!greeted) {\n greeted = true;\n if (latest !== undefined) {\n host.setColorScheme(latest);\n }\n }\n host.receive(frame);\n }, handle);\n entries.set(id, { handle, transport, host });\n const appearance = options.colorScheme?.subscribe(scheme => {\n latest = scheme;\n if (greeted) {\n host.setColorScheme(scheme);\n }\n });\n if (appearance !== undefined) {\n subscriptions.set(id, appearance);\n }\n count.next(order.length);\n return handle;\n },\n get windows(): readonly DesktopWindowHandle[] {\n return [...order];\n },\n windowCount: count.asObservable(),\n dispose(): void {\n disposed = true;\n for (const id of new Set(entries.keys())) {\n forget(id, true);\n }\n count.complete();\n }\n };\n\n return app;\n}\n\n/** What a window can see and do about the windows of its application. */\nexport interface DesktopWindowsView {\n /** How many windows this application has open. */\n count: number;\n /** Which one this is, so a screen can say \"window 2 of 3\" without asking. */\n id: number;\n}\n\nexport interface DesktopWindowsCommands {\n open: () => void;\n closeThis: () => void;\n}\n\n/**\n * The channel a window opens another window through.\n *\n * It exists because opening a window is the one native act a screen\n * genuinely needs, and going through a channel keeps the view layer\n * from importing an adapter. the \"native menus bound to\n * store actions\" is the same idea from the other end, and on Linux it\n * is the only end: the runtime has no application menus there, so a\n * menu is a component and this is what it calls.\n */\nexport const DesktopWindows: ChannelToken<DesktopWindowsView, DesktopWindowsCommands> = channel<\n DesktopWindowsView,\n DesktopWindowsCommands\n>('gesso:windows', { count: 0, id: 0 });\n\n/** Serves `DesktopWindows` to one window. Put it in `channels`. */\nexport function windowsChannel(app: DesktopApp, window: DesktopWindowHandle): ServedChannel {\n return {\n token: DesktopWindows,\n source: {\n view: {\n count: app.windowCount,\n id: new BehaviorSubject(window.id)\n },\n commands: {\n open: () => {\n app.openWindow();\n },\n closeThis: () => {\n window.close();\n }\n }\n }\n };\n}\n\nexport { createScreenAgent, type ScreenAgent } from './screen';\nexport {\n messageBoxConfirm,\n serveDesktopAgent,\n type DesktopAgent,\n type DesktopAgentOptions,\n type DesktopServe,\n type ShowMessageBox\n} from './agent';\n"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { RemoteBridge, RemoteBridgeOptions } from "gesso-framework/remote";
1
2
  //#region src/route.d.ts
2
3
  /**
3
4
  * How a new window learns the route it opens at.
@@ -40,5 +41,25 @@ declare function withWindowRoute(viewUrl: string, route: string | null | undefin
40
41
  */
41
42
  declare function windowRoute(hash?: string): string;
42
43
  //#endregion
43
- export { withWindowRoute as n, windowRoute as t };
44
- //# sourceMappingURL=route-CNeMnj_6.d.ts.map
44
+ //#region src/view.d.ts
45
+ type ElectrobunBridgeOptions = RemoteBridgeOptions;
46
+ type ElectrobunBridge = RemoteBridge;
47
+ /** `createRemoteBridge`, by the name the Electrobun template uses. */
48
+ declare const createElectrobunBridge: (options: ElectrobunBridgeOptions) => ElectrobunBridge;
49
+ /** A message between a window's render worker agent port and the main process, as the port carries it. */
50
+ type ScreenAgentMessage = unknown;
51
+ /**
52
+ * The page's half: relays messages between the main process and the
53
+ * render worker's agent port, which it opens on the first one, since the
54
+ * render worker does not exist until the app mounts. Returns what to call
55
+ * with each message the main process sends.
56
+ *
57
+ * const relay = relayScreenAgent(app, message => view.rpc?.send.screenAgent(message));
58
+ * // in the RPC's handlers: screenAgent: message => relay(message)
59
+ */
60
+ declare function relayScreenAgent(app: {
61
+ openRenderPort(key: string): MessagePort | undefined;
62
+ }, send: (message: ScreenAgentMessage) => void): (message: ScreenAgentMessage) => void;
63
+ //#endregion
64
+ export { relayScreenAgent as a, createElectrobunBridge as i, ElectrobunBridgeOptions as n, windowRoute as o, ScreenAgentMessage as r, withWindowRoute as s, ElectrobunBridge as t };
65
+ //# sourceMappingURL=view-Cw282TuC.d.ts.map
package/dist/view.d.ts CHANGED
@@ -1,10 +1,2 @@
1
- import { t as windowRoute } from "./route-CNeMnj_6.js";
2
- import { RemoteBridge, RemoteBridgeOptions } from "gesso-framework/remote";
3
- //#region src/view.d.ts
4
- type ElectrobunBridgeOptions = RemoteBridgeOptions;
5
- type ElectrobunBridge = RemoteBridge;
6
- /** `createRemoteBridge`, by the name the Electrobun template uses. */
7
- declare const createElectrobunBridge: (options: ElectrobunBridgeOptions) => ElectrobunBridge;
8
- //#endregion
9
- export { ElectrobunBridge, ElectrobunBridgeOptions, createElectrobunBridge, windowRoute };
10
- //# sourceMappingURL=view.d.ts.map
1
+ import { a as relayScreenAgent, i as createElectrobunBridge, n as ElectrobunBridgeOptions, o as windowRoute, r as ScreenAgentMessage, t as ElectrobunBridge } from "./view-Cw282TuC.js";
2
+ export { ElectrobunBridge, ElectrobunBridgeOptions, ScreenAgentMessage, createElectrobunBridge, relayScreenAgent, windowRoute };
package/dist/view.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { t as windowRoute } from "./route-B2qU38dY.js";
2
2
  import { createRemoteBridge } from "gesso-framework/remote";
3
+ import { AGENT_PORT } from "gesso-framework/agent";
3
4
  //#region src/view.ts
4
5
  /**
5
6
  * The webview's half of the bridge.
@@ -13,7 +14,27 @@ import { createRemoteBridge } from "gesso-framework/remote";
13
14
  */
14
15
  /** `createRemoteBridge`, by the name the Electrobun template uses. */
15
16
  const createElectrobunBridge = createRemoteBridge;
17
+ /**
18
+ * The page's half: relays messages between the main process and the
19
+ * render worker's agent port, which it opens on the first one, since the
20
+ * render worker does not exist until the app mounts. Returns what to call
21
+ * with each message the main process sends.
22
+ *
23
+ * const relay = relayScreenAgent(app, message => view.rpc?.send.screenAgent(message));
24
+ * // in the RPC's handlers: screenAgent: message => relay(message)
25
+ */
26
+ function relayScreenAgent(app, send) {
27
+ let port;
28
+ return (message) => {
29
+ if (port === void 0) {
30
+ port = app.openRenderPort(AGENT_PORT);
31
+ if (port === void 0) return;
32
+ port.onmessage = (event) => send(event.data);
33
+ }
34
+ port.postMessage(message);
35
+ };
36
+ }
16
37
  //#endregion
17
- export { createElectrobunBridge, windowRoute };
38
+ export { createElectrobunBridge, relayScreenAgent, windowRoute };
18
39
 
19
40
  //# sourceMappingURL=view.js.map
package/dist/view.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"view.js","names":[],"sources":["../src/view.ts"],"sourcesContent":["/**\n * The webview's half of the bridge.\n *\n * The bridge is `createRemoteBridge` from `gesso-framework/remote`,\n * which carries a render worker's channels over any transport that\n * can carry a string. In a desktop window that transport is\n * Electrobun's RPC: pass `view.rpc.send.<name>` as `send`, and feed\n * every frame the main process sends to `receive`. This entry keeps the\n * names a desktop application has always used for it.\n */\nimport { createRemoteBridge, type RemoteBridge, type RemoteBridgeOptions } from 'gesso-framework/remote';\n\nexport { windowRoute } from './route';\n\nexport type ElectrobunBridgeOptions = RemoteBridgeOptions;\nexport type ElectrobunBridge = RemoteBridge;\n\n/** `createRemoteBridge`, by the name the Electrobun template uses. */\nexport const createElectrobunBridge: (options: ElectrobunBridgeOptions) => ElectrobunBridge = createRemoteBridge;\n"],"mappings":";;;;;;;;;;;;;;AAkBA,MAAa,yBAAiF"}
1
+ {"version":3,"file":"view.js","names":[],"sources":["../src/view.ts"],"sourcesContent":["/**\n * The webview's half of the bridge.\n *\n * The bridge is `createRemoteBridge` from `gesso-framework/remote`,\n * which carries a render worker's channels over any transport that\n * can carry a string. In a desktop window that transport is\n * Electrobun's RPC: pass `view.rpc.send.<name>` as `send`, and feed\n * every frame the main process sends to `receive`. This entry keeps the\n * names a desktop application has always used for it.\n */\nimport { AGENT_PORT } from 'gesso-framework/agent';\nimport { createRemoteBridge, type RemoteBridge, type RemoteBridgeOptions } from 'gesso-framework/remote';\n\nexport { windowRoute } from './route';\n\nexport type ElectrobunBridgeOptions = RemoteBridgeOptions;\nexport type ElectrobunBridge = RemoteBridge;\n\n/** `createRemoteBridge`, by the name the Electrobun template uses. */\nexport const createElectrobunBridge: (options: ElectrobunBridgeOptions) => ElectrobunBridge = createRemoteBridge;\n\n/** A message between a window's render worker agent port and the main process, as the port carries it. */\nexport type ScreenAgentMessage = unknown;\n\n/**\n * The page's half: relays messages between the main process and the\n * render worker's agent port, which it opens on the first one, since the\n * render worker does not exist until the app mounts. Returns what to call\n * with each message the main process sends.\n *\n * const relay = relayScreenAgent(app, message => view.rpc?.send.screenAgent(message));\n * // in the RPC's handlers: screenAgent: message => relay(message)\n */\nexport function relayScreenAgent(\n app: { openRenderPort(key: string): MessagePort | undefined },\n send: (message: ScreenAgentMessage) => void\n): (message: ScreenAgentMessage) => void {\n let port: MessagePort | undefined;\n return message => {\n if (port === undefined) {\n port = app.openRenderPort(AGENT_PORT);\n if (port === undefined) return;\n port.onmessage = event => send(event.data as ScreenAgentMessage);\n }\n port.postMessage(message);\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;AAmBA,MAAa,yBAAiF;;;;;;;;;;AAc9F,SAAgB,iBACd,KACA,MACuC;CACvC,IAAI;CACJ,QAAO,YAAW;EAChB,IAAI,SAAS,KAAA,GAAW;GACtB,OAAO,IAAI,eAAe,UAAU;GACpC,IAAI,SAAS,KAAA,GAAW;GACxB,KAAK,aAAY,UAAS,KAAK,MAAM,IAA0B;EACjE;EACA,KAAK,YAAY,OAAO;CAC1B;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gesso-electrobun",
3
- "version": "0.6.12",
3
+ "version": "0.6.14",
4
4
  "description": "Run a Gesso application in an Electrobun window, with its stores in the main process.",
5
5
  "license": "MIT",
6
6
  "author": "Kevin Baker",
@@ -54,7 +54,7 @@
54
54
  "LICENSE"
55
55
  ],
56
56
  "dependencies": {
57
- "gesso-framework": "^0.6.12"
57
+ "gesso-framework": "^0.6.14"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "rxjs": "^7.8.2"