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,152 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import Fastify, { type FastifyInstance } from 'fastify';
|
|
3
|
+
import { initDb, getDb } from '../db/index.ts';
|
|
4
|
+
import { registerTurnRoutes } from './turns.ts';
|
|
5
|
+
|
|
6
|
+
// Mock registry to avoid spawning real provider processes
|
|
7
|
+
const mocks = vi.hoisted(() => ({
|
|
8
|
+
startTurn: vi.fn().mockResolvedValue(undefined),
|
|
9
|
+
interruptTurn: vi.fn().mockResolvedValue(true),
|
|
10
|
+
clearThread: vi.fn(),
|
|
11
|
+
clearAllThreads: vi.fn(),
|
|
12
|
+
getModels: vi.fn().mockResolvedValue([]),
|
|
13
|
+
shutdown: vi.fn().mockResolvedValue(undefined),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock('../providers/registry.ts', () => ({
|
|
17
|
+
getProvider: vi.fn(() => ({
|
|
18
|
+
startTurn: mocks.startTurn,
|
|
19
|
+
interruptTurn: mocks.interruptTurn,
|
|
20
|
+
clearThread: mocks.clearThread,
|
|
21
|
+
clearAllThreads: mocks.clearAllThreads,
|
|
22
|
+
getModels: mocks.getModels,
|
|
23
|
+
shutdown: mocks.shutdown,
|
|
24
|
+
})),
|
|
25
|
+
hasProvider: vi.fn(() => true),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
const NOW = '2025-01-01T00:00:00Z';
|
|
29
|
+
|
|
30
|
+
function insertSession(db: ReturnType<typeof getDb>, id: string, overrides: Record<string, unknown> = {}): void {
|
|
31
|
+
db.prepare(
|
|
32
|
+
`INSERT INTO sessions (id, title, runtime_type, model, model_reasoning_effort, cwd, external_thread_id, status, is_running, lifecycle, has_unreviewed, created_at, updated_at)
|
|
33
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
34
|
+
).run(
|
|
35
|
+
id,
|
|
36
|
+
null,
|
|
37
|
+
'codex',
|
|
38
|
+
null,
|
|
39
|
+
null,
|
|
40
|
+
null,
|
|
41
|
+
(overrides['external_thread_id'] as string | null) ?? null,
|
|
42
|
+
(overrides['status'] as string) ?? 'active',
|
|
43
|
+
(overrides['is_running'] as number) ?? 0,
|
|
44
|
+
'working',
|
|
45
|
+
0,
|
|
46
|
+
NOW,
|
|
47
|
+
NOW,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('Turn routes', () => {
|
|
52
|
+
let app: FastifyInstance;
|
|
53
|
+
|
|
54
|
+
beforeEach(async () => {
|
|
55
|
+
vi.clearAllMocks();
|
|
56
|
+
initDb(':memory:');
|
|
57
|
+
app = Fastify();
|
|
58
|
+
await registerTurnRoutes(app);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('POST /api/sessions/:id/turns', () => {
|
|
62
|
+
it('returns 400 when user_prompt is missing', async () => {
|
|
63
|
+
const res = await app.inject({ method: 'POST', url: '/api/sessions/s1/turns', payload: {} });
|
|
64
|
+
expect(res.statusCode).toBe(400);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('returns 400 when user_prompt has wrong type', async () => {
|
|
68
|
+
const res = await app.inject({
|
|
69
|
+
method: 'POST',
|
|
70
|
+
url: '/api/sessions/s1/turns',
|
|
71
|
+
payload: { user_prompt: { nested: true } },
|
|
72
|
+
});
|
|
73
|
+
expect(res.statusCode).toBe(400);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('returns 400 when user_prompt is empty', async () => {
|
|
77
|
+
const res = await app.inject({
|
|
78
|
+
method: 'POST',
|
|
79
|
+
url: '/api/sessions/s1/turns',
|
|
80
|
+
payload: { user_prompt: ' ' },
|
|
81
|
+
});
|
|
82
|
+
expect(res.statusCode).toBe(400);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('returns 404 for missing session', async () => {
|
|
86
|
+
const res = await app.inject({
|
|
87
|
+
method: 'POST',
|
|
88
|
+
url: '/api/sessions/nonexistent/turns',
|
|
89
|
+
payload: { user_prompt: 'hello' },
|
|
90
|
+
});
|
|
91
|
+
expect(res.statusCode).toBe(404);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('returns 409 when session is already running', async () => {
|
|
95
|
+
const db = getDb();
|
|
96
|
+
insertSession(db, 's1', { is_running: 1 });
|
|
97
|
+
const res = await app.inject({
|
|
98
|
+
method: 'POST',
|
|
99
|
+
url: '/api/sessions/s1/turns',
|
|
100
|
+
payload: { user_prompt: 'hello' },
|
|
101
|
+
});
|
|
102
|
+
expect(res.statusCode).toBe(409);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('creates a turn and calls startTurn', async () => {
|
|
106
|
+
const db = getDb();
|
|
107
|
+
insertSession(db, 's1');
|
|
108
|
+
mocks.startTurn.mockResolvedValue(undefined);
|
|
109
|
+
const res = await app.inject({
|
|
110
|
+
method: 'POST',
|
|
111
|
+
url: '/api/sessions/s1/turns',
|
|
112
|
+
payload: { user_prompt: 'hello' },
|
|
113
|
+
});
|
|
114
|
+
expect(res.statusCode).toBe(200);
|
|
115
|
+
const turn = res.json();
|
|
116
|
+
expect(turn.id).toBeDefined();
|
|
117
|
+
expect(turn.user_prompt).toBe('hello');
|
|
118
|
+
expect(turn.status).toBe('pending');
|
|
119
|
+
|
|
120
|
+
expect(mocks.startTurn).toHaveBeenCalledTimes(1);
|
|
121
|
+
const [sessionArg, turnArg] = mocks.startTurn.mock.calls[0];
|
|
122
|
+
expect(sessionArg.id).toBe('s1');
|
|
123
|
+
expect(turnArg.user_prompt).toBe('hello');
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('POST /api/sessions/:id/cancel', () => {
|
|
128
|
+
it('returns 404 for missing session', async () => {
|
|
129
|
+
const res = await app.inject({ method: 'POST', url: '/api/sessions/nonexistent/cancel' });
|
|
130
|
+
expect(res.statusCode).toBe(404);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('returns 409 when no running turn', async () => {
|
|
134
|
+
const db = getDb();
|
|
135
|
+
insertSession(db, 's1');
|
|
136
|
+
mocks.interruptTurn.mockResolvedValue(false);
|
|
137
|
+
const res = await app.inject({ method: 'POST', url: '/api/sessions/s1/cancel' });
|
|
138
|
+
expect(res.statusCode).toBe(409);
|
|
139
|
+
expect(res.json().error).toBe('No running turn to cancel');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('returns success when turn is cancelled', async () => {
|
|
143
|
+
const db = getDb();
|
|
144
|
+
insertSession(db, 's1');
|
|
145
|
+
mocks.interruptTurn.mockResolvedValue(true);
|
|
146
|
+
const res = await app.inject({ method: 'POST', url: '/api/sessions/s1/cancel' });
|
|
147
|
+
expect(res.statusCode).toBe(200);
|
|
148
|
+
expect(res.json()).toEqual({ success: true });
|
|
149
|
+
expect(mocks.interruptTurn).toHaveBeenCalledWith('s1');
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
import type { FastifyInstance } from 'fastify';
|
|
2
2
|
import { v4 as uuidv4 } from 'uuid';
|
|
3
3
|
import { getDb } from '../db/index.ts';
|
|
4
|
-
import {
|
|
4
|
+
import { getProvider } from '../providers/registry.ts';
|
|
5
5
|
import type { Session, Turn } from '../db/schema.ts';
|
|
6
6
|
|
|
7
7
|
export async function registerTurnRoutes(fastify: FastifyInstance): Promise<void> {
|
|
8
|
-
// POST /api/sessions/:id/turns — submit prompt, starts
|
|
8
|
+
// POST /api/sessions/:id/turns — submit prompt, starts a turn
|
|
9
9
|
fastify.post<{ Params: { id: string }; Body: { user_prompt: string } }>(
|
|
10
10
|
'/api/sessions/:id/turns',
|
|
11
|
+
{
|
|
12
|
+
schema: {
|
|
13
|
+
body: {
|
|
14
|
+
type: 'object',
|
|
15
|
+
required: ['user_prompt'],
|
|
16
|
+
properties: {
|
|
17
|
+
user_prompt: { type: 'string', minLength: 1 },
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
11
22
|
async (request, reply) => {
|
|
12
23
|
const { id } = request.params;
|
|
13
24
|
const { user_prompt } = request.body || {};
|
|
@@ -37,24 +48,33 @@ export async function registerTurnRoutes(fastify: FastifyInstance): Promise<void
|
|
|
37
48
|
const turn = db.prepare('SELECT * FROM turns WHERE id = ?').get(turnId) as Turn;
|
|
38
49
|
|
|
39
50
|
// Fire and forget — events stream via WebSocket
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
51
|
+
getProvider(session.runtime_type)
|
|
52
|
+
.startTurn(session, turn)
|
|
53
|
+
.catch((err) => {
|
|
54
|
+
console.error(`[provider] Unhandled error for session ${id}:`, err);
|
|
55
|
+
});
|
|
43
56
|
|
|
44
57
|
return turn;
|
|
45
58
|
},
|
|
46
59
|
);
|
|
47
60
|
|
|
48
61
|
// POST /api/sessions/:id/cancel — cancel running turn
|
|
49
|
-
fastify.post<{ Params: { id: string } }>(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
fastify.post<{ Params: { id: string } }>('/api/sessions/:id/cancel', async (request, reply) => {
|
|
63
|
+
const { id } = request.params;
|
|
64
|
+
const db = getDb();
|
|
65
|
+
|
|
66
|
+
// Look up session to get runtime_type for provider dispatch
|
|
67
|
+
const session = db.prepare('SELECT runtime_type FROM sessions WHERE id = ?').get(id) as
|
|
68
|
+
| { runtime_type: string }
|
|
69
|
+
| undefined;
|
|
70
|
+
if (!session) {
|
|
71
|
+
return reply.code(404).send({ error: 'Session not found' });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const cancelled = await getProvider(session.runtime_type).interruptTurn(id);
|
|
75
|
+
if (!cancelled) {
|
|
76
|
+
return reply.code(409).send({ error: 'No running turn to cancel' });
|
|
77
|
+
}
|
|
78
|
+
return { success: true };
|
|
79
|
+
});
|
|
60
80
|
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import Fastify, { type FastifyInstance } from 'fastify';
|
|
3
|
+
import { initDb, getDb } from '../db/index.ts';
|
|
4
|
+
import { registerWorkspaceRoutes } from './workspaces.ts';
|
|
5
|
+
import { execFile } from 'node:child_process';
|
|
6
|
+
|
|
7
|
+
// Mock child_process to avoid spawning osascript in tests
|
|
8
|
+
vi.mock('node:child_process', () => ({
|
|
9
|
+
execFile: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
const NOW = '2025-01-01T00:00:00Z';
|
|
13
|
+
|
|
14
|
+
function insertWorkspace(
|
|
15
|
+
db: ReturnType<typeof getDb>,
|
|
16
|
+
id: string,
|
|
17
|
+
path: string,
|
|
18
|
+
overrides: Record<string, unknown> = {},
|
|
19
|
+
): void {
|
|
20
|
+
db.prepare('INSERT INTO workspaces (id, path, label, deleted, created_at) VALUES (?, ?, ?, ?, ?)').run(
|
|
21
|
+
id,
|
|
22
|
+
path,
|
|
23
|
+
(overrides['label'] as string | null) ?? null,
|
|
24
|
+
(overrides['deleted'] as number) ?? 0,
|
|
25
|
+
(overrides['created_at'] as string) ?? NOW,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Configure execFile mock to call callback with success.
|
|
30
|
+
* Passes an object as the first result arg so promisify resolves with { stdout, stderr }.
|
|
31
|
+
*/
|
|
32
|
+
function mockExecFileSuccess(stdout: string): void {
|
|
33
|
+
const mock = execFile as unknown as ReturnType<typeof vi.fn>;
|
|
34
|
+
mock.mockImplementation((...args: unknown[]) => {
|
|
35
|
+
const cb = args[args.length - 1] as ((err: Error | null, ...results: unknown[]) => void) | undefined;
|
|
36
|
+
if (typeof cb === 'function') cb(null, { stdout, stderr: '' });
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Configure execFile mock to call callback with error (simulates user cancel). */
|
|
41
|
+
function mockExecFileError(error: Error): void {
|
|
42
|
+
const mock = execFile as unknown as ReturnType<typeof vi.fn>;
|
|
43
|
+
mock.mockImplementation((...args: unknown[]) => {
|
|
44
|
+
const cb = args[args.length - 1] as ((err: Error | null, ...results: unknown[]) => void) | undefined;
|
|
45
|
+
if (typeof cb === 'function') cb(error);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('Workspace routes', () => {
|
|
50
|
+
let app: FastifyInstance;
|
|
51
|
+
|
|
52
|
+
beforeEach(async () => {
|
|
53
|
+
vi.clearAllMocks();
|
|
54
|
+
initDb(':memory:');
|
|
55
|
+
app = Fastify();
|
|
56
|
+
await registerWorkspaceRoutes(app);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('GET /api/workspaces', () => {
|
|
60
|
+
it('returns empty array when no workspaces', async () => {
|
|
61
|
+
const res = await app.inject({ method: 'GET', url: '/api/workspaces' });
|
|
62
|
+
expect(res.statusCode).toBe(200);
|
|
63
|
+
expect(res.json()).toEqual([]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('returns all workspaces ordered by created_at ASC', async () => {
|
|
67
|
+
const db = getDb();
|
|
68
|
+
insertWorkspace(db, 'w1', '/path/a', { created_at: '2025-01-02T00:00:00Z' });
|
|
69
|
+
insertWorkspace(db, 'w2', '/path/b', { created_at: '2025-01-01T00:00:00Z' });
|
|
70
|
+
const res = await app.inject({ method: 'GET', url: '/api/workspaces' });
|
|
71
|
+
const workspaces = res.json();
|
|
72
|
+
expect(workspaces).toHaveLength(2);
|
|
73
|
+
expect(workspaces[0].id).toBe('w2');
|
|
74
|
+
expect(workspaces[1].id).toBe('w1');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('POST /api/workspaces', () => {
|
|
79
|
+
it('returns 400 when path is missing', async () => {
|
|
80
|
+
const res = await app.inject({ method: 'POST', url: '/api/workspaces', payload: {} });
|
|
81
|
+
expect(res.statusCode).toBe(400);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('returns 400 when path is empty', async () => {
|
|
85
|
+
const res = await app.inject({ method: 'POST', url: '/api/workspaces', payload: { path: ' ' } });
|
|
86
|
+
expect(res.statusCode).toBe(400);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('returns 400 when path has wrong type', async () => {
|
|
90
|
+
const res = await app.inject({ method: 'POST', url: '/api/workspaces', payload: { path: { nested: true } } });
|
|
91
|
+
expect(res.statusCode).toBe(400);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('returns 409 for duplicate active workspace', async () => {
|
|
95
|
+
const db = getDb();
|
|
96
|
+
insertWorkspace(db, 'w1', '/existing/path');
|
|
97
|
+
const res = await app.inject({ method: 'POST', url: '/api/workspaces', payload: { path: '/existing/path' } });
|
|
98
|
+
expect(res.statusCode).toBe(409);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('creates a new workspace', async () => {
|
|
102
|
+
const res = await app.inject({
|
|
103
|
+
method: 'POST',
|
|
104
|
+
url: '/api/workspaces',
|
|
105
|
+
payload: { path: '/new/path', label: 'Test' },
|
|
106
|
+
});
|
|
107
|
+
expect(res.statusCode).toBe(200);
|
|
108
|
+
const ws = res.json();
|
|
109
|
+
expect(ws.id).toBeDefined();
|
|
110
|
+
expect(ws.path).toBe('/new/path');
|
|
111
|
+
expect(ws.label).toBe('Test');
|
|
112
|
+
expect(ws.deleted).toBe(0);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('restores a soft-deleted workspace', async () => {
|
|
116
|
+
const db = getDb();
|
|
117
|
+
insertWorkspace(db, 'w1', '/deleted/path', { deleted: 1, label: 'Old' });
|
|
118
|
+
const res = await app.inject({
|
|
119
|
+
method: 'POST',
|
|
120
|
+
url: '/api/workspaces',
|
|
121
|
+
payload: { path: '/deleted/path', label: 'Restored' },
|
|
122
|
+
});
|
|
123
|
+
expect(res.statusCode).toBe(200);
|
|
124
|
+
const ws = res.json();
|
|
125
|
+
expect(ws.id).toBe('w1');
|
|
126
|
+
expect(ws.deleted).toBe(0);
|
|
127
|
+
expect(ws.label).toBe('Restored');
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('POST /api/workspaces/hide', () => {
|
|
132
|
+
it('returns 400 when path is missing', async () => {
|
|
133
|
+
const res = await app.inject({ method: 'POST', url: '/api/workspaces/hide', payload: {} });
|
|
134
|
+
expect(res.statusCode).toBe(400);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('soft-deletes existing workspace', async () => {
|
|
138
|
+
const db = getDb();
|
|
139
|
+
insertWorkspace(db, 'w1', '/path/to/hide');
|
|
140
|
+
const res = await app.inject({ method: 'POST', url: '/api/workspaces/hide', payload: { path: '/path/to/hide' } });
|
|
141
|
+
expect(res.statusCode).toBe(200);
|
|
142
|
+
expect(res.json()).toEqual({ success: true });
|
|
143
|
+
const ws = db.prepare('SELECT deleted FROM workspaces WHERE id = ?').get('w1') as { deleted: number };
|
|
144
|
+
expect(ws.deleted).toBe(1);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('creates and soft-deletes new workspace when path does not exist', async () => {
|
|
148
|
+
const res = await app.inject({ method: 'POST', url: '/api/workspaces/hide', payload: { path: '/new/path' } });
|
|
149
|
+
expect(res.statusCode).toBe(200);
|
|
150
|
+
const db = getDb();
|
|
151
|
+
const ws = db.prepare('SELECT * FROM workspaces WHERE path = ?').get('/new/path') as {
|
|
152
|
+
deleted: number;
|
|
153
|
+
id: string;
|
|
154
|
+
};
|
|
155
|
+
expect(ws.deleted).toBe(1);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('DELETE /api/workspaces/:id', () => {
|
|
160
|
+
it('returns 404 for missing workspace', async () => {
|
|
161
|
+
const res = await app.inject({ method: 'DELETE', url: '/api/workspaces/nonexistent' });
|
|
162
|
+
expect(res.statusCode).toBe(404);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('returns 404 for already-deleted workspace', async () => {
|
|
166
|
+
const db = getDb();
|
|
167
|
+
insertWorkspace(db, 'w1', '/path', { deleted: 1 });
|
|
168
|
+
const res = await app.inject({ method: 'DELETE', url: '/api/workspaces/w1' });
|
|
169
|
+
expect(res.statusCode).toBe(404);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('soft-deletes an active workspace', async () => {
|
|
173
|
+
const db = getDb();
|
|
174
|
+
insertWorkspace(db, 'w1', '/path');
|
|
175
|
+
const res = await app.inject({ method: 'DELETE', url: '/api/workspaces/w1' });
|
|
176
|
+
expect(res.statusCode).toBe(200);
|
|
177
|
+
expect(res.json()).toEqual({ success: true });
|
|
178
|
+
const ws = db.prepare('SELECT deleted FROM workspaces WHERE id = ?').get('w1') as { deleted: number };
|
|
179
|
+
expect(ws.deleted).toBe(1);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('GET /api/pick-directory', () => {
|
|
184
|
+
it('returns path on success', async () => {
|
|
185
|
+
mockExecFileSuccess('/selected/path/\n');
|
|
186
|
+
const res = await app.inject({ method: 'GET', url: '/api/pick-directory' });
|
|
187
|
+
expect(res.statusCode).toBe(200);
|
|
188
|
+
expect(res.json()).toEqual({ path: '/selected/path' });
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('returns 204 when user cancels', async () => {
|
|
192
|
+
mockExecFileError(new Error('User cancelled'));
|
|
193
|
+
const res = await app.inject({ method: 'GET', url: '/api/pick-directory' });
|
|
194
|
+
expect(res.statusCode).toBe(204);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
});
|
|
@@ -10,48 +10,57 @@ const execFileAsync = promisify(execFile);
|
|
|
10
10
|
export async function registerWorkspaceRoutes(fastify: FastifyInstance): Promise<void> {
|
|
11
11
|
// GET /api/workspaces — list ALL workspaces (active + soft-deleted)
|
|
12
12
|
fastify.get('/api/workspaces', async () => {
|
|
13
|
-
const rows = getDb()
|
|
14
|
-
.prepare('SELECT * FROM workspaces ORDER BY created_at ASC')
|
|
15
|
-
.all() as Workspace[];
|
|
13
|
+
const rows = getDb().prepare('SELECT * FROM workspaces ORDER BY created_at ASC').all() as Workspace[];
|
|
16
14
|
return rows;
|
|
17
15
|
});
|
|
18
16
|
|
|
19
17
|
// POST /api/workspaces — create workspace (or restore if soft-deleted)
|
|
20
|
-
fastify.post<{ Body: { path: string; label?: string } }>(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
18
|
+
fastify.post<{ Body: { path: string; label?: string } }>('/api/workspaces', {
|
|
19
|
+
schema: {
|
|
20
|
+
body: {
|
|
21
|
+
type: 'object',
|
|
22
|
+
required: ['path'],
|
|
23
|
+
properties: {
|
|
24
|
+
path: { type: 'string' },
|
|
25
|
+
label: { type: 'string' },
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
}, async (request, reply) => {
|
|
30
|
+
const { path, label = null } = request.body || {};
|
|
31
|
+
if (!path || !path.trim()) {
|
|
32
|
+
return reply.code(400).send({ error: 'Path is required' });
|
|
33
|
+
}
|
|
27
34
|
|
|
28
|
-
|
|
29
|
-
|
|
35
|
+
const trimmedPath = path.trim();
|
|
36
|
+
const db = getDb();
|
|
30
37
|
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
// Check if workspace with this path already exists
|
|
39
|
+
const existing = db.prepare('SELECT * FROM workspaces WHERE path = ?').get(trimmedPath) as Workspace | undefined;
|
|
33
40
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
// Restore soft-deleted workspace
|
|
39
|
-
db.prepare('UPDATE workspaces SET deleted = 0, label = ? WHERE id = ?').run(label, existing.id);
|
|
40
|
-
const restored = db.prepare('SELECT * FROM workspaces WHERE id = ?').get(existing.id) as Workspace;
|
|
41
|
-
return restored;
|
|
41
|
+
if (existing) {
|
|
42
|
+
if (existing.deleted === 0) {
|
|
43
|
+
return reply.code(409).send({ error: 'Workspace with this path already exists' });
|
|
42
44
|
}
|
|
45
|
+
// Restore soft-deleted workspace
|
|
46
|
+
db.prepare('UPDATE workspaces SET deleted = 0, label = ? WHERE id = ?').run(label, existing.id);
|
|
47
|
+
const restored = db.prepare('SELECT * FROM workspaces WHERE id = ?').get(existing.id) as Workspace;
|
|
48
|
+
return restored;
|
|
49
|
+
}
|
|
43
50
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
// Create new workspace
|
|
52
|
+
const id = uuidv4();
|
|
53
|
+
const now = new Date().toISOString();
|
|
54
|
+
db.prepare('INSERT INTO workspaces (id, path, label, deleted, created_at) VALUES (?, ?, ?, 0, ?)').run(
|
|
55
|
+
id,
|
|
56
|
+
trimmedPath,
|
|
57
|
+
label,
|
|
58
|
+
now,
|
|
59
|
+
);
|
|
50
60
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
);
|
|
61
|
+
const workspace = db.prepare('SELECT * FROM workspaces WHERE id = ?').get(id) as Workspace;
|
|
62
|
+
return workspace;
|
|
63
|
+
});
|
|
55
64
|
|
|
56
65
|
// GET /api/pick-directory — open native OS folder picker (macOS only)
|
|
57
66
|
fastify.get('/api/pick-directory', async (_request, reply) => {
|
|
@@ -69,30 +78,39 @@ export async function registerWorkspaceRoutes(fastify: FastifyInstance): Promise
|
|
|
69
78
|
});
|
|
70
79
|
|
|
71
80
|
// POST /api/workspaces/hide — soft-delete workspace by path (creates record if not exists)
|
|
72
|
-
fastify.post<{ Body: { path: string } }>(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
fastify.post<{ Body: { path: string } }>('/api/workspaces/hide', {
|
|
82
|
+
schema: {
|
|
83
|
+
body: {
|
|
84
|
+
type: 'object',
|
|
85
|
+
required: ['path'],
|
|
86
|
+
properties: {
|
|
87
|
+
path: { type: 'string' },
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
}, async (request, reply) => {
|
|
92
|
+
const { path } = request.body || {};
|
|
93
|
+
if (!path || !path.trim()) {
|
|
94
|
+
return reply.code(400).send({ error: 'Path is required' });
|
|
95
|
+
}
|
|
96
|
+
const trimmedPath = path.trim();
|
|
97
|
+
const db = getDb();
|
|
81
98
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
99
|
+
const existing = db.prepare('SELECT * FROM workspaces WHERE path = ?').get(trimmedPath) as Workspace | undefined;
|
|
100
|
+
if (existing) {
|
|
101
|
+
db.prepare('UPDATE workspaces SET deleted = 1 WHERE id = ?').run(existing.id);
|
|
102
|
+
} else {
|
|
103
|
+
const id = uuidv4();
|
|
104
|
+
const now = new Date().toISOString();
|
|
105
|
+
db.prepare('INSERT INTO workspaces (id, path, label, deleted, created_at) VALUES (?, ?, NULL, 1, ?)').run(
|
|
106
|
+
id,
|
|
107
|
+
trimmedPath,
|
|
108
|
+
now,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
92
111
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
);
|
|
112
|
+
return { success: true };
|
|
113
|
+
});
|
|
96
114
|
|
|
97
115
|
// DELETE /api/workspaces/:id — soft-delete workspace by ID (does NOT delete sessions)
|
|
98
116
|
fastify.delete<{ Params: { id: string } }>('/api/workspaces/:id', async (request, reply) => {
|
|
@@ -107,4 +125,4 @@ export async function registerWorkspaceRoutes(fastify: FastifyInstance): Promise
|
|
|
107
125
|
db.prepare('UPDATE workspaces SET deleted = 1 WHERE id = ?').run(id);
|
|
108
126
|
return { success: true };
|
|
109
127
|
});
|
|
110
|
-
}
|
|
128
|
+
}
|