repoburg 1.3.196 → 1.3.197
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/data.db-shm +0 -0
- package/agent-pane/backend/data.db-wal +0 -0
- package/agent-pane/backend/dist/index.js +13399 -10390
- package/agent-pane/backend/package-lock.json +162 -92
- package/agent-pane/backend/scripts/backfill-from-jsonl.ts +206 -0
- package/agent-pane/backend/scripts/import-session-from-jsonl.ts +264 -0
- package/agent-pane/backend/src/claude/claude-message-mapper.test.ts +116 -8
- package/agent-pane/backend/src/claude/claude-message-mapper.ts +114 -9
- package/agent-pane/backend/src/claude/claude-provider.ts +918 -84
- package/agent-pane/backend/src/claude/claude-types.ts +16 -4
- package/agent-pane/backend/src/codex/app-server-client.ts +22 -0
- package/agent-pane/backend/src/db/index.ts +102 -0
- package/agent-pane/backend/src/db/schema.ts +4 -0
- package/agent-pane/backend/src/index.ts +3 -1
- package/agent-pane/backend/src/integration/helpers/test-app.ts +2 -0
- package/agent-pane/backend/src/integration/session-operations.test.ts +18 -11
- package/agent-pane/backend/src/integration/threads.test.ts +309 -0
- package/agent-pane/backend/src/providers/runtime-provider.ts +19 -1
- package/agent-pane/backend/src/routes/providers.ts +11 -0
- package/agent-pane/backend/src/routes/sessions.test.ts +143 -9
- package/agent-pane/backend/src/routes/sessions.ts +133 -28
- package/agent-pane/backend/src/routes/threads.ts +308 -0
- package/agent-pane/backend/src/routes/turns.ts +32 -9
- package/agent-pane/frontend/dist/assets/index-BYo-lnxm.js +362 -0
- package/agent-pane/frontend/dist/assets/index-Dwwhx7YN.css +1 -0
- package/agent-pane/frontend/dist/index.html +2 -2
- package/agent-pane/frontend/package-lock.json +51 -9
- package/agent-pane/frontend/src/__tests__/ThreadsPanel.test.tsx +192 -0
- package/agent-pane/frontend/src/api/providers.ts +6 -1
- package/agent-pane/frontend/src/api/sessions.test.ts +4 -0
- package/agent-pane/frontend/src/api/sessions.ts +13 -5
- package/agent-pane/frontend/src/api/websocket.ts +65 -1
- package/agent-pane/frontend/src/components/CollapsibleToolCalls.test.tsx +4 -4
- package/agent-pane/frontend/src/components/CollapsibleToolCalls.tsx +13 -4
- package/agent-pane/frontend/src/components/CommandInputBar.test.tsx +2 -2
- package/agent-pane/frontend/src/components/CommandInputBar.tsx +292 -29
- package/agent-pane/frontend/src/components/CommandPalette.test.tsx +0 -4
- package/agent-pane/frontend/src/components/CommandPalette.tsx +1 -17
- package/agent-pane/frontend/src/components/CommentableText.tsx +291 -0
- package/agent-pane/frontend/src/components/RenderArea.tsx +9 -2
- package/agent-pane/frontend/src/components/RightPanel.test.tsx +5 -5
- package/agent-pane/frontend/src/components/RightPanel.tsx +20 -14
- package/agent-pane/frontend/src/components/SessionTabs.tsx +7 -2
- package/agent-pane/frontend/src/components/StatusBar.tsx +68 -1
- package/agent-pane/frontend/src/components/TurnContent.test.tsx +29 -18
- package/agent-pane/frontend/src/components/TurnContent.tsx +199 -28
- package/agent-pane/frontend/src/components/input/SlashPopup.tsx +24 -5
- package/agent-pane/frontend/src/components/panels/TasksPanel.test.tsx +97 -0
- package/agent-pane/frontend/src/components/panels/TasksPanel.tsx +208 -0
- package/agent-pane/frontend/src/components/panels/ThreadsPanel.tsx +350 -0
- package/agent-pane/frontend/src/components/panels/TurnsPanel.tsx +47 -25
- package/agent-pane/frontend/src/hooks/useSessionStatus.test.ts +2 -0
- package/agent-pane/frontend/src/store/sessionSlice.ts +29 -6
- package/agent-pane/frontend/src/store/store.test.ts +5 -5
- package/agent-pane/frontend/src/store/store.ts +4 -1
- package/agent-pane/frontend/src/store/threadSlice.ts +136 -0
- package/agent-pane/frontend/src/store/wsSlice.ts +72 -3
- package/agent-pane/frontend/src/styles/app.css +150 -19
- package/agent-pane/frontend/src/test/fixtures.ts +4 -0
- package/agent-pane/frontend/src/types/index.ts +5 -2
- package/agent-pane/frontend/src/utils/duration.test.ts +4 -0
- package/agent-pane/frontend/vite.config.ts +2 -2
- package/agent-pane/shared/provider-types.ts +14 -0
- package/agent-pane/shared/types.ts +60 -0
- package/package.json +1 -1
- package/agent-pane/frontend/dist/assets/index-Bg0PF2aV.css +0 -1
- package/agent-pane/frontend/dist/assets/index-BlybeX5e.js +0 -281
- package/agent-pane/frontend/src/components/panels/LogsPanel.test.tsx +0 -58
- package/agent-pane/frontend/src/components/panels/LogsPanel.tsx +0 -25
- package/agent-pane/frontend/src/components/panels/MetricsPanel.test.tsx +0 -71
- package/agent-pane/frontend/src/components/panels/MetricsPanel.tsx +0 -114
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
+
import { getDb } from '../db/index.ts';
|
|
4
|
+
import { getProvider } from '../providers/registry.ts';
|
|
5
|
+
import { broadcast } from '../ws/index.ts';
|
|
6
|
+
import type { Session, Turn, DiscussionThread } from '../db/schema.ts';
|
|
7
|
+
|
|
8
|
+
/** Raw DB row for turns — token_usage is a JSON string, not parsed; no items field. */
|
|
9
|
+
interface TurnRow extends Omit<Turn, 'token_usage' | 'items'> {
|
|
10
|
+
token_usage: string | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Raw DB row for items — data is a JSON string, not parsed. */
|
|
14
|
+
interface ItemRow extends Omit<Turn['items'][number], 'data'> {
|
|
15
|
+
data: string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function registerThreadRoutes(fastify: FastifyInstance): Promise<void> {
|
|
19
|
+
// POST /api/sessions/:sessionId/threads — create thread + first AI turn
|
|
20
|
+
fastify.post<{
|
|
21
|
+
Params: { sessionId: string };
|
|
22
|
+
Body: {
|
|
23
|
+
itemId: string;
|
|
24
|
+
turnId: string;
|
|
25
|
+
startOffset: number;
|
|
26
|
+
endOffset: number;
|
|
27
|
+
selectedText: string;
|
|
28
|
+
message: string;
|
|
29
|
+
};
|
|
30
|
+
}>(
|
|
31
|
+
'/api/sessions/:sessionId/threads',
|
|
32
|
+
{
|
|
33
|
+
schema: {
|
|
34
|
+
body: {
|
|
35
|
+
type: 'object',
|
|
36
|
+
required: ['itemId', 'turnId', 'startOffset', 'endOffset', 'selectedText', 'message'],
|
|
37
|
+
properties: {
|
|
38
|
+
itemId: { type: 'string' },
|
|
39
|
+
turnId: { type: 'string' },
|
|
40
|
+
startOffset: { type: 'number' },
|
|
41
|
+
endOffset: { type: 'number' },
|
|
42
|
+
selectedText: { type: 'string' },
|
|
43
|
+
message: { type: 'string', minLength: 1 },
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
async (request, reply) => {
|
|
49
|
+
const { sessionId } = request.params;
|
|
50
|
+
const { itemId, turnId, startOffset, endOffset, selectedText, message } = request.body || {};
|
|
51
|
+
|
|
52
|
+
const db = getDb();
|
|
53
|
+
const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(sessionId) as Session | undefined;
|
|
54
|
+
if (!session) {
|
|
55
|
+
return reply.code(404).send({ error: 'Session not found' });
|
|
56
|
+
}
|
|
57
|
+
if (session.is_running) {
|
|
58
|
+
return reply.code(409).send({ error: 'A turn is already running for this session' });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const threadId = uuidv4();
|
|
62
|
+
const now = new Date().toISOString();
|
|
63
|
+
|
|
64
|
+
// 1. Create the discussion thread row
|
|
65
|
+
db.prepare(
|
|
66
|
+
`INSERT INTO discussion_threads (id, session_id, item_id, turn_id, start_offset, end_offset, selected_text, status, created_at, updated_at)
|
|
67
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 'open', ?, ?)`,
|
|
68
|
+
).run(threadId, sessionId, itemId, turnId, startOffset, endOffset, selectedText, now, now);
|
|
69
|
+
|
|
70
|
+
// 1b. Fork the Claude session at the anchor turn so the thread has its own
|
|
71
|
+
// isolated conversation — main session context stays unaffected.
|
|
72
|
+
const provider = getProvider(session.runtime_type);
|
|
73
|
+
if (provider.forkSession) {
|
|
74
|
+
try {
|
|
75
|
+
const anchorTurn = db.prepare('SELECT * FROM turns WHERE id = ?').get(turnId) as Turn | undefined;
|
|
76
|
+
if (anchorTurn) {
|
|
77
|
+
const forkedSessionId = await provider.forkSession(session, anchorTurn);
|
|
78
|
+
if (forkedSessionId) {
|
|
79
|
+
db.prepare('UPDATE discussion_threads SET external_thread_id = ? WHERE id = ?').run(forkedSessionId, threadId);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch (err) {
|
|
83
|
+
console.error(`[threads] forkSession failed for thread ${threadId}:`, err);
|
|
84
|
+
// Non-fatal — thread falls back to main session conversation
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 2. Format prompt with quoted text for AI context
|
|
89
|
+
const formattedPrompt = `[Quoting from your previous response]:\n> ${selectedText}\n\n${message}`;
|
|
90
|
+
|
|
91
|
+
// 3. Create the turn row with thread_id
|
|
92
|
+
const newTurnId = uuidv4();
|
|
93
|
+
db.prepare(
|
|
94
|
+
`INSERT INTO turns (id, session_id, user_prompt, agent_response, thoughts, token_usage, external_turn_id, status, error, thread_id, created_at, updated_at)
|
|
95
|
+
VALUES (?, ?, ?, NULL, NULL, NULL, NULL, 'pending', NULL, ?, ?, ?)`,
|
|
96
|
+
).run(newTurnId, sessionId, formattedPrompt, threadId, now, now);
|
|
97
|
+
|
|
98
|
+
const turn = db.prepare('SELECT * FROM turns WHERE id = ?').get(newTurnId) as Turn;
|
|
99
|
+
|
|
100
|
+
// 4. Start the AI turn (fire and forget — events stream via WS)
|
|
101
|
+
provider
|
|
102
|
+
.startTurn(session, turn)
|
|
103
|
+
.catch((err) => {
|
|
104
|
+
console.error(`[threads] Unhandled error for session ${sessionId}:`, err);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// 5. Build the thread response
|
|
108
|
+
const threadRow = db.prepare('SELECT * FROM discussion_threads WHERE id = ?').get(threadId) as DiscussionThread;
|
|
109
|
+
const thread: DiscussionThread = {
|
|
110
|
+
...threadRow,
|
|
111
|
+
turns: [{ ...turn, items: [] }],
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// 6. Broadcast thread creation
|
|
115
|
+
broadcast('thread:created', { sessionId, thread, turn });
|
|
116
|
+
|
|
117
|
+
return { thread, turn };
|
|
118
|
+
},
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// POST /api/sessions/:sessionId/threads/:threadId/followup — add follow-up message to thread
|
|
122
|
+
fastify.post<{
|
|
123
|
+
Params: { sessionId: string; threadId: string };
|
|
124
|
+
Body: { message: string };
|
|
125
|
+
}>(
|
|
126
|
+
'/api/sessions/:sessionId/threads/:threadId/followup',
|
|
127
|
+
{
|
|
128
|
+
schema: {
|
|
129
|
+
body: {
|
|
130
|
+
type: 'object',
|
|
131
|
+
required: ['message'],
|
|
132
|
+
properties: {
|
|
133
|
+
message: { type: 'string', minLength: 1 },
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
async (request, reply) => {
|
|
139
|
+
const { sessionId, threadId } = request.params;
|
|
140
|
+
const { message } = request.body || {};
|
|
141
|
+
|
|
142
|
+
const db = getDb();
|
|
143
|
+
const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(sessionId) as Session | undefined;
|
|
144
|
+
if (!session) {
|
|
145
|
+
return reply.code(404).send({ error: 'Session not found' });
|
|
146
|
+
}
|
|
147
|
+
if (session.is_running) {
|
|
148
|
+
return reply.code(409).send({ error: 'A turn is already running for this session' });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const thread = db
|
|
152
|
+
.prepare('SELECT * FROM discussion_threads WHERE id = ? AND session_id = ?')
|
|
153
|
+
.get(threadId, sessionId) as DiscussionThread | undefined;
|
|
154
|
+
if (!thread) {
|
|
155
|
+
return reply.code(404).send({ error: 'Thread not found' });
|
|
156
|
+
}
|
|
157
|
+
if (thread.status !== 'open') {
|
|
158
|
+
return reply.code(409).send({ error: 'Thread is resolved — reopen it first' });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Create new turn with same thread_id
|
|
162
|
+
const newTurnId = uuidv4();
|
|
163
|
+
const now = new Date().toISOString();
|
|
164
|
+
db.prepare(
|
|
165
|
+
`INSERT INTO turns (id, session_id, user_prompt, agent_response, thoughts, token_usage, external_turn_id, status, error, thread_id, created_at, updated_at)
|
|
166
|
+
VALUES (?, ?, ?, NULL, NULL, NULL, NULL, 'pending', NULL, ?, ?, ?)`,
|
|
167
|
+
).run(newTurnId, sessionId, message, threadId, now, now);
|
|
168
|
+
|
|
169
|
+
const turn = db.prepare('SELECT * FROM turns WHERE id = ?').get(newTurnId) as Turn;
|
|
170
|
+
|
|
171
|
+
// Start the AI turn
|
|
172
|
+
getProvider(session.runtime_type)
|
|
173
|
+
.startTurn(session, turn)
|
|
174
|
+
.catch((err) => {
|
|
175
|
+
console.error(`[threads] followup error for session ${sessionId}:`, err);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
broadcast('thread:followup', { sessionId, threadId, turn });
|
|
179
|
+
|
|
180
|
+
return { turn };
|
|
181
|
+
},
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// PATCH /api/sessions/:sessionId/threads/:threadId — resolve or reopen
|
|
185
|
+
fastify.patch<{
|
|
186
|
+
Params: { sessionId: string; threadId: string };
|
|
187
|
+
Body: { status: 'resolved' | 'open' };
|
|
188
|
+
}>(
|
|
189
|
+
'/api/sessions/:sessionId/threads/:threadId',
|
|
190
|
+
{
|
|
191
|
+
schema: {
|
|
192
|
+
body: {
|
|
193
|
+
type: 'object',
|
|
194
|
+
required: ['status'],
|
|
195
|
+
properties: {
|
|
196
|
+
status: { type: 'string', enum: ['resolved', 'open'] },
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
async (request, reply) => {
|
|
202
|
+
const { sessionId, threadId } = request.params;
|
|
203
|
+
const { status } = request.body || {};
|
|
204
|
+
|
|
205
|
+
const db = getDb();
|
|
206
|
+
const thread = db
|
|
207
|
+
.prepare('SELECT * FROM discussion_threads WHERE id = ? AND session_id = ?')
|
|
208
|
+
.get(threadId, sessionId) as DiscussionThread | undefined;
|
|
209
|
+
if (!thread) {
|
|
210
|
+
return reply.code(404).send({ error: 'Thread not found' });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const now = new Date().toISOString();
|
|
214
|
+
const resolvedAt = status === 'resolved' ? now : null;
|
|
215
|
+
db.prepare(
|
|
216
|
+
'UPDATE discussion_threads SET status = ?, resolved_at = ?, updated_at = ? WHERE id = ?',
|
|
217
|
+
).run(status, resolvedAt, now, threadId);
|
|
218
|
+
|
|
219
|
+
broadcast('thread:resolved', { sessionId, threadId, status });
|
|
220
|
+
|
|
221
|
+
return { id: threadId, status, resolved_at: resolvedAt };
|
|
222
|
+
},
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// DELETE /api/sessions/:sessionId/threads/:threadId — delete thread + its turns + items
|
|
226
|
+
fastify.delete<{ Params: { sessionId: string; threadId: string } }>(
|
|
227
|
+
'/api/sessions/:sessionId/threads/:threadId',
|
|
228
|
+
async (request, reply) => {
|
|
229
|
+
const { sessionId, threadId } = request.params;
|
|
230
|
+
const db = getDb();
|
|
231
|
+
|
|
232
|
+
const thread = db
|
|
233
|
+
.prepare('SELECT id FROM discussion_threads WHERE id = ? AND session_id = ?')
|
|
234
|
+
.get(threadId, sessionId);
|
|
235
|
+
if (!thread) {
|
|
236
|
+
return reply.code(404).send({ error: 'Thread not found' });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Clean up the thread's branched session entry (Claude provider)
|
|
240
|
+
const session = db.prepare('SELECT runtime_type FROM sessions WHERE id = ?').get(sessionId) as { runtime_type: string } | undefined;
|
|
241
|
+
if (session) {
|
|
242
|
+
const provider = getProvider(session.runtime_type);
|
|
243
|
+
provider.clearThreadBranch?.(sessionId, threadId);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Delete items belonging to turns in this thread
|
|
247
|
+
db.prepare(
|
|
248
|
+
'DELETE FROM items WHERE turn_id IN (SELECT id FROM turns WHERE thread_id = ?)',
|
|
249
|
+
).run(threadId);
|
|
250
|
+
// Delete turns in this thread
|
|
251
|
+
db.prepare('DELETE FROM turns WHERE thread_id = ?').run(threadId);
|
|
252
|
+
// Delete the thread row
|
|
253
|
+
db.prepare('DELETE FROM discussion_threads WHERE id = ?').run(threadId);
|
|
254
|
+
|
|
255
|
+
broadcast('thread:deleted', { sessionId, threadId });
|
|
256
|
+
|
|
257
|
+
return { success: true };
|
|
258
|
+
},
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
// GET /api/sessions/:sessionId/threads — list all threads with their turns
|
|
262
|
+
fastify.get<{ Params: { sessionId: string } }>('/api/sessions/:sessionId/threads', async (request, reply) => {
|
|
263
|
+
const { sessionId } = request.params;
|
|
264
|
+
const db = getDb();
|
|
265
|
+
|
|
266
|
+
const session = db.prepare('SELECT id FROM sessions WHERE id = ?').get(sessionId);
|
|
267
|
+
if (!session) {
|
|
268
|
+
return reply.code(404).send({ error: 'Session not found' });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const threadRows = db
|
|
272
|
+
.prepare('SELECT * FROM discussion_threads WHERE session_id = ? ORDER BY created_at ASC')
|
|
273
|
+
.all(sessionId) as DiscussionThread[];
|
|
274
|
+
|
|
275
|
+
// Attach turns + items for each thread
|
|
276
|
+
const result: DiscussionThread[] = [];
|
|
277
|
+
for (const tr of threadRows) {
|
|
278
|
+
const turnRows = db
|
|
279
|
+
.prepare('SELECT * FROM turns WHERE thread_id = ? ORDER BY created_at ASC')
|
|
280
|
+
.all(tr.id) as TurnRow[];
|
|
281
|
+
|
|
282
|
+
const itemRows = db
|
|
283
|
+
.prepare(
|
|
284
|
+
'SELECT * FROM items WHERE turn_id IN (SELECT id FROM turns WHERE thread_id = ?) ORDER BY created_at ASC',
|
|
285
|
+
)
|
|
286
|
+
.all(tr.id) as ItemRow[];
|
|
287
|
+
|
|
288
|
+
const itemsByTurn = new Map<string, Turn['items']>();
|
|
289
|
+
for (const row of itemRows) {
|
|
290
|
+
const item = { ...row, data: row.data ? JSON.parse(row.data) : null };
|
|
291
|
+
const list = itemsByTurn.get(item.turn_id) ?? [];
|
|
292
|
+
list.push(item);
|
|
293
|
+
itemsByTurn.set(item.turn_id, list);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
result.push({
|
|
297
|
+
...tr,
|
|
298
|
+
turns: turnRows.map((row) => ({
|
|
299
|
+
...row,
|
|
300
|
+
token_usage: row.token_usage ? JSON.parse(row.token_usage) : null,
|
|
301
|
+
items: itemsByTurn.get(row.id) ?? [],
|
|
302
|
+
})),
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return result;
|
|
307
|
+
});
|
|
308
|
+
}
|
|
@@ -2,11 +2,14 @@ import type { FastifyInstance } from 'fastify';
|
|
|
2
2
|
import { v4 as uuidv4 } from 'uuid';
|
|
3
3
|
import { getDb } from '../db/index.ts';
|
|
4
4
|
import { getProvider } from '../providers/registry.ts';
|
|
5
|
-
import type { Session, Turn } from '../db/schema.ts';
|
|
5
|
+
import type { Session, Turn, Attachment } from '../db/schema.ts';
|
|
6
6
|
|
|
7
7
|
export async function registerTurnRoutes(fastify: FastifyInstance): Promise<void> {
|
|
8
8
|
// POST /api/sessions/:id/turns — submit prompt, starts a turn
|
|
9
|
-
fastify.post<{
|
|
9
|
+
fastify.post<{
|
|
10
|
+
Params: { id: string };
|
|
11
|
+
Body: { user_prompt: string; attachments?: Attachment[] };
|
|
12
|
+
}>(
|
|
10
13
|
'/api/sessions/:id/turns',
|
|
11
14
|
{
|
|
12
15
|
schema: {
|
|
@@ -15,13 +18,25 @@ export async function registerTurnRoutes(fastify: FastifyInstance): Promise<void
|
|
|
15
18
|
required: ['user_prompt'],
|
|
16
19
|
properties: {
|
|
17
20
|
user_prompt: { type: 'string', minLength: 1 },
|
|
21
|
+
attachments: {
|
|
22
|
+
type: 'array',
|
|
23
|
+
items: {
|
|
24
|
+
type: 'object',
|
|
25
|
+
required: ['name', 'media_type', 'data'],
|
|
26
|
+
properties: {
|
|
27
|
+
name: { type: 'string' },
|
|
28
|
+
media_type: { type: 'string' },
|
|
29
|
+
data: { type: 'string' },
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
18
33
|
},
|
|
19
34
|
},
|
|
20
35
|
},
|
|
21
36
|
},
|
|
22
37
|
async (request, reply) => {
|
|
23
38
|
const { id } = request.params;
|
|
24
|
-
const { user_prompt } = request.body || {};
|
|
39
|
+
const { user_prompt, attachments } = request.body || {};
|
|
25
40
|
|
|
26
41
|
if (!user_prompt || !user_prompt.trim()) {
|
|
27
42
|
return reply.code(400).send({ error: 'user_prompt is required' });
|
|
@@ -37,24 +52,32 @@ export async function registerTurnRoutes(fastify: FastifyInstance): Promise<void
|
|
|
37
52
|
return reply.code(409).send({ error: 'A turn is already running for this session' });
|
|
38
53
|
}
|
|
39
54
|
|
|
40
|
-
// Create turn in DB
|
|
55
|
+
// Create turn in DB — store attachments as JSON if provided
|
|
41
56
|
const turnId = uuidv4();
|
|
42
57
|
const now = new Date().toISOString();
|
|
58
|
+
const attachmentsJson = attachments && attachments.length > 0 ? JSON.stringify(attachments) : null;
|
|
43
59
|
db.prepare(
|
|
44
|
-
`INSERT INTO turns (id, session_id, user_prompt, agent_response, thoughts, token_usage, external_turn_id, status, error, created_at, updated_at)
|
|
45
|
-
VALUES (?, ?, ?, NULL, NULL, NULL, NULL, 'pending', NULL, ?, ?)`,
|
|
46
|
-
).run(turnId, id, user_prompt, now, now);
|
|
60
|
+
`INSERT INTO turns (id, session_id, user_prompt, agent_response, thoughts, token_usage, external_turn_id, status, error, attachments, created_at, updated_at)
|
|
61
|
+
VALUES (?, ?, ?, NULL, NULL, NULL, NULL, 'pending', NULL, ?, ?, ?)`,
|
|
62
|
+
).run(turnId, id, user_prompt, attachmentsJson, now, now);
|
|
47
63
|
|
|
48
64
|
const turn = db.prepare('SELECT * FROM turns WHERE id = ?').get(turnId) as Turn;
|
|
49
65
|
|
|
66
|
+
// Parse attachments from JSON string (DB stores as TEXT) before returning to frontend
|
|
67
|
+
const rawTurn = turn as Turn & { attachments: string | null };
|
|
68
|
+
const parsedTurn: Turn = {
|
|
69
|
+
...turn,
|
|
70
|
+
attachments: rawTurn.attachments ? (JSON.parse(rawTurn.attachments) as Attachment[]) : null,
|
|
71
|
+
};
|
|
72
|
+
|
|
50
73
|
// Fire and forget — events stream via WebSocket
|
|
51
74
|
getProvider(session.runtime_type)
|
|
52
|
-
.startTurn(session,
|
|
75
|
+
.startTurn(session, rawTurn)
|
|
53
76
|
.catch((err) => {
|
|
54
77
|
console.error(`[provider] Unhandled error for session ${id}:`, err);
|
|
55
78
|
});
|
|
56
79
|
|
|
57
|
-
return
|
|
80
|
+
return parsedTurn;
|
|
58
81
|
},
|
|
59
82
|
);
|
|
60
83
|
|