@agimon-ai/doompi-web 0.0.1-alpha.0
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/LICENSE +29 -0
- package/README.md +196 -0
- package/dist/_virtual/_rolldown/runtime.cjs +1 -0
- package/dist/bin/serve.cjs +3 -0
- package/dist/bin/serve.cjs.map +1 -0
- package/dist/bin/serve.d.cts +1 -0
- package/dist/bin/serve.d.mts +1 -0
- package/dist/bin/serve.mjs +3 -0
- package/dist/bin/serve.mjs.map +1 -0
- package/dist/bundler.cjs +1 -0
- package/dist/bundler.d.cts +2 -0
- package/dist/bundler.d.mts +2 -0
- package/dist/bundler.mjs +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +7 -0
- package/dist/index.d.mts +7 -0
- package/dist/index.mjs +1 -0
- package/dist/src/adapters/authRoutes.cjs +2 -0
- package/dist/src/adapters/authRoutes.cjs.map +1 -0
- package/dist/src/adapters/authRoutes.mjs +2 -0
- package/dist/src/adapters/authRoutes.mjs.map +1 -0
- package/dist/src/adapters/bundledServer.cjs +2 -0
- package/dist/src/adapters/bundledServer.cjs.map +1 -0
- package/dist/src/adapters/bundledServer.mjs +2 -0
- package/dist/src/adapters/bundledServer.mjs.map +1 -0
- package/dist/src/adapters/directoryListing.cjs +2 -0
- package/dist/src/adapters/directoryListing.cjs.map +1 -0
- package/dist/src/adapters/directoryListing.mjs +2 -0
- package/dist/src/adapters/directoryListing.mjs.map +1 -0
- package/dist/src/adapters/directorySearch.cjs +3 -0
- package/dist/src/adapters/directorySearch.cjs.map +1 -0
- package/dist/src/adapters/directorySearch.mjs +3 -0
- package/dist/src/adapters/directorySearch.mjs.map +1 -0
- package/dist/src/adapters/gitStatus.cjs +2 -0
- package/dist/src/adapters/gitStatus.cjs.map +1 -0
- package/dist/src/adapters/gitStatus.mjs +2 -0
- package/dist/src/adapters/gitStatus.mjs.map +1 -0
- package/dist/src/adapters/httpServer.cjs +2 -0
- package/dist/src/adapters/httpServer.cjs.map +1 -0
- package/dist/src/adapters/httpServer.d.cts +14 -0
- package/dist/src/adapters/httpServer.d.cts.map +1 -0
- package/dist/src/adapters/httpServer.d.mts +14 -0
- package/dist/src/adapters/httpServer.d.mts.map +1 -0
- package/dist/src/adapters/httpServer.mjs +2 -0
- package/dist/src/adapters/httpServer.mjs.map +1 -0
- package/dist/src/adapters/hubProbe.cjs +2 -0
- package/dist/src/adapters/hubProbe.cjs.map +1 -0
- package/dist/src/adapters/hubProbe.mjs +2 -0
- package/dist/src/adapters/hubProbe.mjs.map +1 -0
- package/dist/src/adapters/packageApiProxy.cjs +2 -0
- package/dist/src/adapters/packageApiProxy.cjs.map +1 -0
- package/dist/src/adapters/packageApiProxy.mjs +2 -0
- package/dist/src/adapters/packageApiProxy.mjs.map +1 -0
- package/dist/src/adapters/piHubService.cjs +2 -0
- package/dist/src/adapters/piHubService.cjs.map +1 -0
- package/dist/src/adapters/piHubService.mjs +2 -0
- package/dist/src/adapters/piHubService.mjs.map +1 -0
- package/dist/src/adapters/piWebSocketListener.cjs +2 -0
- package/dist/src/adapters/piWebSocketListener.cjs.map +1 -0
- package/dist/src/adapters/piWebSocketListener.mjs +2 -0
- package/dist/src/adapters/piWebSocketListener.mjs.map +1 -0
- package/dist/src/adapters/providerAuth.cjs +2 -0
- package/dist/src/adapters/providerAuth.cjs.map +1 -0
- package/dist/src/adapters/providerAuth.mjs +2 -0
- package/dist/src/adapters/providerAuth.mjs.map +1 -0
- package/dist/src/adapters/registryWatcher.cjs +2 -0
- package/dist/src/adapters/registryWatcher.cjs.map +1 -0
- package/dist/src/adapters/registryWatcher.mjs +2 -0
- package/dist/src/adapters/registryWatcher.mjs.map +1 -0
- package/dist/src/adapters/serverSpawner.cjs +2 -0
- package/dist/src/adapters/serverSpawner.cjs.map +1 -0
- package/dist/src/adapters/serverSpawner.mjs +2 -0
- package/dist/src/adapters/serverSpawner.mjs.map +1 -0
- package/dist/src/adapters/sessionFiles.cjs +3 -0
- package/dist/src/adapters/sessionFiles.cjs.map +1 -0
- package/dist/src/adapters/sessionFiles.mjs +3 -0
- package/dist/src/adapters/sessionFiles.mjs.map +1 -0
- package/dist/src/adapters/sessionHub.cjs +2 -0
- package/dist/src/adapters/sessionHub.cjs.map +1 -0
- package/dist/src/adapters/sessionHub.mjs +2 -0
- package/dist/src/adapters/sessionHub.mjs.map +1 -0
- package/dist/src/adapters/sessionSocketClient.cjs +2 -0
- package/dist/src/adapters/sessionSocketClient.cjs.map +1 -0
- package/dist/src/adapters/sessionSocketClient.mjs +2 -0
- package/dist/src/adapters/sessionSocketClient.mjs.map +1 -0
- package/dist/src/adapters/syncGuard.cjs +2 -0
- package/dist/src/adapters/syncGuard.cjs.map +1 -0
- package/dist/src/adapters/syncGuard.mjs +2 -0
- package/dist/src/adapters/syncGuard.mjs.map +1 -0
- package/dist/src/adapters/threadJournals.cjs +2 -0
- package/dist/src/adapters/threadJournals.cjs.map +1 -0
- package/dist/src/adapters/threadJournals.mjs +2 -0
- package/dist/src/adapters/threadJournals.mjs.map +1 -0
- package/dist/src/adapters/webBundler.cjs +2 -0
- package/dist/src/adapters/webBundler.cjs.map +1 -0
- package/dist/src/adapters/webBundler.d.cts +28 -0
- package/dist/src/adapters/webBundler.d.cts.map +1 -0
- package/dist/src/adapters/webBundler.d.mts +28 -0
- package/dist/src/adapters/webBundler.d.mts.map +1 -0
- package/dist/src/adapters/webBundler.mjs +2 -0
- package/dist/src/adapters/webBundler.mjs.map +1 -0
- package/dist/src/adapters/webHubPluginLoader.cjs +2 -0
- package/dist/src/adapters/webHubPluginLoader.cjs.map +1 -0
- package/dist/src/adapters/webHubPluginLoader.mjs +2 -0
- package/dist/src/adapters/webHubPluginLoader.mjs.map +1 -0
- package/dist/src/adapters/webHubPlugins.generated.cjs +2 -0
- package/dist/src/adapters/webHubPlugins.generated.cjs.map +1 -0
- package/dist/src/adapters/webHubPlugins.generated.mjs +2 -0
- package/dist/src/adapters/webHubPlugins.generated.mjs.map +1 -0
- package/dist/src/adapters/webPluginGenerate.cjs +4 -0
- package/dist/src/adapters/webPluginGenerate.cjs.map +1 -0
- package/dist/src/adapters/webPluginGenerate.mjs +4 -0
- package/dist/src/adapters/webPluginGenerate.mjs.map +1 -0
- package/dist/src/adapters/webPluginScan.cjs +2 -0
- package/dist/src/adapters/webPluginScan.cjs.map +1 -0
- package/dist/src/adapters/webPluginScan.mjs +2 -0
- package/dist/src/adapters/webPluginScan.mjs.map +1 -0
- package/dist/src/adapters/webPluginVite.cjs +2 -0
- package/dist/src/adapters/webPluginVite.cjs.map +1 -0
- package/dist/src/adapters/webPluginVite.mjs +2 -0
- package/dist/src/adapters/webPluginVite.mjs.map +1 -0
- package/dist/src/services/directoryMatch.cjs +2 -0
- package/dist/src/services/directoryMatch.cjs.map +1 -0
- package/dist/src/services/directoryMatch.mjs +2 -0
- package/dist/src/services/directoryMatch.mjs.map +1 -0
- package/dist/src/services/fileMatch.cjs +2 -0
- package/dist/src/services/fileMatch.cjs.map +1 -0
- package/dist/src/services/fileMatch.mjs +2 -0
- package/dist/src/services/fileMatch.mjs.map +1 -0
- package/dist/src/services/fileMedia.cjs +2 -0
- package/dist/src/services/fileMedia.cjs.map +1 -0
- package/dist/src/services/fileMedia.mjs +2 -0
- package/dist/src/services/fileMedia.mjs.map +1 -0
- package/dist/src/services/frameRing.cjs +2 -0
- package/dist/src/services/frameRing.cjs.map +1 -0
- package/dist/src/services/frameRing.mjs +2 -0
- package/dist/src/services/frameRing.mjs.map +1 -0
- package/dist/src/services/journalTail.cjs +3 -0
- package/dist/src/services/journalTail.cjs.map +1 -0
- package/dist/src/services/journalTail.mjs +3 -0
- package/dist/src/services/journalTail.mjs.map +1 -0
- package/dist/src/services/loginFlow.cjs +2 -0
- package/dist/src/services/loginFlow.cjs.map +1 -0
- package/dist/src/services/loginFlow.mjs +2 -0
- package/dist/src/services/loginFlow.mjs.map +1 -0
- package/dist/src/services/registryStore.cjs +2 -0
- package/dist/src/services/registryStore.cjs.map +1 -0
- package/dist/src/services/registryStore.mjs +2 -0
- package/dist/src/services/registryStore.mjs.map +1 -0
- package/dist/src/services/retryPolicy.cjs +2 -0
- package/dist/src/services/retryPolicy.cjs.map +1 -0
- package/dist/src/services/retryPolicy.mjs +2 -0
- package/dist/src/services/retryPolicy.mjs.map +1 -0
- package/dist/src/services/serveOptions.cjs +2 -0
- package/dist/src/services/serveOptions.cjs.map +1 -0
- package/dist/src/services/serveOptions.d.cts +21 -0
- package/dist/src/services/serveOptions.d.cts.map +1 -0
- package/dist/src/services/serveOptions.d.mts +21 -0
- package/dist/src/services/serveOptions.d.mts.map +1 -0
- package/dist/src/services/serveOptions.mjs +2 -0
- package/dist/src/services/serveOptions.mjs.map +1 -0
- package/dist/src/services/sessionFraming.cjs +4 -0
- package/dist/src/services/sessionFraming.cjs.map +1 -0
- package/dist/src/services/sessionFraming.mjs +4 -0
- package/dist/src/services/sessionFraming.mjs.map +1 -0
- package/dist/src/services/sessionPresence.cjs +2 -0
- package/dist/src/services/sessionPresence.cjs.map +1 -0
- package/dist/src/services/sessionPresence.mjs +2 -0
- package/dist/src/services/sessionPresence.mjs.map +1 -0
- package/dist/src/services/staticAssets.cjs +2 -0
- package/dist/src/services/staticAssets.cjs.map +1 -0
- package/dist/src/services/staticAssets.mjs +2 -0
- package/dist/src/services/staticAssets.mjs.map +1 -0
- package/dist/src/services/webDevRoots.cjs +2 -0
- package/dist/src/services/webDevRoots.cjs.map +1 -0
- package/dist/src/services/webDevRoots.mjs +2 -0
- package/dist/src/services/webDevRoots.mjs.map +1 -0
- package/dist/src/services/webPluginManifest.cjs +2 -0
- package/dist/src/services/webPluginManifest.cjs.map +1 -0
- package/dist/src/services/webPluginManifest.mjs +2 -0
- package/dist/src/services/webPluginManifest.mjs.map +1 -0
- package/dist/src/types/auth.cjs +2 -0
- package/dist/src/types/auth.cjs.map +1 -0
- package/dist/src/types/auth.d.cts +94 -0
- package/dist/src/types/auth.d.cts.map +1 -0
- package/dist/src/types/auth.d.mts +94 -0
- package/dist/src/types/auth.d.mts.map +1 -0
- package/dist/src/types/auth.mjs +2 -0
- package/dist/src/types/auth.mjs.map +1 -0
- package/dist/src/types/bridge.d.cts +31 -0
- package/dist/src/types/bridge.d.cts.map +1 -0
- package/dist/src/types/bridge.d.mts +31 -0
- package/dist/src/types/bridge.d.mts.map +1 -0
- package/dist/src/types/hub.cjs +2 -0
- package/dist/src/types/hub.cjs.map +1 -0
- package/dist/src/types/hub.d.cts +58 -0
- package/dist/src/types/hub.d.cts.map +1 -0
- package/dist/src/types/hub.d.mts +58 -0
- package/dist/src/types/hub.d.mts.map +1 -0
- package/dist/src/types/hub.mjs +2 -0
- package/dist/src/types/hub.mjs.map +1 -0
- package/dist/src/types/media.cjs +2 -0
- package/dist/src/types/media.cjs.map +1 -0
- package/dist/src/types/media.mjs +2 -0
- package/dist/src/types/media.mjs.map +1 -0
- package/dist/src/types/registry.cjs +1 -0
- package/dist/src/types/registry.d.cts +2 -0
- package/dist/src/types/registry.d.mts +2 -0
- package/dist/src/types/registry.mjs +1 -0
- package/dist/src/types/session.cjs +2 -0
- package/dist/src/types/session.cjs.map +1 -0
- package/dist/src/types/session.d.cts +24 -0
- package/dist/src/types/session.d.cts.map +1 -0
- package/dist/src/types/session.d.mts +24 -0
- package/dist/src/types/session.d.mts.map +1 -0
- package/dist/src/types/session.mjs +2 -0
- package/dist/src/types/session.mjs.map +1 -0
- package/dist/web/apple-touch-icon.png +0 -0
- package/dist/web/assets/index-Cg52ngKv.css +2 -0
- package/dist/web/assets/index-DQO7NTIP.js +90 -0
- package/dist/web/assets/index-DQO7NTIP.js.map +1 -0
- package/dist/web/favicon-16.png +0 -0
- package/dist/web/favicon-32.png +0 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/favicon.svg +16 -0
- package/dist/web/icon-512.png +0 -0
- package/dist/web/index.html +17 -0
- package/llms.txt +8 -0
- package/package.json +106 -0
- package/src/adapters/authRoutes.ts +95 -0
- package/src/adapters/bundledServer.ts +64 -0
- package/src/adapters/directoryListing.ts +127 -0
- package/src/adapters/directorySearch.ts +124 -0
- package/src/adapters/gitStatus.ts +27 -0
- package/src/adapters/httpServer.ts +556 -0
- package/src/adapters/hubProbe.ts +27 -0
- package/src/adapters/packageApiProxy.ts +60 -0
- package/src/adapters/piHubService.ts +129 -0
- package/src/adapters/piWebSocketListener.ts +98 -0
- package/src/adapters/providerAuth.ts +181 -0
- package/src/adapters/registryWatcher.ts +112 -0
- package/src/adapters/serverSpawner.ts +181 -0
- package/src/adapters/sessionFiles.ts +99 -0
- package/src/adapters/sessionHub.ts +567 -0
- package/src/adapters/sessionSocketClient.ts +105 -0
- package/src/adapters/syncGuard.ts +119 -0
- package/src/adapters/threadJournals.ts +184 -0
- package/src/adapters/webBundler.ts +83 -0
- package/src/adapters/webHubPluginLoader.ts +67 -0
- package/src/adapters/webHubPlugins.generated.ts +4 -0
- package/src/adapters/webPluginGenerate.ts +172 -0
- package/src/adapters/webPluginScan.ts +62 -0
- package/src/adapters/webPluginVite.ts +51 -0
- package/src/bin/serve.ts +64 -0
- package/src/exports/bundler.ts +1 -0
- package/src/exports/index.ts +9 -0
- package/src/services/directoryMatch.ts +71 -0
- package/src/services/fileMatch.ts +37 -0
- package/src/services/fileMedia.ts +27 -0
- package/src/services/frameRing.ts +33 -0
- package/src/services/journalTail.ts +47 -0
- package/src/services/loginFlow.ts +144 -0
- package/src/services/registryStore.ts +83 -0
- package/src/services/retryPolicy.ts +15 -0
- package/src/services/serveOptions.ts +63 -0
- package/src/services/sessionFraming.ts +32 -0
- package/src/services/sessionPresence.ts +131 -0
- package/src/services/staticAssets.ts +37 -0
- package/src/services/webDevRoots.ts +36 -0
- package/src/services/webPluginManifest.ts +149 -0
- package/src/types/auth.ts +118 -0
- package/src/types/bridge.ts +41 -0
- package/src/types/hub.ts +290 -0
- package/src/types/media.ts +38 -0
- package/src/types/registry.ts +2 -0
- package/src/types/session.ts +36 -0
- package/src/web/app/Providers.tsx +46 -0
- package/src/web/app/protocolRuntime.ts +106 -0
- package/src/web/app/sessionRuntime.ts +189 -0
- package/src/web/app/webPlugins.generated.ts +4 -0
- package/src/web/components/MascotMark.tsx +46 -0
- package/src/web/components/PluginSurface.tsx +10 -0
- package/src/web/components/ToolRendererBoundary.tsx +32 -0
- package/src/web/features/activity/ActivityDock.tsx +125 -0
- package/src/web/features/connection/RefusedCard.tsx +63 -0
- package/src/web/features/dialogs/DialogOverlay.tsx +176 -0
- package/src/web/features/leader/CommandPalette.tsx +386 -0
- package/src/web/features/selection/SelectionBar.tsx +529 -0
- package/src/web/features/session/Composer.tsx +368 -0
- package/src/web/features/session/MentionPreviews.tsx +58 -0
- package/src/web/features/session/ThreadView.tsx +35 -0
- package/src/web/features/session/Timeline.tsx +382 -0
- package/src/web/features/session/ToolCard.tsx +79 -0
- package/src/web/features/sessions/NewSessionDialog.tsx +222 -0
- package/src/web/features/sessions/SessionRail.tsx +411 -0
- package/src/web/features/settings/AppearanceSettings.tsx +88 -0
- package/src/web/features/settings/LoginFlowDialog.tsx +221 -0
- package/src/web/features/settings/PluginSettings.tsx +92 -0
- package/src/web/features/settings/ProviderSettings.tsx +238 -0
- package/src/web/features/settings/SettingsMenu.tsx +32 -0
- package/src/web/features/status/TopBar.tsx +208 -0
- package/src/web/index.html +16 -0
- package/src/web/lib/authApi.ts +90 -0
- package/src/web/lib/commands.ts +61 -0
- package/src/web/lib/composition.ts +225 -0
- package/src/web/lib/fileMentions.ts +32 -0
- package/src/web/lib/hubApi.ts +86 -0
- package/src/web/lib/leaderTree.ts +159 -0
- package/src/web/lib/piTransport.ts +61 -0
- package/src/web/lib/pluginRegistry.ts +488 -0
- package/src/web/lib/pluginSlotProps.ts +50 -0
- package/src/web/lib/promptFocus.ts +25 -0
- package/src/web/lib/protocolTimeline.ts +73 -0
- package/src/web/lib/sessionModel.ts +673 -0
- package/src/web/lib/sessionSummary.ts +58 -0
- package/src/web/lib/settingsSections.ts +21 -0
- package/src/web/lib/statusLine.ts +90 -0
- package/src/web/lib/threadRenderer.ts +25 -0
- package/src/web/lib/toolMessageProps.ts +21 -0
- package/src/web/lib/transport.ts +30 -0
- package/src/web/lib/wsClient.ts +73 -0
- package/src/web/main.tsx +17 -0
- package/src/web/public/apple-touch-icon.png +0 -0
- package/src/web/public/favicon-16.png +0 -0
- package/src/web/public/favicon-32.png +0 -0
- package/src/web/public/favicon.ico +0 -0
- package/src/web/public/favicon.svg +16 -0
- package/src/web/public/icon-512.png +0 -0
- package/src/web/routes/CockpitPage.tsx +98 -0
- package/src/web/routes/SettingsPage.tsx +60 -0
- package/src/web/routes/routeTree.tsx +49 -0
- package/src/web/stores/menuStore.ts +78 -0
- package/src/web/stores/paletteStore.ts +25 -0
- package/src/web/stores/sessionStore.ts +303 -0
- package/src/web/stores/sessionsStore.ts +155 -0
- package/src/web/stores/themeStore.ts +52 -0
- package/src/web/stores/threadStore.ts +68 -0
- package/src/web/stores/transientTabsStore.ts +63 -0
- package/src/web/stores/uiStore.ts +28 -0
- package/src/web/stores/useOpenTab.ts +19 -0
- package/src/web/stores/usePluginSlotProps.ts +22 -0
- package/src/web/styles/app.css +15 -0
- package/src/web/styles/webPluginSources.generated.css +1 -0
- package/src/web/vite-env.d.ts +1 -0
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ActivityGroupContribution,
|
|
3
|
+
LeaderBindingContribution,
|
|
4
|
+
MinorModeContribution,
|
|
5
|
+
PaletteCommandContribution,
|
|
6
|
+
SelectionAxisContribution,
|
|
7
|
+
SessionChannelContribution,
|
|
8
|
+
SlotDeclaration,
|
|
9
|
+
SlotFillContribution,
|
|
10
|
+
SurfaceContribution,
|
|
11
|
+
TabContribution,
|
|
12
|
+
ToolRendererContribution,
|
|
13
|
+
WebPluginDefinition,
|
|
14
|
+
WebPluginRuntime,
|
|
15
|
+
WebPluginSlotProps,
|
|
16
|
+
} from '@agimon-ai/doompi-web-contracts';
|
|
17
|
+
import type { ComponentType } from 'react';
|
|
18
|
+
import { leaderConflicts } from './leaderTree.ts';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The installed plugin set.
|
|
22
|
+
*
|
|
23
|
+
* Plain module state rather than a reactive store: the generated plugin list
|
|
24
|
+
* is installed once at startup, before the first render, so every read during
|
|
25
|
+
* render is stable. Install runs in two phases. The first collects every
|
|
26
|
+
* plugin's contributions in the generated order, which is (registrationOrder,
|
|
27
|
+
* pluginId); the second resolves every relation between plugins by name, so
|
|
28
|
+
* a plugin may fill a slot whichever of the two installs first.
|
|
29
|
+
*
|
|
30
|
+
* Plugins are independent and a user may install any of them, so a collision
|
|
31
|
+
* between two plugins never fails the install: a shared name goes to the
|
|
32
|
+
* first plugin (a Leader Space leaf to the later one, as the TUI documents)
|
|
33
|
+
* and the loser is recorded as a diagnostic. Only a malformed contribution
|
|
34
|
+
* inside one plugin throws, because that is a developer error its own tests
|
|
35
|
+
* catch. resetWebPlugins exists for unit tests only.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
export type InstallDiagnosticKind =
|
|
39
|
+
| 'duplicate-plugin'
|
|
40
|
+
| 'duplicate-tab'
|
|
41
|
+
| 'duplicate-channel'
|
|
42
|
+
| 'duplicate-tool'
|
|
43
|
+
| 'duplicate-activity-group'
|
|
44
|
+
| 'duplicate-minor-mode'
|
|
45
|
+
| 'duplicate-selection-axis'
|
|
46
|
+
| 'duplicate-palette-command'
|
|
47
|
+
| 'leader-leaf-override'
|
|
48
|
+
| 'leader-group-label'
|
|
49
|
+
| 'orphan-fill'
|
|
50
|
+
| 'rejected-fill';
|
|
51
|
+
|
|
52
|
+
export interface InstallDiagnostic {
|
|
53
|
+
/** The plugin that lost or misfiled the contribution. */
|
|
54
|
+
pluginId: string;
|
|
55
|
+
kind: InstallDiagnosticKind;
|
|
56
|
+
message: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** One placed fill, as the owner's renderSlot and slotData read it. */
|
|
60
|
+
export interface ResolvedFill {
|
|
61
|
+
slot: string;
|
|
62
|
+
pluginId: string;
|
|
63
|
+
id: string;
|
|
64
|
+
order: number;
|
|
65
|
+
data?: unknown;
|
|
66
|
+
component?: ComponentType<WebPluginSlotProps>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The slots the host itself declares; a plugin's own slots are namespaced by its id instead. */
|
|
70
|
+
export const HOST_SLOTS = {
|
|
71
|
+
overlay: 'overlay',
|
|
72
|
+
rail: 'rail',
|
|
73
|
+
selectionBar: 'selection-bar',
|
|
74
|
+
activity: 'activity',
|
|
75
|
+
} as const;
|
|
76
|
+
|
|
77
|
+
const SLOT_SEPARATOR = '.';
|
|
78
|
+
const DEFAULT_FILL_ORDER = 1000;
|
|
79
|
+
const RESERVED_PLUGIN_IDS: ReadonlySet<string> = new Set(Object.values(HOST_SLOTS));
|
|
80
|
+
|
|
81
|
+
/** The keyed slot an activity group opens: sections fill it and render as the group's body. */
|
|
82
|
+
export function activityGroupSlot(name: string): string {
|
|
83
|
+
return `${HOST_SLOTS.activity}${SLOT_SEPARATOR}${name}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface RegistryState {
|
|
87
|
+
plugins: WebPluginDefinition[];
|
|
88
|
+
tabs: TabContribution[];
|
|
89
|
+
channels: Map<string, SessionChannelContribution>;
|
|
90
|
+
commands: PaletteCommandContribution[];
|
|
91
|
+
leaderBindings: LeaderBindingContribution[];
|
|
92
|
+
selectionAxes: SelectionAxisContribution[];
|
|
93
|
+
minorModes: MinorModeContribution[];
|
|
94
|
+
activityGroups: ActivityGroupContribution[];
|
|
95
|
+
toolRenderers: Map<string, ToolRendererContribution>;
|
|
96
|
+
toolMatchers: ToolRendererContribution[];
|
|
97
|
+
slots: Map<string, SlotDeclaration>;
|
|
98
|
+
fills: Map<string, ResolvedFill[]>;
|
|
99
|
+
diagnostics: InstallDiagnostic[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function emptyState(): RegistryState {
|
|
103
|
+
return {
|
|
104
|
+
plugins: [],
|
|
105
|
+
tabs: [],
|
|
106
|
+
channels: new Map(),
|
|
107
|
+
commands: [],
|
|
108
|
+
leaderBindings: [],
|
|
109
|
+
selectionAxes: [],
|
|
110
|
+
minorModes: [],
|
|
111
|
+
activityGroups: [],
|
|
112
|
+
toolRenderers: new Map(),
|
|
113
|
+
toolMatchers: [],
|
|
114
|
+
slots: new Map(),
|
|
115
|
+
fills: new Map(),
|
|
116
|
+
diagnostics: [],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let state = emptyState();
|
|
121
|
+
let installed = false;
|
|
122
|
+
|
|
123
|
+
/** The TUI's leader alphabet: one lowercase letter or digit per segment. */
|
|
124
|
+
const LEADER_KEY = /^[a-z0-9]$/;
|
|
125
|
+
const MAX_LEADER_PATH = 4;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* A malformed binding fails the install rather than the keypress: the plugin
|
|
129
|
+
* set is generated at build time, so this is a developer error, and the TUI's
|
|
130
|
+
* registry refuses the same shapes.
|
|
131
|
+
*/
|
|
132
|
+
function checkLeaderBinding(pluginId: string, binding: LeaderBindingContribution): void {
|
|
133
|
+
if (binding.path.length === 0 || binding.path.length > MAX_LEADER_PATH) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`Web plugin '${pluginId}' leader binding '${binding.id}' needs 1 to ${MAX_LEADER_PATH} path segments.`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
for (const segment of binding.path) {
|
|
139
|
+
if (!LEADER_KEY.test(segment.key)) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`Web plugin '${pluginId}' leader binding '${binding.id}' key '${segment.key}' must be one lowercase letter or digit.`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
if (segment.label.trim() === '') {
|
|
145
|
+
throw new Error(`Web plugin '${pluginId}' leader binding '${binding.id}' has an unlabeled segment.`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
interface PendingFill {
|
|
151
|
+
pluginId: string;
|
|
152
|
+
fill: SlotFillContribution;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
interface PendingSection {
|
|
156
|
+
pluginId: string;
|
|
157
|
+
section: SurfaceContribution;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
interface PendingBinding {
|
|
161
|
+
pluginId: string;
|
|
162
|
+
binding: LeaderBindingContribution;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
type SharedNamespace =
|
|
166
|
+
| 'plugin'
|
|
167
|
+
| 'tab'
|
|
168
|
+
| 'channel'
|
|
169
|
+
| 'tool'
|
|
170
|
+
| 'activity-group'
|
|
171
|
+
| 'minor-mode'
|
|
172
|
+
| 'selection-axis'
|
|
173
|
+
| 'palette-command';
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Claims `id` in one shared namespace: false, with a diagnostic, when another
|
|
177
|
+
* plugin already holds it; a throw when this plugin declared it twice, which
|
|
178
|
+
* is a developer error rather than a composition.
|
|
179
|
+
*/
|
|
180
|
+
function claim(owners: Map<string, string>, thing: SharedNamespace, id: string, pluginId: string): boolean {
|
|
181
|
+
const holder = owners.get(id);
|
|
182
|
+
if (holder === pluginId) {
|
|
183
|
+
throw new Error(`Web plugin '${pluginId}' declares ${thing.replace('-', ' ')} '${id}' twice.`);
|
|
184
|
+
}
|
|
185
|
+
if (holder !== undefined) {
|
|
186
|
+
state.diagnostics.push({
|
|
187
|
+
pluginId,
|
|
188
|
+
kind: `duplicate-${thing}`,
|
|
189
|
+
message: `Web plugin '${pluginId}' ${thing.replace('-', ' ')} '${id}' is already provided by '${holder}'; keeping the first.`,
|
|
190
|
+
});
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
owners.set(id, pluginId);
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function surfaceFill(slot: string, surface: SurfaceContribution): SlotFillContribution {
|
|
198
|
+
return { slot, id: surface.id, component: surface.component };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function checkSlotDeclaration(pluginId: string, declared: Set<string>, declaration: SlotDeclaration): void {
|
|
202
|
+
const prefix = `${pluginId}${SLOT_SEPARATOR}`;
|
|
203
|
+
if (!declaration.slot.startsWith(prefix) || declaration.slot.length === prefix.length) {
|
|
204
|
+
throw new Error(
|
|
205
|
+
`Web plugin '${pluginId}' declares slot '${declaration.slot}', which must be named '${prefix}<name>'.`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
if (declared.has(declaration.slot)) {
|
|
209
|
+
throw new Error(`Web plugin '${pluginId}' declares slot '${declaration.slot}' twice.`);
|
|
210
|
+
}
|
|
211
|
+
declared.add(declaration.slot);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function checkFill(pluginId: string, seen: Set<string>, fill: SlotFillContribution): void {
|
|
215
|
+
if (fill.id === '') throw new Error(`Web plugin '${pluginId}' has a fill into '${fill.slot}' with an empty id.`);
|
|
216
|
+
if (fill.component === undefined && fill.data === undefined) {
|
|
217
|
+
throw new Error(`Web plugin '${pluginId}' fill '${fill.id}' into '${fill.slot}' has neither a component nor data.`);
|
|
218
|
+
}
|
|
219
|
+
const key = `${fill.slot} ${fill.id}`;
|
|
220
|
+
if (seen.has(key)) throw new Error(`Web plugin '${pluginId}' fills '${fill.slot}' with id '${fill.id}' twice.`);
|
|
221
|
+
seen.add(key);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const byFillOrder = (left: ResolvedFill, right: ResolvedFill): number =>
|
|
225
|
+
left.order - right.order || left.pluginId.localeCompare(right.pluginId) || left.id.localeCompare(right.id);
|
|
226
|
+
|
|
227
|
+
const byDisplayOrder = (left: { order?: number; name: string }, right: { order?: number; name: string }): number =>
|
|
228
|
+
(left.order ?? DEFAULT_FILL_ORDER) - (right.order ?? DEFAULT_FILL_ORDER) || left.name.localeCompare(right.name);
|
|
229
|
+
|
|
230
|
+
function placeFill(pluginId: string, fill: SlotFillContribution): void {
|
|
231
|
+
const declaration = state.slots.get(fill.slot);
|
|
232
|
+
if (declaration === undefined) {
|
|
233
|
+
state.diagnostics.push({
|
|
234
|
+
pluginId,
|
|
235
|
+
kind: 'orphan-fill',
|
|
236
|
+
message: `Web plugin '${pluginId}' fills slot '${fill.slot}', which no installed plugin declares; nothing is placed.`,
|
|
237
|
+
});
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
let data = fill.data;
|
|
241
|
+
if (data !== undefined && declaration.parse !== undefined) {
|
|
242
|
+
const parsed = declaration.parse(data);
|
|
243
|
+
if (parsed === null) {
|
|
244
|
+
state.diagnostics.push({
|
|
245
|
+
pluginId,
|
|
246
|
+
kind: 'rejected-fill',
|
|
247
|
+
message: `Web plugin '${pluginId}' fill '${fill.id}' into '${fill.slot}' was rejected by the slot's parse gate.`,
|
|
248
|
+
});
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
data = parsed;
|
|
252
|
+
}
|
|
253
|
+
const placed: ResolvedFill = {
|
|
254
|
+
slot: fill.slot,
|
|
255
|
+
pluginId,
|
|
256
|
+
id: fill.id,
|
|
257
|
+
order: fill.order ?? DEFAULT_FILL_ORDER,
|
|
258
|
+
...(data === undefined ? {} : { data }),
|
|
259
|
+
...(fill.component === undefined ? {} : { component: fill.component }),
|
|
260
|
+
};
|
|
261
|
+
const list = state.fills.get(fill.slot);
|
|
262
|
+
if (list === undefined) state.fills.set(fill.slot, [placed]);
|
|
263
|
+
else list.push(placed);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function resolveLeaderConflicts(bindings: readonly PendingBinding[]): void {
|
|
267
|
+
const owner = new Map<LeaderBindingContribution, string>(bindings.map((entry) => [entry.binding, entry.pluginId]));
|
|
268
|
+
for (const conflict of leaderConflicts(bindings.map((entry) => entry.binding))) {
|
|
269
|
+
const winner = owner.get(conflict.winner) ?? '';
|
|
270
|
+
const loser = owner.get(conflict.loser) ?? '';
|
|
271
|
+
if (winner === loser) {
|
|
272
|
+
throw new Error(
|
|
273
|
+
`Web plugin '${winner}' binds Leader Space '${conflict.path}' twice ('${conflict.loser.id}' and '${conflict.winner.id}').`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
if (conflict.kind === 'leaf-override') {
|
|
277
|
+
state.diagnostics.push({
|
|
278
|
+
pluginId: loser,
|
|
279
|
+
kind: 'leader-leaf-override',
|
|
280
|
+
message: `Web plugin '${loser}' Leader Space leaf '${conflict.path}' ('${conflict.loser.id}') is taken over by '${winner}' ('${conflict.winner.id}').`,
|
|
281
|
+
});
|
|
282
|
+
} else {
|
|
283
|
+
state.diagnostics.push({
|
|
284
|
+
pluginId: loser,
|
|
285
|
+
kind: 'leader-group-label',
|
|
286
|
+
message: `Web plugin '${loser}' words Leader Space group '${conflict.path}' differently from '${winner}', which named it first; keeping the first label.`,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function installWebPlugins(plugins: readonly WebPluginDefinition[]): void {
|
|
293
|
+
if (installed) throw new Error('Web plugins are already installed.');
|
|
294
|
+
const owners: Record<SharedNamespace, Map<string, string>> = {
|
|
295
|
+
plugin: new Map(),
|
|
296
|
+
tab: new Map(),
|
|
297
|
+
channel: new Map(),
|
|
298
|
+
tool: new Map(),
|
|
299
|
+
'activity-group': new Map(),
|
|
300
|
+
'minor-mode': new Map(),
|
|
301
|
+
'selection-axis': new Map(),
|
|
302
|
+
'palette-command': new Map(),
|
|
303
|
+
};
|
|
304
|
+
const pendingFills: PendingFill[] = [];
|
|
305
|
+
const pendingSections: PendingSection[] = [];
|
|
306
|
+
const pendingBindings: PendingBinding[] = [];
|
|
307
|
+
const pendingSlots: SlotDeclaration[] = [];
|
|
308
|
+
|
|
309
|
+
// Phase 1: collect, in the generated (registrationOrder, pluginId) order.
|
|
310
|
+
for (const plugin of plugins) {
|
|
311
|
+
if (RESERVED_PLUGIN_IDS.has(plugin.id)) {
|
|
312
|
+
throw new Error(`Web plugin id '${plugin.id}' is reserved for a host slot.`);
|
|
313
|
+
}
|
|
314
|
+
if (owners.plugin.has(plugin.id)) {
|
|
315
|
+
state.diagnostics.push({
|
|
316
|
+
pluginId: plugin.id,
|
|
317
|
+
kind: 'duplicate-plugin',
|
|
318
|
+
message: `Web plugin id '${plugin.id}' is declared twice; keeping the first definition.`,
|
|
319
|
+
});
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
owners.plugin.set(plugin.id, plugin.id);
|
|
323
|
+
state.plugins.push(plugin);
|
|
324
|
+
for (const tab of plugin.tabs ?? []) {
|
|
325
|
+
if (claim(owners.tab, 'tab', tab.id, plugin.id)) state.tabs.push(tab);
|
|
326
|
+
}
|
|
327
|
+
for (const channel of plugin.channels ?? []) {
|
|
328
|
+
if (claim(owners.channel, 'channel', channel.channel, plugin.id)) state.channels.set(channel.channel, channel);
|
|
329
|
+
}
|
|
330
|
+
for (const command of plugin.paletteCommands ?? []) {
|
|
331
|
+
if (claim(owners['palette-command'], 'palette-command', command.id, plugin.id)) state.commands.push(command);
|
|
332
|
+
}
|
|
333
|
+
for (const binding of plugin.leaderBindings ?? []) {
|
|
334
|
+
checkLeaderBinding(plugin.id, binding);
|
|
335
|
+
pendingBindings.push({ pluginId: plugin.id, binding });
|
|
336
|
+
}
|
|
337
|
+
for (const axis of plugin.selectionAxes ?? []) {
|
|
338
|
+
if (claim(owners['selection-axis'], 'selection-axis', axis.name, plugin.id)) state.selectionAxes.push(axis);
|
|
339
|
+
}
|
|
340
|
+
for (const mode of plugin.minorModes ?? []) {
|
|
341
|
+
if (claim(owners['minor-mode'], 'minor-mode', mode.name, plugin.id)) state.minorModes.push(mode);
|
|
342
|
+
}
|
|
343
|
+
for (const group of plugin.activityGroups ?? []) {
|
|
344
|
+
if (claim(owners['activity-group'], 'activity-group', group.name, plugin.id)) state.activityGroups.push(group);
|
|
345
|
+
}
|
|
346
|
+
for (const renderer of plugin.toolRenderers ?? []) {
|
|
347
|
+
for (const tool of renderer.tools) {
|
|
348
|
+
if (claim(owners.tool, 'tool', tool, plugin.id)) state.toolRenderers.set(tool, renderer);
|
|
349
|
+
}
|
|
350
|
+
if (renderer.matches !== undefined) state.toolMatchers.push(renderer);
|
|
351
|
+
}
|
|
352
|
+
const declared = new Set<string>();
|
|
353
|
+
for (const declaration of plugin.slots ?? []) {
|
|
354
|
+
checkSlotDeclaration(plugin.id, declared, declaration);
|
|
355
|
+
pendingSlots.push(declaration);
|
|
356
|
+
}
|
|
357
|
+
const filled = new Set<string>();
|
|
358
|
+
for (const fill of plugin.fills ?? []) {
|
|
359
|
+
checkFill(plugin.id, filled, fill);
|
|
360
|
+
pendingFills.push({ pluginId: plugin.id, fill });
|
|
361
|
+
}
|
|
362
|
+
for (const surface of plugin.overlays ?? []) {
|
|
363
|
+
pendingFills.push({ pluginId: plugin.id, fill: surfaceFill(HOST_SLOTS.overlay, surface) });
|
|
364
|
+
}
|
|
365
|
+
for (const surface of plugin.railSections ?? []) {
|
|
366
|
+
pendingFills.push({ pluginId: plugin.id, fill: surfaceFill(HOST_SLOTS.rail, surface) });
|
|
367
|
+
}
|
|
368
|
+
for (const surface of plugin.selectionBarItems ?? []) {
|
|
369
|
+
pendingFills.push({ pluginId: plugin.id, fill: surfaceFill(HOST_SLOTS.selectionBar, surface) });
|
|
370
|
+
}
|
|
371
|
+
for (const section of plugin.activitySections ?? []) pendingSections.push({ pluginId: plugin.id, section });
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Phase 2: resolve by name, now that every declaration is known.
|
|
375
|
+
for (const slot of Object.values(HOST_SLOTS)) state.slots.set(slot, { slot });
|
|
376
|
+
for (const group of state.activityGroups) {
|
|
377
|
+
state.slots.set(activityGroupSlot(group.name), { slot: activityGroupSlot(group.name) });
|
|
378
|
+
}
|
|
379
|
+
for (const declaration of pendingSlots) state.slots.set(declaration.slot, declaration);
|
|
380
|
+
for (const { pluginId, section } of pendingSections) {
|
|
381
|
+
const keyed = activityGroupSlot(section.id);
|
|
382
|
+
pendingFills.push({ pluginId, fill: surfaceFill(state.slots.has(keyed) ? keyed : HOST_SLOTS.activity, section) });
|
|
383
|
+
}
|
|
384
|
+
for (const { pluginId, fill } of pendingFills) placeFill(pluginId, fill);
|
|
385
|
+
for (const list of state.fills.values()) list.sort(byFillOrder);
|
|
386
|
+
|
|
387
|
+
resolveLeaderConflicts(pendingBindings);
|
|
388
|
+
state.leaderBindings.push(...pendingBindings.map((entry) => entry.binding));
|
|
389
|
+
state.selectionAxes.sort(byDisplayOrder);
|
|
390
|
+
state.minorModes.sort(byDisplayOrder);
|
|
391
|
+
state.activityGroups.sort(byDisplayOrder);
|
|
392
|
+
installed = true;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** Unit-test escape hatch; production installs exactly once. */
|
|
396
|
+
export function resetWebPlugins(): void {
|
|
397
|
+
state = emptyState();
|
|
398
|
+
installed = false;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** What the install had to resolve between plugins; empty for a composition whose plugins never collide. */
|
|
402
|
+
export function webPluginDiagnostics(): readonly InstallDiagnostic[] {
|
|
403
|
+
return state.diagnostics;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** Every installed plugin definition, in install order; the settings page lists them. */
|
|
407
|
+
export function installedWebPlugins(): readonly WebPluginDefinition[] {
|
|
408
|
+
return state.plugins;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export function webTabs(): readonly TabContribution[] {
|
|
412
|
+
return state.tabs;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** The fills placed into one slot, in slot order; empty for a slot nobody declared. */
|
|
416
|
+
export function slotFills(slot: string): readonly ResolvedFill[] {
|
|
417
|
+
return state.fills.get(slot) ?? [];
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export function paletteCommands(): readonly PaletteCommandContribution[] {
|
|
421
|
+
return state.commands;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** Leader Space bindings from every installed plugin, in install order: a later binding on a bound leaf wins. */
|
|
425
|
+
export function pluginLeaderBindings(): readonly LeaderBindingContribution[] {
|
|
426
|
+
return state.leaderBindings;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** Selection-axis declarations from every installed plugin, in display order. */
|
|
430
|
+
export function pluginSelectionAxes(): readonly SelectionAxisContribution[] {
|
|
431
|
+
return state.selectionAxes;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** Minor-mode declarations from every installed plugin, in display order. */
|
|
435
|
+
export function pluginMinorModes(): readonly MinorModeContribution[] {
|
|
436
|
+
return state.minorModes;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** Activity-dock group declarations from every installed plugin, in display order. */
|
|
440
|
+
export function pluginActivityGroups(): readonly ActivityGroupContribution[] {
|
|
441
|
+
return state.activityGroups;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* The renderer a plugin registered for one tool: the exact name claim first,
|
|
446
|
+
* then the first runtime matcher that accepts it, in install order (the one
|
|
447
|
+
* place besides `start` where that order is observable); undefined leaves
|
|
448
|
+
* the host's default card.
|
|
449
|
+
*/
|
|
450
|
+
export function pluginToolRenderer(
|
|
451
|
+
toolName: string,
|
|
452
|
+
statuses: Readonly<Record<string, string>> = {},
|
|
453
|
+
): ToolRendererContribution | undefined {
|
|
454
|
+
return (
|
|
455
|
+
state.toolRenderers.get(toolName) ?? state.toolMatchers.find((renderer) => renderer.matches?.(toolName, statuses))
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Routes one wire frame to the channel owning its type. Returns false when no
|
|
461
|
+
* channel claims it, so the demux can fall through silently the way unknown
|
|
462
|
+
* frames always have.
|
|
463
|
+
*/
|
|
464
|
+
export function dispatchChannelFrame(frame: Record<string, unknown>): boolean {
|
|
465
|
+
if (typeof frame.type !== 'string' || typeof frame.sessionId !== 'string') return false;
|
|
466
|
+
const channel = state.channels.get(frame.type);
|
|
467
|
+
if (channel === undefined) return false;
|
|
468
|
+
const payload = channel.parse(frame.payload);
|
|
469
|
+
if (payload !== null) channel.apply(frame.sessionId, payload);
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/** Session teardown: every channel forgets the session's data. */
|
|
474
|
+
export function dropPluginSessionData(sessionId: string): void {
|
|
475
|
+
for (const channel of state.channels.values()) channel.drop(sessionId);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** Starts every plugin runtime in install order; the disposer runs in reverse. */
|
|
479
|
+
export function startWebPlugins(runtime: WebPluginRuntime): () => void {
|
|
480
|
+
const disposers: Array<() => void> = [];
|
|
481
|
+
for (const plugin of state.plugins) {
|
|
482
|
+
const dispose = plugin.start?.(runtime);
|
|
483
|
+
if (dispose) disposers.push(dispose);
|
|
484
|
+
}
|
|
485
|
+
return () => {
|
|
486
|
+
for (const dispose of disposers.reverse()) dispose();
|
|
487
|
+
};
|
|
488
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { SlotDataFill, SlotDeclaration, TransientTab, WebPluginSlotProps } from '@agimon-ai/doompi-web-contracts';
|
|
2
|
+
import { createElement, type ReactNode } from 'react';
|
|
3
|
+
import { slotFills } from './pluginRegistry.ts';
|
|
4
|
+
import { renderThread } from './threadRenderer.ts';
|
|
5
|
+
import { sendFrame } from './transport.ts';
|
|
6
|
+
|
|
7
|
+
/** The host's hold on the focused session's runtime tabs, bound in by the caller that owns the store. */
|
|
8
|
+
export interface TransientTabActions {
|
|
9
|
+
open: (tab: TransientTab) => void;
|
|
10
|
+
close: (tabId: string) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The one place the props every plugin component receives are built. The
|
|
15
|
+
* same object is handed to every fill a slot renders, so a fill that is
|
|
16
|
+
* itself a slot owner can render its own slots with it.
|
|
17
|
+
*/
|
|
18
|
+
export function pluginSlotProps(
|
|
19
|
+
sessionId: string | null,
|
|
20
|
+
openTab: (tabId: string | null) => void,
|
|
21
|
+
statuses: Readonly<Record<string, string>>,
|
|
22
|
+
tabs: TransientTabActions,
|
|
23
|
+
): WebPluginSlotProps {
|
|
24
|
+
const props: WebPluginSlotProps = {
|
|
25
|
+
sessionId,
|
|
26
|
+
openTab,
|
|
27
|
+
openTransientTab: tabs.open,
|
|
28
|
+
closeTransientTab: tabs.close,
|
|
29
|
+
sendSessionFrame: sendFrame,
|
|
30
|
+
statuses,
|
|
31
|
+
renderThread(threadId): ReactNode {
|
|
32
|
+
return sessionId === null ? null : renderThread(sessionId, threadId);
|
|
33
|
+
},
|
|
34
|
+
renderSlot(slot): ReactNode {
|
|
35
|
+
return slotFills(slot).flatMap((fill) =>
|
|
36
|
+
fill.component === undefined
|
|
37
|
+
? []
|
|
38
|
+
: [createElement(fill.component, { ...props, key: `${fill.pluginId}:${fill.id}` })],
|
|
39
|
+
);
|
|
40
|
+
},
|
|
41
|
+
slotData<Data>(declaration: SlotDeclaration<Data>): readonly SlotDataFill<Data>[] {
|
|
42
|
+
return slotFills(declaration.slot).flatMap((fill) =>
|
|
43
|
+
fill.data === undefined
|
|
44
|
+
? []
|
|
45
|
+
: [{ pluginId: fill.pluginId, id: fill.id, order: fill.order, data: fill.data as Data }],
|
|
46
|
+
);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
return props;
|
|
50
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the keyboard belongs when no overlay holds it.
|
|
3
|
+
*
|
|
4
|
+
* Overlays opened by a shortcut have no trigger element, so the browser has
|
|
5
|
+
* nothing to restore focus to when they close and the caret lands on the
|
|
6
|
+
* body: the next keystroke goes nowhere. The composer registers its input
|
|
7
|
+
* here and every overlay hands focus back to it, which is what makes
|
|
8
|
+
* "ctrl+k, escape, keep typing" work. A module-level ref rather than a store
|
|
9
|
+
* because nothing renders from it.
|
|
10
|
+
*/
|
|
11
|
+
let promptInput: HTMLTextAreaElement | null = null;
|
|
12
|
+
|
|
13
|
+
/** The composer claims the keyboard on mount; the returned disposer releases it. */
|
|
14
|
+
export function registerPromptInput(element: HTMLTextAreaElement | null): () => void {
|
|
15
|
+
promptInput = element;
|
|
16
|
+
return () => {
|
|
17
|
+
if (promptInput === element) promptInput = null;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Returns the caret to the composer, if there is one and it can take it. */
|
|
22
|
+
export function focusPrompt(): void {
|
|
23
|
+
if (promptInput === null || promptInput.disabled) return;
|
|
24
|
+
promptInput.focus();
|
|
25
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AssistantTranscriptItem,
|
|
3
|
+
ToolTranscriptItem,
|
|
4
|
+
TranscriptItem,
|
|
5
|
+
UserTranscriptItem,
|
|
6
|
+
} from '@earendil-works/pi-protocol';
|
|
7
|
+
import type { ToolResultView } from '@agimon-ai/doompi-web-contracts';
|
|
8
|
+
import {
|
|
9
|
+
summariseArgs,
|
|
10
|
+
type AssistantEntry,
|
|
11
|
+
type TimelineEntry,
|
|
12
|
+
type ToolEntry,
|
|
13
|
+
type UserEntry,
|
|
14
|
+
} from './sessionModel.ts';
|
|
15
|
+
|
|
16
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
17
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function userEntry(item: UserTranscriptItem): UserEntry {
|
|
21
|
+
const text = item.content
|
|
22
|
+
.filter((part): part is { type: 'text'; text: string } => part.type === 'text')
|
|
23
|
+
.map((part) => part.text)
|
|
24
|
+
.join('');
|
|
25
|
+
return { kind: 'user', id: item.id, text };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function assistantEntry(item: AssistantTranscriptItem): AssistantEntry {
|
|
29
|
+
let text = '';
|
|
30
|
+
let thinking = '';
|
|
31
|
+
for (const part of item.content) {
|
|
32
|
+
if (part.type === 'text') text += part.text;
|
|
33
|
+
else if (part.type === 'thinking') thinking += part.thinking;
|
|
34
|
+
}
|
|
35
|
+
return { kind: 'assistant', id: item.id, text, thinking, streaming: item.status === 'streaming' };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Rebuilds the tool card's view from the protocol item.
|
|
40
|
+
*
|
|
41
|
+
* `details` is the one field the protocol leaves open on a tool item, which is
|
|
42
|
+
* where a DoomPi tool's own payload travels; handing it back whole is what
|
|
43
|
+
* lets the owning plugin render its call the way it does in the terminal.
|
|
44
|
+
*/
|
|
45
|
+
function toolEntry(item: ToolTranscriptItem): ToolEntry {
|
|
46
|
+
const output = item.content
|
|
47
|
+
.filter((part): part is { type: 'text'; text: string } => part.type === 'text')
|
|
48
|
+
.map((part) => part.text)
|
|
49
|
+
.join('\n');
|
|
50
|
+
const result: ToolResultView | null =
|
|
51
|
+
item.content.length > 0 || item.details !== undefined
|
|
52
|
+
? { content: [...item.content], details: item.details }
|
|
53
|
+
: null;
|
|
54
|
+
return {
|
|
55
|
+
kind: 'tool',
|
|
56
|
+
id: item.id,
|
|
57
|
+
toolCallId: item.toolCallId,
|
|
58
|
+
name: item.toolName,
|
|
59
|
+
args: isRecord(item.input) ? item.input : {},
|
|
60
|
+
argSummary: summariseArgs(item.input),
|
|
61
|
+
result,
|
|
62
|
+
output,
|
|
63
|
+
isError: item.isError,
|
|
64
|
+
running: item.status === 'running',
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Projects the protocol transcript onto the timeline the cockpit renders. */
|
|
69
|
+
export function toTimelineEntries(transcript: readonly TranscriptItem[]): TimelineEntry[] {
|
|
70
|
+
return transcript.map((item) =>
|
|
71
|
+
item.role === 'user' ? userEntry(item) : item.role === 'assistant' ? assistantEntry(item) : toolEntry(item),
|
|
72
|
+
);
|
|
73
|
+
}
|