From 896eb41cdc795bca2c023c26e39c008a29abc0c6 Mon Sep 17 00:00:00 2001 From: meetsu <96637888+klNuno@users.noreply.github.com> Date: Sun, 18 Oct 2026 20:27:05 +0000 Subject: [PATCH 1/3] fix(ui): open Plugins on this PC and keep the project menu at the right --- docs/plugins.md | 5 +++ packages/ui/src/components/PluginsPage.svelte | 42 ++++++++++++++++--- .../ui/src/components/PluginsPage.test.ts | 35 ++++++++++++++++ packages/ui/src/components/Sidebar.svelte | 29 +++++-------- packages/ui/src/lib/strings.en.ts | 2 + packages/ui/src/lib/strings.fr.ts | 1 + tests/e2e/project-views.test.ts | 4 ++ 7 files changed, 93 insertions(+), 25 deletions(-) diff --git a/docs/plugins.md b/docs/plugins.md index 31cac02d6..41ecf25f8 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -29,6 +29,11 @@ Plugins are administration of the machine that hosts the core, so every `plugins.*` method is owner only and the phone settings leave the page to the desktop app ([phone](phone.md)). +Each core has its own plugins. The page opens on this PC when the window has +one, since a desktop app shows its window there, whichever machine the open +conversation runs on. With several machines connected, a menu beside the +heading names the machine shown and switches to another one this window owns. + ## The manifest Put `boite-plugin.json` at the root of the repository: diff --git a/packages/ui/src/components/PluginsPage.svelte b/packages/ui/src/components/PluginsPage.svelte index d9aeb6f83..958cfb44c 100644 --- a/packages/ui/src/components/PluginsPage.svelte +++ b/packages/ui/src/components/PluginsPage.svelte @@ -1,8 +1,7 @@ + +{#snippet refusal(rejected: PluginRejected)} + +{/snippet} + +{#snippet autostartSwitch(plugin: PluginState, on: boolean | undefined)} + + {#if on !== undefined} + + {/if} +{/snippet} + +{#snippet row(plugin: PluginState)} + {@const working = busy[plugin.id] === true} + {@const state = stateLine(plugin)} +
+
+ +
+

