@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,368 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Badge,
|
|
3
|
+
Button,
|
|
4
|
+
Kbd,
|
|
5
|
+
OptionLabel,
|
|
6
|
+
OptionRow,
|
|
7
|
+
Popover,
|
|
8
|
+
PopoverAnchor,
|
|
9
|
+
PopoverContent,
|
|
10
|
+
PopoverFooter,
|
|
11
|
+
StopIcon,
|
|
12
|
+
Textarea,
|
|
13
|
+
} from '@agimon-ai/doompi-web-components';
|
|
14
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
15
|
+
import { searchSessionFiles } from '../../lib/hubApi.ts';
|
|
16
|
+
import { registerPromptInput } from '../../lib/promptFocus.ts';
|
|
17
|
+
import { abortRun, queueFollowUp, submitMessage, useActiveSession } from '../../stores/sessionStore.ts';
|
|
18
|
+
import { activeSessionId, useActiveSessionMeta } from '../../stores/sessionsStore.ts';
|
|
19
|
+
import { openPalette } from '../../stores/paletteStore.ts';
|
|
20
|
+
|
|
21
|
+
/** The input grows with the draft up to this many pixels, then scrolls. */
|
|
22
|
+
const MAX_INPUT_HEIGHT_PX = 192;
|
|
23
|
+
const MAX_COMPLETION_ITEMS = 8;
|
|
24
|
+
const FILE_SEARCH_DEBOUNCE_MS = 150;
|
|
25
|
+
|
|
26
|
+
interface CompletionItem {
|
|
27
|
+
/** What replaces the trigger token when accepted. */
|
|
28
|
+
insert: string;
|
|
29
|
+
label: string;
|
|
30
|
+
detail?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface CompletionState {
|
|
34
|
+
kind: 'command' | 'file';
|
|
35
|
+
/** Index of the trigger character in the draft. */
|
|
36
|
+
tokenStart: number;
|
|
37
|
+
query: string;
|
|
38
|
+
items: CompletionItem[];
|
|
39
|
+
selected: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Whether accepting the highlighted item would leave the draft as it is. The
|
|
44
|
+
* popup still shows what a longer name would complete to, but the keystroke
|
|
45
|
+
* that would change nothing is handed back to the composer to send.
|
|
46
|
+
*/
|
|
47
|
+
function isCompletionRedundant(state: CompletionState): boolean {
|
|
48
|
+
const item = state.items[state.selected];
|
|
49
|
+
return item !== undefined && item.insert.trimEnd() === `${state.kind === 'command' ? '/' : '@'}${state.query}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The /command or @file token the caret sits in, or null when there is none. */
|
|
53
|
+
function triggerTokenAt(
|
|
54
|
+
draft: string,
|
|
55
|
+
caret: number,
|
|
56
|
+
): { kind: 'command' | 'file'; start: number; query: string } | null {
|
|
57
|
+
const before = draft.slice(0, caret);
|
|
58
|
+
const start = Math.max(before.lastIndexOf(' '), before.lastIndexOf('\n')) + 1;
|
|
59
|
+
const token = before.slice(start);
|
|
60
|
+
if (token.startsWith('/') && start === 0) return { kind: 'command', start, query: token.slice(1) };
|
|
61
|
+
if (token.startsWith('@') && token.length <= 64) return { kind: 'file', start, query: token.slice(1) };
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function Composer() {
|
|
66
|
+
const meta = useActiveSessionMeta();
|
|
67
|
+
const streaming = useActiveSession((state) => state.streaming);
|
|
68
|
+
const commands = useActiveSession((state) => state.commands);
|
|
69
|
+
const [draft, setDraft] = useState('');
|
|
70
|
+
const [caret, setCaret] = useState(0);
|
|
71
|
+
const [dismissedToken, setDismissedToken] = useState<number | null>(null);
|
|
72
|
+
const [completion, setCompletion] = useState<CompletionState | null>(null);
|
|
73
|
+
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
74
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
75
|
+
const searchTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
76
|
+
|
|
77
|
+
const attached = meta?.attach === 'attached';
|
|
78
|
+
const queued = meta?.summary.pendingMessageCount ?? 0;
|
|
79
|
+
|
|
80
|
+
// Overlays hand the keyboard back here when they close.
|
|
81
|
+
useEffect(() => registerPromptInput(inputRef.current), []);
|
|
82
|
+
|
|
83
|
+
// Auto-grow: measure after every draft change so pasted stack traces are
|
|
84
|
+
// actually visible instead of scrolling inside one line.
|
|
85
|
+
const resize = useCallback((): void => {
|
|
86
|
+
const input = inputRef.current;
|
|
87
|
+
if (!input) return;
|
|
88
|
+
input.style.height = 'auto';
|
|
89
|
+
input.style.height = `${String(Math.min(input.scrollHeight, MAX_INPUT_HEIGHT_PX))}px`;
|
|
90
|
+
}, []);
|
|
91
|
+
useEffect(() => resize(), [draft, resize]);
|
|
92
|
+
|
|
93
|
+
const closeCompletion = useCallback((): void => {
|
|
94
|
+
if (searchTimer.current) clearTimeout(searchTimer.current);
|
|
95
|
+
setCompletion(null);
|
|
96
|
+
}, []);
|
|
97
|
+
|
|
98
|
+
/** Esc dismisses the popup for the token being typed; edits re-arm it. */
|
|
99
|
+
const dismissCompletion = useCallback((): void => {
|
|
100
|
+
const trigger = triggerTokenAt(draft, caret);
|
|
101
|
+
setDismissedToken(trigger?.start ?? null);
|
|
102
|
+
closeCompletion();
|
|
103
|
+
}, [draft, caret, closeCompletion]);
|
|
104
|
+
|
|
105
|
+
// The popup is derived state: any change to the draft, caret, session
|
|
106
|
+
// commands, or attachment recomputes it, so data arriving after a keystroke
|
|
107
|
+
// still opens it.
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
const trigger = triggerTokenAt(draft, caret);
|
|
110
|
+
if (!trigger || !attached || dismissedToken === trigger.start) {
|
|
111
|
+
closeCompletion();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (trigger.kind === 'command') {
|
|
115
|
+
const items = commands
|
|
116
|
+
.filter((command) => command.name.toLowerCase().startsWith(trigger.query.toLowerCase()))
|
|
117
|
+
.slice(0, MAX_COMPLETION_ITEMS)
|
|
118
|
+
.map((command) => ({ insert: `/${command.name} `, label: `/${command.name}`, detail: command.description }));
|
|
119
|
+
setCompletion(
|
|
120
|
+
items.length > 0
|
|
121
|
+
? { kind: 'command', tokenStart: trigger.start, query: trigger.query, items, selected: 0 }
|
|
122
|
+
: null,
|
|
123
|
+
);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (searchTimer.current) clearTimeout(searchTimer.current);
|
|
127
|
+
searchTimer.current = setTimeout(() => {
|
|
128
|
+
const sessionId = activeSessionId();
|
|
129
|
+
if (sessionId === null) return;
|
|
130
|
+
void searchSessionFiles(sessionId, trigger.query).then((files) => {
|
|
131
|
+
// The draft may have moved on while the lookup ran.
|
|
132
|
+
const input = inputRef.current;
|
|
133
|
+
if (!input || triggerTokenAt(input.value, input.selectionStart)?.query !== trigger.query) return;
|
|
134
|
+
const items = files.slice(0, MAX_COMPLETION_ITEMS).map((file) => ({ insert: `@${file} `, label: `@${file}` }));
|
|
135
|
+
setCompletion(
|
|
136
|
+
items.length > 0
|
|
137
|
+
? { kind: 'file', tokenStart: trigger.start, query: trigger.query, items, selected: 0 }
|
|
138
|
+
: null,
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
}, FILE_SEARCH_DEBOUNCE_MS);
|
|
142
|
+
return () => {
|
|
143
|
+
if (searchTimer.current) clearTimeout(searchTimer.current);
|
|
144
|
+
};
|
|
145
|
+
}, [draft, caret, commands, attached, dismissedToken, closeCompletion]);
|
|
146
|
+
|
|
147
|
+
// Clicking anywhere outside the composer closes the popup.
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
if (!completion) return;
|
|
150
|
+
const onPointerDown = (event: MouseEvent): void => {
|
|
151
|
+
if (containerRef.current && event.target instanceof Node && !containerRef.current.contains(event.target)) {
|
|
152
|
+
closeCompletion();
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
document.addEventListener('mousedown', onPointerDown);
|
|
156
|
+
return () => document.removeEventListener('mousedown', onPointerDown);
|
|
157
|
+
}, [completion, closeCompletion]);
|
|
158
|
+
|
|
159
|
+
const accept = (state: CompletionState, index: number): void => {
|
|
160
|
+
const item = state.items[index];
|
|
161
|
+
if (!item) return;
|
|
162
|
+
const next = draft.slice(0, state.tokenStart) + item.insert + draft.slice(caret);
|
|
163
|
+
setDraft(next);
|
|
164
|
+
const position = state.tokenStart + item.insert.length;
|
|
165
|
+
setCaret(position);
|
|
166
|
+
setDismissedToken(state.tokenStart);
|
|
167
|
+
requestAnimationFrame(() => {
|
|
168
|
+
inputRef.current?.focus();
|
|
169
|
+
inputRef.current?.setSelectionRange(position, position);
|
|
170
|
+
});
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const submit = (): void => {
|
|
174
|
+
if (!draft.trim() || !attached) return;
|
|
175
|
+
submitMessage(draft);
|
|
176
|
+
setDraft('');
|
|
177
|
+
setCaret(0);
|
|
178
|
+
setDismissedToken(null);
|
|
179
|
+
closeCompletion();
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const queue = (): void => {
|
|
183
|
+
if (!draft.trim() || !attached) return;
|
|
184
|
+
queueFollowUp(draft);
|
|
185
|
+
setDraft('');
|
|
186
|
+
setCaret(0);
|
|
187
|
+
setDismissedToken(null);
|
|
188
|
+
closeCompletion();
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const placeholder = !attached
|
|
192
|
+
? 'waiting for the session…'
|
|
193
|
+
: streaming
|
|
194
|
+
? 'steer the run without stopping it…'
|
|
195
|
+
: 'ask anything · / for commands · @ for files…';
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<div className="shrink-0 border-t border-doom-border bg-doom-rail px-5 pt-3 pb-2.5">
|
|
199
|
+
<Popover
|
|
200
|
+
open={completion !== null}
|
|
201
|
+
onOpenChange={(next) => {
|
|
202
|
+
if (!next) dismissCompletion();
|
|
203
|
+
}}
|
|
204
|
+
>
|
|
205
|
+
<PopoverAnchor asChild>
|
|
206
|
+
<div
|
|
207
|
+
ref={containerRef}
|
|
208
|
+
className={`relative rounded-lg border bg-doom-deep transition-colors focus-within:border-doom-blue/60 ${
|
|
209
|
+
streaming ? 'border-doom-edge-yellow' : 'border-doom-border'
|
|
210
|
+
}`}
|
|
211
|
+
>
|
|
212
|
+
{completion ? (
|
|
213
|
+
<PopoverContent
|
|
214
|
+
side="top"
|
|
215
|
+
align="start"
|
|
216
|
+
sideOffset={8}
|
|
217
|
+
data-testid="composer-completion"
|
|
218
|
+
// The keys belong to the textarea, so the menu never takes
|
|
219
|
+
// focus; a click in the composer is a caret move, not a
|
|
220
|
+
// dismissal, which is why the anchor is exempt from outside.
|
|
221
|
+
onOpenAutoFocus={(event) => event.preventDefault()}
|
|
222
|
+
onCloseAutoFocus={(event) => event.preventDefault()}
|
|
223
|
+
onInteractOutside={(event) => {
|
|
224
|
+
if (containerRef.current?.contains(event.target as Node) === true) event.preventDefault();
|
|
225
|
+
}}
|
|
226
|
+
className="w-[420px] max-w-[85vw] rounded-md p-0 shadow-xl"
|
|
227
|
+
>
|
|
228
|
+
{completion.items.map((item, index) => (
|
|
229
|
+
<OptionRow
|
|
230
|
+
key={item.label}
|
|
231
|
+
density="compact"
|
|
232
|
+
active={index === completion.selected}
|
|
233
|
+
data-testid={`composer-completion-item-${String(index)}`}
|
|
234
|
+
onMouseEnter={() => setCompletion({ ...completion, selected: index })}
|
|
235
|
+
onMouseDown={(event) => {
|
|
236
|
+
event.preventDefault();
|
|
237
|
+
accept(completion, index);
|
|
238
|
+
}}
|
|
239
|
+
className="w-full items-baseline gap-2.5 rounded-none px-3 py-1.5"
|
|
240
|
+
>
|
|
241
|
+
<span className="shrink-0 text-[12px] font-bold text-doom-blue">{item.label}</span>
|
|
242
|
+
{item.detail ? (
|
|
243
|
+
<OptionLabel density="compact" className="text-[10px] text-doom-dim">
|
|
244
|
+
{item.detail}
|
|
245
|
+
</OptionLabel>
|
|
246
|
+
) : null}
|
|
247
|
+
</OptionRow>
|
|
248
|
+
))}
|
|
249
|
+
<PopoverFooter className="py-1">
|
|
250
|
+
<span className="flex items-center gap-1.5">
|
|
251
|
+
<Kbd>tab</Kbd> or <Kbd>enter</Kbd> completes · <Kbd>esc</Kbd> closes
|
|
252
|
+
</span>
|
|
253
|
+
</PopoverFooter>
|
|
254
|
+
</PopoverContent>
|
|
255
|
+
) : null}
|
|
256
|
+
<div className="flex items-start gap-2.5 px-3.5 pt-3">
|
|
257
|
+
<span className="mt-[3px] shrink-0 select-none text-[13px] leading-none text-doom-green">></span>
|
|
258
|
+
<Textarea
|
|
259
|
+
variant="bare"
|
|
260
|
+
ref={inputRef}
|
|
261
|
+
data-testid="composer-input"
|
|
262
|
+
value={draft}
|
|
263
|
+
disabled={!attached}
|
|
264
|
+
onChange={(event) => {
|
|
265
|
+
setDraft(event.target.value);
|
|
266
|
+
setCaret(event.target.selectionStart);
|
|
267
|
+
setDismissedToken(null);
|
|
268
|
+
}}
|
|
269
|
+
onClick={(event) => setCaret(event.currentTarget.selectionStart)}
|
|
270
|
+
onKeyUp={(event) => setCaret(event.currentTarget.selectionStart)}
|
|
271
|
+
onKeyDown={(event) => {
|
|
272
|
+
// The TUI's leader key: space with nothing typed opens Leader Space.
|
|
273
|
+
if (event.key === ' ' && draft === '') {
|
|
274
|
+
event.preventDefault();
|
|
275
|
+
openPalette();
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (completion) {
|
|
279
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
280
|
+
event.preventDefault();
|
|
281
|
+
const delta = event.key === 'ArrowDown' ? 1 : -1;
|
|
282
|
+
const count = completion.items.length;
|
|
283
|
+
setCompletion({ ...completion, selected: (completion.selected + delta + count) % count });
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (event.key === 'Tab') {
|
|
287
|
+
event.preventDefault();
|
|
288
|
+
accept(completion, completion.selected);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
// Enter completes, unless the token is already the whole
|
|
292
|
+
// thing: completing then would only add a space, so the
|
|
293
|
+
// keystroke would read as doing nothing and the reader would
|
|
294
|
+
// have to press enter twice to send what they had typed.
|
|
295
|
+
if (event.key === 'Enter' && !isCompletionRedundant(completion)) {
|
|
296
|
+
event.preventDefault();
|
|
297
|
+
accept(completion, completion.selected);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (event.key === 'Escape') {
|
|
301
|
+
event.preventDefault();
|
|
302
|
+
dismissCompletion();
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (event.key === 'Enter' && !event.shiftKey) {
|
|
307
|
+
event.preventDefault();
|
|
308
|
+
submit();
|
|
309
|
+
}
|
|
310
|
+
if (event.key === 'Escape' && streaming) {
|
|
311
|
+
event.preventDefault();
|
|
312
|
+
abortRun();
|
|
313
|
+
}
|
|
314
|
+
}}
|
|
315
|
+
rows={1}
|
|
316
|
+
placeholder={placeholder}
|
|
317
|
+
className="min-h-[20px] flex-1 text-[13px] leading-relaxed"
|
|
318
|
+
/>
|
|
319
|
+
</div>
|
|
320
|
+
<div className="flex items-center gap-2 px-3.5 pt-2 pb-2.5">
|
|
321
|
+
<span data-testid="composer-hint" className="text-[10px] text-doom-faint">
|
|
322
|
+
{streaming
|
|
323
|
+
? 'enter steers the run · esc aborts'
|
|
324
|
+
: 'enter sends · shift+enter for a new line · space opens leader'}
|
|
325
|
+
</span>
|
|
326
|
+
{queued > 0 ? (
|
|
327
|
+
<Badge
|
|
328
|
+
size="xs"
|
|
329
|
+
data-testid="composer-queued"
|
|
330
|
+
className="rounded-full border-transparent bg-doom-panel py-0.5 text-[9px] text-doom-dim"
|
|
331
|
+
>
|
|
332
|
+
{queued} queued
|
|
333
|
+
</Badge>
|
|
334
|
+
) : null}
|
|
335
|
+
<span className="min-w-0 flex-1" />
|
|
336
|
+
{streaming ? (
|
|
337
|
+
<Button variant="danger-outline" size="md" data-testid="composer-abort" onClick={() => abortRun()}>
|
|
338
|
+
<StopIcon className="h-2 w-2 fill-current" />
|
|
339
|
+
abort
|
|
340
|
+
</Button>
|
|
341
|
+
) : null}
|
|
342
|
+
<Button
|
|
343
|
+
variant="outline"
|
|
344
|
+
size="md"
|
|
345
|
+
data-testid="composer-queue"
|
|
346
|
+
onClick={queue}
|
|
347
|
+
disabled={!attached || !draft.trim()}
|
|
348
|
+
title="deliver after the current run settles"
|
|
349
|
+
>
|
|
350
|
+
queue
|
|
351
|
+
</Button>
|
|
352
|
+
<Button
|
|
353
|
+
variant="primary"
|
|
354
|
+
size="md"
|
|
355
|
+
data-testid="composer-send"
|
|
356
|
+
onClick={submit}
|
|
357
|
+
disabled={!attached || !draft.trim()}
|
|
358
|
+
className="px-3.5"
|
|
359
|
+
>
|
|
360
|
+
{streaming ? 'steer' : 'send'}
|
|
361
|
+
</Button>
|
|
362
|
+
</div>
|
|
363
|
+
</div>
|
|
364
|
+
</PopoverAnchor>
|
|
365
|
+
</Popover>
|
|
366
|
+
</div>
|
|
367
|
+
);
|
|
368
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Badge, FileIcon } from '@agimon-ai/doompi-web-components';
|
|
2
|
+
import type { FileMention } from '../../lib/fileMentions.ts';
|
|
3
|
+
import { sessionFileUrl } from '../../../types/media.ts';
|
|
4
|
+
|
|
5
|
+
function Preview({ mention, url }: { mention: FileMention; url: string }) {
|
|
6
|
+
if (mention.kind === 'image') {
|
|
7
|
+
return (
|
|
8
|
+
<a href={url} target="_blank" rel="noreferrer">
|
|
9
|
+
<img src={url} alt={mention.path} className="max-h-[360px] max-w-full rounded border border-doom-border" />
|
|
10
|
+
</a>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
if (mention.kind === 'video') {
|
|
14
|
+
return (
|
|
15
|
+
<video
|
|
16
|
+
src={url}
|
|
17
|
+
controls
|
|
18
|
+
preload="metadata"
|
|
19
|
+
className="max-h-[360px] max-w-full rounded border border-doom-border"
|
|
20
|
+
/>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
if (mention.kind === 'pdf') {
|
|
24
|
+
return (
|
|
25
|
+
<iframe
|
|
26
|
+
src={url}
|
|
27
|
+
title={mention.path}
|
|
28
|
+
className="h-[480px] w-full rounded border border-doom-border bg-doom-deep"
|
|
29
|
+
/>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return (
|
|
33
|
+
<Badge asChild size="md" className="self-start bg-doom-panel text-doom-text hover:border-doom-blue/50">
|
|
34
|
+
<a href={url}>
|
|
35
|
+
<FileIcon className="h-3 w-3 shrink-0 text-doom-faint" />
|
|
36
|
+
{mention.path}
|
|
37
|
+
</a>
|
|
38
|
+
</Badge>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The files a message mentioned with @, shown under its text. The hub serves
|
|
44
|
+
* each from the session's working directory, so a preview is only as fresh
|
|
45
|
+
* as the file on disk when the timeline renders it.
|
|
46
|
+
*/
|
|
47
|
+
export function MentionPreviews({ sessionId, mentions }: { sessionId: string; mentions: FileMention[] }) {
|
|
48
|
+
if (mentions.length === 0) return null;
|
|
49
|
+
return (
|
|
50
|
+
<div data-testid="mention-previews" className="flex flex-col gap-2">
|
|
51
|
+
{mentions.map((mention) => (
|
|
52
|
+
<div key={mention.path} data-testid="mention-preview" data-kind={mention.kind} data-path={mention.path}>
|
|
53
|
+
<Preview mention={mention} url={sessionFileUrl(sessionId, mention.path)} />
|
|
54
|
+
</div>
|
|
55
|
+
))}
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { EmptyState } from '@agimon-ai/doompi-web-components';
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
import { sessionStoreFor } from '../../stores/sessionStore.ts';
|
|
4
|
+
import { subscribeThread, threadStoreKey, unsubscribeThread } from '../../stores/threadStore.ts';
|
|
5
|
+
import { Transcript } from './Timeline.tsx';
|
|
6
|
+
|
|
7
|
+
const THREAD_TEST_ID = 'thread-timeline';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A thread of the focused session, such as a subagent run's own journal, on
|
|
11
|
+
* the same transcript as the session itself and followed only while shown.
|
|
12
|
+
* The parent session id goes down with it so tool cards resolve renderers
|
|
13
|
+
* and slot props exactly as the main thread does.
|
|
14
|
+
*/
|
|
15
|
+
export function ThreadView({ sessionId, threadId }: { sessionId: string; threadId: string }) {
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
subscribeThread(sessionId, threadId);
|
|
18
|
+
return () => unsubscribeThread(sessionId, threadId);
|
|
19
|
+
}, [sessionId, threadId]);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<Transcript
|
|
23
|
+
store={sessionStoreFor(threadStoreKey(sessionId, threadId))}
|
|
24
|
+
sessionId={sessionId}
|
|
25
|
+
testId={THREAD_TEST_ID}
|
|
26
|
+
empty={
|
|
27
|
+
<EmptyState
|
|
28
|
+
data-testid="thread-empty"
|
|
29
|
+
title="no messages yet"
|
|
30
|
+
description="the agent's journal shows here as soon as it starts writing."
|
|
31
|
+
/>
|
|
32
|
+
}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
}
|