repoburg 1.3.188 → 1.3.196
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/agent-pane/.prettierignore +5 -0
- package/agent-pane/.prettierrc +8 -0
- package/agent-pane/backend/coverage/base.css +224 -0
- package/agent-pane/backend/coverage/block-navigation.js +87 -0
- package/agent-pane/backend/coverage/clover.xml +887 -0
- package/agent-pane/backend/coverage/coverage-final.json +10 -0
- package/agent-pane/backend/coverage/favicon.png +0 -0
- package/agent-pane/backend/coverage/index.html +176 -0
- package/agent-pane/backend/coverage/prettify.css +1 -0
- package/agent-pane/backend/coverage/prettify.js +2 -0
- package/agent-pane/backend/coverage/sort-arrow-sprite.png +0 -0
- package/agent-pane/backend/coverage/sorter.js +210 -0
- package/agent-pane/backend/coverage/src/codex/codex-provider.ts.html +1402 -0
- package/agent-pane/backend/coverage/src/codex/index.html +131 -0
- package/agent-pane/backend/coverage/src/codex/item-mapper.ts.html +331 -0
- package/agent-pane/backend/coverage/src/db/index.html +131 -0
- package/agent-pane/backend/coverage/src/db/index.ts.html +436 -0
- package/agent-pane/backend/coverage/src/db/schema.ts.html +175 -0
- package/agent-pane/backend/coverage/src/index.html +116 -0
- package/agent-pane/backend/coverage/src/index.ts.html +262 -0
- package/agent-pane/backend/coverage/src/routes/index.html +146 -0
- package/agent-pane/backend/coverage/src/routes/sessions.ts.html +991 -0
- package/agent-pane/backend/coverage/src/routes/turns.ts.html +256 -0
- package/agent-pane/backend/coverage/src/routes/workspaces.ts.html +406 -0
- package/agent-pane/backend/coverage/src/ws/index.html +116 -0
- package/agent-pane/backend/coverage/src/ws/index.ts.html +238 -0
- package/agent-pane/backend/data.db +0 -0
- package/agent-pane/backend/data.db-shm +0 -0
- package/agent-pane/backend/data.db-wal +0 -0
- package/agent-pane/backend/dist/index.js +70869 -0
- package/agent-pane/backend/package-lock.json +4529 -1013
- package/agent-pane/backend/package.json +10 -6
- package/agent-pane/backend/src/claude/claude-message-mapper.test.ts +163 -0
- package/agent-pane/backend/src/claude/claude-message-mapper.ts +154 -0
- package/agent-pane/backend/src/claude/claude-provider.test.ts +65 -0
- package/agent-pane/backend/src/claude/claude-provider.ts +482 -0
- package/agent-pane/backend/src/claude/claude-types.ts +33 -0
- package/agent-pane/backend/src/codex/app-server-client.test.ts +183 -0
- package/agent-pane/backend/src/codex/app-server-client.ts +225 -0
- package/agent-pane/backend/src/codex/app-server-types.ts +259 -0
- package/agent-pane/backend/src/codex/codex-provider-runtime-config.test.ts +56 -0
- package/agent-pane/backend/src/codex/codex-provider.test.ts +60 -0
- package/agent-pane/backend/src/codex/codex-provider.ts +521 -265
- package/agent-pane/backend/src/codex/item-mapper.test.ts +221 -0
- package/agent-pane/backend/src/codex/item-mapper.ts +35 -8
- package/agent-pane/backend/src/codex/shared-app-server.ts +25 -0
- package/agent-pane/backend/src/db/index.test.ts +125 -0
- package/agent-pane/backend/src/db/index.ts +39 -8
- package/agent-pane/backend/src/db/schema.ts +28 -74
- package/agent-pane/backend/src/index.ts +29 -2
- package/agent-pane/backend/src/integration/__fixtures__/cancel-turn.json +29 -0
- package/agent-pane/backend/src/integration/__fixtures__/delta-streaming.json +40 -0
- package/agent-pane/backend/src/integration/__fixtures__/failed-turn.json +16 -0
- package/agent-pane/backend/src/integration/__fixtures__/mcp-tool-call.json +60 -0
- package/agent-pane/backend/src/integration/__fixtures__/missing-item-started.json +34 -0
- package/agent-pane/backend/src/integration/__fixtures__/multi-item-turn.json +85 -0
- package/agent-pane/backend/src/integration/__fixtures__/multi-turn.json +48 -0
- package/agent-pane/backend/src/integration/__fixtures__/simple-turn.json +35 -0
- package/agent-pane/backend/src/integration/delta-streaming.test.ts +130 -0
- package/agent-pane/backend/src/integration/helpers/app-server-mock.ts +272 -0
- package/agent-pane/backend/src/integration/helpers/test-app.ts +108 -0
- package/agent-pane/backend/src/integration/helpers/ws-capture.ts +64 -0
- package/agent-pane/backend/src/integration/item-lifecycle.test.ts +133 -0
- package/agent-pane/backend/src/integration/mcp-tool-call.test.ts +102 -0
- package/agent-pane/backend/src/integration/missing-item-started.test.ts +91 -0
- package/agent-pane/backend/src/integration/multi-turn.test.ts +121 -0
- package/agent-pane/backend/src/integration/session-operations.test.ts +168 -0
- package/agent-pane/backend/src/integration/title-generation.test.ts +109 -0
- package/agent-pane/backend/src/integration/turn-cancel.test.ts +111 -0
- package/agent-pane/backend/src/integration/turn-failure.test.ts +97 -0
- package/agent-pane/backend/src/integration/turn-lifecycle.test.ts +141 -0
- package/agent-pane/backend/src/providers/index.ts +14 -0
- package/agent-pane/backend/src/providers/registry.test.ts +92 -0
- package/agent-pane/backend/src/providers/registry.ts +43 -0
- package/agent-pane/backend/src/providers/runtime-provider.ts +25 -0
- package/agent-pane/backend/src/routes/files.ts +31 -0
- package/agent-pane/backend/src/routes/mcp.ts +33 -0
- package/agent-pane/backend/src/routes/models.test.ts +92 -0
- package/agent-pane/backend/src/routes/models.ts +16 -0
- package/agent-pane/backend/src/routes/plugins.ts +40 -0
- package/agent-pane/backend/src/routes/providers.test.ts +69 -0
- package/agent-pane/backend/src/routes/providers.ts +57 -0
- package/agent-pane/backend/src/routes/sessions.test.ts +424 -0
- package/agent-pane/backend/src/routes/sessions.ts +295 -42
- package/agent-pane/backend/src/routes/skills.test.ts +114 -0
- package/agent-pane/backend/src/routes/skills.ts +70 -0
- package/agent-pane/backend/src/routes/turns.test.ts +152 -0
- package/agent-pane/backend/src/routes/turns.ts +36 -16
- package/agent-pane/backend/src/routes/workspaces.test.ts +197 -0
- package/agent-pane/backend/src/routes/workspaces.ts +73 -55
- package/agent-pane/backend/src/ws/index.test.ts +107 -0
- package/agent-pane/backend/src/ws/index.ts +19 -11
- package/agent-pane/backend/tsconfig.json +4 -2
- package/agent-pane/backend/vitest.config.ts +20 -0
- package/agent-pane/eslint.config.js +33 -0
- package/agent-pane/frontend/coverage/base.css +224 -0
- package/agent-pane/frontend/coverage/block-navigation.js +87 -0
- package/agent-pane/frontend/coverage/clover.xml +2962 -0
- package/agent-pane/frontend/coverage/coverage-final.json +37 -0
- package/agent-pane/frontend/coverage/favicon.png +0 -0
- package/agent-pane/frontend/coverage/index.html +251 -0
- package/agent-pane/frontend/coverage/prettify.css +1 -0
- package/agent-pane/frontend/coverage/prettify.js +2 -0
- package/agent-pane/frontend/coverage/sort-arrow-sprite.png +0 -0
- package/agent-pane/frontend/coverage/sorter.js +210 -0
- package/agent-pane/frontend/coverage/src/App.tsx.html +217 -0
- package/agent-pane/frontend/coverage/src/AppInitializer.tsx.html +223 -0
- package/agent-pane/frontend/coverage/src/api/client.ts.html +187 -0
- package/agent-pane/frontend/coverage/src/api/index.html +161 -0
- package/agent-pane/frontend/coverage/src/api/sessions.ts.html +316 -0
- package/agent-pane/frontend/coverage/src/api/websocket.ts.html +544 -0
- package/agent-pane/frontend/coverage/src/api/workspaces.ts.html +199 -0
- package/agent-pane/frontend/coverage/src/components/CollapsibleToolCalls.tsx.html +226 -0
- package/agent-pane/frontend/coverage/src/components/CommandInputBar.tsx.html +382 -0
- package/agent-pane/frontend/coverage/src/components/CommandPalette.tsx.html +715 -0
- package/agent-pane/frontend/coverage/src/components/IdeWorkspace.tsx.html +160 -0
- package/agent-pane/frontend/coverage/src/components/ModelSelector.tsx.html +478 -0
- package/agent-pane/frontend/coverage/src/components/NewSessionDialog.tsx.html +472 -0
- package/agent-pane/frontend/coverage/src/components/RenderArea.tsx.html +574 -0
- package/agent-pane/frontend/coverage/src/components/RightPanel.tsx.html +205 -0
- package/agent-pane/frontend/coverage/src/components/RuntimeIcon.tsx.html +133 -0
- package/agent-pane/frontend/coverage/src/components/SessionTabs.tsx.html +217 -0
- package/agent-pane/frontend/coverage/src/components/StatusBar.tsx.html +271 -0
- package/agent-pane/frontend/coverage/src/components/StatusDot.tsx.html +109 -0
- package/agent-pane/frontend/coverage/src/components/Toast.tsx.html +313 -0
- package/agent-pane/frontend/coverage/src/components/TurnContent.tsx.html +1216 -0
- package/agent-pane/frontend/coverage/src/components/WorkspaceSidebar.tsx.html +1138 -0
- package/agent-pane/frontend/coverage/src/components/index.html +326 -0
- package/agent-pane/frontend/coverage/src/components/panels/ChangesPanel.tsx.html +247 -0
- package/agent-pane/frontend/coverage/src/components/panels/LogsPanel.tsx.html +160 -0
- package/agent-pane/frontend/coverage/src/components/panels/MetricsPanel.tsx.html +427 -0
- package/agent-pane/frontend/coverage/src/components/panels/TurnsPanel.tsx.html +397 -0
- package/agent-pane/frontend/coverage/src/components/panels/index.html +161 -0
- package/agent-pane/frontend/coverage/src/constants/index.html +116 -0
- package/agent-pane/frontend/coverage/src/constants/models.ts.html +406 -0
- package/agent-pane/frontend/coverage/src/hooks/index.html +146 -0
- package/agent-pane/frontend/coverage/src/hooks/useEscapeKey.ts.html +124 -0
- package/agent-pane/frontend/coverage/src/hooks/useOutsideClick.ts.html +142 -0
- package/agent-pane/frontend/coverage/src/hooks/useSessionStatus.ts.html +187 -0
- package/agent-pane/frontend/coverage/src/index.html +146 -0
- package/agent-pane/frontend/coverage/src/main.tsx.html +115 -0
- package/agent-pane/frontend/coverage/src/store/index.html +116 -0
- package/agent-pane/frontend/coverage/src/store/store.ts.html +2404 -0
- package/agent-pane/frontend/coverage/src/test/component-helpers.ts.html +190 -0
- package/agent-pane/frontend/coverage/src/test/fixtures.ts.html +367 -0
- package/agent-pane/frontend/coverage/src/test/index.html +131 -0
- package/agent-pane/frontend/coverage/src/types/index.html +116 -0
- package/agent-pane/frontend/coverage/src/types/index.ts.html +466 -0
- package/agent-pane/frontend/coverage/src/utils/changes.ts.html +202 -0
- package/agent-pane/frontend/coverage/src/utils/duration.ts.html +127 -0
- package/agent-pane/frontend/coverage/src/utils/index.html +131 -0
- package/agent-pane/frontend/dist/assets/index-Bg0PF2aV.css +1 -0
- package/agent-pane/frontend/dist/assets/index-BlybeX5e.js +281 -0
- package/agent-pane/frontend/dist/index.html +19 -0
- package/agent-pane/frontend/package-lock.json +2434 -213
- package/agent-pane/frontend/package.json +10 -3
- package/agent-pane/frontend/src/App.test.tsx +74 -0
- package/agent-pane/frontend/src/App.tsx +1 -1
- package/agent-pane/frontend/src/AppInitializer.test.tsx +142 -0
- package/agent-pane/frontend/src/AppInitializer.tsx +1 -1
- package/agent-pane/frontend/src/api/client.test.ts +126 -0
- package/agent-pane/frontend/src/api/client.ts +22 -29
- package/agent-pane/frontend/src/api/models.ts +7 -0
- package/agent-pane/frontend/src/api/providers.ts +12 -0
- package/agent-pane/frontend/src/api/sessions.test.ts +241 -0
- package/agent-pane/frontend/src/api/sessions.ts +43 -1
- package/agent-pane/frontend/src/api/skills.ts +12 -0
- package/agent-pane/frontend/src/api/websocket.test.ts +288 -0
- package/agent-pane/frontend/src/api/websocket.ts +27 -2
- package/agent-pane/frontend/src/api/workspaces.test.ts +112 -0
- package/agent-pane/frontend/src/api/workspaces.ts +17 -3
- package/agent-pane/frontend/src/components/CollapsibleToolCalls.test.tsx +49 -0
- package/agent-pane/frontend/src/components/CollapsibleToolCalls.tsx +2 -5
- package/agent-pane/frontend/src/components/CommandInputBar.test.tsx +133 -0
- package/agent-pane/frontend/src/components/CommandInputBar.tsx +296 -17
- package/agent-pane/frontend/src/components/CommandPalette.test.tsx +230 -0
- package/agent-pane/frontend/src/components/CommandPalette.tsx +26 -23
- package/agent-pane/frontend/src/components/IdeWorkspace.tsx +1 -1
- package/agent-pane/frontend/src/components/ModelSelector.test.tsx +129 -0
- package/agent-pane/frontend/src/components/ModelSelector.tsx +191 -0
- package/agent-pane/frontend/src/components/NewSessionDialog.test.tsx +95 -0
- package/agent-pane/frontend/src/components/NewSessionDialog.tsx +58 -13
- package/agent-pane/frontend/src/components/RenderArea.test.tsx +160 -0
- package/agent-pane/frontend/src/components/RenderArea.tsx +18 -25
- package/agent-pane/frontend/src/components/RightPanel.test.tsx +43 -0
- package/agent-pane/frontend/src/components/RightPanel.tsx +1 -1
- package/agent-pane/frontend/src/components/RuntimeIcon.test.tsx +32 -0
- package/agent-pane/frontend/src/components/RuntimeIcon.tsx +1 -1
- package/agent-pane/frontend/src/components/SessionTabs.test.tsx +72 -0
- package/agent-pane/frontend/src/components/SessionTabs.tsx +12 -12
- package/agent-pane/frontend/src/components/StatusBar.test.tsx +56 -0
- package/agent-pane/frontend/src/components/StatusBar.tsx +25 -31
- package/agent-pane/frontend/src/components/StatusDot.test.tsx +25 -0
- package/agent-pane/frontend/src/components/StatusDot.tsx +2 -7
- package/agent-pane/frontend/src/components/Toast.test.tsx +79 -0
- package/agent-pane/frontend/src/components/Toast.tsx +2 -6
- package/agent-pane/frontend/src/components/TurnContent.test.tsx +287 -0
- package/agent-pane/frontend/src/components/TurnContent.tsx +107 -24
- package/agent-pane/frontend/src/components/WorkspaceSidebar.test.tsx +297 -0
- package/agent-pane/frontend/src/components/WorkspaceSidebar.tsx +33 -32
- package/agent-pane/frontend/src/components/input/OptionSubPopup.tsx +32 -0
- package/agent-pane/frontend/src/components/input/SkillMentionPopup.test.tsx +63 -0
- package/agent-pane/frontend/src/components/input/SkillMentionPopup.tsx +78 -0
- package/agent-pane/frontend/src/components/input/SlashPopup.test.tsx +96 -0
- package/agent-pane/frontend/src/components/input/SlashPopup.tsx +63 -0
- package/agent-pane/frontend/src/components/panels/ChangesPanel.test.tsx +61 -0
- package/agent-pane/frontend/src/components/panels/ChangesPanel.tsx +12 -39
- package/agent-pane/frontend/src/components/panels/LogsPanel.test.tsx +58 -0
- package/agent-pane/frontend/src/components/panels/LogsPanel.tsx +1 -1
- package/agent-pane/frontend/src/components/panels/MetricsPanel.test.tsx +71 -0
- package/agent-pane/frontend/src/components/panels/MetricsPanel.tsx +63 -31
- package/agent-pane/frontend/src/components/panels/TurnsPanel.test.tsx +83 -0
- package/agent-pane/frontend/src/components/panels/TurnsPanel.tsx +66 -7
- package/agent-pane/frontend/src/constants/models.test.ts +97 -0
- package/agent-pane/frontend/src/constants/models.ts +129 -0
- package/agent-pane/frontend/src/hooks/useEscapeKey.test.ts +42 -0
- package/agent-pane/frontend/src/hooks/useEscapeKey.ts +13 -0
- package/agent-pane/frontend/src/hooks/useOutsideClick.test.ts +92 -0
- package/agent-pane/frontend/src/hooks/useOutsideClick.ts +19 -0
- package/agent-pane/frontend/src/hooks/useProvider.ts +44 -0
- package/agent-pane/frontend/src/hooks/useSessionStatus.test.ts +114 -0
- package/agent-pane/frontend/src/hooks/useSessionStatus.ts +34 -0
- package/agent-pane/frontend/src/main.test.tsx +37 -0
- package/agent-pane/frontend/src/main.tsx +1 -1
- package/agent-pane/frontend/src/store/sessionSlice.ts +417 -0
- package/agent-pane/frontend/src/store/store.test.ts +1284 -0
- package/agent-pane/frontend/src/store/store.ts +16 -617
- package/agent-pane/frontend/src/store/uiSlice.ts +83 -0
- package/agent-pane/frontend/src/store/workspaceSlice.ts +77 -0
- package/agent-pane/frontend/src/store/wsSlice.ts +292 -0
- package/agent-pane/frontend/src/styles/app.css +113 -7
- package/agent-pane/frontend/src/test/component-helpers.ts +36 -0
- package/agent-pane/frontend/src/test/fixtures.ts +96 -0
- package/agent-pane/frontend/src/test-setup.ts +5 -0
- package/agent-pane/frontend/src/types/index.test.ts +128 -0
- package/agent-pane/frontend/src/types/index.ts +80 -104
- package/agent-pane/frontend/src/utils/changes.test.ts +81 -0
- package/agent-pane/frontend/src/utils/changes.ts +39 -0
- package/agent-pane/frontend/src/utils/duration.test.ts +71 -0
- package/agent-pane/frontend/src/utils/duration.ts +14 -0
- package/agent-pane/frontend/tsconfig.json +4 -1
- package/agent-pane/frontend/vite.config.ts +6 -0
- package/agent-pane/frontend/vitest.config.ts +29 -0
- package/agent-pane/package-lock.json +1527 -0
- package/agent-pane/package.json +19 -0
- package/agent-pane/shared/provider-types.ts +53 -0
- package/agent-pane/shared/types.ts +107 -0
- package/agentpane-branch-session.png +0 -0
- package/agentpane-expanded-all.png +0 -0
- package/agentpane-fixed-rendering.png +0 -0
- package/agentpane-focus-check.png +0 -0
- package/agentpane-initial.png +0 -0
- package/agentpane-mcp-detail.png +0 -0
- package/agentpane-no-focus-ring.png +0 -0
- package/agentpane-soft-focus.png +0 -0
- package/backend/dist/src/llm-provider/ollama-llm.provider.js +1 -0
- package/backend/dist/src/llm-provider/ollama-llm.provider.js.map +1 -1
- package/backend/dist/tsconfig.build.tsbuildinfo +1 -1
- package/data.db +0 -0
- package/empty-state.png +0 -0
- package/frontend-home.png +0 -0
- package/model-selector-open.png +0 -0
- package/package.json +1 -1
- package/slash-popup-working.png +0 -0
- package/agent-pane/frontend/src/components/DiffViewer.tsx +0 -27
|
@@ -1,40 +1,319 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
2
|
-
import {
|
|
1
|
+
import { useRef, useState, useEffect } from 'react';
|
|
2
|
+
import { ArrowUp, Square } from 'lucide-react';
|
|
3
3
|
import { useStore } from '../store/store.ts';
|
|
4
|
+
import SlashPopup, { type SlashCommand, filterCommands } from './input/SlashPopup.tsx';
|
|
5
|
+
import OptionSubPopup from './input/OptionSubPopup.tsx';
|
|
6
|
+
import SkillMentionPopup from './input/SkillMentionPopup.tsx';
|
|
7
|
+
import { useProvider } from '../hooks/useProvider.ts';
|
|
8
|
+
import type { SessionRuntimeConfig, Extension } from '../types/index.ts';
|
|
9
|
+
|
|
10
|
+
const MIN_HEIGHT = 32;
|
|
11
|
+
const MAX_HEIGHT = 200;
|
|
12
|
+
|
|
13
|
+
/** Parse runtime_config from the active session detail. */
|
|
14
|
+
function parseRuntimeConfig(json: string | null | undefined): SessionRuntimeConfig {
|
|
15
|
+
if (!json) return {};
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(json) as SessionRuntimeConfig;
|
|
18
|
+
} catch {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
4
22
|
|
|
5
23
|
export default function CommandInputBar() {
|
|
6
24
|
const [value, setValue] = useState('');
|
|
7
|
-
const
|
|
25
|
+
const taRef = useRef<HTMLTextAreaElement>(null);
|
|
26
|
+
const { submitPrompt, cancelTurn, updateRuntimeConfig, setShowModelSelector } = useStore();
|
|
27
|
+
const activeSessionId = useStore((s) => s.activeSessionId);
|
|
8
28
|
const detail = useStore((s) => (s.activeSessionId ? s.sessionDetails[s.activeSessionId] : null));
|
|
29
|
+
const restoredPrompt = useStore((s) => s.restoredPrompt);
|
|
9
30
|
const isRunning = detail?.is_running === 1;
|
|
31
|
+
const noSession = !activeSessionId;
|
|
32
|
+
|
|
33
|
+
const { capabilities } = useProvider(detail?.runtime_type);
|
|
34
|
+
|
|
35
|
+
// Slash popup state
|
|
36
|
+
const [slashOpen, setSlashOpen] = useState(false);
|
|
37
|
+
const [slashFilter, setSlashFilter] = useState('');
|
|
38
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
39
|
+
const [subPopup, setSubPopup] = useState<SlashCommand['subPopup']>(undefined);
|
|
40
|
+
const [skillPopup, setSkillPopup] = useState(false);
|
|
41
|
+
const [skillFilter, setSkillFilter] = useState('');
|
|
42
|
+
const [selectedSkills, setSelectedSkills] = useState<Extension[]>([]);
|
|
43
|
+
|
|
44
|
+
const commands = slashOpen ? filterCommands(slashFilter, capabilities) : [];
|
|
45
|
+
|
|
46
|
+
// Restore prompt after cancel
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (restoredPrompt !== null) {
|
|
49
|
+
setValue(restoredPrompt);
|
|
50
|
+
requestAnimationFrame(() => {
|
|
51
|
+
if (taRef.current) {
|
|
52
|
+
taRef.current.style.height = 'auto';
|
|
53
|
+
taRef.current.style.height = Math.min(taRef.current.scrollHeight, MAX_HEIGHT) + 'px';
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
useStore.setState({ restoredPrompt: null });
|
|
57
|
+
}
|
|
58
|
+
}, [restoredPrompt]);
|
|
59
|
+
|
|
60
|
+
// ESC to cancel running turn (global — textarea is disabled while running)
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (!isRunning) return;
|
|
63
|
+
function onKey(e: KeyboardEvent) {
|
|
64
|
+
if (e.key === 'Escape') {
|
|
65
|
+
e.preventDefault();
|
|
66
|
+
cancelTurn();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
window.addEventListener('keydown', onKey);
|
|
70
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
71
|
+
}, [isRunning, cancelTurn]);
|
|
72
|
+
|
|
73
|
+
// Close slash popup when session changes
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
setSlashOpen(false);
|
|
76
|
+
setSubPopup(undefined);
|
|
77
|
+
setSkillPopup(false);
|
|
78
|
+
setSelectedSkills([]);
|
|
79
|
+
}, [activeSessionId]);
|
|
80
|
+
|
|
81
|
+
function autoResize() {
|
|
82
|
+
const el = taRef.current;
|
|
83
|
+
if (!el) return;
|
|
84
|
+
el.style.height = 'auto';
|
|
85
|
+
el.style.height = Math.min(el.scrollHeight, MAX_HEIGHT) + 'px';
|
|
86
|
+
}
|
|
10
87
|
|
|
11
88
|
function handleSend() {
|
|
12
89
|
const v = value.trim();
|
|
13
90
|
if (!v || isRunning) return;
|
|
14
|
-
|
|
15
|
-
|
|
91
|
+
|
|
92
|
+
// Build prompt with skill prefixes
|
|
93
|
+
let prompt = v;
|
|
94
|
+
if (selectedSkills.length > 0) {
|
|
95
|
+
const skillPrefix = selectedSkills.map((s) => `$${s.name}`).join(' ');
|
|
96
|
+
prompt = `${skillPrefix}\n\n${v}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
setValue('');
|
|
100
|
+
setSelectedSkills([]);
|
|
101
|
+
if (taRef.current) taRef.current.style.height = MIN_HEIGHT + 'px';
|
|
102
|
+
setSlashOpen(false);
|
|
103
|
+
setSubPopup(undefined);
|
|
104
|
+
setSkillPopup(false);
|
|
105
|
+
submitPrompt(prompt);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function handleSlashCommand(cmd: SlashCommand) {
|
|
109
|
+
if (cmd.subPopup) {
|
|
110
|
+
setSubPopup(cmd.subPopup);
|
|
111
|
+
} else if (cmd.id === 'skills') {
|
|
112
|
+
setSkillPopup(true);
|
|
113
|
+
setSkillFilter('');
|
|
114
|
+
setSlashOpen(false);
|
|
115
|
+
setValue('');
|
|
116
|
+
} else if (cmd.id === 'model') {
|
|
117
|
+
setShowModelSelector(true);
|
|
118
|
+
setSlashOpen(false);
|
|
119
|
+
setValue('');
|
|
120
|
+
} else {
|
|
121
|
+
setSlashOpen(false);
|
|
122
|
+
setValue('');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function handleSkillSelect(skill: Extension) {
|
|
127
|
+
setSelectedSkills((prev) => [...prev, skill]);
|
|
128
|
+
setSkillPopup(false);
|
|
129
|
+
setValue('');
|
|
130
|
+
if (taRef.current) taRef.current.style.height = MIN_HEIGHT + 'px';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function handleOptionSelect(optionValue: string) {
|
|
134
|
+
const config = parseRuntimeConfig(detail?.runtime_config);
|
|
135
|
+
|
|
136
|
+
if (subPopup === 'sandbox') {
|
|
137
|
+
config.sandboxMode = optionValue as SessionRuntimeConfig['sandboxMode'];
|
|
138
|
+
} else if (subPopup === 'websearch') {
|
|
139
|
+
config.webSearchMode = optionValue as SessionRuntimeConfig['webSearchMode'];
|
|
140
|
+
} else if (subPopup === 'network') {
|
|
141
|
+
config.networkAccessEnabled = optionValue === 'true';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
updateRuntimeConfig(config as Record<string, unknown>);
|
|
145
|
+
setSubPopup(undefined);
|
|
146
|
+
setSlashOpen(false);
|
|
147
|
+
setValue('');
|
|
148
|
+
if (taRef.current) taRef.current.style.height = MIN_HEIGHT + 'px';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function handleKeyDown(e: React.KeyboardEvent) {
|
|
152
|
+
// Slash popup keyboard navigation
|
|
153
|
+
if (slashOpen && !subPopup) {
|
|
154
|
+
if (e.key === 'ArrowDown') {
|
|
155
|
+
e.preventDefault();
|
|
156
|
+
setSelectedIndex((i) => Math.min(i + 1, commands.length - 1));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (e.key === 'ArrowUp') {
|
|
160
|
+
e.preventDefault();
|
|
161
|
+
setSelectedIndex((i) => Math.max(i - 1, 0));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (e.key === 'Enter') {
|
|
165
|
+
e.preventDefault();
|
|
166
|
+
if (commands[selectedIndex]) {
|
|
167
|
+
handleSlashCommand(commands[selectedIndex]);
|
|
168
|
+
}
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (e.key === 'Escape') {
|
|
172
|
+
e.preventDefault();
|
|
173
|
+
setSlashOpen(false);
|
|
174
|
+
setValue('');
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (subPopup && e.key === 'Escape') {
|
|
180
|
+
e.preventDefault();
|
|
181
|
+
setSubPopup(undefined);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Cmd/Ctrl+Enter to send
|
|
186
|
+
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey) && !isRunning) {
|
|
187
|
+
e.preventDefault();
|
|
188
|
+
handleSend();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ESC to cancel while running
|
|
193
|
+
if (e.key === 'Escape' && isRunning) {
|
|
194
|
+
e.preventDefault();
|
|
195
|
+
cancelTurn();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function handleChange(e: React.ChangeEvent<HTMLTextAreaElement>) {
|
|
200
|
+
const newValue = e.target.value;
|
|
201
|
+
setValue(newValue);
|
|
202
|
+
autoResize();
|
|
203
|
+
|
|
204
|
+
// Detect `/` at start of input → slash popup
|
|
205
|
+
if (newValue.startsWith('/') && !newValue.includes(' ') && !isRunning) {
|
|
206
|
+
setSlashOpen(true);
|
|
207
|
+
setSlashFilter(newValue.slice(1));
|
|
208
|
+
setSelectedIndex(0);
|
|
209
|
+
} else {
|
|
210
|
+
setSlashOpen(false);
|
|
211
|
+
setSubPopup(undefined);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Detect `$` trigger anywhere → skill popup
|
|
215
|
+
if (newValue.startsWith('$') && !isRunning) {
|
|
216
|
+
setSkillPopup(true);
|
|
217
|
+
setSkillFilter(newValue.slice(1));
|
|
218
|
+
} else if (!newValue.startsWith('$')) {
|
|
219
|
+
setSkillPopup(false);
|
|
220
|
+
}
|
|
16
221
|
}
|
|
17
222
|
|
|
223
|
+
const currentConfig = parseRuntimeConfig(detail?.runtime_config);
|
|
224
|
+
|
|
225
|
+
const sandboxOptions = [
|
|
226
|
+
{ value: 'read-only', label: 'Read Only', description: 'Agent can only read files' },
|
|
227
|
+
{ value: 'workspace-write', label: 'Workspace Write', description: 'Agent can read and write in workspace' },
|
|
228
|
+
{ value: 'danger-full-access', label: 'Danger Full Access', description: 'Agent has unrestricted access' },
|
|
229
|
+
];
|
|
230
|
+
|
|
231
|
+
const webSearchOptions = [
|
|
232
|
+
{ value: 'disabled', label: 'Disabled', description: 'No web search' },
|
|
233
|
+
{ value: 'cached', label: 'Cached', description: 'Use cached search results' },
|
|
234
|
+
{ value: 'live', label: 'Live', description: 'Perform live web searches' },
|
|
235
|
+
];
|
|
236
|
+
|
|
237
|
+
const networkOptions = [
|
|
238
|
+
{ value: 'false', label: 'Disabled', description: 'No network access' },
|
|
239
|
+
{ value: 'true', label: 'Enabled', description: 'Allow network access in sandbox' },
|
|
240
|
+
];
|
|
241
|
+
|
|
18
242
|
return (
|
|
19
243
|
<div className="cmd-bar">
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
244
|
+
{slashOpen && !subPopup && (
|
|
245
|
+
<SlashPopup
|
|
246
|
+
commands={commands}
|
|
247
|
+
selectedIndex={selectedIndex}
|
|
248
|
+
onSelect={handleSlashCommand}
|
|
249
|
+
onHover={setSelectedIndex}
|
|
250
|
+
/>
|
|
251
|
+
)}
|
|
252
|
+
{slashOpen && subPopup === 'sandbox' && (
|
|
253
|
+
<OptionSubPopup
|
|
254
|
+
title="Sandbox Mode"
|
|
255
|
+
options={sandboxOptions}
|
|
256
|
+
currentValue={currentConfig.sandboxMode}
|
|
257
|
+
onSelect={handleOptionSelect}
|
|
258
|
+
onClose={() => setSubPopup(undefined)}
|
|
259
|
+
/>
|
|
260
|
+
)}
|
|
261
|
+
{slashOpen && subPopup === 'websearch' && (
|
|
262
|
+
<OptionSubPopup
|
|
263
|
+
title="Web Search"
|
|
264
|
+
options={webSearchOptions}
|
|
265
|
+
currentValue={currentConfig.webSearchMode}
|
|
266
|
+
onSelect={handleOptionSelect}
|
|
267
|
+
onClose={() => setSubPopup(undefined)}
|
|
268
|
+
/>
|
|
269
|
+
)}
|
|
270
|
+
{slashOpen && subPopup === 'network' && (
|
|
271
|
+
<OptionSubPopup
|
|
272
|
+
title="Network Access"
|
|
273
|
+
options={networkOptions}
|
|
274
|
+
currentValue={currentConfig.networkAccessEnabled === true ? 'true' : 'false'}
|
|
275
|
+
onSelect={handleOptionSelect}
|
|
276
|
+
onClose={() => setSubPopup(undefined)}
|
|
277
|
+
/>
|
|
278
|
+
)}
|
|
279
|
+
{skillPopup && (
|
|
280
|
+
<SkillMentionPopup
|
|
281
|
+
filter={skillFilter}
|
|
282
|
+
onSelect={handleSkillSelect}
|
|
283
|
+
onClose={() => { setSkillPopup(false); setValue(''); }}
|
|
284
|
+
/>
|
|
285
|
+
)}
|
|
286
|
+
{selectedSkills.length > 0 && (
|
|
287
|
+
<div className="skill-badges">
|
|
288
|
+
{selectedSkills.map((s) => (
|
|
289
|
+
<span key={s.id} className="skill-badge">
|
|
290
|
+
${s.name}
|
|
291
|
+
<button className="skill-badge-remove" onClick={() => setSelectedSkills((prev) => prev.filter((x) => x.id !== s.id))}>×</button>
|
|
292
|
+
</span>
|
|
293
|
+
))}
|
|
294
|
+
</div>
|
|
295
|
+
)}
|
|
296
|
+
<textarea
|
|
297
|
+
ref={taRef}
|
|
298
|
+
className="cmd-textarea"
|
|
299
|
+
placeholder={
|
|
300
|
+
noSession ? 'Select or create a session…' : isRunning ? 'Agent is running…' : 'Type a message… Use / for commands'
|
|
301
|
+
}
|
|
23
302
|
autoComplete="off"
|
|
24
303
|
value={value}
|
|
25
|
-
disabled={isRunning}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
304
|
+
disabled={isRunning || noSession}
|
|
305
|
+
rows={1}
|
|
306
|
+
style={{ minHeight: MIN_HEIGHT, maxHeight: MAX_HEIGHT }}
|
|
307
|
+
onChange={handleChange}
|
|
308
|
+
onKeyDown={handleKeyDown}
|
|
30
309
|
/>
|
|
31
310
|
{isRunning ? (
|
|
32
|
-
<button className="cmd-
|
|
33
|
-
<Square size={
|
|
311
|
+
<button className="cmd-stop-btn" onClick={() => cancelTurn()} title="Stop (Esc)">
|
|
312
|
+
<Square size={15} />
|
|
34
313
|
</button>
|
|
35
314
|
) : (
|
|
36
|
-
<button className="cmd-send" onClick={handleSend} disabled={!value.trim()}>
|
|
37
|
-
<
|
|
315
|
+
<button className="cmd-send-btn" onClick={handleSend} disabled={!value.trim() || noSession} title="Send (⌘↵)">
|
|
316
|
+
<ArrowUp size={16} />
|
|
38
317
|
</button>
|
|
39
318
|
)}
|
|
40
319
|
</div>
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { render, screen, fireEvent, cleanup } from '@testing-library/react';
|
|
3
|
+
import CommandPalette from './CommandPalette.tsx';
|
|
4
|
+
import { resetStore } from '../test/component-helpers.ts';
|
|
5
|
+
import { makeSession, makeDetail } from '../test/fixtures.ts';
|
|
6
|
+
|
|
7
|
+
describe('CommandPalette', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
resetStore();
|
|
10
|
+
});
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
cleanup();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('returns null when show=false', () => {
|
|
16
|
+
const { container } = render(<CommandPalette />);
|
|
17
|
+
expect(container.firstChild).toBeNull();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('renders search input when show=true', () => {
|
|
21
|
+
resetStore({ showCommandPalette: true });
|
|
22
|
+
render(<CommandPalette />);
|
|
23
|
+
expect(screen.getByPlaceholderText('Type a command…')).toBeInTheDocument();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('shows all available commands when query is empty', () => {
|
|
27
|
+
resetStore({ showCommandPalette: true });
|
|
28
|
+
render(<CommandPalette />);
|
|
29
|
+
// "New Session" is always available
|
|
30
|
+
expect(screen.getByText('New Session')).toBeInTheDocument();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('filters commands by query', () => {
|
|
34
|
+
resetStore({ showCommandPalette: true });
|
|
35
|
+
render(<CommandPalette />);
|
|
36
|
+
const input = screen.getByPlaceholderText('Type a command…');
|
|
37
|
+
fireEvent.change(input, { target: { value: 'new' } });
|
|
38
|
+
expect(screen.getByText('New Session')).toBeInTheDocument();
|
|
39
|
+
expect(screen.queryByText('Close Tab')).not.toBeInTheDocument();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('shows "No matching commands" when filter empty', () => {
|
|
43
|
+
resetStore({ showCommandPalette: true });
|
|
44
|
+
render(<CommandPalette />);
|
|
45
|
+
const input = screen.getByPlaceholderText('Type a command…');
|
|
46
|
+
fireEvent.change(input, { target: { value: 'xyzzy' } });
|
|
47
|
+
expect(screen.getByText('No matching commands')).toBeInTheDocument();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('ArrowDown moves selection down', () => {
|
|
51
|
+
// Need active session so multiple commands are available
|
|
52
|
+
const session = makeSession('s1');
|
|
53
|
+
const detail = makeDetail('s1', []);
|
|
54
|
+
resetStore({ showCommandPalette: true, activeSessionId: 's1', sessions: [session], sessionDetails: { s1: detail } });
|
|
55
|
+
render(<CommandPalette />);
|
|
56
|
+
const input = screen.getByPlaceholderText('Type a command…');
|
|
57
|
+
fireEvent.keyDown(input, { key: 'ArrowDown' });
|
|
58
|
+
const items = document.querySelectorAll('.palette-item');
|
|
59
|
+
expect(items[1]).toHaveClass('selected');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('ArrowUp moves selection up', () => {
|
|
63
|
+
const session = makeSession('s1');
|
|
64
|
+
const detail = makeDetail('s1', []);
|
|
65
|
+
resetStore({ showCommandPalette: true, activeSessionId: 's1', sessions: [session], sessionDetails: { s1: detail } });
|
|
66
|
+
render(<CommandPalette />);
|
|
67
|
+
const input = screen.getByPlaceholderText('Type a command…');
|
|
68
|
+
// Move down twice, then up once
|
|
69
|
+
fireEvent.keyDown(input, { key: 'ArrowDown' });
|
|
70
|
+
fireEvent.keyDown(input, { key: 'ArrowDown' });
|
|
71
|
+
fireEvent.keyDown(input, { key: 'ArrowUp' });
|
|
72
|
+
const items = document.querySelectorAll('.palette-item');
|
|
73
|
+
expect(items[1]).toHaveClass('selected');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('Enter executes selected command and closes palette', () => {
|
|
77
|
+
const setShowCommandPalette = vi.fn();
|
|
78
|
+
const setShowNewSessionDialog = vi.fn();
|
|
79
|
+
resetStore({ showCommandPalette: true, setShowCommandPalette, setShowNewSessionDialog });
|
|
80
|
+
render(<CommandPalette />);
|
|
81
|
+
const input = screen.getByPlaceholderText('Type a command…');
|
|
82
|
+
fireEvent.keyDown(input, { key: 'Enter' });
|
|
83
|
+
// First command is "New Session" — always available
|
|
84
|
+
expect(setShowNewSessionDialog).toHaveBeenCalledWith(true);
|
|
85
|
+
expect(setShowCommandPalette).toHaveBeenCalledWith(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('ESC closes palette', () => {
|
|
89
|
+
const setShowCommandPalette = vi.fn();
|
|
90
|
+
resetStore({ showCommandPalette: true, setShowCommandPalette });
|
|
91
|
+
render(<CommandPalette />);
|
|
92
|
+
fireEvent.keyDown(screen.getByPlaceholderText('Type a command…'), { key: 'Escape' });
|
|
93
|
+
expect(setShowCommandPalette).toHaveBeenCalledWith(false);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('click on command executes and closes', () => {
|
|
97
|
+
const setShowCommandPalette = vi.fn();
|
|
98
|
+
const setShowNewSessionDialog = vi.fn();
|
|
99
|
+
resetStore({ showCommandPalette: true, setShowCommandPalette, setShowNewSessionDialog });
|
|
100
|
+
render(<CommandPalette />);
|
|
101
|
+
fireEvent.click(screen.getByText('New Session'));
|
|
102
|
+
expect(setShowNewSessionDialog).toHaveBeenCalledWith(true);
|
|
103
|
+
expect(setShowCommandPalette).toHaveBeenCalledWith(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// --- command actions ---
|
|
107
|
+
|
|
108
|
+
it('Delete Session action calls deleteSession', () => {
|
|
109
|
+
const setShowCommandPalette = vi.fn();
|
|
110
|
+
const deleteSession = vi.fn().mockResolvedValue(undefined);
|
|
111
|
+
const session = makeSession('s1');
|
|
112
|
+
resetStore({
|
|
113
|
+
showCommandPalette: true,
|
|
114
|
+
activeSessionId: 's1',
|
|
115
|
+
sessions: [session],
|
|
116
|
+
setShowCommandPalette,
|
|
117
|
+
deleteSession,
|
|
118
|
+
});
|
|
119
|
+
render(<CommandPalette />);
|
|
120
|
+
fireEvent.click(screen.getByText('Delete Session'));
|
|
121
|
+
expect(deleteSession).toHaveBeenCalledWith('s1');
|
|
122
|
+
expect(setShowCommandPalette).toHaveBeenCalledWith(false);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('Send Message action focuses .cmd-input element', () => {
|
|
126
|
+
// Stub a mock input element with focus
|
|
127
|
+
const mockFocus = vi.fn();
|
|
128
|
+
const mockInput = { focus: mockFocus } as unknown as HTMLInputElement;
|
|
129
|
+
const origQuery = document.querySelector;
|
|
130
|
+
document.querySelector = vi.fn((sel: string) =>
|
|
131
|
+
sel === '.cmd-input' ? mockInput : origQuery.call(document, sel),
|
|
132
|
+
) as typeof document.querySelector;
|
|
133
|
+
|
|
134
|
+
const session = makeSession('s1');
|
|
135
|
+
const detail = makeDetail('s1', []);
|
|
136
|
+
resetStore({
|
|
137
|
+
showCommandPalette: true,
|
|
138
|
+
activeSessionId: 's1',
|
|
139
|
+
sessions: [session],
|
|
140
|
+
sessionDetails: { s1: detail },
|
|
141
|
+
});
|
|
142
|
+
render(<CommandPalette />);
|
|
143
|
+
fireEvent.click(screen.getByText('Send Message'));
|
|
144
|
+
expect(mockFocus).toHaveBeenCalled();
|
|
145
|
+
document.querySelector = origQuery;
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('Cancel Turn action calls cancelTurn when running', () => {
|
|
149
|
+
const setShowCommandPalette = vi.fn();
|
|
150
|
+
const cancelTurn = vi.fn().mockResolvedValue(undefined);
|
|
151
|
+
const session = makeSession('s1', { is_running: 1 });
|
|
152
|
+
const detail = makeDetail('s1', [], { is_running: 1 });
|
|
153
|
+
resetStore({
|
|
154
|
+
showCommandPalette: true,
|
|
155
|
+
activeSessionId: 's1',
|
|
156
|
+
sessions: [session],
|
|
157
|
+
sessionDetails: { s1: detail },
|
|
158
|
+
setShowCommandPalette,
|
|
159
|
+
cancelTurn,
|
|
160
|
+
});
|
|
161
|
+
render(<CommandPalette />);
|
|
162
|
+
fireEvent.click(screen.getByText('Cancel Turn'));
|
|
163
|
+
expect(cancelTurn).toHaveBeenCalled();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('tab-switch commands call switchRightTab with correct tab', () => {
|
|
167
|
+
const switchRightTab = vi.fn();
|
|
168
|
+
const session = makeSession('s1');
|
|
169
|
+
const detail = makeDetail('s1', []);
|
|
170
|
+
resetStore({
|
|
171
|
+
showCommandPalette: true,
|
|
172
|
+
activeSessionId: 's1',
|
|
173
|
+
sessions: [session],
|
|
174
|
+
sessionDetails: { s1: detail },
|
|
175
|
+
switchRightTab,
|
|
176
|
+
});
|
|
177
|
+
render(<CommandPalette />);
|
|
178
|
+
fireEvent.click(screen.getByText('Switch to Turns Tab'));
|
|
179
|
+
expect(switchRightTab).toHaveBeenCalledWith('turns');
|
|
180
|
+
fireEvent.click(screen.getByText('Switch to Metrics Tab'));
|
|
181
|
+
expect(switchRightTab).toHaveBeenCalledWith('metrics');
|
|
182
|
+
fireEvent.click(screen.getByText('Switch to Changes Tab'));
|
|
183
|
+
expect(switchRightTab).toHaveBeenCalledWith('changes');
|
|
184
|
+
fireEvent.click(screen.getByText('Switch to Logs Tab'));
|
|
185
|
+
expect(switchRightTab).toHaveBeenCalledWith('logs');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('Close Tab action calls closeTab', () => {
|
|
189
|
+
const setShowCommandPalette = vi.fn();
|
|
190
|
+
const closeTab = vi.fn().mockResolvedValue(undefined);
|
|
191
|
+
const session = makeSession('s1');
|
|
192
|
+
resetStore({
|
|
193
|
+
showCommandPalette: true,
|
|
194
|
+
activeSessionId: 's1',
|
|
195
|
+
sessions: [session],
|
|
196
|
+
setShowCommandPalette,
|
|
197
|
+
closeTab,
|
|
198
|
+
});
|
|
199
|
+
render(<CommandPalette />);
|
|
200
|
+
fireEvent.click(screen.getByText('Close Tab'));
|
|
201
|
+
expect(closeTab).toHaveBeenCalledWith('s1');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// --- UI interactions ---
|
|
205
|
+
|
|
206
|
+
it('overlay click closes palette', () => {
|
|
207
|
+
const setShowCommandPalette = vi.fn();
|
|
208
|
+
resetStore({ showCommandPalette: true, setShowCommandPalette });
|
|
209
|
+
render(<CommandPalette />);
|
|
210
|
+
fireEvent.click(document.querySelector('.palette-overlay')!);
|
|
211
|
+
expect(setShowCommandPalette).toHaveBeenCalledWith(false);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('mouse enter on item sets selected class', () => {
|
|
215
|
+
const session = makeSession('s1');
|
|
216
|
+
const detail = makeDetail('s1', []);
|
|
217
|
+
resetStore({
|
|
218
|
+
showCommandPalette: true,
|
|
219
|
+
activeSessionId: 's1',
|
|
220
|
+
sessions: [session],
|
|
221
|
+
sessionDetails: { s1: detail },
|
|
222
|
+
});
|
|
223
|
+
render(<CommandPalette />);
|
|
224
|
+
const items = document.querySelectorAll('.palette-item');
|
|
225
|
+
// Hover over second item
|
|
226
|
+
fireEvent.mouseEnter(items[1]);
|
|
227
|
+
expect(items[1]).toHaveClass('selected');
|
|
228
|
+
expect(items[0]).not.toHaveClass('selected');
|
|
229
|
+
});
|
|
230
|
+
});
|
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
import { useState, useEffect, useRef, useMemo } from 'react';
|
|
2
2
|
import { useStore } from '../store/store.ts';
|
|
3
|
-
import {
|
|
4
|
-
Plus,
|
|
5
|
-
Trash2,
|
|
6
|
-
Send,
|
|
7
|
-
Square,
|
|
8
|
-
List,
|
|
9
|
-
BarChart3,
|
|
10
|
-
FileCode,
|
|
11
|
-
ScrollText,
|
|
12
|
-
X,
|
|
13
|
-
CornerDownLeft,
|
|
14
|
-
} from 'lucide-react';
|
|
3
|
+
import { Plus, Trash2, Send, Square, List, BarChart3, FileCode, ScrollText, X, CornerDownLeft } from 'lucide-react';
|
|
15
4
|
import type { RightTab } from '../types/index.ts';
|
|
16
5
|
|
|
17
6
|
interface Command {
|
|
@@ -51,7 +40,9 @@ export default function CommandPalette() {
|
|
|
51
40
|
label: 'Delete Session',
|
|
52
41
|
hint: 'Delete the active session',
|
|
53
42
|
icon: Trash2,
|
|
54
|
-
action: () => {
|
|
43
|
+
action: () => {
|
|
44
|
+
if (store.activeSessionId) store.deleteSession(store.activeSessionId);
|
|
45
|
+
},
|
|
55
46
|
available: !!store.activeSessionId,
|
|
56
47
|
},
|
|
57
48
|
{
|
|
@@ -59,7 +50,10 @@ export default function CommandPalette() {
|
|
|
59
50
|
label: 'Send Message',
|
|
60
51
|
hint: 'Focus the message input',
|
|
61
52
|
icon: Send,
|
|
62
|
-
action: () => {
|
|
53
|
+
action: () => {
|
|
54
|
+
const el = document.querySelector<HTMLInputElement>('.cmd-input');
|
|
55
|
+
el?.focus();
|
|
56
|
+
},
|
|
63
57
|
available: !!store.activeSessionId && !isRunning,
|
|
64
58
|
},
|
|
65
59
|
{
|
|
@@ -107,7 +101,9 @@ export default function CommandPalette() {
|
|
|
107
101
|
label: 'Close Tab',
|
|
108
102
|
hint: 'Close the active session tab',
|
|
109
103
|
icon: X,
|
|
110
|
-
action: () => {
|
|
104
|
+
action: () => {
|
|
105
|
+
if (store.activeSessionId) store.closeTab(store.activeSessionId);
|
|
106
|
+
},
|
|
111
107
|
available: !!store.activeSessionId,
|
|
112
108
|
},
|
|
113
109
|
];
|
|
@@ -118,9 +114,7 @@ export default function CommandPalette() {
|
|
|
118
114
|
const q = query.toLowerCase().trim();
|
|
119
115
|
const available = commands.filter((c) => c.available);
|
|
120
116
|
if (!q) return available;
|
|
121
|
-
return available.filter((c) =>
|
|
122
|
-
c.label.toLowerCase().includes(q) || c.hint.toLowerCase().includes(q),
|
|
123
|
-
);
|
|
117
|
+
return available.filter((c) => c.label.toLowerCase().includes(q) || c.hint.toLowerCase().includes(q));
|
|
124
118
|
}, [commands, query]);
|
|
125
119
|
|
|
126
120
|
// Reset on open
|
|
@@ -163,7 +157,10 @@ export default function CommandPalette() {
|
|
|
163
157
|
type="text"
|
|
164
158
|
placeholder="Type a command…"
|
|
165
159
|
value={query}
|
|
166
|
-
onChange={(e) => {
|
|
160
|
+
onChange={(e) => {
|
|
161
|
+
setQuery(e.target.value);
|
|
162
|
+
setSelectedIdx(0);
|
|
163
|
+
}}
|
|
167
164
|
onKeyDown={handleKeyDown}
|
|
168
165
|
/>
|
|
169
166
|
<span className="palette-esc">ESC</span>
|
|
@@ -178,10 +175,15 @@ export default function CommandPalette() {
|
|
|
178
175
|
<div
|
|
179
176
|
key={cmd.id}
|
|
180
177
|
className={`palette-item ${i === selectedIdx ? 'selected' : ''}`}
|
|
181
|
-
onClick={() => {
|
|
178
|
+
onClick={() => {
|
|
179
|
+
cmd.action();
|
|
180
|
+
setShow(false);
|
|
181
|
+
}}
|
|
182
182
|
onMouseEnter={() => setSelectedIdx(i)}
|
|
183
183
|
>
|
|
184
|
-
<span className="palette-item-icon"
|
|
184
|
+
<span className="palette-item-icon">
|
|
185
|
+
<Icon size={16} />
|
|
186
|
+
</span>
|
|
185
187
|
<span className="palette-item-label">{cmd.label}</span>
|
|
186
188
|
<span className="palette-item-hint">{cmd.hint}</span>
|
|
187
189
|
{i === selectedIdx && <CornerDownLeft size={12} className="palette-item-enter" />}
|
|
@@ -192,7 +194,8 @@ export default function CommandPalette() {
|
|
|
192
194
|
)}
|
|
193
195
|
<div className="palette-footer">
|
|
194
196
|
<span className="palette-footer-hint">
|
|
195
|
-
<span className="kbd">↑</span
|
|
197
|
+
<span className="kbd">↑</span>
|
|
198
|
+
<span className="kbd">↓</span> Navigate
|
|
196
199
|
</span>
|
|
197
200
|
<span className="palette-footer-hint">
|
|
198
201
|
<span className="kbd">↵</span> Select
|
|
@@ -204,4 +207,4 @@ export default function CommandPalette() {
|
|
|
204
207
|
</div>
|
|
205
208
|
</div>
|
|
206
209
|
);
|
|
207
|
-
}
|
|
210
|
+
}
|