+ {plugin.name} + {#if plugin.version ?? plugin.availableVersion}v{plugin.version ?? plugin.availableVersion}{/if} + {#if plugin.homepage && !plugin.source} + + {/if} +

+ {#if state} +

+ {state} +

+ {/if} + {#if plugin.description}

{plugin.description}

{/if} +
+
+ {#if plugin.status === 'installing'} + + {:else if plugin.status === 'not-installed'} + + {:else} + {#if plugin.status === 'installed' && plugin.availableVersion && plugin.availableVersion !== plugin.version} + + {/if} + {#if plugin.status === 'error' || (plugin.status === 'rejected' && plugin.origin === 'recommended')} + + {/if} + {#if plugin.updates && plugin.version !== null} + {@const looking = plugin.updates.checking || checking[plugin.id] === true} + + {/if} + + {/if} +
+
+ {#if plugin.status === 'installing'} +
+ +
+ {/if} + {#if plugin.source} +

+ {bare(plugin.source.url)} + {fill(t.atCommit, { ref: plugin.source.ref, commit: short(plugin.source.commit) })} +

+ {/if} + {#if plugin.updates && plugin.version !== null} + {@const line = updatesLine(plugin.updates)} + {#if line} +

{line}

+ {/if} + {/if} + {#if plugin.error}{/if} + {#if errors[plugin.id]}{/if} + {#if plugin.rejected}{@render refusal(plugin.rejected)}{/if} + {#if plugin.status === 'installed' && plugin.app} + {@const app = plugin.app} + {@const live = app.status === 'running' || app.status === 'starting'} +
+
+

+ {t.app}{appLine(app)} +

+
+ {#if live} + + + {:else} + + {/if} +
+
+ {@render autostartSwitch(plugin, app.autostart)} + {#if app.error}

{app.error}

{/if} +
+ {/if} + {#if plugin.status === 'installed' && plugin.gateway} + {@const gateway = plugin.gateway} + {@const live = gateway.status !== 'stopped' && gateway.status !== 'crashed'} + {@const dashboard = gatewayDashboard(gateway)} +
+
+

+ {t.gateway}{gatewayLine(gateway)} + +

+
+ {#if live} + + + {:else} + + {/if} +
+
+ {#if !plugin.app}{@render autostartSwitch(plugin, gateway.autostart)}{/if} + {#if gateway.error}

{gateway.error}

{/if} + {#if gateway.status === 'running' && (dashboard || plugin.id === 'douane')} +
+ {#if dashboard}{/if} + {#if plugin.id === 'douane' && store.core?.features?.gatewayMove === true} + + {/if} +
+ {/if} +
+ {/if} + {#if plugin.status === 'installed' && plugin.pools.length > 0} +
+
+ + +
+ {#each pools[plugin.id] ?? [] as pool (pool.provider)} +
+
+
{providerName(pool.provider)}
+ +
+ {#if pool.accounts.length === 0}

{t.empty}

{/if} + {#each pool.accounts as account (account.email)} + + {/each} +
+ {/each} +
+ {/if} +
+{/snippet} + +
+
+

{t.heading}

+ {@render machineMenu?.()} +
+ {#if error}{/if} + + {#if installed.length > 0} +
+ +
+ {#each installed as plugin (plugin.id)}{@render row(plugin)}{/each} +
+
+ {/if} + + {#if recommended.length > 0} +
+ +
+ {#each recommended as plugin (plugin.id)}{@render row(plugin)}{/each} +
+
+ {/if} + +
+ +
+
+ + + +
+ {#if inspectError}{/if} + {#if preview} +
+ {#if preview.manifest} +
+ +
+

{preview.manifest.name}v{preview.manifest.version}

+

{preview.manifest.description}

+
+
+ {/if} + {#if preview.rejected} + {@render refusal(preview.rejected)} + {/if} +
+
{t.previewSource}
+
{bare(preview.source.url)} {fill(t.atCommit, { ref: preview.source.ref, commit: short(preview.source.commit) })}
+ {#if preview.replaces}
{t.previewReplaces}
v{preview.replaces}
{/if} + {#if preview.rejected === null && preview.artifact} +
{t.previewDownloads}
{preview.artifact.url}
+
{t.previewDigest}
{preview.artifact.sha256}
+
{t.previewRuns}
+
    {#each preview.commands as command (command)}
  • {command}
  • {/each}
+ {#if preview.manifest?.provides.accountPools} +
{t.previewPools}
+
{preview.manifest.provides.accountPools.providers.map(providerName).join(', ')}
+ {/if} + {#if preview.manifest?.provides.desktopApp} +
{t.previewApp}
+
{t.previewAppBody}
+ {/if} + {#if preview.manifest?.provides.gateway} +
{t.previewGateway}
+
{t.previewGatewayBody}
+ {/if} + {/if} +
+ {#if preview.rejected === null} +

{t.trust}

+ {/if} +
+ + {#if preview.previewId && preview.manifest} + + {/if} +
+
+ {/if} +
+
+
+ + { moving = false; }} /> + + diff --git a/packages/ui/src/components/PluginsPage.svelte b/packages/ui/src/components/PluginsPage.svelte index 958cfb44c..a721fd96f 100644 --- a/packages/ui/src/components/PluginsPage.svelte +++ b/packages/ui/src/components/PluginsPage.svelte @@ -1,22 +1,12 @@ -{#snippet refusal(rejected: PluginRejected)} - -{/snippet} - -{#snippet autostartSwitch(plugin: PluginState, on: boolean | undefined)} - - {#if on !== undefined} - +{#snippet machineMenu()} + {#if machines.length > 1} + { picked = id; }}> + {chosen.label} + {/if} {/snippet} -{#snippet row(plugin: PluginState)} - {@const working = busy[plugin.id] === true} - {@const state = stateLine(plugin)} -
-
- -
-

- {plugin.name} - {#if plugin.version ?? plugin.availableVersion}v{plugin.version ?? plugin.availableVersion}{/if} - {#if plugin.homepage && !plugin.source} - - {/if} -

- {#if state} -

- {state} -

- {/if} - {#if plugin.description}

{plugin.description}

{/if} -
-
- {#if plugin.status === 'installing'} - - {:else if plugin.status === 'not-installed'} - - {:else} - {#if plugin.status === 'installed' && plugin.availableVersion && plugin.availableVersion !== plugin.version} - - {/if} - {#if plugin.status === 'error' || (plugin.status === 'rejected' && plugin.origin === 'recommended')} - - {/if} - {#if plugin.updates && plugin.version !== null} - {@const looking = plugin.updates.checking || checking[plugin.id] === true} - - {/if} - - {/if} -
-
- {#if plugin.status === 'installing'} -
- -
- {/if} - {#if plugin.source} -

- {bare(plugin.source.url)} - {fill(t.atCommit, { ref: plugin.source.ref, commit: short(plugin.source.commit) })} -

- {/if} - {#if plugin.updates && plugin.version !== null} - {@const line = updatesLine(plugin.updates)} - {#if line} -

{line}

- {/if} - {/if} - {#if plugin.error}{/if} - {#if errors[plugin.id]}{/if} - {#if plugin.rejected}{@render refusal(plugin.rejected)}{/if} - {#if plugin.status === 'installed' && plugin.app} - {@const app = plugin.app} - {@const live = app.status === 'running' || app.status === 'starting'} -
-
-

- {t.app}{appLine(app)} -

-
- {#if live} - - - {:else} - - {/if} -
-
- {@render autostartSwitch(plugin, app.autostart)} - {#if app.error}

{app.error}

{/if} -
- {/if} - {#if plugin.status === 'installed' && plugin.gateway} - {@const gateway = plugin.gateway} - {@const live = gateway.status !== 'stopped' && gateway.status !== 'crashed'} - {@const dashboard = gatewayDashboard(gateway)} -
-
-

- {t.gateway}{gatewayLine(gateway)} - -

-
- {#if live} - - - {:else} - - {/if} -
-
- {#if !plugin.app}{@render autostartSwitch(plugin, gateway.autostart)}{/if} - {#if gateway.error}

{gateway.error}

{/if} - {#if gateway.status === 'running' && (dashboard || plugin.id === 'douane')} -
- {#if dashboard}{/if} - {#if plugin.id === 'douane' && store.core?.features?.gatewayMove === true} - - {/if} -
- {/if} -
- {/if} - {#if plugin.status === 'installed' && plugin.pools.length > 0} -
-
- - -
- {#each pools[plugin.id] ?? [] as pool (pool.provider)} -
-
-
{providerName(pool.provider)}
- -
- {#if pool.accounts.length === 0}

{t.empty}

{/if} - {#each pool.accounts as account (account.email)} - - {/each} -
- {/each} -
- {/if} -
-{/snippet} - -
-
-

{t.heading}

- {#if machines.length > 1} - { picked = id; }}> - {chosen.label} - - {/if} -
- {#if error}{/if} - - {#if installed.length > 0} -
- -
- {#each installed as plugin (plugin.id)}{@render row(plugin)}{/each} -
-
- {/if} - - {#if recommended.length > 0} -
- -
- {#each recommended as plugin (plugin.id)}{@render row(plugin)}{/each} -
-
- {/if} - -
- -
-
- - - -
- {#if inspectError}{/if} - {#if preview} -
- {#if preview.manifest} -
- -
-

{preview.manifest.name}v{preview.manifest.version}

-

{preview.manifest.description}

-
-
- {/if} - {#if preview.rejected} - {@render refusal(preview.rejected)} - {/if} -
-
{t.previewSource}
-
{bare(preview.source.url)} {fill(t.atCommit, { ref: preview.source.ref, commit: short(preview.source.commit) })}
- {#if preview.replaces}
{t.previewReplaces}
v{preview.replaces}
{/if} - {#if preview.rejected === null && preview.artifact} -
{t.previewDownloads}
{preview.artifact.url}
-
{t.previewDigest}
{preview.artifact.sha256}
-
{t.previewRuns}
-
    {#each preview.commands as command (command)}
  • {command}
  • {/each}
- {#if preview.manifest?.provides.accountPools} -
{t.previewPools}
-
{preview.manifest.provides.accountPools.providers.map(providerName).join(', ')}
- {/if} - {#if preview.manifest?.provides.desktopApp} -
{t.previewApp}
-
{t.previewAppBody}
- {/if} - {#if preview.manifest?.provides.gateway} -
{t.previewGateway}
-
{t.previewGatewayBody}
- {/if} - {/if} -
- {#if preview.rejected === null} -

{t.trust}

- {/if} -
- - {#if preview.previewId && preview.manifest} - - {/if} -
-
- {/if} -
-
-
- - { moving = false; }} /> +{#key chosen.store} + +{/key} diff --git a/packages/ui/src/components/PluginsPage.test.ts b/packages/ui/src/components/PluginsPage.test.ts index 2252277e3..f50723ff5 100644 --- a/packages/ui/src/components/PluginsPage.test.ts +++ b/packages/ui/src/components/PluginsPage.test.ts @@ -275,6 +275,14 @@ test('opens on this PC rather than on the server the chat is on, and switches ma const pc = new FakeClient({ delayMs: 0 }); const server = new FakeClient({ delayMs: 0 }); await Promise.all([pc.connect(), server.connect()]); + const pcCall = pc.call.bind(pc); + // The PC answers Stop only when the test lets it, after the page has moved to the server. + let answer: () => void = () => {}; + const held = new Promise((resolve) => { answer = resolve; }); + vi.spyOn(pc, 'call').mockImplementation((async (method: string, params: unknown) => { + if (method === 'plugins.app') await held; + return pcCall(method as never, params as never); + }) as never); const serverList = server.call.bind(server); // Nothing is installed on the server. vi.spyOn(server, 'call').mockImplementation((async (method: string, params: unknown) => { @@ -293,12 +301,21 @@ test('opens on this PC rather than on the server the chat is on, and switches ma mounted = mount(PluginsPage, { target: document.body, props: { store: serverStore } }); await vi.waitFor(() => expect(row('bots')?.closest('[data-testid="plugins-installed"]') ?? null).not.toBeNull()); expect(q('[data-testid="plugins-machine"]')?.textContent).toContain('nuno-pc'); + await vi.waitFor(() => expect(appButton('bots', 'stop')).not.toBeNull()); + appButton('bots', 'stop')!.click(); q('[data-testid="plugins-machine"]')!.click(); flushSync(); q('[data-testid="plugins-machine-menu"] [data-value="https://server.test"]')!.click(); await vi.waitFor(() => expect(row('bots')?.closest('[data-testid="plugins-recommended"]') ?? null).not.toBeNull()); expect(q('[data-testid="plugins-machine"]')?.textContent).toContain('server'); + + // The PC's late answer stays out of the server's page. + answer(); + await new Promise((resolve) => setTimeout(resolve, 50)); + flushSync(); + expect(row('bots')?.closest('[data-testid="plugins-recommended"]') ?? null).not.toBeNull(); + expect(app('bots')).toBeNull(); } finally { workspace.machines = []; pc.close(); server.close(); From 1882954b1bb612748eb7f922dd37347fb44970db Mon Sep 17 00:00:00 2001 From: meetsu <96637888+klNuno@users.noreply.github.com> Date: Sun, 18 Oct 2026 21:11:11 +0000 Subject: [PATCH 3/3] ci: raise the UI budget for the plugins machine menu --- docs/ci.md | 7 +++++++ scripts/ci/budgets.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/ci.md b/docs/ci.md index 19f49992a..2562e605d 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -375,6 +375,13 @@ Windows, against 5,037,724 for those two files at that main, 1,607 more; the entry chunk stays at 596,622. The UI limit rises to 5,040,000; the other limits stay. +On 2026-10-18, Plugins opening on this PC with a list mounted per machine and +the project menu back at the right end of the sidebar header (PR #606) merged +with `origin/main` at `e22bb094` measure 4,869,757 UI bytes after a fresh +`bun run build:ui` on Linux, against 4,869,093 for that main: the machine menu +and `PluginsMachine.svelte` add 664 bytes. The UI limit rises to 4,869,800; +the other limits stay. + On 2026-10-17, PR #566 merged with `origin/main` at `b6ae659a`, which brings the thread links and agent mail direction (PR #562), measures 595,205 entry bytes, 5,036,527 UI bytes and 4,390,768 core JavaScript bytes after fresh diff --git a/scripts/ci/budgets.json b/scripts/ci/budgets.json index 2f6dbc2b8..ce7104f4b 100644 --- a/scripts/ci/budgets.json +++ b/scripts/ci/budgets.json @@ -1,6 +1,6 @@ { "uiEntryChunk": 492200, - "uiDist": 4869600, + "uiDist": 4869800, "coreBundle": 995000, "coreJavaScript": 4479600 }