repoburg 1.3.202 → 1.3.209
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/backend/dist/index.js +2103 -32003
- package/agent-pane/backend/dist/server.js +45313 -0
- package/backend/dist/src/agent-runtime/agent-runtime.types.d.ts +1 -0
- package/backend/dist/src/llm-orchestration/hooks/frontend-notification.hook.js +6 -6
- package/backend/dist/src/llm-orchestration/hooks/frontend-notification.hook.js.map +1 -1
- package/backend/dist/src/llm-orchestration/llm-turn-processor.service.js +1 -0
- package/backend/dist/src/llm-orchestration/llm-turn-processor.service.js.map +1 -1
- package/backend/dist/tsconfig.build.tsbuildinfo +1 -1
- package/daemon/dist/agentPane.js +128 -0
- package/daemon/dist/agentPane.spec.js +183 -0
- package/daemon/dist/index.js +28 -8
- package/daemon/package.json +1 -1
- package/model-selector-scroll.png +0 -0
- package/package.json +10 -2
- package/agent-pane/.prettierignore +0 -5
- package/agent-pane/.prettierrc +0 -8
- package/agent-pane/backend/coverage/base.css +0 -224
- package/agent-pane/backend/coverage/block-navigation.js +0 -87
- package/agent-pane/backend/coverage/clover.xml +0 -887
- package/agent-pane/backend/coverage/coverage-final.json +0 -10
- package/agent-pane/backend/coverage/favicon.png +0 -0
- package/agent-pane/backend/coverage/index.html +0 -176
- package/agent-pane/backend/coverage/prettify.css +0 -1
- package/agent-pane/backend/coverage/prettify.js +0 -2
- package/agent-pane/backend/coverage/sort-arrow-sprite.png +0 -0
- package/agent-pane/backend/coverage/sorter.js +0 -210
- package/agent-pane/backend/coverage/src/codex/codex-provider.ts.html +0 -1402
- package/agent-pane/backend/coverage/src/codex/index.html +0 -131
- package/agent-pane/backend/coverage/src/codex/item-mapper.ts.html +0 -331
- package/agent-pane/backend/coverage/src/db/index.html +0 -131
- package/agent-pane/backend/coverage/src/db/index.ts.html +0 -436
- package/agent-pane/backend/coverage/src/db/schema.ts.html +0 -175
- package/agent-pane/backend/coverage/src/index.html +0 -116
- package/agent-pane/backend/coverage/src/index.ts.html +0 -262
- package/agent-pane/backend/coverage/src/routes/index.html +0 -146
- package/agent-pane/backend/coverage/src/routes/sessions.ts.html +0 -991
- package/agent-pane/backend/coverage/src/routes/turns.ts.html +0 -256
- package/agent-pane/backend/coverage/src/routes/workspaces.ts.html +0 -406
- package/agent-pane/backend/coverage/src/ws/index.html +0 -116
- package/agent-pane/backend/coverage/src/ws/index.ts.html +0 -238
- 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/package-lock.json +0 -5945
- package/agent-pane/backend/package.json +0 -33
- package/agent-pane/backend/scripts/backfill-from-jsonl.ts +0 -206
- package/agent-pane/backend/scripts/import-session-from-jsonl.ts +0 -264
- package/agent-pane/backend/src/claude/claude-message-mapper.test.ts +0 -271
- package/agent-pane/backend/src/claude/claude-message-mapper.ts +0 -259
- package/agent-pane/backend/src/claude/claude-provider.test.ts +0 -455
- package/agent-pane/backend/src/claude/claude-provider.ts +0 -1319
- package/agent-pane/backend/src/claude/claude-types.ts +0 -45
- package/agent-pane/backend/src/codex/app-server-client.test.ts +0 -183
- package/agent-pane/backend/src/codex/app-server-client.ts +0 -247
- package/agent-pane/backend/src/codex/app-server-types.ts +0 -259
- package/agent-pane/backend/src/codex/codex-provider-runtime-config.test.ts +0 -56
- package/agent-pane/backend/src/codex/codex-provider.test.ts +0 -60
- package/agent-pane/backend/src/codex/codex-provider.ts +0 -619
- package/agent-pane/backend/src/codex/item-mapper.test.ts +0 -221
- package/agent-pane/backend/src/codex/item-mapper.ts +0 -86
- package/agent-pane/backend/src/codex/shared-app-server.ts +0 -25
- package/agent-pane/backend/src/db/index.test.ts +0 -125
- package/agent-pane/backend/src/db/index.ts +0 -236
- package/agent-pane/backend/src/db/schema.ts +0 -34
- package/agent-pane/backend/src/index.ts +0 -89
- package/agent-pane/backend/src/integration/__fixtures__/cancel-turn.json +0 -29
- package/agent-pane/backend/src/integration/__fixtures__/delta-streaming.json +0 -40
- package/agent-pane/backend/src/integration/__fixtures__/failed-turn.json +0 -16
- package/agent-pane/backend/src/integration/__fixtures__/mcp-tool-call.json +0 -60
- package/agent-pane/backend/src/integration/__fixtures__/missing-item-started.json +0 -34
- package/agent-pane/backend/src/integration/__fixtures__/multi-item-turn.json +0 -85
- package/agent-pane/backend/src/integration/__fixtures__/multi-turn.json +0 -48
- package/agent-pane/backend/src/integration/__fixtures__/simple-turn.json +0 -35
- package/agent-pane/backend/src/integration/delta-streaming.test.ts +0 -130
- package/agent-pane/backend/src/integration/helpers/app-server-mock.ts +0 -272
- package/agent-pane/backend/src/integration/helpers/test-app.ts +0 -110
- package/agent-pane/backend/src/integration/helpers/ws-capture.ts +0 -64
- package/agent-pane/backend/src/integration/item-lifecycle.test.ts +0 -133
- package/agent-pane/backend/src/integration/mcp-tool-call.test.ts +0 -102
- package/agent-pane/backend/src/integration/missing-item-started.test.ts +0 -91
- package/agent-pane/backend/src/integration/multi-turn.test.ts +0 -121
- package/agent-pane/backend/src/integration/session-operations.test.ts +0 -175
- package/agent-pane/backend/src/integration/threads-claude-fork.test.ts +0 -241
- package/agent-pane/backend/src/integration/threads.test.ts +0 -309
- package/agent-pane/backend/src/integration/title-generation.test.ts +0 -109
- package/agent-pane/backend/src/integration/turn-cancel.test.ts +0 -111
- package/agent-pane/backend/src/integration/turn-failure.test.ts +0 -97
- package/agent-pane/backend/src/integration/turn-lifecycle.test.ts +0 -141
- package/agent-pane/backend/src/providers/index.ts +0 -16
- package/agent-pane/backend/src/providers/registry.test.ts +0 -92
- package/agent-pane/backend/src/providers/registry.ts +0 -43
- package/agent-pane/backend/src/providers/runtime-provider.ts +0 -49
- package/agent-pane/backend/src/repoburg/action-mapping.test.ts +0 -122
- package/agent-pane/backend/src/repoburg/action-mapping.ts +0 -294
- package/agent-pane/backend/src/repoburg/agent-runtime-client.test.ts +0 -271
- package/agent-pane/backend/src/repoburg/agent-runtime-client.ts +0 -285
- package/agent-pane/backend/src/repoburg/backend-resolver.test.ts +0 -356
- package/agent-pane/backend/src/repoburg/backend-resolver.ts +0 -261
- package/agent-pane/backend/src/repoburg/repoburg-client.test.ts +0 -180
- package/agent-pane/backend/src/repoburg/repoburg-client.ts +0 -194
- package/agent-pane/backend/src/repoburg/repoburg-provider.test.ts +0 -780
- package/agent-pane/backend/src/repoburg/repoburg-provider.ts +0 -471
- package/agent-pane/backend/src/repoburg/turn-projector.test.ts +0 -621
- package/agent-pane/backend/src/repoburg/turn-projector.ts +0 -637
- package/agent-pane/backend/src/routes/files.ts +0 -31
- package/agent-pane/backend/src/routes/mcp.ts +0 -33
- package/agent-pane/backend/src/routes/models.test.ts +0 -92
- package/agent-pane/backend/src/routes/models.ts +0 -16
- package/agent-pane/backend/src/routes/plugins.ts +0 -40
- package/agent-pane/backend/src/routes/providers.test.ts +0 -86
- package/agent-pane/backend/src/routes/providers.ts +0 -86
- package/agent-pane/backend/src/routes/sessions.test.ts +0 -772
- package/agent-pane/backend/src/routes/sessions.ts +0 -565
- package/agent-pane/backend/src/routes/skills.test.ts +0 -114
- package/agent-pane/backend/src/routes/skills.ts +0 -70
- package/agent-pane/backend/src/routes/threads.ts +0 -323
- package/agent-pane/backend/src/routes/turns.test.ts +0 -196
- package/agent-pane/backend/src/routes/turns.ts +0 -103
- package/agent-pane/backend/src/routes/workspaces.test.ts +0 -197
- package/agent-pane/backend/src/routes/workspaces.ts +0 -128
- package/agent-pane/backend/src/ws/index.test.ts +0 -107
- package/agent-pane/backend/src/ws/index.ts +0 -51
- package/agent-pane/backend/tsconfig.json +0 -25
- package/agent-pane/backend/vitest.config.ts +0 -20
- package/agent-pane/eslint.config.js +0 -33
- package/agent-pane/frontend/coverage/base.css +0 -224
- package/agent-pane/frontend/coverage/block-navigation.js +0 -87
- package/agent-pane/frontend/coverage/clover.xml +0 -2962
- package/agent-pane/frontend/coverage/coverage-final.json +0 -37
- package/agent-pane/frontend/coverage/favicon.png +0 -0
- package/agent-pane/frontend/coverage/index.html +0 -251
- package/agent-pane/frontend/coverage/prettify.css +0 -1
- package/agent-pane/frontend/coverage/prettify.js +0 -2
- package/agent-pane/frontend/coverage/sort-arrow-sprite.png +0 -0
- package/agent-pane/frontend/coverage/sorter.js +0 -210
- package/agent-pane/frontend/coverage/src/App.tsx.html +0 -217
- package/agent-pane/frontend/coverage/src/AppInitializer.tsx.html +0 -223
- package/agent-pane/frontend/coverage/src/api/client.ts.html +0 -187
- package/agent-pane/frontend/coverage/src/api/index.html +0 -161
- package/agent-pane/frontend/coverage/src/api/sessions.ts.html +0 -316
- package/agent-pane/frontend/coverage/src/api/websocket.ts.html +0 -544
- package/agent-pane/frontend/coverage/src/api/workspaces.ts.html +0 -199
- package/agent-pane/frontend/coverage/src/components/CollapsibleToolCalls.tsx.html +0 -226
- package/agent-pane/frontend/coverage/src/components/CommandInputBar.tsx.html +0 -382
- package/agent-pane/frontend/coverage/src/components/CommandPalette.tsx.html +0 -715
- package/agent-pane/frontend/coverage/src/components/IdeWorkspace.tsx.html +0 -160
- package/agent-pane/frontend/coverage/src/components/ModelSelector.tsx.html +0 -478
- package/agent-pane/frontend/coverage/src/components/NewSessionDialog.tsx.html +0 -472
- package/agent-pane/frontend/coverage/src/components/RenderArea.tsx.html +0 -574
- package/agent-pane/frontend/coverage/src/components/RightPanel.tsx.html +0 -205
- package/agent-pane/frontend/coverage/src/components/RuntimeIcon.tsx.html +0 -133
- package/agent-pane/frontend/coverage/src/components/SessionTabs.tsx.html +0 -217
- package/agent-pane/frontend/coverage/src/components/StatusBar.tsx.html +0 -271
- package/agent-pane/frontend/coverage/src/components/StatusDot.tsx.html +0 -109
- package/agent-pane/frontend/coverage/src/components/Toast.tsx.html +0 -313
- package/agent-pane/frontend/coverage/src/components/TurnContent.tsx.html +0 -1216
- package/agent-pane/frontend/coverage/src/components/WorkspaceSidebar.tsx.html +0 -1138
- package/agent-pane/frontend/coverage/src/components/index.html +0 -326
- package/agent-pane/frontend/coverage/src/components/panels/ChangesPanel.tsx.html +0 -247
- package/agent-pane/frontend/coverage/src/components/panels/LogsPanel.tsx.html +0 -160
- package/agent-pane/frontend/coverage/src/components/panels/MetricsPanel.tsx.html +0 -427
- package/agent-pane/frontend/coverage/src/components/panels/TurnsPanel.tsx.html +0 -397
- package/agent-pane/frontend/coverage/src/components/panels/index.html +0 -161
- package/agent-pane/frontend/coverage/src/constants/index.html +0 -116
- package/agent-pane/frontend/coverage/src/constants/models.ts.html +0 -406
- package/agent-pane/frontend/coverage/src/hooks/index.html +0 -146
- package/agent-pane/frontend/coverage/src/hooks/useEscapeKey.ts.html +0 -124
- package/agent-pane/frontend/coverage/src/hooks/useOutsideClick.ts.html +0 -142
- package/agent-pane/frontend/coverage/src/hooks/useSessionStatus.ts.html +0 -187
- package/agent-pane/frontend/coverage/src/index.html +0 -146
- package/agent-pane/frontend/coverage/src/main.tsx.html +0 -115
- package/agent-pane/frontend/coverage/src/store/index.html +0 -116
- package/agent-pane/frontend/coverage/src/store/store.ts.html +0 -2404
- package/agent-pane/frontend/coverage/src/test/component-helpers.ts.html +0 -190
- package/agent-pane/frontend/coverage/src/test/fixtures.ts.html +0 -367
- package/agent-pane/frontend/coverage/src/test/index.html +0 -131
- package/agent-pane/frontend/coverage/src/types/index.html +0 -116
- package/agent-pane/frontend/coverage/src/types/index.ts.html +0 -466
- package/agent-pane/frontend/coverage/src/utils/changes.ts.html +0 -202
- package/agent-pane/frontend/coverage/src/utils/duration.ts.html +0 -127
- package/agent-pane/frontend/coverage/src/utils/index.html +0 -131
- package/agent-pane/frontend/dist/assets/index-CJxD4YmJ.js +0 -370
- package/agent-pane/frontend/dist/assets/index-r-xwfOKQ.css +0 -1
- package/agent-pane/frontend/dist/index.html +0 -19
- package/agent-pane/frontend/index.html +0 -18
- package/agent-pane/frontend/package-lock.json +0 -4211
- package/agent-pane/frontend/package.json +0 -35
- package/agent-pane/frontend/src/App.test.tsx +0 -74
- package/agent-pane/frontend/src/App.tsx +0 -44
- package/agent-pane/frontend/src/AppInitializer.test.tsx +0 -142
- package/agent-pane/frontend/src/AppInitializer.tsx +0 -46
- package/agent-pane/frontend/src/__tests__/ThreadsPanel.test.tsx +0 -192
- package/agent-pane/frontend/src/api/client.test.ts +0 -126
- package/agent-pane/frontend/src/api/client.ts +0 -34
- package/agent-pane/frontend/src/api/models.ts +0 -7
- package/agent-pane/frontend/src/api/providers.ts +0 -17
- package/agent-pane/frontend/src/api/sessions.test.ts +0 -246
- package/agent-pane/frontend/src/api/sessions.ts +0 -97
- package/agent-pane/frontend/src/api/skills.ts +0 -12
- package/agent-pane/frontend/src/api/websocket.test.ts +0 -288
- package/agent-pane/frontend/src/api/websocket.ts +0 -240
- package/agent-pane/frontend/src/api/workspaces.test.ts +0 -112
- package/agent-pane/frontend/src/api/workspaces.ts +0 -38
- package/agent-pane/frontend/src/components/AskUserItem.test.tsx +0 -143
- package/agent-pane/frontend/src/components/AskUserItem.tsx +0 -207
- package/agent-pane/frontend/src/components/CollapsibleToolCalls.test.tsx +0 -49
- package/agent-pane/frontend/src/components/CollapsibleToolCalls.tsx +0 -56
- package/agent-pane/frontend/src/components/CommandInputBar.test.tsx +0 -133
- package/agent-pane/frontend/src/components/CommandInputBar.tsx +0 -584
- package/agent-pane/frontend/src/components/CommandPalette.test.tsx +0 -226
- package/agent-pane/frontend/src/components/CommandPalette.tsx +0 -194
- package/agent-pane/frontend/src/components/CommentableText.test.tsx +0 -196
- package/agent-pane/frontend/src/components/CommentableText.tsx +0 -379
- package/agent-pane/frontend/src/components/IdeWorkspace.test.tsx +0 -39
- package/agent-pane/frontend/src/components/IdeWorkspace.tsx +0 -51
- package/agent-pane/frontend/src/components/ModelSelector.test.tsx +0 -129
- package/agent-pane/frontend/src/components/ModelSelector.tsx +0 -191
- package/agent-pane/frontend/src/components/NewSessionDialog.test.tsx +0 -95
- package/agent-pane/frontend/src/components/NewSessionDialog.tsx +0 -129
- package/agent-pane/frontend/src/components/RenderArea.test.tsx +0 -160
- package/agent-pane/frontend/src/components/RenderArea.tsx +0 -170
- package/agent-pane/frontend/src/components/ResizeHandle.tsx +0 -29
- package/agent-pane/frontend/src/components/RightPanel.test.tsx +0 -43
- package/agent-pane/frontend/src/components/RightPanel.tsx +0 -46
- package/agent-pane/frontend/src/components/RuntimeIcon.test.tsx +0 -32
- package/agent-pane/frontend/src/components/RuntimeIcon.tsx +0 -16
- package/agent-pane/frontend/src/components/SessionTabs.test.tsx +0 -72
- package/agent-pane/frontend/src/components/SessionTabs.tsx +0 -49
- package/agent-pane/frontend/src/components/StatusBar.test.tsx +0 -56
- package/agent-pane/frontend/src/components/StatusBar.tsx +0 -129
- package/agent-pane/frontend/src/components/StatusDot.test.tsx +0 -25
- package/agent-pane/frontend/src/components/StatusDot.tsx +0 -8
- package/agent-pane/frontend/src/components/Toast.test.tsx +0 -79
- package/agent-pane/frontend/src/components/Toast.tsx +0 -76
- package/agent-pane/frontend/src/components/TurnContent.test.tsx +0 -377
- package/agent-pane/frontend/src/components/TurnContent.tsx +0 -594
- package/agent-pane/frontend/src/components/WorkspaceSidebar.test.tsx +0 -297
- package/agent-pane/frontend/src/components/WorkspaceSidebar.tsx +0 -351
- package/agent-pane/frontend/src/components/input/OptionSubPopup.tsx +0 -32
- package/agent-pane/frontend/src/components/input/SkillMentionPopup.test.tsx +0 -63
- package/agent-pane/frontend/src/components/input/SkillMentionPopup.tsx +0 -78
- package/agent-pane/frontend/src/components/input/SlashPopup.test.tsx +0 -96
- package/agent-pane/frontend/src/components/input/SlashPopup.tsx +0 -82
- package/agent-pane/frontend/src/components/panels/ChangesPanel.test.tsx +0 -61
- package/agent-pane/frontend/src/components/panels/ChangesPanel.tsx +0 -54
- package/agent-pane/frontend/src/components/panels/TasksPanel.test.tsx +0 -97
- package/agent-pane/frontend/src/components/panels/TasksPanel.tsx +0 -208
- package/agent-pane/frontend/src/components/panels/ThreadsPanel.tsx +0 -382
- package/agent-pane/frontend/src/components/panels/TurnsPanel.test.tsx +0 -131
- package/agent-pane/frontend/src/components/panels/TurnsPanel.tsx +0 -131
- package/agent-pane/frontend/src/constants/models.test.ts +0 -97
- package/agent-pane/frontend/src/constants/models.ts +0 -166
- package/agent-pane/frontend/src/hooks/useEscapeKey.test.ts +0 -42
- package/agent-pane/frontend/src/hooks/useEscapeKey.ts +0 -13
- package/agent-pane/frontend/src/hooks/useOutsideClick.test.ts +0 -92
- package/agent-pane/frontend/src/hooks/useOutsideClick.ts +0 -19
- package/agent-pane/frontend/src/hooks/useProvider.ts +0 -44
- package/agent-pane/frontend/src/hooks/useResizable.test.ts +0 -97
- package/agent-pane/frontend/src/hooks/useResizable.ts +0 -75
- package/agent-pane/frontend/src/hooks/useSessionStatus.test.ts +0 -116
- package/agent-pane/frontend/src/hooks/useSessionStatus.ts +0 -34
- package/agent-pane/frontend/src/main.test.tsx +0 -37
- package/agent-pane/frontend/src/main.tsx +0 -10
- package/agent-pane/frontend/src/store/sessionSlice.ts +0 -440
- package/agent-pane/frontend/src/store/store.test.ts +0 -1338
- package/agent-pane/frontend/src/store/store.ts +0 -37
- package/agent-pane/frontend/src/store/threadSlice.ts +0 -136
- package/agent-pane/frontend/src/store/uiSlice.ts +0 -133
- package/agent-pane/frontend/src/store/workspaceSlice.ts +0 -77
- package/agent-pane/frontend/src/store/wsSlice.ts +0 -368
- package/agent-pane/frontend/src/styles/app.css +0 -627
- package/agent-pane/frontend/src/styles/tokens.css +0 -62
- package/agent-pane/frontend/src/test/component-helpers.ts +0 -38
- package/agent-pane/frontend/src/test/fixtures.ts +0 -101
- package/agent-pane/frontend/src/test-setup.ts +0 -5
- package/agent-pane/frontend/src/types/index.test.ts +0 -128
- package/agent-pane/frontend/src/types/index.ts +0 -134
- package/agent-pane/frontend/src/utils/changes.test.ts +0 -81
- package/agent-pane/frontend/src/utils/changes.ts +0 -39
- package/agent-pane/frontend/src/utils/duration.test.ts +0 -76
- package/agent-pane/frontend/src/utils/duration.ts +0 -14
- package/agent-pane/frontend/tsconfig.json +0 -25
- package/agent-pane/frontend/vite.config.ts +0 -22
- package/agent-pane/frontend/vitest.config.ts +0 -29
- package/agent-pane/package-lock.json +0 -1527
- package/agent-pane/package.json +0 -19
- package/agent-pane/shared/provider-types.ts +0 -67
- package/agent-pane/shared/types.ts +0 -184
|
@@ -1,565 +0,0 @@
|
|
|
1
|
-
import type { FastifyInstance } from 'fastify';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
4
|
-
import { getDb } from '../db/index.ts';
|
|
5
|
-
import { getProvider } from '../providers/registry.ts';
|
|
6
|
-
import type { Session, SessionDetail, Turn, Item, DiscussionThread, Attachment } from '../db/schema.ts';
|
|
7
|
-
|
|
8
|
-
/** Raw DB row for turns — token_usage, attachments and runtime_input_ids are
|
|
9
|
-
* JSON strings, not parsed; no items field. */
|
|
10
|
-
interface TurnRow extends Omit<Turn, 'token_usage' | 'items' | 'attachments' | 'runtime_input_ids'> {
|
|
11
|
-
token_usage: string | null;
|
|
12
|
-
attachments: string | null;
|
|
13
|
-
runtime_input_ids: string | null;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** Raw DB row for items — data is a JSON string, not parsed. */
|
|
17
|
-
interface ItemRow extends Omit<Item, 'data'> {
|
|
18
|
-
data: string | null;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export async function registerSessionRoutes(fastify: FastifyInstance): Promise<void> {
|
|
22
|
-
// GET /api/sessions — list all sessions
|
|
23
|
-
fastify.get('/api/sessions', async () => {
|
|
24
|
-
const rows = getDb().prepare('SELECT * FROM sessions ORDER BY created_at DESC').all() as Session[];
|
|
25
|
-
return rows;
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
// GET /api/sessions/:id — session with turns + items
|
|
29
|
-
fastify.get<{ Params: { id: string } }>('/api/sessions/:id', async (request, reply) => {
|
|
30
|
-
const { id } = request.params;
|
|
31
|
-
const db = getDb();
|
|
32
|
-
|
|
33
|
-
const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(id) as Session | undefined;
|
|
34
|
-
if (!session) {
|
|
35
|
-
return reply.code(404).send({ error: 'Session not found' });
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// If the session is rewound, only show turns up to and including the rewind point
|
|
39
|
-
const turnFilter = session.rewound_at_turn_id
|
|
40
|
-
? db.prepare('SELECT created_at FROM turns WHERE id = ?').get(session.rewound_at_turn_id) as { created_at: string } | undefined
|
|
41
|
-
: null;
|
|
42
|
-
|
|
43
|
-
const turnRows = turnFilter
|
|
44
|
-
? db
|
|
45
|
-
.prepare('SELECT * FROM turns WHERE session_id = ? AND created_at <= ? ORDER BY created_at ASC')
|
|
46
|
-
.all(id, turnFilter.created_at) as TurnRow[]
|
|
47
|
-
: db
|
|
48
|
-
.prepare('SELECT * FROM turns WHERE session_id = ? ORDER BY created_at ASC')
|
|
49
|
-
.all(id) as TurnRow[];
|
|
50
|
-
|
|
51
|
-
const itemRows = db
|
|
52
|
-
.prepare(
|
|
53
|
-
'SELECT * FROM items WHERE turn_id IN (SELECT id FROM turns WHERE session_id = ?' +
|
|
54
|
-
(turnFilter ? ' AND created_at <= ?' : '') +
|
|
55
|
-
') ORDER BY created_at ASC',
|
|
56
|
-
)
|
|
57
|
-
.all(...(turnFilter ? [id, turnFilter.created_at] : [id])) as ItemRow[];
|
|
58
|
-
|
|
59
|
-
// Parse item data from JSON string and group by turn_id
|
|
60
|
-
const itemsByTurn = new Map<string, Item[]>();
|
|
61
|
-
for (const row of itemRows) {
|
|
62
|
-
const item: Item = {
|
|
63
|
-
...row,
|
|
64
|
-
data: row.data ? JSON.parse(row.data) : null,
|
|
65
|
-
};
|
|
66
|
-
const list = itemsByTurn.get(item.turn_id) ?? [];
|
|
67
|
-
list.push(item);
|
|
68
|
-
itemsByTurn.set(item.turn_id, list);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const detail: SessionDetail = {
|
|
72
|
-
...session,
|
|
73
|
-
turns: turnRows.map((row) => ({
|
|
74
|
-
...row,
|
|
75
|
-
token_usage: row.token_usage ? JSON.parse(row.token_usage) : null,
|
|
76
|
-
attachments: row.attachments ? (JSON.parse(row.attachments) as Attachment[]) : null,
|
|
77
|
-
runtime_input_ids: row.runtime_input_ids ? (JSON.parse(row.runtime_input_ids) as string[]) : null,
|
|
78
|
-
items: itemsByTurn.get(row.id) ?? [],
|
|
79
|
-
})),
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
// Fetch discussion threads for this session and attach to the response
|
|
83
|
-
const threadRows = db
|
|
84
|
-
.prepare('SELECT * FROM discussion_threads WHERE session_id = ? ORDER BY created_at ASC')
|
|
85
|
-
.all(id) as DiscussionThread[];
|
|
86
|
-
|
|
87
|
-
if (threadRows.length > 0) {
|
|
88
|
-
detail.threads = threadRows.map((tr) => ({
|
|
89
|
-
...tr,
|
|
90
|
-
turns: detail.turns.filter((t) => t.thread_id === tr.id),
|
|
91
|
-
}));
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Restore persisted context-window usage so the status-bar counter survives reload.
|
|
95
|
-
const cuRow = db.prepare('SELECT context_usage FROM sessions WHERE id = ?').get(id) as
|
|
96
|
-
| { context_usage: string | null }
|
|
97
|
-
| undefined;
|
|
98
|
-
if (cuRow?.context_usage) {
|
|
99
|
-
try {
|
|
100
|
-
detail.contextUsage = JSON.parse(cuRow.context_usage) as SessionDetail['contextUsage'];
|
|
101
|
-
} catch {
|
|
102
|
-
// Corrupt JSON — leave contextUsage unset.
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return detail;
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
// POST /api/sessions — create session
|
|
110
|
-
fastify.post<{
|
|
111
|
-
Body: { title?: string; runtime_type?: string; model?: string; model_reasoning_effort?: string; cwd?: string; runtime_config?: string };
|
|
112
|
-
}>('/api/sessions', {
|
|
113
|
-
schema: {
|
|
114
|
-
body: {
|
|
115
|
-
type: 'object',
|
|
116
|
-
properties: {
|
|
117
|
-
title: { type: 'string' },
|
|
118
|
-
runtime_type: { type: 'string' },
|
|
119
|
-
model: { type: ['string', 'null'] },
|
|
120
|
-
model_reasoning_effort: { type: ['string', 'null'] },
|
|
121
|
-
cwd: { type: ['string', 'null'] },
|
|
122
|
-
runtime_config: { type: ['string', 'null'] },
|
|
123
|
-
},
|
|
124
|
-
},
|
|
125
|
-
},
|
|
126
|
-
}, async (request) => {
|
|
127
|
-
const {
|
|
128
|
-
title = null,
|
|
129
|
-
runtime_type = 'codex',
|
|
130
|
-
model = null,
|
|
131
|
-
model_reasoning_effort = null,
|
|
132
|
-
cwd = null,
|
|
133
|
-
runtime_config = null,
|
|
134
|
-
} = request.body || {};
|
|
135
|
-
const id = uuidv4();
|
|
136
|
-
const now = new Date().toISOString();
|
|
137
|
-
|
|
138
|
-
getDb()
|
|
139
|
-
.prepare(
|
|
140
|
-
`INSERT INTO sessions (id, title, runtime_type, model, model_reasoning_effort, cwd, external_thread_id, status, is_running, created_at, updated_at, runtime_config)
|
|
141
|
-
VALUES (?, ?, ?, ?, ?, ?, NULL, 'active', 0, ?, ?, ?)`,
|
|
142
|
-
)
|
|
143
|
-
.run(id, title, runtime_type, model, model_reasoning_effort, cwd, now, now, runtime_config);
|
|
144
|
-
|
|
145
|
-
const session = getDb().prepare('SELECT * FROM sessions WHERE id = ?').get(id) as Session;
|
|
146
|
-
return session;
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
// DELETE /api/sessions/:id — delete session (cascade)
|
|
150
|
-
fastify.delete<{ Params: { id: string } }>('/api/sessions/:id', async (request, reply) => {
|
|
151
|
-
const { id } = request.params;
|
|
152
|
-
const db = getDb();
|
|
153
|
-
|
|
154
|
-
const existing = db.prepare('SELECT id FROM sessions WHERE id = ?').get(id);
|
|
155
|
-
if (!existing) {
|
|
156
|
-
return reply.code(404).send({ error: 'Session not found' });
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Delete items first (turns cascade via FK, but better-sqlite3 needs explicit cascade if pragma is set)
|
|
160
|
-
db.prepare('DELETE FROM items WHERE turn_id IN (SELECT id FROM turns WHERE session_id = ?)').run(id);
|
|
161
|
-
db.prepare('DELETE FROM turns WHERE session_id = ?').run(id);
|
|
162
|
-
db.prepare('DELETE FROM sessions WHERE id = ?').run(id);
|
|
163
|
-
|
|
164
|
-
return { success: true };
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
// PATCH /api/sessions/:id — update session (title, lifecycle, has_unreviewed, model, model_reasoning_effort, runtime_config, cwd)
|
|
168
|
-
fastify.patch<{
|
|
169
|
-
Params: { id: string };
|
|
170
|
-
Body: {
|
|
171
|
-
title?: string;
|
|
172
|
-
lifecycle?: string;
|
|
173
|
-
has_unreviewed?: number;
|
|
174
|
-
model?: string | null;
|
|
175
|
-
model_reasoning_effort?: string | null;
|
|
176
|
-
runtime_config?: string | null;
|
|
177
|
-
runtime_type?: string;
|
|
178
|
-
cwd?: string | null;
|
|
179
|
-
};
|
|
180
|
-
}>('/api/sessions/:id', {
|
|
181
|
-
schema: {
|
|
182
|
-
body: {
|
|
183
|
-
type: 'object',
|
|
184
|
-
properties: {
|
|
185
|
-
title: { type: 'string' },
|
|
186
|
-
lifecycle: { type: 'string' },
|
|
187
|
-
has_unreviewed: { type: 'number' },
|
|
188
|
-
model: { type: ['string', 'null'] },
|
|
189
|
-
model_reasoning_effort: { type: ['string', 'null'] },
|
|
190
|
-
runtime_config: { type: ['string', 'null'] },
|
|
191
|
-
runtime_type: { type: 'string' },
|
|
192
|
-
cwd: { type: ['string', 'null'] },
|
|
193
|
-
},
|
|
194
|
-
},
|
|
195
|
-
},
|
|
196
|
-
}, async (request, reply) => {
|
|
197
|
-
const { id } = request.params;
|
|
198
|
-
const { title, lifecycle, has_unreviewed, model, model_reasoning_effort, runtime_config, runtime_type, cwd } = request.body || {};
|
|
199
|
-
const db = getDb();
|
|
200
|
-
|
|
201
|
-
const existing = db.prepare('SELECT * FROM sessions WHERE id = ?').get(id) as Session | undefined;
|
|
202
|
-
if (!existing) {
|
|
203
|
-
return reply.code(404).send({ error: 'Session not found' });
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Block provider switch while a turn is running
|
|
207
|
-
if (runtime_type !== undefined && runtime_type !== existing.runtime_type && existing.is_running) {
|
|
208
|
-
return reply.code(409).send({ error: 'Cannot switch provider while a turn is running' });
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const now = new Date().toISOString();
|
|
212
|
-
const updates: string[] = ['updated_at = ?'];
|
|
213
|
-
const params: (string | number | null)[] = [now];
|
|
214
|
-
|
|
215
|
-
let providerChanged = false;
|
|
216
|
-
|
|
217
|
-
if (title !== undefined) {
|
|
218
|
-
updates.push('title = ?');
|
|
219
|
-
params.push(title);
|
|
220
|
-
}
|
|
221
|
-
if (lifecycle !== undefined) {
|
|
222
|
-
updates.push('lifecycle = ?');
|
|
223
|
-
params.push(lifecycle);
|
|
224
|
-
}
|
|
225
|
-
if (has_unreviewed !== undefined) {
|
|
226
|
-
updates.push('has_unreviewed = ?');
|
|
227
|
-
params.push(has_unreviewed);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Provider switch — clear model/reasoning if not explicitly provided
|
|
231
|
-
if (runtime_type !== undefined && runtime_type !== existing.runtime_type) {
|
|
232
|
-
providerChanged = true; // different provider can't resume the old thread
|
|
233
|
-
updates.push('runtime_type = ?');
|
|
234
|
-
params.push(runtime_type);
|
|
235
|
-
|
|
236
|
-
// Clear model if not explicitly set in this request
|
|
237
|
-
if (model === undefined) {
|
|
238
|
-
updates.push('model = ?');
|
|
239
|
-
params.push(null);
|
|
240
|
-
}
|
|
241
|
-
// Clear reasoning if not explicitly set
|
|
242
|
-
if (model_reasoning_effort === undefined) {
|
|
243
|
-
updates.push('model_reasoning_effort = ?');
|
|
244
|
-
params.push(null);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Workspace (cwd) change — the runtime thread belongs to the old
|
|
249
|
-
// workspace backend, so it is cleared like a provider switch; the next
|
|
250
|
-
// turn creates a fresh runtime session on the new workspace.
|
|
251
|
-
// Compare resolve-normalized paths — cosmetic differences (trailing
|
|
252
|
-
// slash) are not a workspace change.
|
|
253
|
-
let workspaceChanged = false;
|
|
254
|
-
if (cwd !== undefined) {
|
|
255
|
-
const oldAbs = existing.cwd ? path.resolve(existing.cwd) : null;
|
|
256
|
-
const newAbs = cwd ? path.resolve(cwd) : null;
|
|
257
|
-
workspaceChanged = oldAbs !== newAbs;
|
|
258
|
-
if (workspaceChanged && existing.is_running) {
|
|
259
|
-
return reply.code(409).send({ error: 'Cannot change workspace while a turn is running' });
|
|
260
|
-
}
|
|
261
|
-
updates.push('cwd = ?');
|
|
262
|
-
params.push(cwd);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Model and reasoning changes update DB columns but do NOT clear the thread.
|
|
266
|
-
// The Claude SDK supports query({ resume: sessionId, model: newModel }) —
|
|
267
|
-
// changing the model within the same provider preserves conversation context.
|
|
268
|
-
// In streaming-input mode, also call query.setModel() on the live session
|
|
269
|
-
// so the CLI switches models without restarting.
|
|
270
|
-
if (model !== undefined) {
|
|
271
|
-
updates.push('model = ?');
|
|
272
|
-
params.push(model);
|
|
273
|
-
// If the provider supports mid-session model switching and the model
|
|
274
|
-
// actually changed (same provider), update the live query.
|
|
275
|
-
if (model !== existing.model && !providerChanged) {
|
|
276
|
-
const provider = getProvider(existing.runtime_type);
|
|
277
|
-
if (provider.setModel) {
|
|
278
|
-
provider.setModel(id, model ?? '').catch((err) => {
|
|
279
|
-
console.error(`[patch] setModel failed for session ${id}:`, err);
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
if (model_reasoning_effort !== undefined) {
|
|
285
|
-
updates.push('model_reasoning_effort = ?');
|
|
286
|
-
params.push(model_reasoning_effort);
|
|
287
|
-
}
|
|
288
|
-
if (runtime_config !== undefined) {
|
|
289
|
-
updates.push('runtime_config = ?');
|
|
290
|
-
params.push(runtime_config);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
db.prepare(`UPDATE sessions SET ${updates.join(', ')} WHERE id = ?`).run(...params, id);
|
|
294
|
-
|
|
295
|
-
// Clear the thread when the PROVIDER or WORKSPACE changes — the runtime
|
|
296
|
-
// session belongs to the old provider/backend and cannot be resumed.
|
|
297
|
-
// Model/reasoning changes within the same provider keep the thread intact.
|
|
298
|
-
if (providerChanged || workspaceChanged) {
|
|
299
|
-
if (existing.external_thread_id) {
|
|
300
|
-
db.prepare('UPDATE sessions SET external_thread_id = NULL, rewound_at_turn_id = NULL, pre_rewind_thread_id = NULL, updated_at = ? WHERE id = ?').run(now, id);
|
|
301
|
-
}
|
|
302
|
-
// Drop the in-memory entry even without a persisted thread id (stale state).
|
|
303
|
-
getProvider(existing.runtime_type).clearThread(id);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(id) as Session;
|
|
307
|
-
return session;
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
// POST /api/sessions/:id/rewind — non-destructive rewind: hide turns after target, fork provider session
|
|
311
|
-
fastify.post<{ Params: { id: string }; Body: { turn_id: string } }>(
|
|
312
|
-
'/api/sessions/:id/rewind',
|
|
313
|
-
{
|
|
314
|
-
schema: {
|
|
315
|
-
body: {
|
|
316
|
-
type: 'object',
|
|
317
|
-
required: ['turn_id'],
|
|
318
|
-
properties: {
|
|
319
|
-
turn_id: { type: 'string' },
|
|
320
|
-
},
|
|
321
|
-
},
|
|
322
|
-
},
|
|
323
|
-
},
|
|
324
|
-
async (request, reply) => {
|
|
325
|
-
const { id } = request.params;
|
|
326
|
-
const { turn_id } = request.body || {};
|
|
327
|
-
if (!turn_id) {
|
|
328
|
-
return reply.code(400).send({ error: 'turn_id is required' });
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const db = getDb();
|
|
332
|
-
const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(id) as Session | undefined;
|
|
333
|
-
if (!session) {
|
|
334
|
-
return reply.code(404).send({ error: 'Session not found' });
|
|
335
|
-
}
|
|
336
|
-
if (session.is_running) {
|
|
337
|
-
return reply.code(409).send({ error: 'Cannot rewind while a turn is running' });
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
const targetTurnFull = db.prepare('SELECT * FROM turns WHERE id = ? AND session_id = ?').get(turn_id, id) as
|
|
341
|
-
| Turn
|
|
342
|
-
| undefined;
|
|
343
|
-
if (!targetTurnFull) {
|
|
344
|
-
return reply.code(404).send({ error: 'Turn not found' });
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
const provider = getProvider(session.runtime_type);
|
|
348
|
-
|
|
349
|
-
// RUNTIME-FIRST (repoburg in-place truncation): hard-delete the runtime
|
|
350
|
-
// history after the target turn BEFORE any local change. Errors pass
|
|
351
|
-
// through — on failure the local view stays untouched and consistent
|
|
352
|
-
// with the runtime (e.g. 400 summary-in-range from until-here).
|
|
353
|
-
if (provider.rewindRuntime) {
|
|
354
|
-
await provider.rewindRuntime(session, targetTurnFull);
|
|
355
|
-
|
|
356
|
-
// Platform-parity HARD DELETE: the runtime history after the anchor
|
|
357
|
-
// is permanently gone, so mirror it locally — delete the local turns
|
|
358
|
-
// after the target and leave NO rewind marker (nothing to restore).
|
|
359
|
-
// Stale markers are cleared too: a marker would hide every turn
|
|
360
|
-
// created after the rewind (GET /:id filters by rewound_at_turn_id).
|
|
361
|
-
const now2 = new Date().toISOString();
|
|
362
|
-
db.prepare(
|
|
363
|
-
'DELETE FROM items WHERE turn_id IN (SELECT id FROM turns WHERE session_id = ? AND created_at > ?)',
|
|
364
|
-
).run(id, targetTurnFull.created_at);
|
|
365
|
-
db.prepare('DELETE FROM turns WHERE session_id = ? AND created_at > ?').run(
|
|
366
|
-
id,
|
|
367
|
-
targetTurnFull.created_at,
|
|
368
|
-
);
|
|
369
|
-
db.prepare(
|
|
370
|
-
'UPDATE sessions SET rewound_at_turn_id = NULL, pre_rewind_thread_id = NULL, updated_at = ? WHERE id = ?',
|
|
371
|
-
).run(now2, id);
|
|
372
|
-
const truncated = db.prepare('SELECT * FROM sessions WHERE id = ?').get(id) as Session;
|
|
373
|
-
return truncated;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
const now = new Date().toISOString();
|
|
377
|
-
|
|
378
|
-
// Mark the session as rewound (non-destructive — turns are NOT deleted, just hidden)
|
|
379
|
-
// Save the original external_thread_id only on the first rewind (don't overwrite on subsequent rewinds)
|
|
380
|
-
// Use explicit two-step logic instead of COALESCE to avoid any evaluation-order ambiguity
|
|
381
|
-
if (!session.pre_rewind_thread_id && session.external_thread_id) {
|
|
382
|
-
db.prepare(
|
|
383
|
-
'UPDATE sessions SET rewound_at_turn_id = ?, pre_rewind_thread_id = ?, updated_at = ? WHERE id = ?',
|
|
384
|
-
).run(turn_id, session.external_thread_id, now, id);
|
|
385
|
-
console.log(`[rewind] saved pre_rewind_thread_id=${session.external_thread_id} for session ${id}`);
|
|
386
|
-
} else {
|
|
387
|
-
db.prepare(
|
|
388
|
-
'UPDATE sessions SET rewound_at_turn_id = ?, updated_at = ? WHERE id = ?',
|
|
389
|
-
).run(turn_id, now, id);
|
|
390
|
-
console.log(`[rewind] pre_rewind_thread_id already set (${session.pre_rewind_thread_id}) or external_thread_id is null for session ${id}`);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// Fork the provider-native session so the provider has conversation context up to the target turn
|
|
394
|
-
if (provider.forkSession) {
|
|
395
|
-
try {
|
|
396
|
-
const forkedId = await provider.forkSession(session, targetTurnFull);
|
|
397
|
-
if (forkedId) {
|
|
398
|
-
db.prepare('UPDATE sessions SET external_thread_id = ?, updated_at = ? WHERE id = ?')
|
|
399
|
-
.run(forkedId, now, id);
|
|
400
|
-
console.log(`[rewind] forked to ${forkedId} for session ${id}`);
|
|
401
|
-
} else {
|
|
402
|
-
console.warn(`[rewind] forkSession returned null for session ${id} (external_thread_id=${session.external_thread_id}, upToMessageId=${targetTurnFull.external_turn_id})`);
|
|
403
|
-
}
|
|
404
|
-
} catch (err) {
|
|
405
|
-
console.error(`[rewind] forkSession threw for session ${id}:`, err);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// Clear in-memory thread entry (the Claude session ID changed)
|
|
410
|
-
provider.clearThread(id);
|
|
411
|
-
|
|
412
|
-
const updated = db.prepare('SELECT * FROM sessions WHERE id = ?').get(id) as Session;
|
|
413
|
-
return updated;
|
|
414
|
-
},
|
|
415
|
-
);
|
|
416
|
-
|
|
417
|
-
// POST /api/sessions/:id/restore — undo rewind: show all turns again, restore original thread
|
|
418
|
-
fastify.post<{ Params: { id: string } }>('/api/sessions/:id/restore', async (request, reply) => {
|
|
419
|
-
const { id } = request.params;
|
|
420
|
-
const db = getDb();
|
|
421
|
-
const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(id) as Session | undefined;
|
|
422
|
-
if (!session) {
|
|
423
|
-
return reply.code(404).send({ error: 'Session not found' });
|
|
424
|
-
}
|
|
425
|
-
if (!session.rewound_at_turn_id) {
|
|
426
|
-
return reply.code(409).send({ error: 'Session is not rewound' });
|
|
427
|
-
}
|
|
428
|
-
// Repoburg rewind = platform-parity HARD DELETE — the runtime history was
|
|
429
|
-
// permanently truncated, so local turns can never be restored.
|
|
430
|
-
if (session.runtime_type === 'repoburg') {
|
|
431
|
-
return reply.code(409).send({
|
|
432
|
-
error: 'Restore is not available for repoburg sessions — the rewind permanently deleted the runtime history.',
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
const now = new Date().toISOString();
|
|
437
|
-
// Restore the original external_thread_id and clear rewind markers
|
|
438
|
-
db.prepare(
|
|
439
|
-
'UPDATE sessions SET rewound_at_turn_id = NULL, pre_rewind_thread_id = NULL, external_thread_id = COALESCE(pre_rewind_thread_id, external_thread_id), updated_at = ? WHERE id = ?',
|
|
440
|
-
).run(now, id);
|
|
441
|
-
|
|
442
|
-
// Clear in-memory thread entry (the Claude session ID changed back)
|
|
443
|
-
getProvider(session.runtime_type).clearThread(id);
|
|
444
|
-
|
|
445
|
-
const updated = db.prepare('SELECT * FROM sessions WHERE id = ?').get(id) as Session;
|
|
446
|
-
return updated;
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
// POST /api/sessions/:id/branch — create new session with turns up to and including target turn
|
|
450
|
-
fastify.post<{ Params: { id: string }; Body: { turn_id: string; title?: string } }>(
|
|
451
|
-
'/api/sessions/:id/branch',
|
|
452
|
-
{
|
|
453
|
-
schema: {
|
|
454
|
-
body: {
|
|
455
|
-
type: 'object',
|
|
456
|
-
required: ['turn_id'],
|
|
457
|
-
properties: {
|
|
458
|
-
turn_id: { type: 'string' },
|
|
459
|
-
title: { type: 'string' },
|
|
460
|
-
},
|
|
461
|
-
},
|
|
462
|
-
},
|
|
463
|
-
},
|
|
464
|
-
async (request, reply) => {
|
|
465
|
-
const { id } = request.params;
|
|
466
|
-
const { turn_id, title } = request.body || {};
|
|
467
|
-
if (!turn_id) {
|
|
468
|
-
return reply.code(400).send({ error: 'turn_id is required' });
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
const db = getDb();
|
|
472
|
-
const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(id) as Session | undefined;
|
|
473
|
-
if (!session) {
|
|
474
|
-
return reply.code(404).send({ error: 'Session not found' });
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
const targetTurn = db.prepare('SELECT created_at FROM turns WHERE id = ? AND session_id = ?').get(turn_id, id) as
|
|
478
|
-
{ created_at: string } | undefined;
|
|
479
|
-
if (!targetTurn) {
|
|
480
|
-
return reply.code(404).send({ error: 'Turn not found' });
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// Create new session
|
|
484
|
-
const newSessionId = uuidv4();
|
|
485
|
-
const now = new Date().toISOString();
|
|
486
|
-
const newTitle = title || `Branch of ${session.title || session.id.slice(0, 8)}`;
|
|
487
|
-
|
|
488
|
-
db.prepare(
|
|
489
|
-
`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, runtime_config)
|
|
490
|
-
VALUES (?, ?, ?, ?, ?, ?, NULL, 'active', 0, 'working', 0, ?, ?, ?)`,
|
|
491
|
-
).run(
|
|
492
|
-
newSessionId,
|
|
493
|
-
newTitle,
|
|
494
|
-
session.runtime_type,
|
|
495
|
-
session.model,
|
|
496
|
-
session.model_reasoning_effort,
|
|
497
|
-
session.cwd,
|
|
498
|
-
now,
|
|
499
|
-
now,
|
|
500
|
-
session.runtime_config,
|
|
501
|
-
);
|
|
502
|
-
|
|
503
|
-
// Copy turns up to and including the target turn
|
|
504
|
-
const turnRows = db
|
|
505
|
-
.prepare('SELECT * FROM turns WHERE session_id = ? AND created_at <= ? ORDER BY created_at ASC')
|
|
506
|
-
.all(id, targetTurn.created_at) as TurnRow[];
|
|
507
|
-
|
|
508
|
-
for (const turn of turnRows) {
|
|
509
|
-
const newTurnId = uuidv4();
|
|
510
|
-
db.prepare(
|
|
511
|
-
`INSERT INTO turns (id, session_id, user_prompt, agent_response, thoughts, token_usage, external_turn_id, status, error, attachments, created_at, updated_at)
|
|
512
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
513
|
-
).run(
|
|
514
|
-
newTurnId,
|
|
515
|
-
newSessionId,
|
|
516
|
-
turn.user_prompt,
|
|
517
|
-
turn.agent_response,
|
|
518
|
-
turn.thoughts,
|
|
519
|
-
turn.token_usage,
|
|
520
|
-
turn.external_turn_id,
|
|
521
|
-
turn.status,
|
|
522
|
-
turn.error,
|
|
523
|
-
turn.attachments,
|
|
524
|
-
turn.created_at,
|
|
525
|
-
turn.updated_at,
|
|
526
|
-
);
|
|
527
|
-
|
|
528
|
-
// Copy items for this turn
|
|
529
|
-
const itemRows = db
|
|
530
|
-
.prepare('SELECT * FROM items WHERE turn_id = ? ORDER BY created_at ASC')
|
|
531
|
-
.all(turn.id) as ItemRow[];
|
|
532
|
-
for (const item of itemRows) {
|
|
533
|
-
const newItemId = uuidv4();
|
|
534
|
-
db.prepare(
|
|
535
|
-
`INSERT INTO items (id, turn_id, external_item_id, type, data, status, created_at, updated_at)
|
|
536
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
537
|
-
).run(
|
|
538
|
-
newItemId,
|
|
539
|
-
newTurnId,
|
|
540
|
-
item.external_item_id,
|
|
541
|
-
item.type,
|
|
542
|
-
item.data,
|
|
543
|
-
item.status,
|
|
544
|
-
item.created_at,
|
|
545
|
-
item.updated_at,
|
|
546
|
-
);
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
// Fork the provider-native session so the provider has conversation context
|
|
551
|
-
const provider = getProvider(session.runtime_type);
|
|
552
|
-
if (provider.forkSession) {
|
|
553
|
-
const targetTurnFull = db.prepare('SELECT * FROM turns WHERE id = ?').get(turn_id) as Turn;
|
|
554
|
-
const forkedId = await provider.forkSession(session, targetTurnFull);
|
|
555
|
-
if (forkedId) {
|
|
556
|
-
db.prepare('UPDATE sessions SET external_thread_id = ?, updated_at = ? WHERE id = ?')
|
|
557
|
-
.run(forkedId, now, newSessionId);
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
const newSession = db.prepare('SELECT * FROM sessions WHERE id = ?').get(newSessionId) as Session;
|
|
562
|
-
return newSession;
|
|
563
|
-
},
|
|
564
|
-
);
|
|
565
|
-
}
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
-
import Fastify, { type FastifyInstance } from 'fastify';
|
|
3
|
-
|
|
4
|
-
// Mock shared-app-server to return a mock client
|
|
5
|
-
const mockRequest = vi.fn();
|
|
6
|
-
|
|
7
|
-
vi.mock('../codex/shared-app-server.ts', () => ({
|
|
8
|
-
getSharedAppServer: vi.fn(() => ({
|
|
9
|
-
request: mockRequest,
|
|
10
|
-
isClosed: vi.fn(() => false),
|
|
11
|
-
initialize: vi.fn(() => Promise.resolve()),
|
|
12
|
-
close: vi.fn(() => Promise.resolve()),
|
|
13
|
-
})),
|
|
14
|
-
}));
|
|
15
|
-
|
|
16
|
-
// Mock node:fs for readFileSync used in GET /api/skills/:name
|
|
17
|
-
vi.mock('node:fs', () => ({
|
|
18
|
-
readFileSync: vi.fn(),
|
|
19
|
-
}));
|
|
20
|
-
|
|
21
|
-
import { registerSkillRoutes } from './skills.ts';
|
|
22
|
-
import { readFileSync } from 'node:fs';
|
|
23
|
-
import type { SkillsListResult } from '../codex/app-server-types.ts';
|
|
24
|
-
|
|
25
|
-
const MOCK_SKILLS_RESULT: SkillsListResult = {
|
|
26
|
-
data: [
|
|
27
|
-
{
|
|
28
|
-
cwd: '/tmp',
|
|
29
|
-
skills: [
|
|
30
|
-
{
|
|
31
|
-
name: 'cloudflare',
|
|
32
|
-
description: 'Cloudflare skill',
|
|
33
|
-
path: '/home/.codex/skills/cloudflare/SKILL.md',
|
|
34
|
-
scope: 'user',
|
|
35
|
-
enabled: true,
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
name: 'imagegen',
|
|
39
|
-
description: 'Image generation skill',
|
|
40
|
-
path: '/home/.codex/skills/.system/imagegen/SKILL.md',
|
|
41
|
-
scope: 'system',
|
|
42
|
-
enabled: true,
|
|
43
|
-
interface: {
|
|
44
|
-
displayName: 'Image Gen',
|
|
45
|
-
shortDescription: 'Generate images',
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
],
|
|
49
|
-
errors: [],
|
|
50
|
-
},
|
|
51
|
-
],
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
describe('Skill routes', () => {
|
|
55
|
-
let app: FastifyInstance;
|
|
56
|
-
|
|
57
|
-
beforeEach(async () => {
|
|
58
|
-
vi.clearAllMocks();
|
|
59
|
-
mockRequest.mockResolvedValue(MOCK_SKILLS_RESULT);
|
|
60
|
-
app = Fastify();
|
|
61
|
-
await registerSkillRoutes(app);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
describe('GET /api/skills', () => {
|
|
65
|
-
it('returns skills from app-server', async () => {
|
|
66
|
-
const res = await app.inject({ method: 'GET', url: '/api/skills' });
|
|
67
|
-
expect(res.statusCode).toBe(200);
|
|
68
|
-
const skills = res.json();
|
|
69
|
-
expect(skills).toHaveLength(2);
|
|
70
|
-
expect(skills[0].name).toBe('cloudflare');
|
|
71
|
-
expect(skills[0].type).toBe('skill');
|
|
72
|
-
expect(skills[0].metadata.scope).toBe('user');
|
|
73
|
-
expect(skills[1].name).toBe('Image Gen');
|
|
74
|
-
expect(skills[1].metadata.scope).toBe('system');
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('calls skills/list with cwd from query', async () => {
|
|
78
|
-
await app.inject({ method: 'GET', url: '/api/skills?cwd=/custom/path' });
|
|
79
|
-
expect(mockRequest).toHaveBeenCalledWith('skills/list', { cwds: ['/custom/path'] });
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('defaults cwd to process.cwd()', async () => {
|
|
83
|
-
await app.inject({ method: 'GET', url: '/api/skills' });
|
|
84
|
-
expect(mockRequest).toHaveBeenCalledWith('skills/list', {
|
|
85
|
-
cwds: [process.cwd()],
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('returns empty array when app-server returns no skills', async () => {
|
|
90
|
-
mockRequest.mockResolvedValue({ data: [{ cwd: '/tmp', skills: [], errors: [] }] });
|
|
91
|
-
const res = await app.inject({ method: 'GET', url: '/api/skills' });
|
|
92
|
-
expect(res.statusCode).toBe(200);
|
|
93
|
-
expect(res.json()).toEqual([]);
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
describe('GET /api/skills/:name', () => {
|
|
98
|
-
it('returns 404 for unknown skill', async () => {
|
|
99
|
-
mockRequest.mockResolvedValue(MOCK_SKILLS_RESULT);
|
|
100
|
-
const res = await app.inject({ method: 'GET', url: '/api/skills/nonexistent' });
|
|
101
|
-
expect(res.statusCode).toBe(404);
|
|
102
|
-
expect(res.json()).toEqual({ error: 'Skill not found' });
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('returns SKILL.md content for known skill', async () => {
|
|
106
|
-
vi.mocked(readFileSync).mockReturnValue('# Cloudflare\nSkill content here');
|
|
107
|
-
const res = await app.inject({ method: 'GET', url: '/api/skills/cloudflare' });
|
|
108
|
-
expect(res.statusCode).toBe(200);
|
|
109
|
-
expect(res.json().name).toBe('cloudflare');
|
|
110
|
-
expect(res.json().content).toContain('# Cloudflare');
|
|
111
|
-
expect(res.json().path).toBe('/home/.codex/skills/cloudflare/SKILL.md');
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
});
|