remcodex 0.1.0-beta.1

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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +331 -0
  3. package/dist/server/src/app.js +186 -0
  4. package/dist/server/src/cli.js +270 -0
  5. package/dist/server/src/controllers/codex-options.controller.js +199 -0
  6. package/dist/server/src/controllers/message.controller.js +21 -0
  7. package/dist/server/src/controllers/project.controller.js +44 -0
  8. package/dist/server/src/controllers/session.controller.js +175 -0
  9. package/dist/server/src/db/client.js +10 -0
  10. package/dist/server/src/db/migrations.js +32 -0
  11. package/dist/server/src/gateways/ws.gateway.js +60 -0
  12. package/dist/server/src/services/codex-app-server-runner.js +363 -0
  13. package/dist/server/src/services/codex-exec-runner.js +147 -0
  14. package/dist/server/src/services/codex-rollout-sync.js +977 -0
  15. package/dist/server/src/services/codex-runner.js +11 -0
  16. package/dist/server/src/services/codex-stream-events.js +478 -0
  17. package/dist/server/src/services/event-store.js +328 -0
  18. package/dist/server/src/services/project-manager.js +130 -0
  19. package/dist/server/src/services/pty-runner.js +72 -0
  20. package/dist/server/src/services/session-manager.js +1586 -0
  21. package/dist/server/src/services/session-timeline-service.js +181 -0
  22. package/dist/server/src/types/codex-launch.js +2 -0
  23. package/dist/server/src/types/models.js +37 -0
  24. package/dist/server/src/utils/ansi.js +143 -0
  25. package/dist/server/src/utils/codex-launch.js +102 -0
  26. package/dist/server/src/utils/codex-quota.js +179 -0
  27. package/dist/server/src/utils/codex-status.js +163 -0
  28. package/dist/server/src/utils/codex-ui-options.js +114 -0
  29. package/dist/server/src/utils/command.js +46 -0
  30. package/dist/server/src/utils/errors.js +16 -0
  31. package/dist/server/src/utils/ids.js +7 -0
  32. package/dist/server/src/utils/node-pty.js +29 -0
  33. package/package.json +36 -0
  34. package/scripts/fix-node-pty-helper.js +36 -0
  35. package/web/api.js +175 -0
  36. package/web/app.js +8082 -0
  37. package/web/components/composer.js +627 -0
  38. package/web/components/session-workbench.js +173 -0
  39. package/web/i18n/index.js +171 -0
  40. package/web/i18n/locales/de.js +50 -0
  41. package/web/i18n/locales/en.js +320 -0
  42. package/web/i18n/locales/es.js +50 -0
  43. package/web/i18n/locales/fr.js +50 -0
  44. package/web/i18n/locales/ja.js +50 -0
  45. package/web/i18n/locales/ko.js +50 -0
  46. package/web/i18n/locales/pt-BR.js +50 -0
  47. package/web/i18n/locales/ru.js +50 -0
  48. package/web/i18n/locales/zh-CN.js +320 -0
  49. package/web/i18n/locales/zh-Hant.js +53 -0
  50. package/web/index.html +23 -0
  51. package/web/message-rich-text.js +218 -0
  52. package/web/session-command-activity.js +980 -0
  53. package/web/session-event-adapter.js +826 -0
  54. package/web/session-timeline-reducer.js +728 -0
  55. package/web/session-timeline-renderer.js +656 -0
  56. package/web/session-ws.js +31 -0
  57. package/web/styles.css +5665 -0
  58. package/web/vendor/markdown-it.js +6969 -0
