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
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
|
+
import { render, screen, fireEvent, cleanup } from '@testing-library/react';
|
|
3
|
+
import SlashPopup, { filterCommands, type SlashCommand } from './SlashPopup.tsx';
|
|
4
|
+
|
|
5
|
+
const ALL_CAPS = {
|
|
6
|
+
dynamicModels: true,
|
|
7
|
+
skills: true,
|
|
8
|
+
plugins: true,
|
|
9
|
+
mcpServers: true,
|
|
10
|
+
imageAttachments: true,
|
|
11
|
+
sandbox: true,
|
|
12
|
+
webSearch: true,
|
|
13
|
+
networkAccess: true,
|
|
14
|
+
fileReferences: true,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const NO_CAPS = {
|
|
18
|
+
dynamicModels: false,
|
|
19
|
+
skills: false,
|
|
20
|
+
plugins: false,
|
|
21
|
+
mcpServers: false,
|
|
22
|
+
imageAttachments: false,
|
|
23
|
+
sandbox: false,
|
|
24
|
+
webSearch: false,
|
|
25
|
+
networkAccess: false,
|
|
26
|
+
fileReferences: false,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
describe('filterCommands', () => {
|
|
30
|
+
it('returns all commands when all capabilities enabled and no filter', () => {
|
|
31
|
+
const cmds = filterCommands('', ALL_CAPS);
|
|
32
|
+
expect(cmds.length).toBe(7);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('filters by text', () => {
|
|
36
|
+
const cmds = filterCommands('sand', ALL_CAPS);
|
|
37
|
+
expect(cmds).toHaveLength(1);
|
|
38
|
+
expect(cmds[0].id).toBe('sandbox');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('hides commands when capability is false', () => {
|
|
42
|
+
const cmds = filterCommands('', NO_CAPS);
|
|
43
|
+
// All commands have a capability requirement, so none should show
|
|
44
|
+
expect(cmds).toHaveLength(0);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('shows only commands with matching capability', () => {
|
|
48
|
+
const partial = { ...NO_CAPS, sandbox: true, webSearch: true };
|
|
49
|
+
const cmds = filterCommands('', partial);
|
|
50
|
+
expect(cmds).toHaveLength(2);
|
|
51
|
+
expect(cmds.map((c) => c.id)).toEqual(['sandbox', 'websearch']);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('SlashPopup', () => {
|
|
56
|
+
afterEach(cleanup);
|
|
57
|
+
|
|
58
|
+
const commands: SlashCommand[] = [
|
|
59
|
+
{ id: 'sandbox', label: '/sandbox', hint: 'Sandbox mode', capability: 'sandbox', subPopup: 'sandbox' },
|
|
60
|
+
{ id: 'websearch', label: '/websearch', hint: 'Web search', capability: 'webSearch', subPopup: 'websearch' },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
it('renders all commands', () => {
|
|
64
|
+
render(<SlashPopup commands={commands} selectedIndex={0} onSelect={() => {}} onHover={() => {}} />);
|
|
65
|
+
expect(screen.getByText('/sandbox')).toBeInTheDocument();
|
|
66
|
+
expect(screen.getByText('/websearch')).toBeInTheDocument();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('shows empty state when no commands', () => {
|
|
70
|
+
render(<SlashPopup commands={[]} selectedIndex={0} onSelect={() => {}} onHover={() => {}} />);
|
|
71
|
+
expect(screen.getByText('No matching commands')).toBeInTheDocument();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('calls onSelect when command is clicked', () => {
|
|
75
|
+
const onSelect = vi.fn();
|
|
76
|
+
render(<SlashPopup commands={commands} selectedIndex={0} onSelect={onSelect} onHover={() => {}} />);
|
|
77
|
+
fireEvent.click(screen.getByText('/sandbox'));
|
|
78
|
+
expect(onSelect).toHaveBeenCalledWith(commands[0]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('applies selected class to the selected index', () => {
|
|
82
|
+
const { container } = render(
|
|
83
|
+
<SlashPopup commands={commands} selectedIndex={1} onSelect={() => {}} onHover={() => {}} />,
|
|
84
|
+
);
|
|
85
|
+
const items = container.querySelectorAll('.slash-popup-item');
|
|
86
|
+
expect(items[1]).toHaveClass('selected');
|
|
87
|
+
expect(items[0]).not.toHaveClass('selected');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('calls onHover on mouse enter', () => {
|
|
91
|
+
const onHover = vi.fn();
|
|
92
|
+
render(<SlashPopup commands={commands} selectedIndex={0} onSelect={() => {}} onHover={onHover} />);
|
|
93
|
+
fireEvent.mouseEnter(screen.getByText('/websearch'));
|
|
94
|
+
expect(onHover).toHaveBeenCalledWith(1);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ProviderCapabilities } from '../../types/index.ts';
|
|
2
|
+
|
|
3
|
+
/** A slash command definition. */
|
|
4
|
+
export interface SlashCommand {
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
hint: string;
|
|
8
|
+
/** Capability required to show this command. If null, always visible. */
|
|
9
|
+
capability: keyof ProviderCapabilities | null;
|
|
10
|
+
/** Sub-popup type to open when selected. */
|
|
11
|
+
subPopup?: 'sandbox' | 'websearch' | 'network';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** All available slash commands. */
|
|
15
|
+
const ALL_COMMANDS: SlashCommand[] = [
|
|
16
|
+
{ id: 'model', label: '/model', hint: 'Change model', capability: 'dynamicModels' },
|
|
17
|
+
{ id: 'sandbox', label: '/sandbox', hint: 'Sandbox mode', capability: 'sandbox', subPopup: 'sandbox' },
|
|
18
|
+
{ id: 'websearch', label: '/websearch', hint: 'Web search mode', capability: 'webSearch', subPopup: 'websearch' },
|
|
19
|
+
{ id: 'network', label: '/network', hint: 'Network access', capability: 'networkAccess', subPopup: 'network' },
|
|
20
|
+
{ id: 'skills', label: '/skills', hint: 'Browse skills', capability: 'skills' },
|
|
21
|
+
{ id: 'plugins', label: '/plugins', hint: 'Browse plugins', capability: 'plugins' },
|
|
22
|
+
{ id: 'mcp', label: '/mcp', hint: 'Browse MCP servers', capability: 'mcpServers' },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/** Filter commands by capability and text filter. */
|
|
26
|
+
export function filterCommands(
|
|
27
|
+
filter: string,
|
|
28
|
+
capabilities: ProviderCapabilities,
|
|
29
|
+
): SlashCommand[] {
|
|
30
|
+
return ALL_COMMANDS.filter((cmd) => {
|
|
31
|
+
if (cmd.capability && !capabilities[cmd.capability]) return false;
|
|
32
|
+
if (filter && !cmd.label.toLowerCase().includes(filter.toLowerCase())) return false;
|
|
33
|
+
return true;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface SlashPopupProps {
|
|
38
|
+
commands: SlashCommand[];
|
|
39
|
+
selectedIndex: number;
|
|
40
|
+
onSelect: (cmd: SlashCommand) => void;
|
|
41
|
+
onHover: (index: number) => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Presentational slash command popup. Keyboard navigation is handled by the parent. */
|
|
45
|
+
export default function SlashPopup({ commands, selectedIndex, onSelect, onHover }: SlashPopupProps) {
|
|
46
|
+
return (
|
|
47
|
+
<div className="slash-popup">
|
|
48
|
+
<div className="slash-popup-header">Commands</div>
|
|
49
|
+
{commands.length === 0 && <div className="slash-popup-empty">No matching commands</div>}
|
|
50
|
+
{commands.map((cmd, i) => (
|
|
51
|
+
<div
|
|
52
|
+
key={cmd.id}
|
|
53
|
+
className={`slash-popup-item ${i === selectedIndex ? 'selected' : ''}`}
|
|
54
|
+
onMouseEnter={() => onHover(i)}
|
|
55
|
+
onClick={() => onSelect(cmd)}
|
|
56
|
+
>
|
|
57
|
+
<span className="slash-popup-label">{cmd.label}</span>
|
|
58
|
+
<span className="slash-popup-hint">{cmd.hint}</span>
|
|
59
|
+
</div>
|
|
60
|
+
))}
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { render, screen, fireEvent, cleanup } from '@testing-library/react';
|
|
3
|
+
import ChangesPanel from './ChangesPanel.tsx';
|
|
4
|
+
import { resetStore } from '../../test/component-helpers.ts';
|
|
5
|
+
import { makeDetail, makeTurn, makeItem } from '../../test/fixtures.ts';
|
|
6
|
+
|
|
7
|
+
describe('ChangesPanel', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
resetStore();
|
|
10
|
+
});
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
cleanup();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('shows empty state when no file changes', () => {
|
|
16
|
+
const detail = makeDetail('s1', []);
|
|
17
|
+
resetStore({ activeSessionId: 's1', sessionDetails: { s1: detail } });
|
|
18
|
+
render(<ChangesPanel />);
|
|
19
|
+
expect(screen.getByText('No file changes in this scope')).toBeInTheDocument();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('session scope button is active by default', () => {
|
|
23
|
+
const detail = makeDetail('s1', []);
|
|
24
|
+
resetStore({ activeSessionId: 's1', sessionDetails: { s1: detail } });
|
|
25
|
+
render(<ChangesPanel />);
|
|
26
|
+
const sessionBtn = screen.getByText('Session');
|
|
27
|
+
expect(sessionBtn.closest('.scope-btn')).toHaveClass('active');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('click "Current Turn" switches scope to turngroup', () => {
|
|
31
|
+
const setChangesScope = vi.fn();
|
|
32
|
+
const detail = makeDetail('s1', []);
|
|
33
|
+
resetStore({ activeSessionId: 's1', sessionDetails: { s1: detail }, setChangesScope });
|
|
34
|
+
render(<ChangesPanel />);
|
|
35
|
+
fireEvent.click(screen.getByText('Current Turn'));
|
|
36
|
+
expect(setChangesScope).toHaveBeenCalledWith('turngroup');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('renders file change entries with A/D/M status', () => {
|
|
40
|
+
const item = makeItem('i1', 't1', 'file_change');
|
|
41
|
+
item.data = { changes: [{ path: 'src/main.ts', kind: 'add' }, { path: 'src/old.ts', kind: 'delete' }] };
|
|
42
|
+
const turn = makeTurn('t1', 's1', { items: [item] });
|
|
43
|
+
const detail = makeDetail('s1', [turn]);
|
|
44
|
+
resetStore({ activeSessionId: 's1', sessionDetails: { s1: detail } });
|
|
45
|
+
render(<ChangesPanel />);
|
|
46
|
+
expect(screen.getByText(/main\.ts/)).toBeInTheDocument();
|
|
47
|
+
expect(screen.getByText(/old\.ts/)).toBeInTheDocument();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('click on file change calls setSelectedChangePath', () => {
|
|
51
|
+
const setSelectedChangePath = vi.fn();
|
|
52
|
+
const item = makeItem('i1', 't1', 'file_change');
|
|
53
|
+
item.data = { changes: [{ path: 'src/app.ts', kind: 'modify' }] };
|
|
54
|
+
const turn = makeTurn('t1', 's1', { items: [item] });
|
|
55
|
+
const detail = makeDetail('s1', [turn]);
|
|
56
|
+
resetStore({ activeSessionId: 's1', sessionDetails: { s1: detail }, setSelectedChangePath });
|
|
57
|
+
render(<ChangesPanel />);
|
|
58
|
+
fireEvent.click(screen.getByText(/app\.ts/).closest('.file-change')!);
|
|
59
|
+
expect(setSelectedChangePath).toHaveBeenCalledWith('src/app.ts');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -1,40 +1,13 @@
|
|
|
1
1
|
import { useStore } from '../../store/store.ts';
|
|
2
|
+
import { collectFileChanges } from '../../utils/changes.ts';
|
|
2
3
|
|
|
3
4
|
export default function ChangesPanel() {
|
|
4
|
-
const { activeSessionId, sessionDetails, changesScope, setChangesScope, activeTurnIndex, setSelectedChangePath } =
|
|
5
|
+
const { activeSessionId, sessionDetails, changesScope, setChangesScope, activeTurnIndex, setSelectedChangePath } =
|
|
6
|
+
useStore();
|
|
5
7
|
const detail = activeSessionId ? sessionDetails[activeSessionId] : null;
|
|
6
8
|
const turns = detail?.turns ?? [];
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
type ChangeEntry = { path: string; kind: string };
|
|
10
|
-
const entries: ChangeEntry[] = [];
|
|
11
|
-
|
|
12
|
-
if (changesScope === 'session') {
|
|
13
|
-
turns.forEach((turn) => {
|
|
14
|
-
turn.items.forEach((item) => {
|
|
15
|
-
if (item.type === 'file_change' && item.data?.changes) {
|
|
16
|
-
const changes = item.data.changes as Array<{ path: string; kind: string }>;
|
|
17
|
-
changes.forEach((c) => entries.push({ path: c.path, kind: c.kind }));
|
|
18
|
-
}
|
|
19
|
-
});
|
|
20
|
-
});
|
|
21
|
-
} else {
|
|
22
|
-
// Current turn only
|
|
23
|
-
const turn = turns[activeTurnIndex];
|
|
24
|
-
if (turn) {
|
|
25
|
-
turn.items.forEach((item) => {
|
|
26
|
-
if (item.type === 'file_change' && item.data?.changes) {
|
|
27
|
-
const changes = item.data.changes as Array<{ path: string; kind: string }>;
|
|
28
|
-
changes.forEach((c) => entries.push({ path: c.path, kind: c.kind }));
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Deduplicate by path (keep last occurrence)
|
|
35
|
-
const seen = new Map<string, ChangeEntry>();
|
|
36
|
-
for (const e of entries) seen.set(e.path, e);
|
|
37
|
-
const deduped = Array.from(seen.values());
|
|
10
|
+
const deduped = collectFileChanges(turns, changesScope, activeTurnIndex);
|
|
38
11
|
|
|
39
12
|
return (
|
|
40
13
|
<>
|
|
@@ -42,11 +15,15 @@ export default function ChangesPanel() {
|
|
|
42
15
|
<button
|
|
43
16
|
className={`scope-btn ${changesScope === 'session' ? 'active' : ''}`}
|
|
44
17
|
onClick={() => setChangesScope('session')}
|
|
45
|
-
>
|
|
18
|
+
>
|
|
19
|
+
Session
|
|
20
|
+
</button>
|
|
46
21
|
<button
|
|
47
22
|
className={`scope-btn ${changesScope === 'turngroup' ? 'active' : ''}`}
|
|
48
23
|
onClick={() => setChangesScope('turngroup')}
|
|
49
|
-
>
|
|
24
|
+
>
|
|
25
|
+
Current Turn
|
|
26
|
+
</button>
|
|
50
27
|
</div>
|
|
51
28
|
|
|
52
29
|
{deduped.length === 0 ? (
|
|
@@ -61,11 +38,7 @@ export default function ChangesPanel() {
|
|
|
61
38
|
const dir = entry.path.replace('/' + fileName, '');
|
|
62
39
|
|
|
63
40
|
return (
|
|
64
|
-
<div
|
|
65
|
-
key={entry.path}
|
|
66
|
-
className="file-change clickable"
|
|
67
|
-
onClick={() => setSelectedChangePath(entry.path)}
|
|
68
|
-
>
|
|
41
|
+
<div key={entry.path} className="file-change clickable" onClick={() => setSelectedChangePath(entry.path)}>
|
|
69
42
|
<span className={`fc-status ${statusClass}`}>{statusChar}</span>
|
|
70
43
|
<span className="fc-name">
|
|
71
44
|
{dir && <span style={{ color: 'var(--text-muted)' }}>{dir}/</span>}
|
|
@@ -78,4 +51,4 @@ export default function ChangesPanel() {
|
|
|
78
51
|
)}
|
|
79
52
|
</>
|
|
80
53
|
);
|
|
81
|
-
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { render, screen, cleanup } from '@testing-library/react';
|
|
3
|
+
import LogsPanel from './LogsPanel.tsx';
|
|
4
|
+
import { useStore } from '../../store/store.ts';
|
|
5
|
+
import { resetStore } from '../../test/component-helpers.ts';
|
|
6
|
+
|
|
7
|
+
describe('LogsPanel', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
resetStore();
|
|
10
|
+
});
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
cleanup();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('shows empty state when no events', () => {
|
|
16
|
+
render(<LogsPanel />);
|
|
17
|
+
expect(screen.getByText('No events logged yet')).toBeInTheDocument();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('renders log entries', () => {
|
|
21
|
+
useStore.setState({
|
|
22
|
+
eventLog: [
|
|
23
|
+
{ time: '10:00:00', level: 'INFO', message: 'First event' },
|
|
24
|
+
{ time: '10:01:00', level: 'ERROR', message: 'Second event' },
|
|
25
|
+
],
|
|
26
|
+
});
|
|
27
|
+
render(<LogsPanel />);
|
|
28
|
+
expect(screen.getByText('First event')).toBeInTheDocument();
|
|
29
|
+
expect(screen.getByText('Second event')).toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('shows entries in reverse chronological order', () => {
|
|
33
|
+
useStore.setState({
|
|
34
|
+
eventLog: [
|
|
35
|
+
{ time: '10:00:00', level: 'INFO', message: 'First' },
|
|
36
|
+
{ time: '10:01:00', level: 'INFO', message: 'Second' },
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
render(<LogsPanel />);
|
|
40
|
+
const entries = document.querySelectorAll('.log-entry');
|
|
41
|
+
expect(entries[0].textContent).toContain('Second');
|
|
42
|
+
expect(entries[1].textContent).toContain('First');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('renders log level with correct CSS class', () => {
|
|
46
|
+
useStore.setState({
|
|
47
|
+
eventLog: [
|
|
48
|
+
{ time: '10:00:00', level: 'INFO', message: 'OK' },
|
|
49
|
+
{ time: '10:01:00', level: 'ERROR', message: 'Failed' },
|
|
50
|
+
],
|
|
51
|
+
});
|
|
52
|
+
render(<LogsPanel />);
|
|
53
|
+
// Entries are reversed (newest first): ERROR then INFO
|
|
54
|
+
const levels = document.querySelectorAll('.log-level');
|
|
55
|
+
expect(levels[0]).toHaveClass('log-error');
|
|
56
|
+
expect(levels[1]).toHaveClass('log-info');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { render, screen, cleanup } from '@testing-library/react';
|
|
3
|
+
import MetricsPanel from './MetricsPanel.tsx';
|
|
4
|
+
import { resetStore } from '../../test/component-helpers.ts';
|
|
5
|
+
import { makeSession, makeDetail, makeTurn, makeUsage } from '../../test/fixtures.ts';
|
|
6
|
+
|
|
7
|
+
describe('MetricsPanel', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
resetStore();
|
|
10
|
+
});
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
cleanup();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('shows empty state when no session', () => {
|
|
16
|
+
render(<MetricsPanel />);
|
|
17
|
+
expect(screen.getByText('No session selected')).toBeInTheDocument();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('shows session model or "default" when null', () => {
|
|
21
|
+
const session = makeSession('s1', { model: null });
|
|
22
|
+
resetStore({ activeSessionId: 's1', sessions: [session] });
|
|
23
|
+
render(<MetricsPanel />);
|
|
24
|
+
expect(screen.getByText('default')).toBeInTheDocument();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('shows repo name from cwd', () => {
|
|
28
|
+
const session = makeSession('s1', { cwd: '/home/user/my-project' });
|
|
29
|
+
resetStore({ activeSessionId: 's1', sessions: [session] });
|
|
30
|
+
render(<MetricsPanel />);
|
|
31
|
+
expect(screen.getByText('my-project')).toBeInTheDocument();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('shows "—" when cwd is null', () => {
|
|
35
|
+
const session = makeSession('s1', { cwd: null });
|
|
36
|
+
resetStore({ activeSessionId: 's1', sessions: [session] });
|
|
37
|
+
render(<MetricsPanel />);
|
|
38
|
+
// "—" appears in both Repo and Cost rows — check the Repo label row
|
|
39
|
+
const repoLabel = screen.getByText('Repo');
|
|
40
|
+
const repoVal = repoLabel.closest('.metric-row')!.querySelector('.metric-val')!;
|
|
41
|
+
expect(repoVal.textContent).toBe('—');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('shows turn count', () => {
|
|
45
|
+
const session = makeSession('s1');
|
|
46
|
+
const detail = makeDetail('s1', [makeTurn('t1', 's1'), makeTurn('t2', 's1')]);
|
|
47
|
+
resetStore({ activeSessionId: 's1', sessions: [session], sessionDetails: { s1: detail } });
|
|
48
|
+
render(<MetricsPanel />);
|
|
49
|
+
expect(screen.getByText('2')).toBeInTheDocument();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('shows token totals from all turns', () => {
|
|
53
|
+
const session = makeSession('s1');
|
|
54
|
+
const t1 = makeTurn('t1', 's1', { token_usage: makeUsage({ input_tokens: 100, output_tokens: 200 }) });
|
|
55
|
+
const t2 = makeTurn('t2', 's1', { token_usage: makeUsage({ input_tokens: 300, output_tokens: 400 }) });
|
|
56
|
+
const detail = makeDetail('s1', [t1, t2]);
|
|
57
|
+
resetStore({ activeSessionId: 's1', sessions: [session], sessionDetails: { s1: detail } });
|
|
58
|
+
render(<MetricsPanel />);
|
|
59
|
+
expect(screen.getByText('400')).toBeInTheDocument(); // total input
|
|
60
|
+
expect(screen.getByText('600')).toBeInTheDocument(); // total output
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('shows reasoning tokens only when > 0', () => {
|
|
64
|
+
const session = makeSession('s1');
|
|
65
|
+
const t1 = makeTurn('t1', 's1', { token_usage: makeUsage({ reasoning_output_tokens: 500 }) });
|
|
66
|
+
const detail = makeDetail('s1', [t1]);
|
|
67
|
+
resetStore({ activeSessionId: 's1', sessions: [session], sessionDetails: { s1: detail } });
|
|
68
|
+
render(<MetricsPanel />);
|
|
69
|
+
expect(screen.getByText('500')).toBeInTheDocument();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { useStore } from '../../store/store.ts';
|
|
2
|
-
import { statusToDot, statusToLabel, formatDuration } from '../../types/index.ts';
|
|
3
2
|
import StatusDot from '../StatusDot.tsx';
|
|
3
|
+
import { deriveSessionStatus } from '../../hooks/useSessionStatus.ts';
|
|
4
|
+
import { calcSessionDuration } from '../../utils/duration.ts';
|
|
4
5
|
|
|
5
6
|
export default function MetricsPanel() {
|
|
6
|
-
const
|
|
7
|
-
const session =
|
|
8
|
-
|
|
7
|
+
const activeSessionId = useStore((s) => s.activeSessionId);
|
|
8
|
+
const session = useStore((s) =>
|
|
9
|
+
activeSessionId ? (s.sessions.find((x) => x.id === activeSessionId) ?? null) : null,
|
|
10
|
+
);
|
|
11
|
+
const detail = useStore((s) => (activeSessionId ? (s.sessionDetails[activeSessionId] ?? null) : null));
|
|
9
12
|
|
|
10
13
|
if (!session) {
|
|
11
14
|
return <div className="empty-state">No session selected</div>;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const label = statusToLabel(session.status, isRunning);
|
|
17
|
+
const status = deriveSessionStatus(session);
|
|
18
|
+
const duration = calcSessionDuration(detail, status.isRunning);
|
|
17
19
|
|
|
18
20
|
// Aggregate token usage from all completed turns
|
|
19
21
|
let totalIn = 0;
|
|
@@ -30,33 +32,39 @@ export default function MetricsPanel() {
|
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
const turnCount = detail?.turns.length ?? 0;
|
|
33
|
-
|
|
34
|
-
// Calculate duration from first turn created_at to last turn updated_at (or now if running)
|
|
35
|
-
let duration = '—';
|
|
36
|
-
if (detail && detail.turns.length > 0) {
|
|
37
|
-
const firstTurn = detail.turns[0];
|
|
38
|
-
const lastTurn = detail.turns[detail.turns.length - 1];
|
|
39
|
-
const endISO = isRunning ? null : lastTurn.updated_at;
|
|
40
|
-
duration = formatDuration(firstTurn.created_at, endISO);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Repo name = basename of cwd
|
|
44
35
|
const repoName = session.cwd ? session.cwd.split('/').pop() || session.cwd : '—';
|
|
45
36
|
|
|
46
37
|
return (
|
|
47
38
|
<>
|
|
48
39
|
<div className="right-section">
|
|
49
40
|
<div className="right-section-title">Session</div>
|
|
50
|
-
<div className="metric-row"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
<div className="metric-row"
|
|
41
|
+
<div className="metric-row">
|
|
42
|
+
<span className="metric-label">Runtime</span>
|
|
43
|
+
<span className="metric-val">Codex</span>
|
|
44
|
+
</div>
|
|
45
|
+
<div className="metric-row">
|
|
46
|
+
<span className="metric-label">Model</span>
|
|
47
|
+
<span className="metric-val">{session.model || 'default'}</span>
|
|
48
|
+
</div>
|
|
49
|
+
<div className="metric-row">
|
|
50
|
+
<span className="metric-label">Repo</span>
|
|
51
|
+
<span className="metric-val" title={session.cwd || undefined}>
|
|
52
|
+
{repoName}
|
|
53
|
+
</span>
|
|
54
|
+
</div>
|
|
55
|
+
<div className="metric-row">
|
|
56
|
+
<span className="metric-label">Turns</span>
|
|
57
|
+
<span className="metric-val">{turnCount}</span>
|
|
58
|
+
</div>
|
|
59
|
+
<div className="metric-row">
|
|
60
|
+
<span className="metric-label">Duration</span>
|
|
61
|
+
<span className="metric-val">{duration}</span>
|
|
62
|
+
</div>
|
|
55
63
|
<div className="metric-row">
|
|
56
64
|
<span className="metric-label">Status</span>
|
|
57
65
|
<span className="metric-val">
|
|
58
66
|
<span className="badge badge-running">
|
|
59
|
-
<StatusDot status={dot} size={5} /> {label}
|
|
67
|
+
<StatusDot status={status.dot} size={5} /> {status.label}
|
|
60
68
|
</span>
|
|
61
69
|
</span>
|
|
62
70
|
</div>
|
|
@@ -64,19 +72,43 @@ export default function MetricsPanel() {
|
|
|
64
72
|
|
|
65
73
|
<div className="right-section">
|
|
66
74
|
<div className="right-section-title">Resources</div>
|
|
67
|
-
<div className="metric-row"
|
|
68
|
-
|
|
75
|
+
<div className="metric-row">
|
|
76
|
+
<span className="metric-label">Tokens In</span>
|
|
77
|
+
<span className="metric-val">{totalIn.toLocaleString()}</span>
|
|
78
|
+
</div>
|
|
79
|
+
<div className="metric-row">
|
|
80
|
+
<span className="metric-label">Tokens Out</span>
|
|
81
|
+
<span className="metric-val">{totalOut.toLocaleString()}</span>
|
|
82
|
+
</div>
|
|
69
83
|
{totalReasoning > 0 && (
|
|
70
|
-
<div className="metric-row"
|
|
84
|
+
<div className="metric-row">
|
|
85
|
+
<span className="metric-label">Reasoning</span>
|
|
86
|
+
<span className="metric-val">{totalReasoning.toLocaleString()}</span>
|
|
87
|
+
</div>
|
|
71
88
|
)}
|
|
72
|
-
<div className="metric-row"
|
|
89
|
+
<div className="metric-row">
|
|
90
|
+
<span className="metric-label">Cost</span>
|
|
91
|
+
<span className="metric-val" style={{ color: 'var(--amber)' }}>
|
|
92
|
+
—
|
|
93
|
+
</span>
|
|
94
|
+
</div>
|
|
73
95
|
</div>
|
|
74
96
|
|
|
75
97
|
<div className="right-section">
|
|
76
98
|
<div className="right-section-title">Metadata</div>
|
|
77
|
-
<div className="metric-row"
|
|
78
|
-
|
|
99
|
+
<div className="metric-row">
|
|
100
|
+
<span className="metric-label">Created</span>
|
|
101
|
+
<span className="metric-val" style={{ fontSize: '11px' }}>
|
|
102
|
+
{new Date(session.created_at).toLocaleString()}
|
|
103
|
+
</span>
|
|
104
|
+
</div>
|
|
105
|
+
<div className="metric-row">
|
|
106
|
+
<span className="metric-label">Thread ID</span>
|
|
107
|
+
<span className="metric-val" style={{ fontSize: '10px', color: 'var(--text-muted)' }}>
|
|
108
|
+
{session.external_thread_id ? session.external_thread_id.slice(0, 16) + '…' : '—'}
|
|
109
|
+
</span>
|
|
110
|
+
</div>
|
|
79
111
|
</div>
|
|
80
112
|
</>
|
|
81
113
|
);
|
|
82
|
-
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { render, screen, fireEvent, cleanup } from '@testing-library/react';
|
|
3
|
+
import TurnsPanel from './TurnsPanel.tsx';
|
|
4
|
+
import { resetStore } from '../../test/component-helpers.ts';
|
|
5
|
+
import { makeDetail, makeTurn } from '../../test/fixtures.ts';
|
|
6
|
+
|
|
7
|
+
describe('TurnsPanel', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
resetStore();
|
|
10
|
+
});
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
cleanup();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('shows empty state when no turns', () => {
|
|
16
|
+
render(<TurnsPanel />);
|
|
17
|
+
expect(screen.getByText('No turns yet')).toBeInTheDocument();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('renders turn items with preview text', () => {
|
|
21
|
+
const detail = makeDetail('s1', [makeTurn('t1', 's1', { user_prompt: 'Fix the bug' })]);
|
|
22
|
+
resetStore({ activeSessionId: 's1', sessionDetails: { s1: detail } });
|
|
23
|
+
render(<TurnsPanel />);
|
|
24
|
+
expect(screen.getByText('Fix the bug')).toBeInTheDocument();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('truncates preview text longer than 50 chars', () => {
|
|
28
|
+
const longPrompt = 'A'.repeat(60);
|
|
29
|
+
const detail = makeDetail('s1', [makeTurn('t1', 's1', { user_prompt: longPrompt })]);
|
|
30
|
+
resetStore({ activeSessionId: 's1', sessionDetails: { s1: detail } });
|
|
31
|
+
render(<TurnsPanel />);
|
|
32
|
+
expect(screen.getByText('A'.repeat(50) + '…')).toBeInTheDocument();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('active turn has active class', () => {
|
|
36
|
+
const detail = makeDetail('s1', [makeTurn('t1', 's1')]);
|
|
37
|
+
resetStore({ activeSessionId: 's1', sessionDetails: { s1: detail }, activeTurnIndex: 0 });
|
|
38
|
+
render(<TurnsPanel />);
|
|
39
|
+
const turnItem = screen.getByText('test prompt').closest('.turn-item');
|
|
40
|
+
expect(turnItem).toHaveClass('active');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('click calls setActiveTurnIndex', () => {
|
|
44
|
+
const setActiveTurnIndex = vi.fn();
|
|
45
|
+
const detail = makeDetail('s1', [makeTurn('t1', 's1'), makeTurn('t2', 's1')]);
|
|
46
|
+
resetStore({ activeSessionId: 's1', sessionDetails: { s1: detail }, activeTurnIndex: 0, setActiveTurnIndex });
|
|
47
|
+
render(<TurnsPanel />);
|
|
48
|
+
const items = document.querySelectorAll('.turn-item');
|
|
49
|
+
fireEvent.click(items[1]);
|
|
50
|
+
expect(setActiveTurnIndex).toHaveBeenCalledWith(1);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('context menu shows rewind and branch options', () => {
|
|
54
|
+
const detail = makeDetail('s1', [makeTurn('t1', 's1'), makeTurn('t2', 's1')]);
|
|
55
|
+
resetStore({ activeSessionId: 's1', sessionDetails: { s1: detail } });
|
|
56
|
+
render(<TurnsPanel />);
|
|
57
|
+
const turnItem = document.querySelectorAll('.turn-item')[0];
|
|
58
|
+
fireEvent.contextMenu(turnItem);
|
|
59
|
+
expect(screen.getByText('Rewind to Here')).toBeInTheDocument();
|
|
60
|
+
expect(screen.getByText('Branch from Here')).toBeInTheDocument();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('rewind is disabled on last turn', () => {
|
|
64
|
+
const detail = makeDetail('s1', [makeTurn('t1', 's1')]);
|
|
65
|
+
resetStore({ activeSessionId: 's1', sessionDetails: { s1: detail } });
|
|
66
|
+
render(<TurnsPanel />);
|
|
67
|
+
const turnItem = document.querySelector('.turn-item')!;
|
|
68
|
+
fireEvent.contextMenu(turnItem);
|
|
69
|
+
const rewindItem = screen.getByText('Rewind to Here').closest('.ctx-menu-item');
|
|
70
|
+
expect(rewindItem).toHaveClass('ctx-disabled');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('branch click calls branchFromTurn', () => {
|
|
74
|
+
const branchFromTurn = vi.fn().mockResolvedValue(undefined);
|
|
75
|
+
const detail = makeDetail('s1', [makeTurn('t1', 's1'), makeTurn('t2', 's1')]);
|
|
76
|
+
resetStore({ activeSessionId: 's1', sessionDetails: { s1: detail }, branchFromTurn });
|
|
77
|
+
render(<TurnsPanel />);
|
|
78
|
+
const turnItem = document.querySelectorAll('.turn-item')[0];
|
|
79
|
+
fireEvent.contextMenu(turnItem);
|
|
80
|
+
fireEvent.click(screen.getByText('Branch from Here'));
|
|
81
|
+
expect(branchFromTurn).toHaveBeenCalledWith('s1', 't1');
|
|
82
|
+
});
|
|
83
|
+
});
|