@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,673 @@
|
|
|
1
|
+
import type { ToolResultView } from '@agimon-ai/doompi-web-contracts';
|
|
2
|
+
import { DIALOG_ANSWERED_TYPE, MINOR_MODE_ENTRY_TYPE, type MinorModeProjection } from '../../types/hub.ts';
|
|
3
|
+
|
|
4
|
+
export type EntryKind = 'user' | 'assistant' | 'tool' | 'notice';
|
|
5
|
+
|
|
6
|
+
export interface UserEntry {
|
|
7
|
+
kind: 'user';
|
|
8
|
+
id: string;
|
|
9
|
+
text: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface AssistantEntry {
|
|
13
|
+
kind: 'assistant';
|
|
14
|
+
id: string;
|
|
15
|
+
text: string;
|
|
16
|
+
thinking: string;
|
|
17
|
+
streaming: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ToolEntry {
|
|
21
|
+
kind: 'tool';
|
|
22
|
+
id: string;
|
|
23
|
+
toolCallId: string;
|
|
24
|
+
name: string;
|
|
25
|
+
/** The call arguments as the frame carried them, for a plugin's own renderer. */
|
|
26
|
+
args: Record<string, unknown>;
|
|
27
|
+
argSummary: string;
|
|
28
|
+
/** The newest result, partial while running; null before the tool produced any. */
|
|
29
|
+
result: ToolResultView | null;
|
|
30
|
+
output: string;
|
|
31
|
+
isError: boolean;
|
|
32
|
+
running: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface NoticeEntry {
|
|
36
|
+
kind: 'notice';
|
|
37
|
+
id: string;
|
|
38
|
+
text: string;
|
|
39
|
+
tone: 'info' | 'error';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SettledEntry {
|
|
43
|
+
kind: 'settled';
|
|
44
|
+
id: string;
|
|
45
|
+
tools: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface QueuedEntry {
|
|
49
|
+
kind: 'queued';
|
|
50
|
+
id: string;
|
|
51
|
+
text: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type TimelineEntry = UserEntry | AssistantEntry | ToolEntry | NoticeEntry | SettledEntry | QueuedEntry;
|
|
55
|
+
|
|
56
|
+
export interface SessionStats {
|
|
57
|
+
cost: number;
|
|
58
|
+
totalTokens: number;
|
|
59
|
+
contextPercent: number | null;
|
|
60
|
+
contextTokens: number | null;
|
|
61
|
+
contextWindow: number | null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface AgentInfo {
|
|
65
|
+
model: string;
|
|
66
|
+
/** The provider Pi resolved the model under; set_model needs both halves. */
|
|
67
|
+
provider: string;
|
|
68
|
+
thinkingLevel: string;
|
|
69
|
+
sessionId: string;
|
|
70
|
+
sessionName: string;
|
|
71
|
+
messageCount: number;
|
|
72
|
+
isStreaming: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface CommandInfo {
|
|
76
|
+
name: string;
|
|
77
|
+
description: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** One model the session can switch to, as get_available_models lists it. */
|
|
81
|
+
export interface ModelChoice {
|
|
82
|
+
provider: string;
|
|
83
|
+
id: string;
|
|
84
|
+
name: string;
|
|
85
|
+
reasoning: boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type DialogMethod = 'select' | 'confirm' | 'input' | 'editor';
|
|
89
|
+
|
|
90
|
+
export interface DialogRequest {
|
|
91
|
+
id: string;
|
|
92
|
+
method: DialogMethod;
|
|
93
|
+
title: string;
|
|
94
|
+
message: string;
|
|
95
|
+
options: string[];
|
|
96
|
+
placeholder: string;
|
|
97
|
+
prefill: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface SessionState {
|
|
101
|
+
entries: TimelineEntry[];
|
|
102
|
+
/**
|
|
103
|
+
* Footer status entries keyed as the publishing extension named them, raw so
|
|
104
|
+
* the colour survives. A key that is absent means the package is not in this
|
|
105
|
+
* composition; a key holding an empty string means it is loaded but off.
|
|
106
|
+
*/
|
|
107
|
+
statuses: Record<string, string>;
|
|
108
|
+
/** Widget keys the session has announced, the only signal some modes give. */
|
|
109
|
+
widgets: string[];
|
|
110
|
+
streaming: boolean;
|
|
111
|
+
settled: boolean;
|
|
112
|
+
stats: SessionStats | null;
|
|
113
|
+
agent: AgentInfo | null;
|
|
114
|
+
commands: CommandInfo[];
|
|
115
|
+
/** Models the session offered, empty until the picker asks. */
|
|
116
|
+
models: ModelChoice[];
|
|
117
|
+
/** Thinking levels the current model accepts, empty until the picker asks. */
|
|
118
|
+
thinkingLevels: string[];
|
|
119
|
+
dialog: DialogRequest | null;
|
|
120
|
+
/** The runtime's minor-mode catalog as last journaled, or null before it reports. */
|
|
121
|
+
minorModes: MinorModeProjection | null;
|
|
122
|
+
/** Tool calls seen since the current run began, reported when it settles. */
|
|
123
|
+
toolsThisRun: number;
|
|
124
|
+
/**
|
|
125
|
+
* Journal entry ids already folded into the timeline. The hub re-reads the
|
|
126
|
+
* journal on every attach, so a reconnect replays entries this page already
|
|
127
|
+
* has; folding by id is what keeps a restored transcript from doubling.
|
|
128
|
+
*/
|
|
129
|
+
restoredIds: string[];
|
|
130
|
+
/**
|
|
131
|
+
* Prompts this page put on screen before the journal recorded them, newest
|
|
132
|
+
* last. The cockpit shows your message the moment you send it, but the
|
|
133
|
+
* journal is what carries every message, including the ones an extension
|
|
134
|
+
* sends on your behalf. Matching a journal message against this list is what
|
|
135
|
+
* lets both appear exactly once.
|
|
136
|
+
*/
|
|
137
|
+
pendingUserEntries: { id: string; text: string }[];
|
|
138
|
+
nextId: number;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export const initialSessionState: SessionState = {
|
|
142
|
+
entries: [],
|
|
143
|
+
statuses: {},
|
|
144
|
+
widgets: [],
|
|
145
|
+
streaming: false,
|
|
146
|
+
settled: false,
|
|
147
|
+
stats: null,
|
|
148
|
+
agent: null,
|
|
149
|
+
commands: [],
|
|
150
|
+
models: [],
|
|
151
|
+
thinkingLevels: [],
|
|
152
|
+
dialog: null,
|
|
153
|
+
minorModes: null,
|
|
154
|
+
toolsThisRun: 0,
|
|
155
|
+
restoredIds: [],
|
|
156
|
+
pendingUserEntries: [],
|
|
157
|
+
nextId: 1,
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
type Frame = Record<string, unknown>;
|
|
161
|
+
|
|
162
|
+
function isRecord(value: unknown): value is Frame {
|
|
163
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function asString(value: unknown, fallback = ''): string {
|
|
167
|
+
return typeof value === 'string' ? value : fallback;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function asNumber(value: unknown): number | null {
|
|
171
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Joins the text blocks of a Pi content array, ignoring non-text parts. */
|
|
175
|
+
export function textFromContent(content: unknown): string {
|
|
176
|
+
if (typeof content === 'string') return content;
|
|
177
|
+
if (!Array.isArray(content)) return '';
|
|
178
|
+
return content
|
|
179
|
+
.map((block) => (isRecord(block) && block.type === 'text' ? asString(block.text) : ''))
|
|
180
|
+
.filter(Boolean)
|
|
181
|
+
.join('');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** A one-line description of a tool call, good enough for a card header. */
|
|
185
|
+
export function summariseArgs(args: unknown): string {
|
|
186
|
+
if (!isRecord(args)) return '';
|
|
187
|
+
for (const key of ['command', 'file_path', 'filePath', 'path', 'pattern', 'query', 'url']) {
|
|
188
|
+
const value = args[key];
|
|
189
|
+
if (typeof value === 'string' && value.length > 0) return value;
|
|
190
|
+
}
|
|
191
|
+
const keys = Object.keys(args);
|
|
192
|
+
return keys.length > 0 ? keys.join(', ') : '';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function withEntry(state: SessionState, entry: TimelineEntry): SessionState {
|
|
196
|
+
return { ...state, entries: [...state.entries, entry], nextId: state.nextId + 1 };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function replaceEntry(state: SessionState, id: string, next: TimelineEntry): SessionState {
|
|
200
|
+
return { ...state, entries: state.entries.map((entry) => (entry.id === id ? next : entry)) };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function openAssistant(state: SessionState): { state: SessionState; entry: AssistantEntry } {
|
|
204
|
+
const last = state.entries[state.entries.length - 1];
|
|
205
|
+
if (last && last.kind === 'assistant' && last.streaming) return { state, entry: last };
|
|
206
|
+
const entry: AssistantEntry = {
|
|
207
|
+
kind: 'assistant',
|
|
208
|
+
id: `a${state.nextId}`,
|
|
209
|
+
text: '',
|
|
210
|
+
thinking: '',
|
|
211
|
+
streaming: true,
|
|
212
|
+
};
|
|
213
|
+
return { state: withEntry(state, entry), entry };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function applyAssistantDelta(state: SessionState, event: Frame): SessionState {
|
|
217
|
+
const type = asString(event.type);
|
|
218
|
+
if (type !== 'text_delta' && type !== 'thinking_delta' && type !== 'text_start' && type !== 'thinking_start') {
|
|
219
|
+
return state;
|
|
220
|
+
}
|
|
221
|
+
const opened = openAssistant(state);
|
|
222
|
+
const delta = asString(event.delta);
|
|
223
|
+
if (!delta) return opened.state;
|
|
224
|
+
const next: AssistantEntry =
|
|
225
|
+
type === 'thinking_delta'
|
|
226
|
+
? { ...opened.entry, thinking: opened.entry.thinking + delta }
|
|
227
|
+
: { ...opened.entry, text: opened.entry.text + delta };
|
|
228
|
+
return replaceEntry(opened.state, opened.entry.id, next);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function closeAssistant(state: SessionState, message: unknown): SessionState {
|
|
232
|
+
const index = state.entries.findLastIndex((entry) => entry.kind === 'assistant' && entry.streaming);
|
|
233
|
+
if (index === -1) return state;
|
|
234
|
+
const current = state.entries[index] as AssistantEntry;
|
|
235
|
+
const finalText = isRecord(message) ? textFromContent(message.content) : '';
|
|
236
|
+
const next: AssistantEntry = {
|
|
237
|
+
...current,
|
|
238
|
+
text: finalText.length > current.text.length ? finalText : current.text,
|
|
239
|
+
streaming: false,
|
|
240
|
+
};
|
|
241
|
+
const entries = [...state.entries];
|
|
242
|
+
entries[index] = next;
|
|
243
|
+
return { ...state, entries };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function applyToolStart(state: SessionState, frame: Frame): SessionState {
|
|
247
|
+
const entry: ToolEntry = {
|
|
248
|
+
kind: 'tool',
|
|
249
|
+
id: `t${state.nextId}`,
|
|
250
|
+
toolCallId: asString(frame.toolCallId),
|
|
251
|
+
name: asString(frame.toolName, 'tool'),
|
|
252
|
+
args: isRecord(frame.args) ? frame.args : {},
|
|
253
|
+
argSummary: summariseArgs(frame.args),
|
|
254
|
+
result: null,
|
|
255
|
+
output: '',
|
|
256
|
+
isError: false,
|
|
257
|
+
running: true,
|
|
258
|
+
};
|
|
259
|
+
const opened = withEntry(closeAssistant(state, undefined), entry);
|
|
260
|
+
return { ...opened, toolsThisRun: opened.toolsThisRun + 1 };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* The result a tool frame carries, kept whole: the text blocks feed the
|
|
265
|
+
* default card, and the content plus details reach the owning plugin's
|
|
266
|
+
* renderer untouched, the way Pi hands them to a TUI renderResult.
|
|
267
|
+
*/
|
|
268
|
+
function toolResult(raw: unknown): ToolResultView | null {
|
|
269
|
+
if (!isRecord(raw)) return null;
|
|
270
|
+
return { content: Array.isArray(raw.content) ? raw.content : [], details: raw.details };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function updateTool(state: SessionState, toolCallId: string, patch: Partial<ToolEntry>): SessionState {
|
|
274
|
+
const index = state.entries.findLastIndex((entry) => entry.kind === 'tool' && entry.toolCallId === toolCallId);
|
|
275
|
+
if (index === -1) return state;
|
|
276
|
+
const entries = [...state.entries];
|
|
277
|
+
entries[index] = { ...(entries[index] as ToolEntry), ...patch };
|
|
278
|
+
return { ...state, entries };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Commands the picker issues; a refusal must show, or the chip just stays put. */
|
|
282
|
+
const PICKER_COMMANDS = new Set(['set_model', 'set_thinking_level']);
|
|
283
|
+
|
|
284
|
+
function modelChoice(value: unknown): ModelChoice | undefined {
|
|
285
|
+
if (!isRecord(value) || typeof value.id !== 'string' || typeof value.provider !== 'string') return undefined;
|
|
286
|
+
return {
|
|
287
|
+
provider: value.provider,
|
|
288
|
+
id: value.id,
|
|
289
|
+
name: asString(value.name, value.id),
|
|
290
|
+
reasoning: value.reasoning === true,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function applyResponse(state: SessionState, frame: Frame): SessionState {
|
|
295
|
+
const command = asString(frame.command);
|
|
296
|
+
if (frame.success === false && PICKER_COMMANDS.has(command)) {
|
|
297
|
+
return withEntry(state, {
|
|
298
|
+
kind: 'notice',
|
|
299
|
+
id: `n${state.nextId}`,
|
|
300
|
+
text: asString(frame.error, `The agent refused ${command}.`),
|
|
301
|
+
tone: 'error',
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
const data = frame.data;
|
|
305
|
+
if (!isRecord(data)) return state;
|
|
306
|
+
|
|
307
|
+
if (command === 'get_state') {
|
|
308
|
+
const model = isRecord(data.model) ? asString(data.model.id ?? data.model.name, 'unknown') : 'unknown';
|
|
309
|
+
const provider = isRecord(data.model) ? asString(data.model.provider) : '';
|
|
310
|
+
return {
|
|
311
|
+
...state,
|
|
312
|
+
agent: {
|
|
313
|
+
model,
|
|
314
|
+
provider,
|
|
315
|
+
thinkingLevel: asString(data.thinkingLevel, 'unknown'),
|
|
316
|
+
sessionId: asString(data.sessionId),
|
|
317
|
+
sessionName: asString(data.sessionName),
|
|
318
|
+
messageCount: asNumber(data.messageCount) ?? 0,
|
|
319
|
+
isStreaming: data.isStreaming === true,
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (command === 'get_available_models') {
|
|
325
|
+
const models = Array.isArray(data.models) ? data.models.map(modelChoice).filter((m) => m !== undefined) : [];
|
|
326
|
+
return { ...state, models };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (command === 'get_available_thinking_levels') {
|
|
330
|
+
const levels = Array.isArray(data.levels) ? data.levels.filter((level) => typeof level === 'string') : [];
|
|
331
|
+
return { ...state, thinkingLevels: levels };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Pi echoes the model it switched to; get_state follows for the rest.
|
|
335
|
+
if (command === 'set_model' && state.agent) {
|
|
336
|
+
const chosen = modelChoice(data);
|
|
337
|
+
return chosen ? { ...state, agent: { ...state.agent, model: chosen.id, provider: chosen.provider } } : state;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (command === 'get_session_stats') {
|
|
341
|
+
const tokens = isRecord(data.tokens) ? asNumber(data.tokens.total) : null;
|
|
342
|
+
const usage = isRecord(data.contextUsage) ? data.contextUsage : undefined;
|
|
343
|
+
return {
|
|
344
|
+
...state,
|
|
345
|
+
stats: {
|
|
346
|
+
cost: asNumber(data.cost) ?? 0,
|
|
347
|
+
totalTokens: tokens ?? 0,
|
|
348
|
+
contextPercent: usage ? asNumber(usage.percent) : null,
|
|
349
|
+
contextTokens: usage ? asNumber(usage.tokens) : null,
|
|
350
|
+
contextWindow: usage ? asNumber(usage.contextWindow) : null,
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (command === 'get_commands') {
|
|
356
|
+
const raw = Array.isArray(data.commands) ? data.commands : [];
|
|
357
|
+
const commands = raw
|
|
358
|
+
.filter(isRecord)
|
|
359
|
+
.map((entry) => ({ name: asString(entry.name), description: asString(entry.description) }))
|
|
360
|
+
.filter((entry) => entry.name.length > 0);
|
|
361
|
+
return { ...state, commands };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return state;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function applyStatus(state: SessionState, frame: Frame): SessionState {
|
|
368
|
+
const key = asString(frame.statusKey);
|
|
369
|
+
if (!key) return state;
|
|
370
|
+
const text = typeof frame.statusText === 'string' ? frame.statusText : '';
|
|
371
|
+
if (state.statuses[key] === text) return state;
|
|
372
|
+
return { ...state, statuses: { ...state.statuses, [key]: text } };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function applyWidget(state: SessionState, frame: Frame): SessionState {
|
|
376
|
+
const key = asString(frame.widgetKey);
|
|
377
|
+
if (!key || state.widgets.includes(key)) return state;
|
|
378
|
+
return { ...state, widgets: [...state.widgets, key] };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function applyDialog(state: SessionState, frame: Frame): SessionState {
|
|
382
|
+
const method = asString(frame.method);
|
|
383
|
+
if (method === 'setStatus') return applyStatus(state, frame);
|
|
384
|
+
if (method === 'setWidget') return applyWidget(state, frame);
|
|
385
|
+
if (method !== 'select' && method !== 'confirm' && method !== 'input' && method !== 'editor') return state;
|
|
386
|
+
const options = Array.isArray(frame.options) ? frame.options.map((option) => asString(option)).filter(Boolean) : [];
|
|
387
|
+
return {
|
|
388
|
+
...state,
|
|
389
|
+
dialog: {
|
|
390
|
+
id: asString(frame.id),
|
|
391
|
+
method,
|
|
392
|
+
title: asString(frame.title, 'The agent needs an answer'),
|
|
393
|
+
message: asString(frame.message),
|
|
394
|
+
options,
|
|
395
|
+
placeholder: asString(frame.placeholder),
|
|
396
|
+
prefill: asString(frame.prefill),
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** The thinking blocks of a journalled assistant message, joined as one passage. */
|
|
402
|
+
function thinkingFromContent(content: readonly Frame[]): string {
|
|
403
|
+
return content
|
|
404
|
+
.map((block) => (block.type === 'thinking' ? asString(block.thinking) : ''))
|
|
405
|
+
.filter(Boolean)
|
|
406
|
+
.join('');
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** Remembers a prompt this page rendered, so the journal's copy of it folds into the same entry. */
|
|
410
|
+
function withPendingUser(state: SessionState, id: string, text: string): SessionState {
|
|
411
|
+
return { ...state, pendingUserEntries: [...state.pendingUserEntries, { id, text }] };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Folds a journalled user message into the entry this page already rendered
|
|
416
|
+
* for it, or returns undefined when no such entry exists.
|
|
417
|
+
*
|
|
418
|
+
* A message the cockpit sent is on screen before it reaches the journal, and a
|
|
419
|
+
* queued one is on screen as queued until the run picks it up. Both are the
|
|
420
|
+
* same message the journal later reports, so it settles the entry that stands
|
|
421
|
+
* rather than adding a second copy of the text.
|
|
422
|
+
*/
|
|
423
|
+
function reconcilePendingUser(state: SessionState, text: string): SessionState | undefined {
|
|
424
|
+
const index = state.pendingUserEntries.findIndex((pending) => pending.text === text);
|
|
425
|
+
if (index === -1) return undefined;
|
|
426
|
+
const { id } = state.pendingUserEntries[index] as { id: string };
|
|
427
|
+
const pendingUserEntries = state.pendingUserEntries.filter((_, at) => at !== index);
|
|
428
|
+
const entries = state.entries.map((entry) =>
|
|
429
|
+
entry.id === id && entry.kind === 'queued' ? { kind: 'user' as const, id: entry.id, text: entry.text } : entry,
|
|
430
|
+
);
|
|
431
|
+
return { ...state, entries, pendingUserEntries };
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Folds one journalled message into the timeline.
|
|
436
|
+
*
|
|
437
|
+
* The journal is the session's own record, so its shape is not the live frame
|
|
438
|
+
* shape: a tool call is a block inside an assistant message (`id`,
|
|
439
|
+
* `arguments`) rather than a tool_execution_start frame, and its result is a
|
|
440
|
+
* message of its own. Mapping them here is what lets a transcript written
|
|
441
|
+
* before this page existed read exactly like one it watched arrive.
|
|
442
|
+
*/
|
|
443
|
+
function applyJournalMessage(state: SessionState, message: Frame): SessionState {
|
|
444
|
+
const role = asString(message.role);
|
|
445
|
+
const content = Array.isArray(message.content) ? message.content.filter(isRecord) : [];
|
|
446
|
+
|
|
447
|
+
if (role === 'user') {
|
|
448
|
+
const text = textFromContent(content);
|
|
449
|
+
if (!text) return state;
|
|
450
|
+
return reconcilePendingUser(state, text) ?? withEntry(state, { kind: 'user', id: `u${state.nextId}`, text });
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (role === 'assistant') {
|
|
454
|
+
const text = textFromContent(content);
|
|
455
|
+
const thinking = thinkingFromContent(content);
|
|
456
|
+
let next =
|
|
457
|
+
text || thinking
|
|
458
|
+
? withEntry(state, { kind: 'assistant', id: `a${state.nextId}`, text, thinking, streaming: false })
|
|
459
|
+
: state;
|
|
460
|
+
for (const block of content) {
|
|
461
|
+
if (block.type !== 'toolCall') continue;
|
|
462
|
+
// The result is a later entry, so the card starts as running and the
|
|
463
|
+
// result that follows settles it, exactly as a live run does.
|
|
464
|
+
next = withEntry(next, {
|
|
465
|
+
kind: 'tool',
|
|
466
|
+
id: `t${next.nextId}`,
|
|
467
|
+
toolCallId: asString(block.id),
|
|
468
|
+
name: asString(block.name, 'tool'),
|
|
469
|
+
args: isRecord(block.arguments) ? block.arguments : {},
|
|
470
|
+
argSummary: summariseArgs(block.arguments),
|
|
471
|
+
result: null,
|
|
472
|
+
output: '',
|
|
473
|
+
isError: false,
|
|
474
|
+
running: true,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
return next;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (role === 'toolResult') {
|
|
481
|
+
return updateTool(state, asString(message.toolCallId), {
|
|
482
|
+
result: { content, details: message.details },
|
|
483
|
+
output: textFromContent(content),
|
|
484
|
+
isError: message.isError === true,
|
|
485
|
+
running: false,
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return state;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Folds one journal entry, once. Entries without an id are the runtime's own
|
|
494
|
+
* bookkeeping and are left alone; a message already folded is skipped, which
|
|
495
|
+
* is what makes a re-attach idempotent.
|
|
496
|
+
*/
|
|
497
|
+
function applyJournalEntry(state: SessionState, entry: Frame): SessionState {
|
|
498
|
+
if (entry.type !== 'message' || !isRecord(entry.message)) return state;
|
|
499
|
+
const id = asString(entry.id);
|
|
500
|
+
if (id === '' || state.restoredIds.includes(id)) return state;
|
|
501
|
+
const next = applyJournalMessage(state, entry.message);
|
|
502
|
+
return { ...next, restoredIds: [...next.restoredIds, id] };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Folds an older window of transcript above what the page already holds.
|
|
507
|
+
*
|
|
508
|
+
* The reducer is append-only by design: it builds the timeline as the session
|
|
509
|
+
* produces it. Paging backwards is the one case that runs the other way, so
|
|
510
|
+
* the window is reduced on its own and the result is prepended, rather than
|
|
511
|
+
* teaching every case in the reducer about a second direction.
|
|
512
|
+
*
|
|
513
|
+
* Entry ids are positional, so a window reduced from scratch would collide
|
|
514
|
+
* with the live entries. Prefixing by page keeps every key unique and stable
|
|
515
|
+
* across re-renders, which is what a virtualised list needs to keep its
|
|
516
|
+
* measurements.
|
|
517
|
+
*/
|
|
518
|
+
export function prependHistory(state: SessionState, frames: readonly Frame[], page: number): SessionState {
|
|
519
|
+
// Wrapped: reduce would otherwise pass the index as the options argument.
|
|
520
|
+
const older = frames.reduce((carried, frame) => reduceSession(carried, frame), initialSessionState);
|
|
521
|
+
const known = new Set(state.restoredIds);
|
|
522
|
+
const restored = older.restoredIds.filter((id) => !known.has(id));
|
|
523
|
+
if (restored.length === 0) return state;
|
|
524
|
+
const prefix = `h${page}-`;
|
|
525
|
+
const entries = older.entries
|
|
526
|
+
.filter((entry) => entry.kind !== 'notice')
|
|
527
|
+
.map((entry) => ({ ...entry, id: `${prefix}${entry.id}` }) as TimelineEntry);
|
|
528
|
+
return {
|
|
529
|
+
...state,
|
|
530
|
+
entries: [...entries, ...state.entries],
|
|
531
|
+
restoredIds: [...restored, ...state.restoredIds],
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Folds one agent frame into the view model.
|
|
537
|
+
*
|
|
538
|
+
* Pure and total: an unrecognised frame returns the state unchanged, so a Pi
|
|
539
|
+
* protocol addition cannot break the cockpit, it just is not rendered yet.
|
|
540
|
+
*/
|
|
541
|
+
/** Frames whose only product is a timeline entry the protocol now publishes. */
|
|
542
|
+
const TRANSCRIPT_FRAMES = new Set([
|
|
543
|
+
'message_update',
|
|
544
|
+
'message_end',
|
|
545
|
+
'tool_execution_start',
|
|
546
|
+
'tool_execution_update',
|
|
547
|
+
'tool_execution_end',
|
|
548
|
+
]);
|
|
549
|
+
|
|
550
|
+
export interface ReduceSessionOptions {
|
|
551
|
+
/**
|
|
552
|
+
* The protocol supplies the transcript, so this reducer must not build one.
|
|
553
|
+
*
|
|
554
|
+
* Both would otherwise write the same timeline from different sources: the
|
|
555
|
+
* snapshot replaces it wholesale while these frames append to it, and the
|
|
556
|
+
* result flickers between the two. The frames still reach here for
|
|
557
|
+
* everything else they carry, which is why this is a filter rather than a
|
|
558
|
+
* missing subscription.
|
|
559
|
+
*/
|
|
560
|
+
transcriptFromProtocol?: boolean;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
export function reduceSession(state: SessionState, frame: Frame, options: ReduceSessionOptions = {}): SessionState {
|
|
564
|
+
const type = asString(frame.type);
|
|
565
|
+
if (options.transcriptFromProtocol && TRANSCRIPT_FRAMES.has(type)) {
|
|
566
|
+
return type === 'tool_execution_start' ? { ...state, toolsThisRun: state.toolsThisRun + 1 } : state;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
switch (type) {
|
|
570
|
+
case 'replay':
|
|
571
|
+
return isRecord(frame.frame) ? reduceSession(state, frame.frame, options) : state;
|
|
572
|
+
|
|
573
|
+
case 'agent_start':
|
|
574
|
+
return { ...state, streaming: true, settled: false, toolsThisRun: 0 };
|
|
575
|
+
|
|
576
|
+
case 'message_update':
|
|
577
|
+
return isRecord(frame.assistantMessageEvent) ? applyAssistantDelta(state, frame.assistantMessageEvent) : state;
|
|
578
|
+
|
|
579
|
+
case 'message_end':
|
|
580
|
+
return closeAssistant(state, frame.message);
|
|
581
|
+
|
|
582
|
+
case 'tool_execution_start':
|
|
583
|
+
return applyToolStart(state, frame);
|
|
584
|
+
|
|
585
|
+
case 'tool_execution_update':
|
|
586
|
+
return updateTool(state, asString(frame.toolCallId), {
|
|
587
|
+
result: toolResult(frame.partialResult),
|
|
588
|
+
output: textFromContent(isRecord(frame.partialResult) ? frame.partialResult.content : undefined),
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
case 'tool_execution_end':
|
|
592
|
+
return updateTool(state, asString(frame.toolCallId), {
|
|
593
|
+
result: toolResult(frame.result),
|
|
594
|
+
output: textFromContent(isRecord(frame.result) ? frame.result.content : undefined),
|
|
595
|
+
isError: frame.isError === true,
|
|
596
|
+
running: false,
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
case 'agent_settled': {
|
|
600
|
+
const closed = options.transcriptFromProtocol ? state : closeAssistant(state, undefined);
|
|
601
|
+
const marked = withEntry(closed, { kind: 'settled', id: `s${closed.nextId}`, tools: closed.toolsThisRun });
|
|
602
|
+
return { ...marked, streaming: false, settled: true };
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
case 'extension_ui_request': {
|
|
606
|
+
// Agent notifications carry the outcome a TUI user would see (a mode
|
|
607
|
+
// switch pending a relaunch, a refused action); dropping them reads as
|
|
608
|
+
// the agent doing nothing.
|
|
609
|
+
if (frame.method === 'notify') {
|
|
610
|
+
const text = asString(frame.message, '');
|
|
611
|
+
if (text === '') return state;
|
|
612
|
+
return withEntry(state, {
|
|
613
|
+
kind: 'notice',
|
|
614
|
+
id: `n${state.nextId}`,
|
|
615
|
+
text,
|
|
616
|
+
tone: frame.notifyType === 'error' ? 'error' : 'info',
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
return applyDialog(state, frame);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Hub-synthesized when any tab answers a dialog: closes it on other live
|
|
623
|
+
// tabs, and on backlog replays keeps an answered request from reopening.
|
|
624
|
+
case DIALOG_ANSWERED_TYPE:
|
|
625
|
+
return state.dialog && state.dialog.id === asString(frame.id) ? { ...state, dialog: null } : state;
|
|
626
|
+
|
|
627
|
+
// The runtime journals its minor-mode catalog as a custom entry; every
|
|
628
|
+
// other custom entry belongs to some extension's own bookkeeping.
|
|
629
|
+
case 'entry_appended': {
|
|
630
|
+
const entry = isRecord(frame.entry) ? frame.entry : undefined;
|
|
631
|
+
if (!entry) return state;
|
|
632
|
+
if (entry.type === 'custom' && entry.customType === MINOR_MODE_ENTRY_TYPE) {
|
|
633
|
+
const data = isRecord(entry.data) ? entry.data : undefined;
|
|
634
|
+
if (!data || !Array.isArray(data.modes)) return state;
|
|
635
|
+
return { ...state, minorModes: data as unknown as MinorModeProjection };
|
|
636
|
+
}
|
|
637
|
+
// A journalled user message is a transcript entry the protocol already
|
|
638
|
+
// publishes; the catalog above is DoomPi's own and always applies.
|
|
639
|
+
return options.transcriptFromProtocol ? state : applyJournalEntry(state, entry);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
case 'response':
|
|
643
|
+
return applyResponse(state, frame);
|
|
644
|
+
|
|
645
|
+
case 'error':
|
|
646
|
+
case 'extension_error':
|
|
647
|
+
return withEntry(state, {
|
|
648
|
+
kind: 'notice',
|
|
649
|
+
id: `n${state.nextId}`,
|
|
650
|
+
text: asString(frame.message ?? frame.error, 'The agent reported an error.'),
|
|
651
|
+
tone: 'error',
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
default:
|
|
655
|
+
return state;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/** Records the prompt locally, because Pi does not echo it back as an event. */
|
|
660
|
+
export function appendUserPrompt(state: SessionState, text: string): SessionState {
|
|
661
|
+
const id = `u${state.nextId}`;
|
|
662
|
+
return withPendingUser(withEntry(state, { kind: 'user', id, text }), id, text);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/** A follow-up waits for the current run, so it is marked rather than shown as sent. */
|
|
666
|
+
export function appendQueued(state: SessionState, text: string): SessionState {
|
|
667
|
+
const id = `q${state.nextId}`;
|
|
668
|
+
return withPendingUser(withEntry(state, { kind: 'queued', id, text }), id, text);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
export function clearDialog(state: SessionState): SessionState {
|
|
672
|
+
return { ...state, dialog: null };
|
|
673
|
+
}
|