@@ -0,0 +1,728 @@
1
+ import { groupTimelineActivities } from "./session-command-activity.js";
2
+ import { t } from "./i18n/index.js";
3
+
4
+ const TURN_STATUS_PRIORITY = {
5
+ idle: 0,
6
+ running: 1,
7
+ completed: 2,
8
+ failed: 3,
9
+ aborted: 4,
10
+ };
11
+
12
+ function createItemIndex() {
13
+ return new Map();
14
+ }
15
+
16
+ function nextTurnFallbackId(event) {
17
+ return event?.turnId || `turn:${event?.id || crypto.randomUUID?.() || Date.now()}`;
18
+ }
19
+
20
+ function nextApprovalFallbackId(event) {
21
+ return event?.requestId || event?.callId || `approval:${event?.id || Date.now()}`;
22
+ }
23
+
24
+ function nextMessageFallbackId(event, prefix = "message") {
25
+ return event?.messageId || `${prefix}:${event?.id || Date.now()}`;
26
+ }
27
+
28
+ function appendDeltaText(currentText, textDelta) {
29
+ const delta = String(textDelta || "");
30
+ if (!delta) {
31
+ return currentText || "";
32
+ }
33
+
34
+ return `${currentText || ""}${delta}`;
35
+ }
36
+
37
+ function reduceLegacyAssistantMessage(state, event) {
38
+ const messageId = nextMessageFallbackId(event, resolveAssistantItemType(event));
39
+ reduceTimeline(state, {
40
+ ...event,
41
+ id: `${event.id}:start`,
42
+ kind: "assistant_message_start",
43
+ messageId,
44
+ payload: { raw: event.payload?.raw || event.payload || {} },
45
+ });
46
+ reduceTimeline(state, {
47
+ ...event,
48
+ id: `${event.id}:delta`,
49
+ kind: "assistant_message_delta",
50
+ messageId,
51
+ payload: {
52
+ textDelta: event.payload?.text || "",
53
+ raw: event.payload?.raw || event.payload || {},
54
+ },
55
+ });
56
+ reduceTimeline(state, {
57
+ ...event,
58
+ id: `${event.id}:end`,
59
+ kind: "assistant_message_end",
60
+ messageId,
61
+ payload: { raw: event.payload?.raw || event.payload || {} },
62
+ });
63
+ }
64
+
65
+ function reduceLegacyReasoning(state, event) {
66
+ const messageId = nextMessageFallbackId(event, "reasoning");
67
+ const reasoningText =
68
+ event.payload?.content || event.payload?.summary || event.payload?.text || "";
69
+ reduceTimeline(state, {
70
+ ...event,
71
+ id: `${event.id}:start`,
72
+ kind: "reasoning_start",
73
+ messageId,
74
+ payload: {
75
+ summary: event.payload?.summary || "",
76
+ raw: event.payload?.raw || event.payload || {},
77
+ },
78
+ });
79
+ reduceTimeline(state, {
80
+ ...event,
81
+ id: `${event.id}:delta`,
82
+ kind: "reasoning_delta",
83
+ messageId,
84
+ payload: {
85
+ textDelta: reasoningText,
86
+ summary: event.payload?.summary || "",
87
+ raw: event.payload?.raw || event.payload || {},
88
+ },
89
+ });
90
+ reduceTimeline(state, {
91
+ ...event,
92
+ id: `${event.id}:end`,
93
+ kind: "reasoning_end",
94
+ messageId,
95
+ payload: { raw: event.payload?.raw || event.payload || {} },
96
+ });
97
+ }
98
+
99
+ function resolveTurnId(state, event) {
100
+ if (event.turnId) {
101
+ return event.turnId;
102
+ }
103
+
104
+ if (event.kind === "user_message") {
105
+ return nextTurnFallbackId(event);
106
+ }
107
+
108
+ return state.activeTurnId || state.turnOrder[state.turnOrder.length - 1] || nextTurnFallbackId(event);
109
+ }
110
+
111
+ function ensureTurn(state, turnId, seed = {}) {
112
+ if (!state.turnsById[turnId]) {
113
+ state.turnsById[turnId] = {
114
+ id: turnId,
115
+ status: "idle",
116
+ startedAt: seed.timestamp || null,
117
+ completedAt: null,
118
+ userMessageId: null,
119
+ lastCommentaryId: null,
120
+ finalMessageId: null,
121
+ reasoningId: null,
122
+ messageIds: [],
123
+ commandIds: [],
124
+ patchIds: [],
125
+ approvalIds: [],
126
+ systemIds: [],
127
+ tokenCountId: null,
128
+ };
129
+ state.turnOrder.push(turnId);
130
+ }
131
+
132
+ return state.turnsById[turnId];
133
+ }
134
+
135
+ function setTurnStatus(turn, nextStatus) {
136
+ const current = turn.status || "idle";
137
+ if ((TURN_STATUS_PRIORITY[nextStatus] || 0) >= (TURN_STATUS_PRIORITY[current] || 0)) {
138
+ turn.status = nextStatus;
139
+ }
140
+ }
141
+
142
+ function insertTimelineItem(state, item) {
143
+ const existingIndex = state.itemIndexById.get(item.id);
144
+ if (typeof existingIndex === "number") {
145
+ state.timelineItems[existingIndex] = {
146
+ ...state.timelineItems[existingIndex],
147
+ ...item,
148
+ };
149
+ return state.timelineItems[existingIndex];
150
+ }
151
+
152
+ const nextItem = { ...item };
153
+ let insertAt = state.timelineItems.findIndex((candidate) => candidate.seq > nextItem.seq);
154
+ if (insertAt === -1) {
155
+ insertAt = state.timelineItems.length;
156
+ }
157
+
158
+ state.timelineItems.splice(insertAt, 0, nextItem);
159
+ state.itemIndexById = createItemIndex();
160
+ state.timelineItems.forEach((candidate, index) => {
161
+ state.itemIndexById.set(candidate.id, index);
162
+ });
163
+ return nextItem;
164
+ }
165
+
166
+ function upsertUserMessage(state, event, turnId) {
167
+ const turn = ensureTurn(state, turnId, event);
168
+ const item = insertTimelineItem(state, {
169
+ id: `user:${turnId}`,
170
+ type: "user",
171
+ turnId,
172
+ seq: event.seq,
173
+ timestamp: event.timestamp,
174
+ role: "user",
175
+ text: event.payload?.text || "",
176
+ });
177
+ turn.userMessageId = item.id;
178
+ state.activeTurnId = turnId;
179
+ }
180
+
181
+ function resolveAssistantItemType(event) {
182
+ return event.phase === "commentary" ? "assistant_commentary" : "assistant_final";
183
+ }
184
+
185
+ function upsertAssistantMessage(state, event, turnId, partial = {}) {
186
+ const turn = ensureTurn(state, turnId, event);
187
+ const phase = resolveAssistantItemType(event);
188
+ const messageId = nextMessageFallbackId(event, phase);
189
+ const itemId = `${phase}:${messageId}`;
190
+ const current = state.messagesById[messageId] || {
191
+ id: itemId,
192
+ type: phase,
193
+ turnId,
194
+ messageId,
195
+ seq: event.seq,
196
+ timestamp: event.timestamp,
197
+ role: "assistant",
198
+ phase: event.phase || "final_answer",
199
+ status: "streaming",
200
+ text: "",
201
+ };
202
+ const item = insertTimelineItem(state, {
203
+ ...current,
204
+ ...partial,
205
+ id: current.id,
206
+ type: current.type,
207
+ turnId,
208
+ messageId,
209
+ seq: current.seq || event.seq,
210
+ timestamp: current.timestamp || event.timestamp,
211
+ role: "assistant",
212
+ phase: event.phase || current.phase || "final_answer",
213
+ });
214
+ state.messagesById[messageId] = item;
215
+ if (!turn.messageIds.includes(item.id)) {
216
+ turn.messageIds.push(item.id);
217
+ }
218
+
219
+ if (phase === "assistant_commentary") {
220
+ turn.lastCommentaryId = item.id;
221
+ } else {
222
+ turn.finalMessageId = item.id;
223
+ }
224
+
225
+ return item;
226
+ }
227
+
228
+ function upsertReasoning(state, event, turnId, partial = {}) {
229
+ const turn = ensureTurn(state, turnId, event);
230
+ const messageId = nextMessageFallbackId(event, "reasoning");
231
+ const current = state.reasoningById[messageId] || {
232
+ id: `reasoning:${messageId}`,
233
+ type: "reasoning",
234
+ turnId,
235
+ messageId,
236
+ seq: event.seq,
237
+ timestamp: event.timestamp,
238
+ status: "thinking",
239
+ summary: "",
240
+ text: "",
241
+ };
242
+ const item = insertTimelineItem(state, {
243
+ ...current,
244
+ ...partial,
245
+ id: current.id,
246
+ type: "reasoning",
247
+ turnId,
248
+ messageId,
249
+ seq: current.seq || event.seq,
250
+ timestamp: current.timestamp || event.timestamp,
251
+ });
252
+ state.reasoningById[messageId] = item;
253
+ turn.reasoningId = item.id;
254
+ return item;
255
+ }
256
+
257
+ function completeReasoningIfPresent(state, turn) {
258
+ if (!turn?.reasoningId) {
259
+ return;
260
+ }
261
+
262
+ insertTimelineItem(state, {
263
+ id: turn.reasoningId,
264
+ status: "done",
265
+ });
266
+ }
267
+
268
+ function upsertCommand(state, event, turnId, partial) {
269
+ const turn = ensureTurn(state, turnId, event);
270
+ const callId = event.callId || `command:${event.id}`;
271
+ const current = state.commandsByCallId[callId] || {
272
+ id: `command:${callId}`,
273
+ type: "command",
274
+ turnId,
275
+ callId,
276
+ seq: event.seq,
277
+ timestamp: event.timestamp,
278
+ status: "pending",
279
+ command: "",
280
+ cwd: null,
281
+ stdout: "",
282
+ stderr: "",
283
+ output: "",
284
+ outputStatus: "idle",
285
+ exitCode: null,
286
+ duration: null,
287
+ justification: null,
288
+ sandboxPermissions: null,
289
+ };
290
+
291
+ const next = {
292
+ ...current,
293
+ ...partial,
294
+ id: current.id,
295
+ type: "command",
296
+ turnId,
297
+ callId,
298
+ seq: current.seq || event.seq,
299
+ timestamp: current.timestamp || event.timestamp,
300
+ };
301
+
302
+ state.commandsByCallId[callId] = next;
303
+ insertTimelineItem(state, next);
304
+ if (!turn.commandIds.includes(next.id)) {
305
+ turn.commandIds.push(next.id);
306
+ }
307
+ return next;
308
+ }
309
+
310
+ function upsertPatch(state, event, turnId, partial) {
311
+ const turn = ensureTurn(state, turnId, event);
312
+ const callId = event.callId || `patch:${event.id}`;
313
+ const current = state.patchesByCallId[callId] || {
314
+ id: `patch:${callId}`,
315
+ type: "patch",
316
+ turnId,
317
+ callId,
318
+ seq: event.seq,
319
+ timestamp: event.timestamp,
320
+ status: "pending",
321
+ patchText: "",
322
+ stdout: "",
323
+ stderr: "",
324
+ output: "",
325
+ outputStatus: "idle",
326
+ changes: {},
327
+ success: null,
328
+ };
329
+
330
+ const next = {
331
+ ...current,
332
+ ...partial,
333
+ id: current.id,
334
+ type: "patch",
335
+ turnId,
336
+ callId,
337
+ seq: current.seq || event.seq,
338
+ timestamp: current.timestamp || event.timestamp,
339
+ };
340
+
341
+ state.patchesByCallId[callId] = next;
342
+ insertTimelineItem(state, next);
343
+ if (!turn.patchIds.includes(next.id)) {
344
+ turn.patchIds.push(next.id);
345
+ }
346
+ return next;
347
+ }
348
+
349
+ function upsertApproval(state, event, turnId, partial) {
350
+ const turn = ensureTurn(state, turnId, event);
351
+ const requestId = nextApprovalFallbackId(event);
352
+ const current = state.approvalsByRequestId[requestId] || {
353
+ id: `approval:${requestId}`,
354
+ type: "approval",
355
+ turnId,
356
+ requestId,
357
+ seq: event.seq,
358
+ timestamp: event.timestamp,
359
+ status: "pending",
360
+ title: "",
361
+ reason: "",
362
+ command: "",
363
+ };
364
+
365
+ const next = {
366
+ ...current,
367
+ ...partial,
368
+ id: current.id,
369
+ type: "approval",
370
+ turnId,
371
+ requestId,
372
+ seq: current.seq || event.seq,
373
+ timestamp: current.timestamp || event.timestamp,
374
+ };
375
+
376
+ state.approvalsByRequestId[requestId] = next;
377
+ insertTimelineItem(state, next);
378
+ if (!turn.approvalIds.includes(next.id)) {
379
+ turn.approvalIds.push(next.id);
380
+ }
381
+ return next;
382
+ }
383
+
384
+ function upsertSystem(state, event, turnId, partial) {
385
+ const turn = ensureTurn(state, turnId, event);
386
+ const item = insertTimelineItem(state, {
387
+ id: `system:${event.id}`,
388
+ type: "system",
389
+ turnId,
390
+ seq: event.seq,
391
+ timestamp: event.timestamp,
392
+ ...partial,
393
+ });
394
+ if (!turn.systemIds.includes(item.id)) {
395
+ turn.systemIds.push(item.id);
396
+ }
397
+ return item;
398
+ }
399
+
400
+ function upsertTokenCount(state, event, turnId) {
401
+ const turn = ensureTurn(state, turnId, event);
402
+ const item = insertTimelineItem(state, {
403
+ id: `token:${turnId}`,
404
+ type: "system",
405
+ subtype: "token_count",
406
+ turnId,
407
+ seq: event.seq,
408
+ timestamp: event.timestamp,
409
+ payload: event.payload,
410
+ });
411
+ turn.tokenCountId = item.id;
412
+ state.latestTokenCount = event.payload || null;
413
+ }
414
+
415
+ export function createEmptyTimelineState() {
416
+ return {
417
+ activeTurnId: null,
418
+ turnsById: {},
419
+ turnOrder: [],
420
+ messagesById: {},
421
+ reasoningById: {},
422
+ commandsByCallId: {},
423
+ patchesByCallId: {},
424
+ approvalsByRequestId: {},
425
+ timelineItems: [],
426
+ itemIndexById: createItemIndex(),
427
+ latestTokenCount: null,
428
+ };
429
+ }
430
+
431
+ export function reduceTimeline(state, event) {
432
+ if (!event) {
433
+ return state;
434
+ }
435
+
436
+ const turnId = resolveTurnId(state, event);
437
+ const turn = ensureTurn(state, turnId, event);
438
+
439
+ switch (event.kind) {
440
+ case "user_message":
441
+ upsertUserMessage(state, event, turnId);
442
+ setTurnStatus(turn, "idle");
443
+ break;
444
+ case "assistant_message_start":
445
+ upsertAssistantMessage(state, event, turnId, {
446
+ status: "streaming",
447
+ text: event.payload?.text || "",
448
+ });
449
+ if (event.phase !== "commentary") {
450
+ state.activeTurnId = turnId;
451
+ }
452
+ break;
453
+ case "assistant_message_delta": {
454
+ const existing = state.messagesById[nextMessageFallbackId(event, resolveAssistantItemType(event))];
455
+ upsertAssistantMessage(state, event, turnId, {
456
+ status: "streaming",
457
+ text: appendDeltaText(existing?.text, event.payload?.textDelta),
458
+ });
459
+ break;
460
+ }
461
+ case "assistant_message_end":
462
+ upsertAssistantMessage(state, event, turnId, {
463
+ text:
464
+ typeof event.payload?.text === "string" && event.payload.text !== ""
465
+ ? event.payload.text
466
+ : state.messagesById[nextMessageFallbackId(event, resolveAssistantItemType(event))]
467
+ ?.text || "",
468
+ status: "completed",
469
+ });
470
+ if (event.phase !== "commentary") {
471
+ completeReasoningIfPresent(state, turn);
472
+ }
473
+ break;
474
+ case "assistant_message":
475
+ reduceLegacyAssistantMessage(state, event);
476
+ break;
477
+ case "reasoning_start":
478
+ upsertReasoning(state, event, turnId, {
479
+ status: "thinking",
480
+ summary: event.payload?.summary || "",
481
+ text: "",
482
+ });
483
+ setTurnStatus(turn, turn.status === "idle" ? "running" : turn.status);
484
+ break;
485
+ case "reasoning_delta": {
486
+ const reasoningId = nextMessageFallbackId(event, "reasoning");
487
+ const existing = state.reasoningById[reasoningId];
488
+ const nextText = appendDeltaText(existing?.text, event.payload?.textDelta);
489
+ upsertReasoning(state, event, turnId, {
490
+ status: "thinking",
491
+ text: nextText,
492
+ summary: event.payload?.summary || nextText || existing?.summary || "",
493
+ });
494
+ setTurnStatus(turn, turn.status === "idle" ? "running" : turn.status);
495
+ break;
496
+ }
497
+ case "reasoning_end":
498
+ upsertReasoning(state, event, turnId, {
499
+ status: "done",
500
+ summary:
501
+ event.payload?.summary ||
502
+ state.reasoningById[nextMessageFallbackId(event, "reasoning")]?.summary ||
503
+ "",
504
+ });
505
+ break;
506
+ case "reasoning":
507
+ reduceLegacyReasoning(state, event);
508
+ break;
509
+ case "command_start":
510
+ upsertCommand(state, event, turnId, {
511
+ status:
512
+ event.payload?.sandboxPermissions === "require_escalated"
513
+ ? "awaiting_approval"
514
+ : "running",
515
+ command: event.payload?.command || "",
516
+ cwd: event.payload?.cwd || null,
517
+ justification: event.payload?.justification || null,
518
+ sandboxPermissions: event.payload?.sandboxPermissions || null,
519
+ });
520
+ setTurnStatus(turn, "running");
521
+ state.activeTurnId = turnId;
522
+ break;
523
+ case "command_output_delta": {
524
+ const commandId = event.callId || `command:${event.id}`;
525
+ const currentCommand = state.commandsByCallId[commandId];
526
+ const nextStdout =
527
+ event.payload?.stream === "stderr"
528
+ ? currentCommand?.stdout || ""
529
+ : appendDeltaText(currentCommand?.stdout, event.payload?.textDelta);
530
+ const nextStderr =
531
+ event.payload?.stream === "stderr"
532
+ ? appendDeltaText(currentCommand?.stderr, event.payload?.textDelta)
533
+ : currentCommand?.stderr || "";
534
+ upsertCommand(state, event, turnId, {
535
+ status: currentCommand?.status === "awaiting_approval" ? "awaiting_approval" : "running",
536
+ stdout: nextStdout,
537
+ stderr: nextStderr,
538
+ outputStatus: "streaming",
539
+ });
540
+ setTurnStatus(turn, "running");
541
+ break;
542
+ }
543
+ case "command_end": {
544
+ const rejected = Boolean(event.payload?.rejected);
545
+ const completedStatus =
546
+ event.payload?.status === "failed" || event.payload?.exitCode > 0
547
+ ? "failed"
548
+ : rejected
549
+ ? "rejected"
550
+ : "completed";
551
+ upsertCommand(state, event, turnId, {
552
+ status: completedStatus,
553
+ command: event.payload?.command || state.commandsByCallId[event.callId]?.command || "",
554
+ cwd: event.payload?.cwd || state.commandsByCallId[event.callId]?.cwd || null,
555
+ stdout: event.payload?.stdout || state.commandsByCallId[event.callId]?.stdout || "",
556
+ stderr: event.payload?.stderr || state.commandsByCallId[event.callId]?.stderr || "",
557
+ output:
558
+ event.payload?.aggregatedOutput ||
559
+ event.payload?.formattedOutput ||
560
+ event.payload?.output ||
561
+ state.commandsByCallId[event.callId]?.output ||
562
+ "",
563
+ exitCode:
564
+ event.payload?.exitCode ?? state.commandsByCallId[event.callId]?.exitCode ?? null,
565
+ duration: event.payload?.duration || state.commandsByCallId[event.callId]?.duration || null,
566
+ outputStatus: "done",
567
+ });
568
+ if (completedStatus === "failed") {
569
+ setTurnStatus(turn, "failed");
570
+ }
571
+ break;
572
+ }
573
+ case "patch_start":
574
+ upsertPatch(state, event, turnId, {
575
+ status: "running",
576
+ patchText: event.payload?.input || "",
577
+ });
578
+ setTurnStatus(turn, "running");
579
+ state.activeTurnId = turnId;
580
+ break;
581
+ case "patch_output_delta": {
582
+ const patchId = event.callId || `patch:${event.id}`;
583
+ const currentPatch = state.patchesByCallId[patchId];
584
+ upsertPatch(state, event, turnId, {
585
+ status: currentPatch?.status || "running",
586
+ output: appendDeltaText(currentPatch?.output, event.payload?.textDelta),
587
+ outputStatus: "streaming",
588
+ });
589
+ setTurnStatus(turn, "running");
590
+ break;
591
+ }
592
+ case "patch_end": {
593
+ const patchStatus =
594
+ event.payload?.status === "failed" || event.payload?.success === false
595
+ ? "failed"
596
+ : "completed";
597
+ upsertPatch(state, event, turnId, {
598
+ status: patchStatus,
599
+ patchText:
600
+ event.payload?.patchText || state.patchesByCallId[event.callId]?.patchText || "",
601
+ output: event.payload?.output || state.patchesByCallId[event.callId]?.output || "",
602
+ stdout: event.payload?.stdout || state.patchesByCallId[event.callId]?.stdout || "",
603
+ stderr: event.payload?.stderr || state.patchesByCallId[event.callId]?.stderr || "",
604
+ changes: event.payload?.changes || state.patchesByCallId[event.callId]?.changes || {},
605
+ success:
606
+ event.payload?.success ?? state.patchesByCallId[event.callId]?.success ?? null,
607
+ outputStatus: "done",
608
+ });
609
+ if (patchStatus === "failed") {
610
+ setTurnStatus(turn, "failed");
611
+ }
612
+ break;
613
+ }
614
+ case "approval_requested":
615
+ upsertApproval(state, event, turnId, {
616
+ status: "pending",
617
+ title: event.payload?.title || t("approval.required"),
618
+ reason: event.payload?.reason || "",
619
+ command: event.payload?.command || "",
620
+ resumable: event.payload?.resumable ?? true,
621
+ });
622
+ break;
623
+ case "approval_resolved":
624
+ upsertApproval(state, event, turnId, {
625
+ status: event.payload?.decision === "decline" ? "rejected" : "resolved",
626
+ decision: event.payload?.decision || null,
627
+ });
628
+ break;
629
+ case "turn_started":
630
+ setTurnStatus(turn, "running");
631
+ state.activeTurnId = turnId;
632
+ break;
633
+ case "turn_completed":
634
+ setTurnStatus(turn, "completed");
635
+ turn.completedAt = event.timestamp;
636
+ completeReasoningIfPresent(state, turn);
637
+ if (state.activeTurnId === turnId) {
638
+ state.activeTurnId = null;
639
+ }
640
+ break;
641
+ case "turn_aborted":
642
+ setTurnStatus(turn, "aborted");
643
+ completeReasoningIfPresent(state, turn);
644
+ upsertSystem(state, event, turnId, {
645
+ subtype: "turn_aborted",
646
+ text: event.payload?.reason || "Turn aborted",
647
+ status: "aborted",
648
+ });
649
+ if (state.activeTurnId === turnId) {
650
+ state.activeTurnId = null;
651
+ }
652
+ break;
653
+ case "error":
654
+ setTurnStatus(turn, "failed");
655
+ completeReasoningIfPresent(state, turn);
656
+ upsertSystem(state, event, turnId, {
657
+ subtype: "error",
658
+ text: event.payload?.message || "Unknown error",
659
+ errorCode: event.payload?.code || null,
660
+ status: "failed",
661
+ });
662
+ break;
663
+ case "token_count":
664
+ upsertTokenCount(state, event, turnId);
665
+ break;
666
+ default:
667
+ break;
668
+ }
669
+
670
+ return state;
671
+ }
672
+
673
+ export function reduceTimelineBatch(state, events) {
674
+ if (!Array.isArray(events)) {
675
+ return state;
676
+ }
677
+
678
+ events.forEach((event) => {
679
+ reduceTimeline(state, event);
680
+ });
681
+ return state;
682
+ }
683
+
684
+ export function buildTimelineView(state) {
685
+ const items = state.timelineItems.filter(
686
+ (item) => !(item.type === "system" && item.subtype === "token_count"),
687
+ );
688
+ const groupedItems = groupTimelineActivities(items);
689
+ let activeTurn = null;
690
+ if (state.activeTurnId && state.turnsById[state.activeTurnId]?.status === "running") {
691
+ activeTurn = state.turnsById[state.activeTurnId];
692
+ } else {
693
+ for (let index = state.turnOrder.length - 1; index >= 0; index -= 1) {
694
+ const turn = state.turnsById[state.turnOrder[index]];
695
+ if (turn?.status === "running") {
696
+ activeTurn = turn;
697
+ break;
698
+ }
699
+ }
700
+ }
701
+
702
+ if (!activeTurn) {
703
+ return groupedItems;
704
+ }
705
+
706
+ const lastTurnItem = [...groupedItems].reverse().find((item) => item.turnId === activeTurn.id) || null;
707
+ const lastSeq = lastTurnItem?.seq ?? groupedItems[groupedItems.length - 1]?.seq ?? 0;
708
+ const lastTimestamp =
709
+ lastTurnItem?.timestamp ??
710
+ activeTurn.startedAt ??
711
+ groupedItems[groupedItems.length - 1]?.timestamp ??
712
+ new Date().toISOString();
713
+
714
+ return [
715
+ ...groupedItems,
716
+ {
717
+ id: `thinking:${activeTurn.id}`,
718
+ type: "reasoning",
719
+ turnId: activeTurn.id,
720
+ seq: lastSeq + 0.01,
721
+ timestamp: lastTimestamp,
722
+ status: "thinking",
723
+ summary: t("timeline.thinking"),
724
+ text: "",
725
+ synthetic: true,
726
+ },
727
+ ];
728
+ }