@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,208 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ActivityIcon,
|
|
3
|
+
Button,
|
|
4
|
+
CloseIcon,
|
|
5
|
+
Dot,
|
|
6
|
+
type DotTone,
|
|
7
|
+
Input,
|
|
8
|
+
NavTab,
|
|
9
|
+
NavTabBadge,
|
|
10
|
+
StatusBadge,
|
|
11
|
+
type StatusTone,
|
|
12
|
+
} from '@agimon-ai/doompi-web-components';
|
|
13
|
+
import type { TabContribution, TransientTab } from '@agimon-ai/doompi-web-contracts';
|
|
14
|
+
import { Link } from '@tanstack/react-router';
|
|
15
|
+
import { useStore } from '@tanstack/react-store';
|
|
16
|
+
import { useState } from 'react';
|
|
17
|
+
import { abbreviateCwd, runningCount, type AttachPhase } from '../../lib/sessionSummary.ts';
|
|
18
|
+
import { webTabs } from '../../lib/pluginRegistry.ts';
|
|
19
|
+
import { renameSession, useActiveSession } from '../../stores/sessionStore.ts';
|
|
20
|
+
import { sessionsStore, useActiveSessionMeta } from '../../stores/sessionsStore.ts';
|
|
21
|
+
import { closeTransientTab, useTransientTabs } from '../../stores/transientTabsStore.ts';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The pill folds attach state and run state into one word, the way the mockup
|
|
25
|
+
* does: a healthy idle session says "attached", a healthy busy one says
|
|
26
|
+
* "running", and anything else names its trouble.
|
|
27
|
+
*/
|
|
28
|
+
function pill(attach: AttachPhase, busy: boolean): { text: string; tone: StatusTone; dot: DotTone; pulse: boolean } {
|
|
29
|
+
if (attach === 'attached' && busy) return { text: 'running', tone: 'running', dot: 'yellow', pulse: true };
|
|
30
|
+
if (attach === 'attached') return { text: 'attached', tone: 'ok', dot: 'green', pulse: false };
|
|
31
|
+
if (attach === 'connecting' || attach === 'detached') {
|
|
32
|
+
return { text: attach, tone: 'running', dot: 'yellow', pulse: attach === 'connecting' };
|
|
33
|
+
}
|
|
34
|
+
return { text: attach, tone: 'error', dot: 'red', pulse: false };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** One registry tab: the badge hook is stable per tab, so the call is unconditional. */
|
|
38
|
+
function PluginTab({ tab, sessionId, active }: { tab: TabContribution; sessionId: string; active: boolean }) {
|
|
39
|
+
const useBadge = tab.useBadge ?? noBadge;
|
|
40
|
+
const count = useBadge(sessionId);
|
|
41
|
+
return (
|
|
42
|
+
<NavTab asChild active={active}>
|
|
43
|
+
<Link
|
|
44
|
+
to="/session/$sessionId/$tabId"
|
|
45
|
+
params={{ sessionId, tabId: tab.id }}
|
|
46
|
+
data-testid={`tab-${tab.id}`}
|
|
47
|
+
className="shrink-0"
|
|
48
|
+
>
|
|
49
|
+
{tab.label}
|
|
50
|
+
{count > 0 ? (
|
|
51
|
+
<NavTabBadge active={active} data-testid={`tab-${tab.id}-count`}>
|
|
52
|
+
{count}
|
|
53
|
+
</NavTabBadge>
|
|
54
|
+
) : null}
|
|
55
|
+
</Link>
|
|
56
|
+
</NavTab>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function noBadge(): number {
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A tab a plugin opened at runtime: its link with the close beside it, since
|
|
66
|
+
* a button cannot live inside a link. Closing the shown tab needs no
|
|
67
|
+
* navigation of its own; the page falls back to the conversation once the
|
|
68
|
+
* id is gone.
|
|
69
|
+
*/
|
|
70
|
+
function TransientTabChip({ tab, sessionId, active }: { tab: TransientTab; sessionId: string; active: boolean }) {
|
|
71
|
+
return (
|
|
72
|
+
<span data-testid={`tab-${tab.id}-chip`} className="flex shrink-0 items-center gap-0.5">
|
|
73
|
+
<NavTab asChild active={active}>
|
|
74
|
+
<Link
|
|
75
|
+
to="/session/$sessionId/$tabId"
|
|
76
|
+
params={{ sessionId, tabId: tab.id }}
|
|
77
|
+
data-testid={`tab-${tab.id}`}
|
|
78
|
+
title={tab.label}
|
|
79
|
+
className="block min-w-0 max-w-[160px] truncate"
|
|
80
|
+
>
|
|
81
|
+
{tab.label}
|
|
82
|
+
</Link>
|
|
83
|
+
</NavTab>
|
|
84
|
+
<Button
|
|
85
|
+
variant="ghost"
|
|
86
|
+
size="icon"
|
|
87
|
+
data-testid={`tab-${tab.id}-close`}
|
|
88
|
+
title="close this tab"
|
|
89
|
+
onClick={() => closeTransientTab(sessionId, tab.id)}
|
|
90
|
+
className="h-5 w-5 shrink-0"
|
|
91
|
+
>
|
|
92
|
+
<CloseIcon className="h-2.5 w-2.5" />
|
|
93
|
+
</Button>
|
|
94
|
+
</span>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function TopBar({ view = 'conversation' }: { view?: string }) {
|
|
99
|
+
const meta = useActiveSessionMeta();
|
|
100
|
+
const session = useActiveSession((state) => state);
|
|
101
|
+
const running = useStore(sessionsStore, (state) =>
|
|
102
|
+
runningCount(state.order.map((id) => state.byId[id].summary.phase)),
|
|
103
|
+
);
|
|
104
|
+
const activeId = useStore(sessionsStore, (state) => state.activeId);
|
|
105
|
+
const transientTabs = useTransientTabs(activeId);
|
|
106
|
+
const attach: AttachPhase = meta?.attach ?? 'offline';
|
|
107
|
+
const busy = session.streaming || (meta !== null && meta.summary.phase !== 'idle');
|
|
108
|
+
const state = pill(attach, busy);
|
|
109
|
+
const title = meta?.summary.name || session.agent?.sessionName || 'untitled';
|
|
110
|
+
const [renaming, setRenaming] = useState(false);
|
|
111
|
+
const [draft, setDraft] = useState('');
|
|
112
|
+
// The name lives in the agent, so the rail's rename and this one are the
|
|
113
|
+
// same act through the same door; the new name arrives back on the socket.
|
|
114
|
+
const commitRename = (): void => {
|
|
115
|
+
const next = draft.trim();
|
|
116
|
+
if (next && next !== title) renameSession(next, activeId);
|
|
117
|
+
setRenaming(false);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<header
|
|
122
|
+
data-testid="top-bar"
|
|
123
|
+
className="flex h-12 shrink-0 items-center justify-between gap-3 border-b border-doom-border px-5"
|
|
124
|
+
>
|
|
125
|
+
<div data-testid="session-switcher" className="flex min-w-0 items-center gap-2.5">
|
|
126
|
+
{renaming && activeId !== null ? (
|
|
127
|
+
<Input
|
|
128
|
+
data-testid="session-title-input"
|
|
129
|
+
value={draft}
|
|
130
|
+
autoFocus
|
|
131
|
+
aria-label="session name"
|
|
132
|
+
onChange={(event) => setDraft(event.target.value)}
|
|
133
|
+
onKeyDown={(event) => {
|
|
134
|
+
if (event.key === 'Enter') commitRename();
|
|
135
|
+
if (event.key === 'Escape') setRenaming(false);
|
|
136
|
+
}}
|
|
137
|
+
onBlur={commitRename}
|
|
138
|
+
className="h-6 w-44 shrink-0 border-doom-blue/60 px-1.5 text-[13px] font-bold"
|
|
139
|
+
/>
|
|
140
|
+
) : (
|
|
141
|
+
<Button
|
|
142
|
+
variant="ghost"
|
|
143
|
+
data-testid="session-title"
|
|
144
|
+
title={activeId === null ? undefined : 'rename this session'}
|
|
145
|
+
disabled={activeId === null}
|
|
146
|
+
onClick={() => {
|
|
147
|
+
setDraft(title);
|
|
148
|
+
setRenaming(true);
|
|
149
|
+
}}
|
|
150
|
+
className="h-6 shrink-0 px-1 text-[13px] font-bold text-doom-hi"
|
|
151
|
+
>
|
|
152
|
+
{title}
|
|
153
|
+
</Button>
|
|
154
|
+
)}
|
|
155
|
+
{meta ? (
|
|
156
|
+
<>
|
|
157
|
+
<span className="text-[12px] text-doom-faint">·</span>
|
|
158
|
+
<span data-testid="top-cwd" className="truncate text-[11px] text-doom-dim">
|
|
159
|
+
{abbreviateCwd(meta.summary.cwd)}
|
|
160
|
+
</span>
|
|
161
|
+
</>
|
|
162
|
+
) : null}
|
|
163
|
+
{activeId !== null ? (
|
|
164
|
+
// Tabs never shrink; past the width the badges leave them, the strip
|
|
165
|
+
// scrolls sideways (scrollbar hidden) instead of running under them.
|
|
166
|
+
<div className="ml-1.5 flex min-w-0 items-center gap-1.5 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
|
167
|
+
<NavTab asChild active={view === 'conversation'}>
|
|
168
|
+
<Link
|
|
169
|
+
to="/session/$sessionId"
|
|
170
|
+
params={{ sessionId: activeId }}
|
|
171
|
+
data-testid="tab-conversation"
|
|
172
|
+
className="shrink-0"
|
|
173
|
+
>
|
|
174
|
+
conversation
|
|
175
|
+
</Link>
|
|
176
|
+
</NavTab>
|
|
177
|
+
{webTabs().map((tab) => (
|
|
178
|
+
<PluginTab key={tab.id} tab={tab} sessionId={activeId} active={view === tab.id} />
|
|
179
|
+
))}
|
|
180
|
+
{transientTabs.map((tab) => (
|
|
181
|
+
<TransientTabChip key={tab.id} tab={tab} sessionId={activeId} active={view === tab.id} />
|
|
182
|
+
))}
|
|
183
|
+
</div>
|
|
184
|
+
) : null}
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<div className="flex shrink-0 items-center gap-2">
|
|
188
|
+
{meta && meta.dropped > 0 ? (
|
|
189
|
+
<span
|
|
190
|
+
data-testid="dropped-count"
|
|
191
|
+
title="frames the hub's ring lost before this page subscribed"
|
|
192
|
+
className="text-[10px] text-doom-yellow"
|
|
193
|
+
>
|
|
194
|
+
{meta.dropped} dropped
|
|
195
|
+
</span>
|
|
196
|
+
) : null}
|
|
197
|
+
<StatusBadge size="md" tone={state.tone} data-testid="connection-status" className="font-normal">
|
|
198
|
+
<Dot tone={state.dot} pulse={state.pulse} />
|
|
199
|
+
{state.text}
|
|
200
|
+
</StatusBadge>
|
|
201
|
+
<StatusBadge size="md" tone="running" data-testid="sessions-running" className="font-normal">
|
|
202
|
+
<ActivityIcon className="h-[10px] w-[10px]" />
|
|
203
|
+
{running} running
|
|
204
|
+
</StatusBadge>
|
|
205
|
+
</div>
|
|
206
|
+
</header>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
|
7
|
+
<link rel="alternate icon" href="/favicon.ico" sizes="48x48 32x32 16x16" />
|
|
8
|
+
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
|
9
|
+
<meta name="theme-color" content="#1c1f24" />
|
|
10
|
+
<title>DoomPi cockpit</title>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div id="root"></div>
|
|
14
|
+
<script type="module" src="./main.tsx"></script>
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AUTH_LOGINS_API_ROUTE,
|
|
3
|
+
AUTH_PROVIDERS_API_ROUTE,
|
|
4
|
+
type AuthMethodType,
|
|
5
|
+
type LoginFlowSnapshot,
|
|
6
|
+
type ProviderAuthSummary,
|
|
7
|
+
} from '../../types/auth.ts';
|
|
8
|
+
|
|
9
|
+
export type ProvidersResult = { providers: ProviderAuthSummary[] } | { error: string };
|
|
10
|
+
export type LoginFlowResult = { flow: LoginFlowSnapshot } | { error: string };
|
|
11
|
+
export type LogoutResult = { ok: true } | { error: string };
|
|
12
|
+
|
|
13
|
+
const UNREACHABLE = 'The cockpit hub is unreachable.';
|
|
14
|
+
const JSON_HEADERS = { 'Content-Type': 'application/json' };
|
|
15
|
+
|
|
16
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
17
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* One shape for every provider-auth call: a network failure, a non-2xx
|
|
22
|
+
* answer, and a 2xx body missing the expected field all come back as
|
|
23
|
+
* `{ error }`, so the settings page has one branch to render.
|
|
24
|
+
*/
|
|
25
|
+
async function request<T>(
|
|
26
|
+
input: string,
|
|
27
|
+
init: RequestInit | undefined,
|
|
28
|
+
pick: (body: Record<string, unknown>) => T | undefined,
|
|
29
|
+
): Promise<T | { error: string }> {
|
|
30
|
+
let response: Response;
|
|
31
|
+
try {
|
|
32
|
+
response = await fetch(input, init);
|
|
33
|
+
} catch {
|
|
34
|
+
return { error: UNREACHABLE };
|
|
35
|
+
}
|
|
36
|
+
let body: unknown;
|
|
37
|
+
try {
|
|
38
|
+
body = await response.json();
|
|
39
|
+
} catch {
|
|
40
|
+
body = undefined;
|
|
41
|
+
}
|
|
42
|
+
if (response.ok && isRecord(body)) {
|
|
43
|
+
const picked = pick(body);
|
|
44
|
+
if (picked !== undefined) return picked;
|
|
45
|
+
}
|
|
46
|
+
const error = isRecord(body) && typeof body.error === 'string' ? body.error : `The hub answered ${response.status}.`;
|
|
47
|
+
return { error };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const pickFlow = (body: Record<string, unknown>): { flow: LoginFlowSnapshot } | undefined =>
|
|
51
|
+
isRecord(body.flow) ? { flow: body.flow as unknown as LoginFlowSnapshot } : undefined;
|
|
52
|
+
|
|
53
|
+
/** Every provider Pi knows on the hub's machine, with its auth state. */
|
|
54
|
+
export function listProviders(): Promise<ProvidersResult> {
|
|
55
|
+
return request(AUTH_PROVIDERS_API_ROUTE, undefined, (body) =>
|
|
56
|
+
Array.isArray(body.providers) ? { providers: body.providers as ProviderAuthSummary[] } : undefined,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Starts a sign-in on the hub; the returned flow is then polled with readLogin. */
|
|
61
|
+
export function startLogin(providerId: string, type: AuthMethodType): Promise<LoginFlowResult> {
|
|
62
|
+
return request(
|
|
63
|
+
AUTH_LOGINS_API_ROUTE,
|
|
64
|
+
{ method: 'POST', headers: JSON_HEADERS, body: JSON.stringify({ providerId, type }) },
|
|
65
|
+
pickFlow,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function readLogin(flowId: string): Promise<LoginFlowResult> {
|
|
70
|
+
return request(`${AUTH_LOGINS_API_ROUTE}/${encodeURIComponent(flowId)}`, undefined, pickFlow);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function answerLogin(flowId: string, promptId: string, value: string): Promise<LoginFlowResult> {
|
|
74
|
+
return request(
|
|
75
|
+
`${AUTH_LOGINS_API_ROUTE}/${encodeURIComponent(flowId)}/answer`,
|
|
76
|
+
{ method: 'POST', headers: JSON_HEADERS, body: JSON.stringify({ promptId, value }) },
|
|
77
|
+
pickFlow,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function cancelLogin(flowId: string): Promise<LoginFlowResult> {
|
|
82
|
+
return request(`${AUTH_LOGINS_API_ROUTE}/${encodeURIComponent(flowId)}`, { method: 'DELETE' }, pickFlow);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Removes the credential /login stored; environment variables are untouched. */
|
|
86
|
+
export function logoutProvider(providerId: string): Promise<LogoutResult> {
|
|
87
|
+
return request(`${AUTH_PROVIDERS_API_ROUTE}/${encodeURIComponent(providerId)}`, { method: 'DELETE' }, () => ({
|
|
88
|
+
ok: true as const,
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
type Frame = Record<string, unknown>;
|
|
2
|
+
|
|
3
|
+
export function promptCommand(message: string): Frame {
|
|
4
|
+
return { type: 'prompt', message };
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function steerCommand(message: string): Frame {
|
|
8
|
+
return { type: 'steer', message };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function followUpCommand(message: string): Frame {
|
|
12
|
+
return { type: 'follow_up', message };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function abortCommand(): Frame {
|
|
16
|
+
return { type: 'abort' };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getStateCommand(): Frame {
|
|
20
|
+
return { type: 'get_state' };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getSessionStatsCommand(): Frame {
|
|
24
|
+
return { type: 'get_session_stats' };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getCommandsCommand(): Frame {
|
|
28
|
+
return { type: 'get_commands' };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getAvailableModelsCommand(): Frame {
|
|
32
|
+
return { type: 'get_available_models' };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getAvailableThinkingLevelsCommand(): Frame {
|
|
36
|
+
return { type: 'get_available_thinking_levels' };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function setModelCommand(provider: string, modelId: string): Frame {
|
|
40
|
+
return { type: 'set_model', provider, modelId };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function setThinkingLevelCommand(level: string): Frame {
|
|
44
|
+
return { type: 'set_thinking_level', level };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function setSessionNameCommand(name: string): Frame {
|
|
48
|
+
return { type: 'set_session_name', name };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function dialogValue(id: string, value: string): Frame {
|
|
52
|
+
return { type: 'extension_ui_response', id, value };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function dialogConfirmed(id: string, confirmed: boolean): Frame {
|
|
56
|
+
return { type: 'extension_ui_response', id, confirmed };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function dialogCancelled(id: string): Frame {
|
|
60
|
+
return { type: 'extension_ui_response', id, cancelled: true };
|
|
61
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import type { SelectionAxisContribution } from '@agimon-ai/doompi-web-contracts';
|
|
2
|
+
import type { MinorModeProjection, MinorModeRecordProjection } from '../../types/hub.ts';
|
|
3
|
+
import { pluginActivityGroups, pluginMinorModes, pluginSelectionAxes } from './pluginRegistry.ts';
|
|
4
|
+
import { stripAnsi } from './statusLine.ts';
|
|
5
|
+
|
|
6
|
+
export interface SelectionAxis {
|
|
7
|
+
name: string;
|
|
8
|
+
command: string;
|
|
9
|
+
/** The current selections, empty while the session reports none; one at most unless multi. */
|
|
10
|
+
values: string[];
|
|
11
|
+
emptyLabel: string;
|
|
12
|
+
multi: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const AXIS_LIST_SEPARATOR = ',';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The fallback axis table for the packaged bundle; a synced bundle carries
|
|
19
|
+
* each axis as its owning package's plugin declaration, and the registry
|
|
20
|
+
* list wins whenever any plugin declares one. Exported only so the workspace
|
|
21
|
+
* composition test can hold it equal to what the packages declare.
|
|
22
|
+
*/
|
|
23
|
+
export const PACKAGED_SELECTION_AXES: readonly SelectionAxisContribution[] = [
|
|
24
|
+
{ name: 'profile', command: 'profile', statusKey: 'doom-profile', emptyLabel: 'no profile', order: 10 },
|
|
25
|
+
{ name: 'domains', command: 'domains', statusKey: 'doom-domain', emptyLabel: 'no domains', multi: true, order: 20 },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The declared axes the session actually offers: an axis whose status key
|
|
30
|
+
* the session never published stays off the bar entirely, which is how a
|
|
31
|
+
* package withholds its axis when there is nothing to select.
|
|
32
|
+
*/
|
|
33
|
+
export function selectionAxes(statuses: Record<string, string>): SelectionAxis[] {
|
|
34
|
+
const declared = pluginSelectionAxes();
|
|
35
|
+
const sources: readonly SelectionAxisContribution[] = declared.length > 0 ? declared : PACKAGED_SELECTION_AXES;
|
|
36
|
+
return sources.flatMap((source) => {
|
|
37
|
+
const raw = statuses[source.statusKey];
|
|
38
|
+
if (raw === undefined) return [];
|
|
39
|
+
const multi = source.multi === true;
|
|
40
|
+
const values = (multi ? stripAnsi(raw).split(AXIS_LIST_SEPARATOR) : [stripAnsi(raw)])
|
|
41
|
+
.map((value) => value.trim())
|
|
42
|
+
.filter(Boolean);
|
|
43
|
+
return [{ name: source.name, command: source.command, values, emptyLabel: source.emptyLabel, multi }];
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type MinorModeAvailability = 'unavailable' | 'off' | 'on';
|
|
48
|
+
|
|
49
|
+
export interface MinorMode {
|
|
50
|
+
name: string;
|
|
51
|
+
/** The catalog id /minor accepts; the display name until the catalog reports. */
|
|
52
|
+
id: string;
|
|
53
|
+
/** Leader Space key path, as the TUI documents it. */
|
|
54
|
+
keys: string;
|
|
55
|
+
availability: MinorModeAvailability;
|
|
56
|
+
/** What the mode itself is reporting while on. */
|
|
57
|
+
detail: string;
|
|
58
|
+
/**
|
|
59
|
+
* Why the mode cannot be driven from here, in the words of the mode itself.
|
|
60
|
+
* Empty when it can be. A mode may be installed and still refuse a cockpit:
|
|
61
|
+
* autonomous voice captures from the terminal's own microphone, which a
|
|
62
|
+
* browser talking to a headless agent does not have.
|
|
63
|
+
*/
|
|
64
|
+
unavailableReason: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface MinorModeSource {
|
|
68
|
+
name: string;
|
|
69
|
+
/** The catalog mode this row drives, when the runtime registers it under another id. */
|
|
70
|
+
modeId?: string;
|
|
71
|
+
keys: string;
|
|
72
|
+
statusKey?: string;
|
|
73
|
+
widgetKey?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The fallback minor-mode table for the packaged bundle.
|
|
78
|
+
*
|
|
79
|
+
* A synced bundle carries each mode as its package's own web plugin
|
|
80
|
+
* contribution, and the registry list wins whenever any plugin declares one.
|
|
81
|
+
* This literal survives only so the built-in-only bundle keeps rendering the
|
|
82
|
+
* modes DoomPi ships; help is listed without a signal on purpose: it
|
|
83
|
+
* registers no footer entry, so the cockpit reports it as unavailable rather
|
|
84
|
+
* than guessing it is off. Exported only for the workspace composition test.
|
|
85
|
+
*/
|
|
86
|
+
export const PACKAGED_MINOR_MODES: readonly MinorModeSource[] = [
|
|
87
|
+
{ name: 'help', keys: 'h e' },
|
|
88
|
+
{ name: 'plan', keys: 'p e', statusKey: 'plan-mode' },
|
|
89
|
+
{ name: 'loop', keys: 'l l', statusKey: 'doom-loop' },
|
|
90
|
+
{ name: 'goal', keys: 'g e', statusKey: 'goal' },
|
|
91
|
+
{ name: 'workflow', keys: 'w e', widgetKey: 'workflow-mcp-progress' },
|
|
92
|
+
// `v e` drives autonomous capture, which the runtime registers as 'voice-auto'.
|
|
93
|
+
{ name: 'voice', modeId: 'voice-auto', keys: 'v e', statusKey: 'doom-voice' },
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
/** The declared source a catalog mode corresponds to, by id, id stem, or label. */
|
|
97
|
+
function sourceFor(sources: readonly MinorModeSource[], mode: MinorModeRecordProjection): MinorModeSource | undefined {
|
|
98
|
+
const candidates = new Set([mode.id.toLowerCase(), mode.id.toLowerCase().split('.')[0], mode.label.toLowerCase()]);
|
|
99
|
+
return sources.find((source) => candidates.has(source.name.toLowerCase()));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Catalog-backed rows: the runtime's own activation state per mode, with the
|
|
104
|
+
* declared leader keys attached where a plugin documents them. Declared modes
|
|
105
|
+
* the catalog does not know are still listed, as unavailable, so a mode that
|
|
106
|
+
* is missing from a composition stays visible rather than silently gone.
|
|
107
|
+
*/
|
|
108
|
+
function catalogMinorModes(sources: readonly MinorModeSource[], projection: MinorModeProjection): MinorMode[] {
|
|
109
|
+
const rows: MinorMode[] = projection.modes.map((mode) => {
|
|
110
|
+
const source = sourceFor(sources, mode);
|
|
111
|
+
const on = mode.activation === 'active' || mode.activation === 'deactivating';
|
|
112
|
+
// The catalog says per action whether it can run here and why not. A mode
|
|
113
|
+
// with nothing runnable is offered as a row that cannot be clicked rather
|
|
114
|
+
// than one that answers "no actions available" after the fact.
|
|
115
|
+
const blocked = mode.actions.length > 0 && mode.actions.every((action) => !action.enabled);
|
|
116
|
+
const reason = blocked ? (mode.actions.find((action) => action.disabledReason)?.disabledReason ?? '') : '';
|
|
117
|
+
return {
|
|
118
|
+
name: source?.name ?? mode.label.toLowerCase(),
|
|
119
|
+
id: mode.id,
|
|
120
|
+
keys: source?.keys ?? '',
|
|
121
|
+
availability: blocked && !on ? 'unavailable' : on ? 'on' : 'off',
|
|
122
|
+
detail: mode.detail ?? (mode.activation === 'activating' ? 'activating' : ''),
|
|
123
|
+
unavailableReason: reason,
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
const seen = new Set(rows.map((row) => row.name));
|
|
127
|
+
for (const source of sources) {
|
|
128
|
+
if (!seen.has(source.name)) {
|
|
129
|
+
rows.push({
|
|
130
|
+
name: source.name,
|
|
131
|
+
id: source.modeId ?? source.name,
|
|
132
|
+
keys: source.keys,
|
|
133
|
+
availability: 'unavailable',
|
|
134
|
+
detail: '',
|
|
135
|
+
unavailableReason: 'No package in this session registers this mode.',
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return rows;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function minorModes(
|
|
143
|
+
statuses: Record<string, string>,
|
|
144
|
+
widgets: readonly string[],
|
|
145
|
+
projection: MinorModeProjection | null = null,
|
|
146
|
+
): MinorMode[] {
|
|
147
|
+
const declared = pluginMinorModes();
|
|
148
|
+
const sources: readonly MinorModeSource[] = declared.length > 0 ? declared : PACKAGED_MINOR_MODES;
|
|
149
|
+
if (projection) return catalogMinorModes(sources, projection);
|
|
150
|
+
return sources.map((source) => {
|
|
151
|
+
if (source.statusKey !== undefined) {
|
|
152
|
+
const raw = statuses[source.statusKey];
|
|
153
|
+
if (raw === undefined) {
|
|
154
|
+
return {
|
|
155
|
+
name: source.name,
|
|
156
|
+
id: source.modeId ?? source.name,
|
|
157
|
+
keys: source.keys,
|
|
158
|
+
availability: 'unavailable' as const,
|
|
159
|
+
detail: '',
|
|
160
|
+
unavailableReason: 'This session has not reported the mode.',
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
const detail = stripAnsi(raw).trim();
|
|
164
|
+
return {
|
|
165
|
+
name: source.name,
|
|
166
|
+
id: source.modeId ?? source.name,
|
|
167
|
+
keys: source.keys,
|
|
168
|
+
availability: detail ? ('on' as const) : ('off' as const),
|
|
169
|
+
detail,
|
|
170
|
+
unavailableReason: '',
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
if (source.widgetKey !== undefined && widgets.includes(source.widgetKey)) {
|
|
174
|
+
return {
|
|
175
|
+
name: source.name,
|
|
176
|
+
id: source.modeId ?? source.name,
|
|
177
|
+
keys: source.keys,
|
|
178
|
+
availability: 'off' as const,
|
|
179
|
+
detail: '',
|
|
180
|
+
unavailableReason: '',
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
name: source.name,
|
|
185
|
+
id: source.name,
|
|
186
|
+
keys: source.keys,
|
|
187
|
+
availability: 'unavailable' as const,
|
|
188
|
+
detail: '',
|
|
189
|
+
unavailableReason: 'This session has not reported the mode.',
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface ActivityGroup {
|
|
195
|
+
name: string;
|
|
196
|
+
keys: string;
|
|
197
|
+
/** What the publishing extension is reporting, empty when it reports nothing. */
|
|
198
|
+
summary: string;
|
|
199
|
+
active: boolean;
|
|
200
|
+
/** The plugin tab the group's key chip opens, when its package declares one. */
|
|
201
|
+
tab?: string;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The declared groups the session actually signals. There is no host-side
|
|
206
|
+
* fallback table: agents, runners, and workflows are each declared by the
|
|
207
|
+
* package that owns them, so a composition without such a package shows no
|
|
208
|
+
* group, and a session that never published a group's signal shows none
|
|
209
|
+
* either.
|
|
210
|
+
*/
|
|
211
|
+
export function activityGroups(statuses: Record<string, string>, widgets: readonly string[]): ActivityGroup[] {
|
|
212
|
+
const groups: ActivityGroup[] = [];
|
|
213
|
+
for (const source of pluginActivityGroups()) {
|
|
214
|
+
const tab = source.tab === undefined ? {} : { tab: source.tab };
|
|
215
|
+
if (source.statusKey !== undefined && statuses[source.statusKey] !== undefined) {
|
|
216
|
+
const summary = stripAnsi(statuses[source.statusKey] ?? '').trim();
|
|
217
|
+
groups.push({ name: source.name, keys: source.keys, summary, active: summary.length > 0, ...tab });
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (source.widgetKeys !== undefined && source.widgetKeys.some((key) => widgets.includes(key))) {
|
|
221
|
+
groups.push({ name: source.name, keys: source.keys, summary: '', active: false, ...tab });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return groups;
|
|
225
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { MEDIA_TYPES, type MediaKind } from '../../types/media.ts';
|
|
2
|
+
|
|
3
|
+
export interface FileMention {
|
|
4
|
+
/** The cwd-relative path as the message spelled it. */
|
|
5
|
+
path: string;
|
|
6
|
+
/** How the timeline previews it; 'file' offers a link instead. */
|
|
7
|
+
kind: MediaKind | 'file';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// An @ token starts the message or follows whitespace, runs to the next
|
|
11
|
+
// whitespace, and sheds the punctuation a sentence hangs on it.
|
|
12
|
+
const MENTION = /(?:^|\s)@([^\s@]+)/gu;
|
|
13
|
+
const TRAILING_PUNCTUATION = /[.,;:!?)\]]+$/u;
|
|
14
|
+
|
|
15
|
+
export function mediaKindFor(filePath: string): MediaKind | 'file' {
|
|
16
|
+
const dot = filePath.lastIndexOf('.');
|
|
17
|
+
if (dot < 0) return 'file';
|
|
18
|
+
return MEDIA_TYPES[filePath.slice(dot + 1).toLowerCase()]?.kind ?? 'file';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The distinct files a message mentions with @, in order of appearance. */
|
|
22
|
+
export function parseFileMentions(text: string): FileMention[] {
|
|
23
|
+
const seen = new Set<string>();
|
|
24
|
+
const mentions: FileMention[] = [];
|
|
25
|
+
for (const match of text.matchAll(MENTION)) {
|
|
26
|
+
const mentionPath = match[1].replace(TRAILING_PUNCTUATION, '');
|
|
27
|
+
if (!mentionPath || seen.has(mentionPath)) continue;
|
|
28
|
+
seen.add(mentionPath);
|
|
29
|
+
mentions.push({ path: mentionPath, kind: mediaKindFor(mentionPath) });
|
|
30
|
+
}
|
|
31
|
+
return mentions;
|
|
32
|
+
}
|