@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,189 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HISTORY_PAGE_TYPE,
|
|
3
|
+
HUB_RESYNCED_TYPE,
|
|
4
|
+
SESSION_BACKLOG_TYPE,
|
|
5
|
+
SESSION_FRAME_TYPE,
|
|
6
|
+
SESSION_REMOVED_TYPE,
|
|
7
|
+
SESSION_UPSERT_TYPE,
|
|
8
|
+
SESSIONS_SNAPSHOT_TYPE,
|
|
9
|
+
subscribeFrame,
|
|
10
|
+
THREAD_BACKLOG_TYPE,
|
|
11
|
+
THREAD_FRAME_TYPE,
|
|
12
|
+
unsubscribeFrame,
|
|
13
|
+
} from '../../types/hub.ts';
|
|
14
|
+
import { dispatchChannelFrame, dropPluginSessionData } from '../lib/pluginRegistry.ts';
|
|
15
|
+
import { startProtocolRuntime } from './protocolRuntime.ts';
|
|
16
|
+
import { bindTransport, releaseTransport, sendHubFrame } from '../lib/transport.ts';
|
|
17
|
+
import { createSessionSocket, sessionSocketUrl } from '../lib/wsClient.ts';
|
|
18
|
+
import { claimDialogMenu, clearPendingMenu } from '../stores/menuStore.ts';
|
|
19
|
+
import {
|
|
20
|
+
applyHistoryPage,
|
|
21
|
+
applySessionFrame,
|
|
22
|
+
applyThreadFrame,
|
|
23
|
+
dropSessionStore,
|
|
24
|
+
refreshSessionFacts,
|
|
25
|
+
resetSessionStore,
|
|
26
|
+
seedHistoryCursor,
|
|
27
|
+
} from '../stores/sessionStore.ts';
|
|
28
|
+
import { dropThreads, resubscribeThreads, threadStoreKey } from '../stores/threadStore.ts';
|
|
29
|
+
import { dropTransientTabs } from '../stores/transientTabsStore.ts';
|
|
30
|
+
import {
|
|
31
|
+
applySessionBacklog,
|
|
32
|
+
applySessionRemoved,
|
|
33
|
+
applySessionsSnapshot,
|
|
34
|
+
applySessionUpsert,
|
|
35
|
+
markSocketClosed,
|
|
36
|
+
sessionsStore,
|
|
37
|
+
} from '../stores/sessionsStore.ts';
|
|
38
|
+
|
|
39
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
40
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Wires the hub socket to the stores.
|
|
45
|
+
*
|
|
46
|
+
* One socket carries every session. The rail runs on hub summaries; the
|
|
47
|
+
* timeline of the focused session is hydrated by subscribing, which replays
|
|
48
|
+
* the hub's ring and then streams live frames. Session facts are pulled after
|
|
49
|
+
* every replay and again once a run settles, because Pi reports model, stats
|
|
50
|
+
* and commands on request rather than pushing them as events.
|
|
51
|
+
*/
|
|
52
|
+
/** The journal id of the oldest restored entry, which is where paging back resumes. */
|
|
53
|
+
function oldestEntryId(frames: readonly Record<string, unknown>[]): string | null {
|
|
54
|
+
for (const frame of frames) {
|
|
55
|
+
if (frame.type !== 'entry_appended') continue;
|
|
56
|
+
const entry = frame.entry;
|
|
57
|
+
if (isRecord(entry) && typeof entry.id === 'string') return entry.id;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function startSessionRuntime(): () => void {
|
|
63
|
+
// The hub-side subscription this page currently holds; it dies with the
|
|
64
|
+
// socket, which is why the snapshot handler re-subscribes.
|
|
65
|
+
let subscribed: string | null = null;
|
|
66
|
+
// The transcript comes from Pi's protocol; this socket keeps carrying what
|
|
67
|
+
// the protocol has no shape for.
|
|
68
|
+
const protocol = startProtocolRuntime();
|
|
69
|
+
|
|
70
|
+
const syncSubscription = (force = false): void => {
|
|
71
|
+
const { activeId, byId } = sessionsStore.state;
|
|
72
|
+
const target = activeId !== null && activeId in byId ? activeId : null;
|
|
73
|
+
protocol.focus(target);
|
|
74
|
+
if (!force && target === subscribed) return;
|
|
75
|
+
if (subscribed !== null && subscribed !== target) sendHubFrame(unsubscribeFrame(subscribed));
|
|
76
|
+
subscribed = target;
|
|
77
|
+
if (target !== null) sendHubFrame(subscribeFrame(target));
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const socket = createSessionSocket(sessionSocketUrl(window.location), {
|
|
81
|
+
onFrame(frame) {
|
|
82
|
+
switch (frame.type) {
|
|
83
|
+
// Sync rebuilt the bundle this page is running. Nothing in a loaded
|
|
84
|
+
// bundle notices its source moved, so the page picks the new one up.
|
|
85
|
+
case HUB_RESYNCED_TYPE:
|
|
86
|
+
window.location.reload();
|
|
87
|
+
return;
|
|
88
|
+
|
|
89
|
+
case SESSIONS_SNAPSHOT_TYPE:
|
|
90
|
+
applySessionsSnapshot(frame);
|
|
91
|
+
// A snapshot means a fresh socket; any prior subscription died with
|
|
92
|
+
// the old one.
|
|
93
|
+
syncSubscription(true);
|
|
94
|
+
resubscribeThreads();
|
|
95
|
+
return;
|
|
96
|
+
case SESSION_UPSERT_TYPE:
|
|
97
|
+
applySessionUpsert(frame);
|
|
98
|
+
syncSubscription();
|
|
99
|
+
return;
|
|
100
|
+
case SESSION_REMOVED_TYPE: {
|
|
101
|
+
if (typeof frame.sessionId !== 'string') return;
|
|
102
|
+
applySessionRemoved(frame);
|
|
103
|
+
dropSessionStore(frame.sessionId);
|
|
104
|
+
dropPluginSessionData(frame.sessionId);
|
|
105
|
+
dropThreads(frame.sessionId);
|
|
106
|
+
dropTransientTabs(frame.sessionId);
|
|
107
|
+
if (subscribed === frame.sessionId) subscribed = null;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
case SESSION_BACKLOG_TYPE: {
|
|
111
|
+
if (typeof frame.sessionId !== 'string' || !Array.isArray(frame.frames)) return;
|
|
112
|
+
const sessionId = frame.sessionId;
|
|
113
|
+
const frames = frame.frames.filter(isRecord);
|
|
114
|
+
const dropped = typeof frame.dropped === 'number' ? frame.dropped : 0;
|
|
115
|
+
applySessionBacklog(sessionId, frames.length, dropped);
|
|
116
|
+
resetSessionStore(sessionId);
|
|
117
|
+
for (const replayed of frames) applySessionFrame(sessionId, replayed);
|
|
118
|
+
// Where the backlog starts is where paging back has to continue
|
|
119
|
+
// from, so the oldest journal id it carried becomes the cursor.
|
|
120
|
+
seedHistoryCursor(sessionId, oldestEntryId(frames));
|
|
121
|
+
refreshSessionFacts(sessionId);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
case HISTORY_PAGE_TYPE: {
|
|
125
|
+
if (typeof frame.sessionId !== 'string' || !Array.isArray(frame.frames)) return;
|
|
126
|
+
applyHistoryPage(frame.sessionId, frame.frames.filter(isRecord), {
|
|
127
|
+
cursor: typeof frame.cursor === 'string' ? frame.cursor : null,
|
|
128
|
+
hasMore: frame.hasMore === true,
|
|
129
|
+
});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
case SESSION_FRAME_TYPE: {
|
|
133
|
+
if (typeof frame.sessionId !== 'string' || !isRecord(frame.frame)) return;
|
|
134
|
+
applySessionFrame(frame.sessionId, frame.frame);
|
|
135
|
+
// A select the bar asked for becomes the bar's popover; the claim is
|
|
136
|
+
// settled here, at frame time, so no surface renders it twice.
|
|
137
|
+
if (frame.frame.type === 'extension_ui_request' && frame.frame.method === 'select') {
|
|
138
|
+
claimDialogMenu(typeof frame.frame.id === 'string' ? frame.frame.id : '');
|
|
139
|
+
}
|
|
140
|
+
if (frame.frame.type === 'agent_settled') {
|
|
141
|
+
clearPendingMenu();
|
|
142
|
+
refreshSessionFacts(frame.sessionId);
|
|
143
|
+
}
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
// A thread folds like a session of its own, under a key of its own;
|
|
147
|
+
// the backlog replaces what the page had, the same as a session's.
|
|
148
|
+
case THREAD_BACKLOG_TYPE: {
|
|
149
|
+
if (typeof frame.sessionId !== 'string' || typeof frame.threadId !== 'string') return;
|
|
150
|
+
if (!Array.isArray(frame.frames)) return;
|
|
151
|
+
const key = threadStoreKey(frame.sessionId, frame.threadId);
|
|
152
|
+
resetSessionStore(key);
|
|
153
|
+
for (const replayed of frame.frames.filter(isRecord)) applyThreadFrame(key, replayed);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
case THREAD_FRAME_TYPE: {
|
|
157
|
+
if (typeof frame.sessionId !== 'string' || typeof frame.threadId !== 'string') return;
|
|
158
|
+
if (!isRecord(frame.frame)) return;
|
|
159
|
+
applyThreadFrame(threadStoreKey(frame.sessionId, frame.threadId), frame.frame);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
default:
|
|
163
|
+
// Any other frame type may be a plugin channel; unclaimed types are
|
|
164
|
+
// dropped silently the way unknown frames always have been.
|
|
165
|
+
dispatchChannelFrame(frame);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
onOpen() {
|
|
170
|
+
// The snapshot that follows the hub's hello is the real "connected".
|
|
171
|
+
},
|
|
172
|
+
onClose() {
|
|
173
|
+
subscribed = null;
|
|
174
|
+
markSocketClosed();
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
bindTransport((frame) => socket.send(frame));
|
|
179
|
+
// Focus changes come from routing; the runtime follows them with
|
|
180
|
+
// subscribe/unsubscribe so features never touch the wire protocol.
|
|
181
|
+
const subscription = sessionsStore.subscribe(() => syncSubscription());
|
|
182
|
+
|
|
183
|
+
return () => {
|
|
184
|
+
subscription.unsubscribe();
|
|
185
|
+
protocol.stop();
|
|
186
|
+
releaseTransport();
|
|
187
|
+
socket.close();
|
|
188
|
+
};
|
|
189
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** The glyph's intrinsic box, tight to the horns and the paws. */
|
|
2
|
+
const VIEW_BOX = '3 2.5 26 25';
|
|
3
|
+
const ASPECT = 26 / 25;
|
|
4
|
+
|
|
5
|
+
export interface MascotMarkProps {
|
|
6
|
+
/** Rendered height in pixels; the width follows the glyph's aspect ratio. */
|
|
7
|
+
size?: number;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The DoomPi mascot reduced to a wordmark glyph, so the pi it is built from
|
|
13
|
+
* can stand in for the "Pi" in DoomPi and the lockup reads DOOM + mark.
|
|
14
|
+
*
|
|
15
|
+
* The traced mascot carries over a thousand points and thins to hairlines
|
|
16
|
+
* below thirty pixels, which is smaller than every place a wordmark sits, so
|
|
17
|
+
* this is the favicon's geometry without its badge: the same silhouette in
|
|
18
|
+
* four flat shapes that still reads at twenty. Decorative by itself, because
|
|
19
|
+
* the lockup around it is what carries the accessible name.
|
|
20
|
+
*/
|
|
21
|
+
export function MascotMark({ size = 24, className }: MascotMarkProps) {
|
|
22
|
+
return (
|
|
23
|
+
<svg
|
|
24
|
+
viewBox={VIEW_BOX}
|
|
25
|
+
height={size}
|
|
26
|
+
width={size * ASPECT}
|
|
27
|
+
className={className}
|
|
28
|
+
aria-hidden="true"
|
|
29
|
+
focusable="false"
|
|
30
|
+
>
|
|
31
|
+
<g className="fill-doom-yellow">
|
|
32
|
+
<path d="M7 10Q6.2 4.2 9.3 3q1.1 3.3 2.8 6.6Z" />
|
|
33
|
+
<path d="M25 10q.8-5.8-2.3-7-1.1 3.3-2.8 6.6Z" />
|
|
34
|
+
</g>
|
|
35
|
+
<g className="fill-doom-magenta">
|
|
36
|
+
<rect x="3.2" y="8.6" width="25.6" height="7.6" rx="3.4" />
|
|
37
|
+
<rect x="6.9" y="14" width="5.5" height="13" rx="2.5" />
|
|
38
|
+
<rect x="19.6" y="14" width="5.5" height="13" rx="2.5" />
|
|
39
|
+
</g>
|
|
40
|
+
<g className="fill-doom-deep">
|
|
41
|
+
<circle cx="10.9" cy="12.3" r="1.9" />
|
|
42
|
+
<circle cx="21.1" cy="12.3" r="1.9" />
|
|
43
|
+
</g>
|
|
44
|
+
</svg>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { usePluginSlotProps } from '../stores/usePluginSlotProps.ts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Renders every fill placed into one of the host's slots. Host surfaces put
|
|
5
|
+
* this where their layout wants plugin content; a slot nobody filled renders
|
|
6
|
+
* nothing.
|
|
7
|
+
*/
|
|
8
|
+
export function PluginSurface({ slot, sessionId }: { slot: string; sessionId: string | null }) {
|
|
9
|
+
return <>{usePluginSlotProps(sessionId).renderSlot(slot)}</>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Component, type ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
interface ToolRendererBoundaryProps {
|
|
4
|
+
toolName: string;
|
|
5
|
+
/** Renders the item; `failed` is true once the plugin renderer threw, so the host item stands in. */
|
|
6
|
+
children: (failed: boolean) => ReactNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface ToolRendererBoundaryState {
|
|
10
|
+
failed: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Keeps one plugin's tool renderer from taking the timeline down with it: a
|
|
15
|
+
* throw during render is logged once and the call falls back to the host's
|
|
16
|
+
* own item, while every other entry keeps rendering.
|
|
17
|
+
*/
|
|
18
|
+
export class ToolRendererBoundary extends Component<ToolRendererBoundaryProps, ToolRendererBoundaryState> {
|
|
19
|
+
override state: ToolRendererBoundaryState = { failed: false };
|
|
20
|
+
|
|
21
|
+
static getDerivedStateFromError(): ToolRendererBoundaryState {
|
|
22
|
+
return { failed: true };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
override componentDidCatch(error: unknown): void {
|
|
26
|
+
console.error(`web plugin tool renderer for '${this.props.toolName}' threw; showing the host item instead`, error);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
override render(): ReactNode {
|
|
30
|
+
return this.props.children(this.state.failed);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { Button, EmptyState, Kbd, SectionLabel, StatusBadge } from '@agimon-ai/doompi-web-components';
|
|
2
|
+
import { useStore } from '@tanstack/react-store';
|
|
3
|
+
import { PluginSurface } from '../../components/PluginSurface.tsx';
|
|
4
|
+
import { activityGroups } from '../../lib/composition.ts';
|
|
5
|
+
import { activityGroupSlot, HOST_SLOTS, slotFills } from '../../lib/pluginRegistry.ts';
|
|
6
|
+
import { usePluginSlotProps } from '../../stores/usePluginSlotProps.ts';
|
|
7
|
+
import { useActiveSession } from '../../stores/sessionStore.ts';
|
|
8
|
+
import { sessionsStore } from '../../stores/sessionsStore.ts';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Asynchronous work that outlives the turn that started it.
|
|
12
|
+
*
|
|
13
|
+
* Agents, runners, and workflows deliberately live outside the transcript, so
|
|
14
|
+
* they get a surface that does not scroll away. The host owns only the frame:
|
|
15
|
+
* which groups exist, their key chips, and what they render come from the
|
|
16
|
+
* packages that declare them. Each declared group opens the keyed slot
|
|
17
|
+
* activity.<name>; the sections any plugin registers under that name render
|
|
18
|
+
* there, in slot order, and a group nobody fills shows the one-line summary
|
|
19
|
+
* the session's footer publishes.
|
|
20
|
+
*/
|
|
21
|
+
export function ActivityDock({ onClose }: { onClose: () => void }) {
|
|
22
|
+
const activeId = useStore(sessionsStore, (state) => state.activeId);
|
|
23
|
+
const statuses = useActiveSession((state) => state.statuses);
|
|
24
|
+
const widgets = useActiveSession((state) => state.widgets);
|
|
25
|
+
const slotProps = usePluginSlotProps(activeId);
|
|
26
|
+
const groups = activityGroups(statuses, widgets);
|
|
27
|
+
const busy = groups.filter((group) => group.active).length;
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<aside
|
|
31
|
+
data-testid="activity-dock"
|
|
32
|
+
className="flex w-[300px] shrink-0 flex-col overflow-hidden border-l border-doom-border bg-doom-rail"
|
|
33
|
+
>
|
|
34
|
+
<div className="flex h-12 shrink-0 items-center justify-between border-b border-doom-border px-4">
|
|
35
|
+
<div className="flex items-center gap-2">
|
|
36
|
+
<SectionLabel className="text-doom-hi">activity</SectionLabel>
|
|
37
|
+
{busy > 0 ? (
|
|
38
|
+
<StatusBadge tone="running" size="xs" data-testid="activity-busy" className="normal-case">
|
|
39
|
+
{busy} running
|
|
40
|
+
</StatusBadge>
|
|
41
|
+
) : null}
|
|
42
|
+
</div>
|
|
43
|
+
<Button variant="ghost" size="xs" data-testid="activity-close" onClick={onClose} className="text-[10px]">
|
|
44
|
+
hide
|
|
45
|
+
</Button>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div className="min-h-0 flex-1 overflow-y-auto">
|
|
49
|
+
{groups.length === 0 ? (
|
|
50
|
+
<EmptyState
|
|
51
|
+
data-testid="activity-empty"
|
|
52
|
+
className="px-4 py-5"
|
|
53
|
+
title="nothing supervised yet"
|
|
54
|
+
description="a package's group appears here once its extension reports in."
|
|
55
|
+
/>
|
|
56
|
+
) : (
|
|
57
|
+
<div className="flex flex-col">
|
|
58
|
+
{groups.map((group) => (
|
|
59
|
+
<div
|
|
60
|
+
key={group.name}
|
|
61
|
+
data-testid={`activity-${group.name}`}
|
|
62
|
+
data-active={group.active}
|
|
63
|
+
className="flex flex-col gap-2 border-b border-doom-border-soft px-3 py-3"
|
|
64
|
+
>
|
|
65
|
+
<div className="flex items-center gap-2 px-1">
|
|
66
|
+
{/* One marker per head. The glyph says "section", so a group
|
|
67
|
+
never reads as one more row among the items beneath it,
|
|
68
|
+
and it carries the busy colour the status dot used to:
|
|
69
|
+
two marks for one fact is one mark too many. A glyph
|
|
70
|
+
rather than an icon because every group is declared by a
|
|
71
|
+
plugin the host knows nothing about. */}
|
|
72
|
+
<span
|
|
73
|
+
aria-hidden
|
|
74
|
+
data-active={group.active}
|
|
75
|
+
className={`text-[11px] font-bold ${
|
|
76
|
+
group.active ? 'animate-pulse text-doom-yellow' : 'text-doom-faint'
|
|
77
|
+
}`}
|
|
78
|
+
>
|
|
79
|
+
#
|
|
80
|
+
</span>
|
|
81
|
+
<span className="flex-1 text-[11px] font-bold text-doom-text">{group.name}</span>
|
|
82
|
+
{group.tab === undefined ? (
|
|
83
|
+
<Kbd data-testid={`activity-keys-${group.name}`} className="bg-doom-panel">
|
|
84
|
+
{group.keys}
|
|
85
|
+
</Kbd>
|
|
86
|
+
) : (
|
|
87
|
+
<Button
|
|
88
|
+
variant="subtle"
|
|
89
|
+
size="xs"
|
|
90
|
+
data-testid={`activity-open-${group.name}`}
|
|
91
|
+
title={`open the ${group.tab} tab`}
|
|
92
|
+
onClick={() => slotProps.openTab(group.tab ?? null)}
|
|
93
|
+
className="h-auto px-1.5 py-0.5 text-[8px] font-bold text-doom-violet hover:text-doom-magenta"
|
|
94
|
+
>
|
|
95
|
+
{group.keys}
|
|
96
|
+
</Button>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
{slotFills(activityGroupSlot(group.name)).length > 0 ? (
|
|
100
|
+
slotProps.renderSlot(activityGroupSlot(group.name))
|
|
101
|
+
) : (
|
|
102
|
+
<p
|
|
103
|
+
data-testid={`activity-summary-${group.name}`}
|
|
104
|
+
className={`px-1 text-[10px] ${group.active ? 'text-doom-yellow' : 'text-doom-faint'}`}
|
|
105
|
+
>
|
|
106
|
+
{group.summary || 'idle'}
|
|
107
|
+
</p>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
113
|
+
|
|
114
|
+
<PluginSurface slot={HOST_SLOTS.activity} sessionId={activeId} />
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div className="border-t border-doom-border px-4 py-3">
|
|
118
|
+
<span className="text-[9px] leading-relaxed text-doom-faint">
|
|
119
|
+
each group is rendered by the package that owns it; the session's summary line stands in until that package
|
|
120
|
+
publishes per-run detail
|
|
121
|
+
</span>
|
|
122
|
+
</div>
|
|
123
|
+
</aside>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle, Spinner } from '@agimon-ai/doompi-web-components';
|
|
2
|
+
import { abbreviateCwd } from '../../lib/sessionSummary.ts';
|
|
3
|
+
import { useActiveSessionMeta } from '../../stores/sessionsStore.ts';
|
|
4
|
+
|
|
5
|
+
function Detail({ label, value }: { label: string; value: string }) {
|
|
6
|
+
return (
|
|
7
|
+
<div className="flex items-center justify-between gap-3">
|
|
8
|
+
<span className="shrink-0 text-[10px] text-doom-faint">{label}</span>
|
|
9
|
+
<span className="truncate text-[10px] text-doom-text">{value}</span>
|
|
10
|
+
</div>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Shown when the focused session will not take this cockpit.
|
|
16
|
+
*
|
|
17
|
+
* doompi-server holds one client at a time so two views cannot fight over the
|
|
18
|
+
* same agent. The hub keeps retrying underneath, so this explains the wait
|
|
19
|
+
* rather than offering a button that would do the same thing again. It is not
|
|
20
|
+
* dismissible for the same reason: nothing behind it would work. Other
|
|
21
|
+
* sessions' refusals stay in their rail cards; only the focused one earns the
|
|
22
|
+
* overlay.
|
|
23
|
+
*/
|
|
24
|
+
export function RefusedCard() {
|
|
25
|
+
const meta = useActiveSessionMeta();
|
|
26
|
+
if (meta === null || meta.attach !== 'refused') return null;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<Dialog open>
|
|
30
|
+
<DialogContent
|
|
31
|
+
width="lg"
|
|
32
|
+
data-testid="refused-card"
|
|
33
|
+
aria-describedby={undefined}
|
|
34
|
+
className="w-[520px] border-doom-red/50"
|
|
35
|
+
onEscapeKeyDown={(event) => event.preventDefault()}
|
|
36
|
+
onPointerDownOutside={(event) => event.preventDefault()}
|
|
37
|
+
onInteractOutside={(event) => event.preventDefault()}
|
|
38
|
+
>
|
|
39
|
+
<DialogHeader className="border-doom-red/40 bg-doom-red/10">
|
|
40
|
+
<DialogTitle data-testid="refused-title" className="text-doom-red">
|
|
41
|
+
session already attached
|
|
42
|
+
</DialogTitle>
|
|
43
|
+
<Spinner className="text-doom-red/70" />
|
|
44
|
+
</DialogHeader>
|
|
45
|
+
<div className="flex flex-col gap-3 px-4 py-4">
|
|
46
|
+
<p className="text-[12px] leading-relaxed text-doom-text">
|
|
47
|
+
doompi-server holds one client at a time so two views cannot fight over the same agent. Another cockpit is
|
|
48
|
+
attached to this socket right now.
|
|
49
|
+
</p>
|
|
50
|
+
<div className="flex flex-col gap-1.5 rounded border border-doom-border bg-doom-deep px-3 py-2.5">
|
|
51
|
+
<Detail label="reason" value={meta.reason || 'attach_error'} />
|
|
52
|
+
<Detail label="socket" value={meta.summary.socketPath} />
|
|
53
|
+
<Detail label="cwd" value={abbreviateCwd(meta.summary.cwd)} />
|
|
54
|
+
<Detail label="retry" value="automatic, with backoff" />
|
|
55
|
+
</div>
|
|
56
|
+
<p data-testid="refused-hint" className="text-[10px] text-doom-faint">
|
|
57
|
+
close the other cockpit and this one takes over on the next attempt.
|
|
58
|
+
</p>
|
|
59
|
+
</div>
|
|
60
|
+
</DialogContent>
|
|
61
|
+
</Dialog>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Button,
|
|
3
|
+
Dialog,
|
|
4
|
+
DialogBody,
|
|
5
|
+
DialogContent,
|
|
6
|
+
DialogFooter,
|
|
7
|
+
DialogHeader,
|
|
8
|
+
DialogTitle,
|
|
9
|
+
handleOptionListKey,
|
|
10
|
+
Kbd,
|
|
11
|
+
OptionList,
|
|
12
|
+
optionListHint,
|
|
13
|
+
Panel,
|
|
14
|
+
ShieldIcon,
|
|
15
|
+
Textarea,
|
|
16
|
+
} from '@agimon-ai/doompi-web-components';
|
|
17
|
+
import { useStore } from '@tanstack/react-store';
|
|
18
|
+
import { useEffect, useState } from 'react';
|
|
19
|
+
import { focusPrompt } from '../../lib/promptFocus.ts';
|
|
20
|
+
import { menuStore } from '../../stores/menuStore.ts';
|
|
21
|
+
import { answerDialogConfirm, answerDialogValue, cancelDialog, useActiveSession } from '../../stores/sessionStore.ts';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Renders the extension UI sub-protocol as a modal.
|
|
25
|
+
*
|
|
26
|
+
* The agent blocks on this answer, so the surface is modal and always offers a
|
|
27
|
+
* way out: an unanswered request would strand the run. A select the user asked
|
|
28
|
+
* for from the selection bar is that bar's popover instead (the claim is
|
|
29
|
+
* settled when the frame arrives, see stores/menuStore.ts), so this overlay
|
|
30
|
+
* leaves it alone.
|
|
31
|
+
*/
|
|
32
|
+
export function DialogOverlay() {
|
|
33
|
+
const dialog = useActiveSession((state) => state.dialog);
|
|
34
|
+
const claimed = useStore(menuStore, (state) => state.claimed);
|
|
35
|
+
const [value, setValue] = useState('');
|
|
36
|
+
const [cursor, setCursor] = useState(0);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
setValue(dialog?.prefill ?? '');
|
|
40
|
+
setCursor(0);
|
|
41
|
+
}, [dialog?.id, dialog?.prefill]);
|
|
42
|
+
|
|
43
|
+
if (!dialog) return null;
|
|
44
|
+
if (dialog.method === 'select' && claimed !== null && claimed.dialogId === dialog.id) return null;
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<Dialog
|
|
48
|
+
open
|
|
49
|
+
onOpenChange={(next) => {
|
|
50
|
+
if (!next) cancelDialog(dialog.id);
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
<DialogContent
|
|
54
|
+
width="lg"
|
|
55
|
+
data-testid="dialog"
|
|
56
|
+
data-dialog-method={dialog.method}
|
|
57
|
+
aria-describedby={undefined}
|
|
58
|
+
// The dialog content is what the overlay focuses, so the option
|
|
59
|
+
// keyboard is handled here rather than on the list itself.
|
|
60
|
+
onKeyDown={(event) => {
|
|
61
|
+
if (dialog.method !== 'select') return;
|
|
62
|
+
handleOptionListKey(event, {
|
|
63
|
+
options: dialog.options,
|
|
64
|
+
cursor,
|
|
65
|
+
onCursorChange: setCursor,
|
|
66
|
+
onSelect: (option) => answerDialogValue(dialog.id, option),
|
|
67
|
+
});
|
|
68
|
+
}}
|
|
69
|
+
// The agent pushed this dialog; nothing on the page opened it, so the
|
|
70
|
+
// keyboard goes back to the composer rather than to the body.
|
|
71
|
+
onCloseAutoFocus={(event) => {
|
|
72
|
+
event.preventDefault();
|
|
73
|
+
focusPrompt();
|
|
74
|
+
}}
|
|
75
|
+
className="rounded-[10px] border-doom-edge-magenta"
|
|
76
|
+
>
|
|
77
|
+
<DialogHeader className="h-[42px] border-doom-edge-magenta bg-doom-tint-magenta py-0">
|
|
78
|
+
<DialogTitle data-testid="dialog-title" className="flex items-center gap-2 text-doom-magenta">
|
|
79
|
+
<ShieldIcon className="h-[13px] w-[13px] shrink-0" />
|
|
80
|
+
{dialog.title}
|
|
81
|
+
</DialogTitle>
|
|
82
|
+
<span className="text-[9px] text-doom-faint">extension · {dialog.method}</span>
|
|
83
|
+
</DialogHeader>
|
|
84
|
+
|
|
85
|
+
<DialogBody>
|
|
86
|
+
{dialog.message && dialog.method === 'select' ? (
|
|
87
|
+
<Panel data-testid="dialog-command" className="bg-doom-deep px-3 py-2.5">
|
|
88
|
+
<div className="flex gap-2">
|
|
89
|
+
<span className="text-[12px] text-doom-green">$</span>
|
|
90
|
+
<pre
|
|
91
|
+
data-testid="dialog-message"
|
|
92
|
+
className="min-w-0 flex-1 whitespace-pre-wrap break-words text-[12px] leading-relaxed text-doom-hi"
|
|
93
|
+
>
|
|
94
|
+
{dialog.message}
|
|
95
|
+
</pre>
|
|
96
|
+
</div>
|
|
97
|
+
</Panel>
|
|
98
|
+
) : dialog.message ? (
|
|
99
|
+
<p data-testid="dialog-message" className="text-[12px] leading-relaxed text-doom-text">
|
|
100
|
+
{dialog.message}
|
|
101
|
+
</p>
|
|
102
|
+
) : null}
|
|
103
|
+
|
|
104
|
+
{dialog.method === 'select' ? (
|
|
105
|
+
<OptionList
|
|
106
|
+
options={dialog.options}
|
|
107
|
+
cursor={cursor}
|
|
108
|
+
onCursorChange={setCursor}
|
|
109
|
+
testIdPrefix="dialog-option"
|
|
110
|
+
onSelect={(option) => answerDialogValue(dialog.id, option)}
|
|
111
|
+
/>
|
|
112
|
+
) : null}
|
|
113
|
+
|
|
114
|
+
{dialog.method === 'input' || dialog.method === 'editor' ? (
|
|
115
|
+
<Textarea
|
|
116
|
+
data-testid="dialog-input"
|
|
117
|
+
value={value}
|
|
118
|
+
autoFocus
|
|
119
|
+
rows={dialog.method === 'editor' ? 5 : 1}
|
|
120
|
+
placeholder={dialog.placeholder}
|
|
121
|
+
onChange={(event) => setValue(event.target.value)}
|
|
122
|
+
onKeyDown={(event) => {
|
|
123
|
+
if (event.key === 'Enter' && !event.shiftKey && dialog.method === 'input') {
|
|
124
|
+
event.preventDefault();
|
|
125
|
+
answerDialogValue(dialog.id, value);
|
|
126
|
+
}
|
|
127
|
+
}}
|
|
128
|
+
/>
|
|
129
|
+
) : null}
|
|
130
|
+
</DialogBody>
|
|
131
|
+
|
|
132
|
+
<DialogFooter variant="bar" className="h-[34px]">
|
|
133
|
+
<span data-testid="dialog-hints" className="flex items-center gap-1.5 text-[10px] text-doom-faint">
|
|
134
|
+
{dialog.method === 'select' ? optionListHint(dialog.options.length) : 'enter confirm'} · <Kbd>esc</Kbd>{' '}
|
|
135
|
+
cancels and tells the agent
|
|
136
|
+
</span>
|
|
137
|
+
<div className="flex items-center gap-2">
|
|
138
|
+
<Button variant="outline" size="sm" data-testid="dialog-cancel" onClick={() => cancelDialog(dialog.id)}>
|
|
139
|
+
cancel
|
|
140
|
+
</Button>
|
|
141
|
+
{dialog.method === 'confirm' ? (
|
|
142
|
+
<>
|
|
143
|
+
<Button
|
|
144
|
+
variant="danger-outline"
|
|
145
|
+
size="sm"
|
|
146
|
+
data-testid="dialog-deny"
|
|
147
|
+
onClick={() => answerDialogConfirm(dialog.id, false)}
|
|
148
|
+
>
|
|
149
|
+
no
|
|
150
|
+
</Button>
|
|
151
|
+
<Button
|
|
152
|
+
variant="success"
|
|
153
|
+
size="sm"
|
|
154
|
+
data-testid="dialog-confirm"
|
|
155
|
+
onClick={() => answerDialogConfirm(dialog.id, true)}
|
|
156
|
+
>
|
|
157
|
+
yes
|
|
158
|
+
</Button>
|
|
159
|
+
</>
|
|
160
|
+
) : null}
|
|
161
|
+
{dialog.method === 'input' || dialog.method === 'editor' ? (
|
|
162
|
+
<Button
|
|
163
|
+
variant="primary"
|
|
164
|
+
size="sm"
|
|
165
|
+
data-testid="dialog-submit"
|
|
166
|
+
onClick={() => answerDialogValue(dialog.id, value)}
|
|
167
|
+
>
|
|
168
|
+
submit
|
|
169
|
+
</Button>
|
|
170
|
+
) : null}
|
|
171
|
+
</div>
|
|
172
|
+
</DialogFooter>
|
|
173
|
+
</DialogContent>
|
|
174
|
+
</Dialog>
|
|
175
|
+
);
|
|
176
|
+
}
|