@ellipsis-dev/sdk 0.27.0 → 0.30.0

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.
@@ -1,6 +1,219 @@
1
1
  import {
2
2
  SESSION_STREAM_PROTOCOL_VERSION
3
- } from "../chunk-RMPAMUAE.js";
3
+ } from "../chunk-K252XHLI.js";
4
+
5
+ // src/store/claude.ts
6
+ function claudePayload(record) {
7
+ if (record.kind === "claude_sdk") return record.payload;
8
+ if (record.kind !== "claude_code") return null;
9
+ const event = record.payload;
10
+ switch (event.type) {
11
+ case "assistant":
12
+ return {
13
+ ...event.message,
14
+ kind: "assistant",
15
+ message_id: event.message.id,
16
+ parent_tool_use_id: event.parent_tool_use_id
17
+ };
18
+ case "user":
19
+ return {
20
+ kind: "user",
21
+ content: event.message.content,
22
+ uuid: event.uuid,
23
+ parent_tool_use_id: event.parent_tool_use_id
24
+ };
25
+ case "system":
26
+ return {
27
+ kind: "system",
28
+ subtype: event.subtype,
29
+ data: event,
30
+ session_id: event.session_id,
31
+ uuid: event.uuid
32
+ };
33
+ case "result":
34
+ return {
35
+ ...event,
36
+ kind: "result",
37
+ cost_usd: event.total_cost_usd,
38
+ model_usage: event.modelUsage
39
+ };
40
+ case "rate_limit_event":
41
+ return {
42
+ kind: "rate_limit",
43
+ status: event.rate_limit_info.status,
44
+ rate_limit_type: event.rate_limit_info.rateLimitType,
45
+ resets_at: event.rate_limit_info.resetsAt,
46
+ utilization: event.rate_limit_info.utilization
47
+ };
48
+ default:
49
+ return null;
50
+ }
51
+ }
52
+
53
+ // src/store/codexAppServer.ts
54
+ function codexAppServerContent(event) {
55
+ if (event.method === "turn/completed") {
56
+ return { kind: "end", isError: event.params.turn.status !== "completed" };
57
+ }
58
+ if (event.method === "error") {
59
+ return event.params.willRetry ? null : { kind: "error", text: event.params.error.message };
60
+ }
61
+ if (event.method !== "item/completed") return null;
62
+ const item = event.params.item;
63
+ switch (item.type) {
64
+ case "agentMessage":
65
+ return { kind: "assistant", text: item.text, phase: item.phase };
66
+ case "plan":
67
+ return { kind: "assistant", text: item.text, phase: "commentary" };
68
+ case "reasoning":
69
+ return {
70
+ kind: "thinking",
71
+ text: (item.summary?.length ? item.summary : item.content ?? []).join(
72
+ "\n"
73
+ )
74
+ };
75
+ case "commandExecution":
76
+ return {
77
+ kind: "tool",
78
+ name: "Bash",
79
+ input: { command: item.command, commandActions: item.commandActions },
80
+ summary: item.command,
81
+ result: item.aggregatedOutput?.trim() || "(no output)",
82
+ isError: item.status === "failed" || item.status === "declined" || typeof item.exitCode === "number" && item.exitCode !== 0
83
+ };
84
+ case "fileChange":
85
+ return {
86
+ kind: "tool",
87
+ name: "Edit",
88
+ input: { changes: item.changes },
89
+ summary: item.changes.map((change) => change.path).join(", "),
90
+ result: item.changes.map((change) => change.diff).join("\n"),
91
+ isError: item.status === "failed" || item.status === "declined"
92
+ };
93
+ case "webSearch":
94
+ return {
95
+ kind: "tool",
96
+ name: "WebSearch",
97
+ input: { query: item.query },
98
+ summary: item.query,
99
+ result: null,
100
+ isError: false
101
+ };
102
+ default:
103
+ return null;
104
+ }
105
+ }
106
+
107
+ // src/store/progress.ts
108
+ function messageProgress(records) {
109
+ const messages = /* @__PURE__ */ new Map();
110
+ const pending = /* @__PURE__ */ new Set();
111
+ const turns = /* @__PURE__ */ new Map();
112
+ let starting = "queued";
113
+ let ready = false;
114
+ const getTurn = (id) => {
115
+ let turn = turns.get(id);
116
+ if (!turn) {
117
+ turn = {
118
+ timing: { id, startedAt: null, completedAt: null },
119
+ phase: ready ? "working" : "starting",
120
+ messages: /* @__PURE__ */ new Set()
121
+ };
122
+ turns.set(id, turn);
123
+ }
124
+ return turn;
125
+ };
126
+ const updatePending = () => {
127
+ for (const id of pending) messages.get(id).phase = starting;
128
+ };
129
+ const attach = (messageId, turnId) => {
130
+ const message = messages.get(messageId);
131
+ if (!message) return;
132
+ const turn = getTurn(turnId);
133
+ if (!turn.messages.has(messageId)) {
134
+ turn.messages.add(messageId);
135
+ message.turns.push(turn.timing);
136
+ }
137
+ pending.delete(messageId);
138
+ message.phase = turn.phase;
139
+ starting = "queued";
140
+ updatePending();
141
+ };
142
+ for (const record of records) {
143
+ if (record.kind === "platform") {
144
+ const p = record.payload;
145
+ const messageId = typeof p.message_id === "string" ? p.message_id : null;
146
+ const turnId = typeof p.turn_id === "string" ? p.turn_id : record.turn_id;
147
+ switch (record.record_type) {
148
+ case "message_received":
149
+ if (messageId && !messages.has(messageId)) {
150
+ messages.set(messageId, { phase: starting, turns: [] });
151
+ pending.add(messageId);
152
+ }
153
+ break;
154
+ case "message_delivered":
155
+ if (messageId && turnId) attach(messageId, turnId);
156
+ break;
157
+ case "message_requeued":
158
+ if (messageId && messages.has(messageId)) {
159
+ pending.add(messageId);
160
+ messages.get(messageId).phase = "queued";
161
+ }
162
+ break;
163
+ case "session_starting":
164
+ case "session_retrying":
165
+ ready = false;
166
+ starting = typeof p.wake_index === "number" && p.wake_index > 0 ? "waking" : "starting";
167
+ updatePending();
168
+ break;
169
+ case "session_resumed":
170
+ starting = "starting";
171
+ updatePending();
172
+ break;
173
+ case "turn_started":
174
+ if (turnId) getTurn(turnId).timing.startedAt = record.created_at;
175
+ break;
176
+ case "turn_completed":
177
+ case "turn_failed":
178
+ if (turnId) {
179
+ const turn = getTurn(turnId);
180
+ turn.timing.completedAt = record.created_at;
181
+ turn.phase = null;
182
+ for (const id of turn.messages) {
183
+ if (!pending.has(id)) messages.get(id).phase = null;
184
+ }
185
+ }
186
+ starting = "queued";
187
+ break;
188
+ case "session_idle":
189
+ case "session_closed":
190
+ case "session_cancelled":
191
+ ready = false;
192
+ starting = "queued";
193
+ for (const id of pending) messages.get(id).phase = null;
194
+ break;
195
+ }
196
+ continue;
197
+ }
198
+ const claude = claudePayload(record);
199
+ const agentStarted = record.kind === "codex_app_server" && record.payload.method === "turn/started" || record.kind === "codex" && record.payload.type === "turn.started" || claude != null && (claude.kind === "assistant" || claude.kind === "user" || claude.kind === "system" && claude.subtype === "init");
200
+ if (agentStarted) {
201
+ ready = true;
202
+ if (record.turn_id) {
203
+ const turn = getTurn(record.turn_id);
204
+ if (turn.timing.completedAt == null) {
205
+ turn.timing.startedAt ??= record.created_at;
206
+ turn.phase = "working";
207
+ for (const id of turn.messages) messages.get(id).phase = "working";
208
+ }
209
+ }
210
+ }
211
+ if (record.session_message_id && record.turn_id) {
212
+ attach(record.session_message_id, record.turn_id);
213
+ }
214
+ }
215
+ return messages;
216
+ }
4
217
 
5
218
  // src/store/lifecycle.ts
6
219
  function sandboxOutputStep(payload) {
@@ -120,8 +333,8 @@ function lifecycleText(recordType, payload) {
120
333
  }
121
334
 
122
335
  // src/store/transcript.ts
123
- function oneLine(text, max) {
124
- const collapsed = text.replace(/\s+/g, " ").trim();
336
+ function oneLine(text2, max) {
337
+ const collapsed = text2.replace(/\s+/g, " ").trim();
125
338
  return collapsed.length <= max ? collapsed : `${collapsed.slice(0, max - 3)}...`;
126
339
  }
127
340
  function summarizeToolInput(name, input) {
@@ -202,11 +415,11 @@ function eventToItems(event, keyBase, options = {}) {
202
415
  return items;
203
416
  }
204
417
  if (event.kind === "rate_limit") return items;
205
- const blocks = blocksOf(event.content);
418
+ const blocks = blocksOf(event.content ?? []);
206
419
  if (event.kind === "user") {
207
420
  for (const block of blocks) {
208
421
  if (block.type === "tool_result") {
209
- const body = toolResultText(block.content);
422
+ const body = toolResultText(block.content ?? null);
210
423
  push({
211
424
  kind: "tool_result",
212
425
  gutter: "\u23BF",
@@ -234,14 +447,14 @@ function eventToItems(event, keyBase, options = {}) {
234
447
  spaceBefore: true
235
448
  });
236
449
  } else if (block.type === "tool_use" || block.type === "server_tool_use") {
237
- const summary = summarizeToolInput(block.name, block.input);
450
+ const summary = summarizeToolInput(block.name, block.input ?? {});
238
451
  push({
239
452
  kind: "tool",
240
453
  gutter: "\u25CF",
241
454
  text: block.name,
242
455
  detail: summary ? `(${summary})` : void 0,
243
456
  spaceBefore: true,
244
- tool: { name: block.name, input: block.input }
457
+ tool: { name: block.name, input: block.input ?? {} }
245
458
  });
246
459
  } else if (block.type === "text" && block.text.trim()) {
247
460
  push({ kind: "assistant", text: block.text.trim(), spaceBefore: true });
@@ -276,20 +489,20 @@ function codexEventToItems(event, keyBase) {
276
489
  const item = event.item;
277
490
  switch (item.type) {
278
491
  case "agent_message": {
279
- const text = item.text.trim();
280
- if (text) push({ kind: "assistant", text, spaceBefore: true });
492
+ const text2 = (item.text ?? "").trim();
493
+ if (text2) push({ kind: "assistant", text: text2, spaceBefore: true });
281
494
  break;
282
495
  }
283
496
  case "reasoning": {
284
497
  const r = item;
285
- const text = (r.text ?? r.summary ?? "").trim();
286
- if (text)
287
- push({ kind: "thinking", gutter: "\u273B", text, spaceBefore: true });
498
+ const text2 = (r.text ?? r.summary ?? "").trim();
499
+ if (text2)
500
+ push({ kind: "thinking", gutter: "\u273B", text: text2, spaceBefore: true });
288
501
  break;
289
502
  }
290
503
  case "command_execution": {
291
504
  const c = item;
292
- const summary = oneLine(c.command, 100);
505
+ const summary = oneLine(c.command ?? "", 100);
293
506
  push({
294
507
  kind: "tool",
295
508
  gutter: "\u25CF",
@@ -361,14 +574,63 @@ function codexMcpToolName(item) {
361
574
  return item.server && item.tool ? `mcp__${item.server}__${item.tool}` : item.tool ?? "mcp_tool_call";
362
575
  }
363
576
  function recordToItems(record, keyBase, options = {}) {
364
- switch (record.record_format) {
365
- case "claude_sdk@1":
366
- return eventToItems(record.payload, keyBase, options);
367
- case "codex_jsonl@1":
577
+ switch (record.kind) {
578
+ case "claude_sdk":
579
+ case "claude_code": {
580
+ const payload = claudePayload(record);
581
+ return payload ? eventToItems(payload, keyBase, options) : [];
582
+ }
583
+ case "codex":
368
584
  return codexEventToItems(record.payload, keyBase);
369
- case "ellipsis_lifecycle@1": {
370
- const text = lifecycleText(record.record_type, record.payload);
371
- return text ? [{ key: keyBase, kind: "notice", text, spaceBefore: true }] : [];
585
+ case "codex_app_server": {
586
+ const content = codexAppServerContent(record.payload);
587
+ if (!content) return [];
588
+ if (content.kind === "end") {
589
+ return content.isError ? [
590
+ {
591
+ key: keyBase,
592
+ kind: "summary",
593
+ text: "turn ended with an error",
594
+ isError: true,
595
+ spaceBefore: true
596
+ }
597
+ ] : [];
598
+ }
599
+ if (content.kind === "tool") {
600
+ const summary = oneLine(content.summary, 100);
601
+ return [
602
+ {
603
+ key: `${keyBase}:0`,
604
+ kind: "tool",
605
+ gutter: "\u25CF",
606
+ text: content.name,
607
+ detail: summary ? `(${summary})` : void 0,
608
+ spaceBefore: true,
609
+ tool: { name: content.name, input: content.input ?? void 0 }
610
+ },
611
+ {
612
+ key: `${keyBase}:1`,
613
+ kind: "tool_result",
614
+ gutter: "\u23BF",
615
+ text: content.result || "(completed)",
616
+ isError: content.isError,
617
+ spaceBefore: false
618
+ }
619
+ ];
620
+ }
621
+ return content.text.trim() ? [
622
+ {
623
+ key: keyBase,
624
+ kind: content.kind,
625
+ text: content.text.trim(),
626
+ isError: content.kind === "error",
627
+ spaceBefore: true
628
+ }
629
+ ] : [];
630
+ }
631
+ case "platform": {
632
+ const text2 = lifecycleText(record.record_type, record.payload);
633
+ return text2 ? [{ key: keyBase, kind: "notice", text: text2, spaceBefore: true }] : [];
372
634
  }
373
635
  default:
374
636
  return [];
@@ -408,9 +670,11 @@ function foldLabel(group, settled) {
408
670
  return `Ran ${n} tool call${plural} (${shown})`;
409
671
  }
410
672
  function foldRun(group, loading) {
673
+ const thinkingOnly = group.every((i) => i.kind === "thinking");
411
674
  return {
412
675
  key: `grp:${group[0].key}`,
413
- kind: "notice",
676
+ kind: thinkingOnly ? "thinking" : "notice",
677
+ ...thinkingOnly ? { gutter: "\u273B" } : {},
414
678
  text: foldLabel(group, !loading),
415
679
  spaceBefore: true,
416
680
  ...loading ? { loading } : {}
@@ -437,9 +701,9 @@ function collapseToolRuns(items, { working = false } = {}) {
437
701
  flush(true);
438
702
  return out;
439
703
  }
440
- function clampLines(text, maxLines) {
441
- const lines = text.split("\n");
442
- if (lines.length <= maxLines) return { body: text, more: 0 };
704
+ function clampLines(text2, maxLines) {
705
+ const lines = text2.split("\n");
706
+ if (lines.length <= maxLines) return { body: text2, more: 0 };
443
707
  return {
444
708
  body: lines.slice(0, maxLines).join("\n"),
445
709
  more: lines.length - maxLines
@@ -481,20 +745,29 @@ var SANDBOX_RECORD_TYPES = /* @__PURE__ */ new Set([
481
745
  "sandbox_output",
482
746
  "sandbox_ready"
483
747
  ]);
484
- function groupRecordsToChatTurns(records) {
748
+ function groupRecordsToChatTurns(records, session) {
485
749
  const turns = [];
486
750
  const toolNodeByUseId = /* @__PURE__ */ new Map();
487
751
  const userTurnByMessageId = /* @__PURE__ */ new Map();
488
- const messageIdByTurnId = /* @__PURE__ */ new Map();
752
+ const deliveredTurnByMessageId = /* @__PURE__ */ new Map();
753
+ const failedTurnIds = /* @__PURE__ */ new Set();
754
+ const activeMessageByTurnId = /* @__PURE__ */ new Map();
755
+ let lastDeliveredMessageId;
489
756
  let seq = 0;
490
757
  const nextKey = (base) => `${base}:${seq++}`;
491
758
  let openIdx = -1;
492
759
  let initCount = 0;
493
760
  let pendingResume = false;
494
- const openAgentTurn = (record) => {
761
+ const openSessionTurn = (record) => {
762
+ const messageId = (record.turn_id ? activeMessageByTurnId.get(record.turn_id) : void 0) ?? lastDeliveredMessageId;
763
+ if (openIdx >= 0 && (turns[openIdx].turnId !== (record.turn_id ?? void 0) || turns[openIdx].messageId !== messageId)) {
764
+ openIdx = -1;
765
+ }
495
766
  if (openIdx < 0) {
496
767
  turns.push({
497
768
  key: nextKey(record.id),
769
+ messageId,
770
+ turnId: record.turn_id ?? void 0,
498
771
  role: "assistant",
499
772
  nodes: [],
500
773
  startedAt: record.created_at,
@@ -511,7 +784,7 @@ function groupRecordsToChatTurns(records) {
511
784
  return turns[openIdx];
512
785
  };
513
786
  for (const record of records) {
514
- if (record.record_format === "ellipsis_lifecycle@1") {
787
+ if (record.kind === "platform") {
515
788
  const messageId = typeof record.payload.message_id === "string" ? record.payload.message_id : null;
516
789
  if (record.record_type === "message_received" && messageId) {
517
790
  const body = typeof record.payload.body === "string" ? record.payload.body : "";
@@ -520,6 +793,7 @@ function groupRecordsToChatTurns(records) {
520
793
  resumed: false,
521
794
  isError: false,
522
795
  key: `msg:${messageId}`,
796
+ messageId,
523
797
  role: "user",
524
798
  nodes: [{ key: `msg:${messageId}`, kind: "user", text: body.trim() }],
525
799
  startedAt: record.created_at,
@@ -533,19 +807,24 @@ function groupRecordsToChatTurns(records) {
533
807
  continue;
534
808
  }
535
809
  if (record.record_type === "message_delivered" && messageId) {
536
- if (typeof record.payload.turn_id === "string")
537
- messageIdByTurnId.set(record.payload.turn_id, messageId);
810
+ if (typeof record.payload.turn_id === "string") {
811
+ deliveredTurnByMessageId.set(messageId, record.payload.turn_id);
812
+ activeMessageByTurnId.set(record.payload.turn_id, messageId);
813
+ lastDeliveredMessageId = messageId;
814
+ }
815
+ continue;
816
+ }
817
+ if (record.record_type === "message_requeued" && messageId) {
818
+ deliveredTurnByMessageId.delete(messageId);
538
819
  continue;
539
820
  }
540
821
  if (record.record_type === "turn_failed") {
541
- const turnId = typeof record.payload.turn_id === "string" ? record.payload.turn_id : null;
542
- const owner = turnId ? messageIdByTurnId.get(turnId) : void 0;
543
- const userTurn = owner ? userTurnByMessageId.get(owner) : void 0;
544
- if (userTurn) userTurn.isError = true;
822
+ if (typeof record.payload.turn_id === "string")
823
+ failedTurnIds.add(record.payload.turn_id);
545
824
  continue;
546
825
  }
547
- const text = lifecycleText(record.record_type, record.payload);
548
- if (!text) continue;
826
+ const text2 = lifecycleText(record.record_type, record.payload);
827
+ if (!text2) continue;
549
828
  const isSandbox = SANDBOX_RECORD_TYPES.has(record.record_type);
550
829
  const isSandboxOutput = record.record_type === "sandbox_output";
551
830
  const step = isSandboxOutput ? sandboxOutputStep(record.payload) : void 0;
@@ -566,13 +845,13 @@ function groupRecordsToChatTurns(records) {
566
845
  if (foldTarget) {
567
846
  const last = foldTarget.nodes[foldTarget.nodes.length - 1];
568
847
  if (isSandboxOutput && last && last.kind === "lifecycle" && last.recordType === "sandbox_output" && last.step === step) {
569
- last.text = text;
848
+ last.text = text2;
570
849
  last.lines = [...last.lines ?? [], ...lines ?? []];
571
850
  } else {
572
851
  foldTarget.nodes.push({
573
852
  key: nextKey(record.id),
574
853
  kind: "lifecycle",
575
- text,
854
+ text: text2,
576
855
  recordType: record.record_type,
577
856
  step,
578
857
  lines,
@@ -589,7 +868,7 @@ function groupRecordsToChatTurns(records) {
589
868
  {
590
869
  key: nextKey(record.id),
591
870
  kind: "lifecycle",
592
- text,
871
+ text: text2,
593
872
  recordType: record.record_type,
594
873
  step,
595
874
  lines,
@@ -606,9 +885,14 @@ function groupRecordsToChatTurns(records) {
606
885
  });
607
886
  continue;
608
887
  }
609
- if (record.record_format === "codex_jsonl@1") {
888
+ if (record.session_message_id) {
889
+ lastDeliveredMessageId = record.session_message_id;
890
+ if (record.turn_id)
891
+ activeMessageByTurnId.set(record.turn_id, record.session_message_id);
892
+ }
893
+ if (record.kind === "codex") {
610
894
  codexEventIntoTurns(record, record.payload, {
611
- openAgentTurn,
895
+ openSessionTurn,
612
896
  closeTurn: (completedAt, isError) => {
613
897
  if (openIdx >= 0) {
614
898
  turns[openIdx].completedAt = completedAt;
@@ -620,7 +904,42 @@ function groupRecordsToChatTurns(records) {
620
904
  });
621
905
  continue;
622
906
  }
623
- const data = record.payload;
907
+ if (record.kind === "codex_app_server") {
908
+ const content = codexAppServerContent(record.payload);
909
+ if (!content) continue;
910
+ if (content.kind === "end") {
911
+ if (openIdx >= 0) {
912
+ turns[openIdx].completedAt = record.created_at;
913
+ if (content.isError) turns[openIdx].isError = true;
914
+ openIdx = -1;
915
+ }
916
+ } else if (content.kind === "tool") {
917
+ openSessionTurn(record).nodes.push({
918
+ key: nextKey(record.id),
919
+ kind: "tool",
920
+ name: content.name,
921
+ input: content.input,
922
+ summary: oneLine(content.summary, 100),
923
+ result: content.result,
924
+ isError: content.isError,
925
+ startedAt: record.created_at,
926
+ completedAt: record.created_at
927
+ });
928
+ } else if (content.text.trim()) {
929
+ const turn2 = openSessionTurn(record);
930
+ if (content.kind === "error") turn2.isError = true;
931
+ turn2.nodes.push({
932
+ key: nextKey(record.id),
933
+ kind: content.kind === "error" ? "assistant" : content.kind,
934
+ text: content.text.trim(),
935
+ createdAt: record.created_at,
936
+ ...content.kind === "assistant" && content.phase != null ? { phase: content.phase } : {}
937
+ });
938
+ }
939
+ continue;
940
+ }
941
+ const data = claudePayload(record);
942
+ if (!data) continue;
624
943
  if (data.kind === "system") {
625
944
  if (isInitEvent(data)) {
626
945
  initCount += 1;
@@ -666,11 +985,14 @@ function groupRecordsToChatTurns(records) {
666
985
  }
667
986
  for (const block of content) {
668
987
  if (block.type !== "tool_result") continue;
669
- const body = toolResultText(block.content) || "(no output)";
988
+ const body = toolResultText(block.content ?? null) || "(no output)";
670
989
  const useId = block.tool_use_id;
671
990
  const node = toolNodeByUseId.get(useId);
672
991
  if (node) {
673
992
  node.result = body;
993
+ if (record.kind === "claude_code" || record.kind === "claude_sdk") {
994
+ node.resultDetails = record.payload.tool_use_result;
995
+ }
674
996
  node.isError = !!block.is_error;
675
997
  node.completedAt = record.created_at;
676
998
  } else {
@@ -685,18 +1007,18 @@ function groupRecordsToChatTurns(records) {
685
1007
  startedAt: null,
686
1008
  completedAt: record.created_at
687
1009
  };
688
- openAgentTurn(record).nodes.push(orphan);
1010
+ openSessionTurn(record).nodes.push(orphan);
689
1011
  toolNodeByUseId.set(useId, orphan);
690
1012
  }
691
1013
  }
692
1014
  continue;
693
1015
  }
694
1016
  if (data.kind !== "assistant") continue;
695
- const turn = openAgentTurn(record);
1017
+ const turn = openSessionTurn(record);
696
1018
  if (data.usage && typeof data.usage.output_tokens === "number") {
697
1019
  turn.tokens = (turn.tokens ?? 0) + data.usage.output_tokens;
698
1020
  }
699
- for (const block of data.content) {
1021
+ for (const block of data.content ?? []) {
700
1022
  if (block.type === "thinking" && block.thinking.trim()) {
701
1023
  turn.nodes.push({
702
1024
  key: nextKey(record.id),
@@ -708,7 +1030,7 @@ function groupRecordsToChatTurns(records) {
708
1030
  key: nextKey(record.id),
709
1031
  kind: "tool",
710
1032
  name: block.name,
711
- input: block.input,
1033
+ input: block.input ?? null,
712
1034
  summary: summarizeToolInput(block.name, block.input),
713
1035
  result: null,
714
1036
  isError: false,
@@ -721,11 +1043,60 @@ function groupRecordsToChatTurns(records) {
721
1043
  turn.nodes.push({
722
1044
  key: nextKey(record.id),
723
1045
  kind: "assistant",
724
- text: block.text.trim()
1046
+ text: block.text.trim(),
1047
+ createdAt: record.created_at
725
1048
  });
726
1049
  }
727
1050
  }
728
1051
  }
1052
+ for (const [messageId, turnId] of deliveredTurnByMessageId) {
1053
+ const turn = userTurnByMessageId.get(messageId);
1054
+ if (turn) turn.isError = failedTurnIds.has(turnId);
1055
+ }
1056
+ for (const [messageId, progress] of messageProgress(records)) {
1057
+ const turn = userTurnByMessageId.get(messageId);
1058
+ if (turn) turn.progress = progress;
1059
+ }
1060
+ const initialTurn = records.find(
1061
+ (r) => r.kind === "platform" && r.record_type === "turn_started" && r.payload.turn_index === 0
1062
+ );
1063
+ const initialDelivery = initialTurn && records.find(
1064
+ (r) => r.kind === "platform" && r.record_type === "message_delivered" && r.payload.turn_id === (initialTurn.turn_id ?? initialTurn.payload.turn_id)
1065
+ );
1066
+ const initialReceived = initialTurn && records.find(
1067
+ (r) => r.kind === "platform" && r.record_type === "message_received" && r.turn_id === (initialTurn.turn_id ?? initialTurn.payload.turn_id)
1068
+ );
1069
+ const initialMessage = initialReceived ?? initialDelivery;
1070
+ const initialId = initialMessage?.kind === "platform" && typeof initialMessage.payload.message_id === "string" ? initialMessage.payload.message_id : void 0;
1071
+ const opening = initialId ? userTurnByMessageId.get(initialId) : void 0;
1072
+ const includesStart = records.length === 0 || records.some((r) => r.record_type === "session_scheduled");
1073
+ const prompt = (session?.claude_code ?? session?.codex)?.prompt;
1074
+ if (prompt?.trim() && session && (opening || includesStart)) {
1075
+ const first = opening ?? {
1076
+ key: "prompt",
1077
+ role: "user",
1078
+ nodes: [{ key: "prompt", kind: "user", text: prompt }],
1079
+ startedAt: session.lifecycle.timestamps.created_at,
1080
+ completedAt: session.lifecycle.timestamps.created_at,
1081
+ durationMs: null,
1082
+ costUsd: null,
1083
+ tokens: null,
1084
+ resumed: false,
1085
+ isError: false,
1086
+ progress: { phase: "starting", turns: [] }
1087
+ };
1088
+ if (opening) turns.splice(turns.indexOf(opening), 1);
1089
+ first.key = "prompt";
1090
+ first.startedAt = session.lifecycle.timestamps.created_at;
1091
+ turns.unshift(first);
1092
+ if (!opening) {
1093
+ for (const turn of turns) {
1094
+ if (turn !== first && turn.role === "user" && turn.progress && !turn.progress.turns.length) {
1095
+ turn.progress.phase = turn.progress.phase === null ? null : "queued";
1096
+ }
1097
+ }
1098
+ }
1099
+ }
729
1100
  return turns;
730
1101
  }
731
1102
  function codexEventIntoTurns(record, event, ops) {
@@ -734,36 +1105,38 @@ function codexEventIntoTurns(record, event, ops) {
734
1105
  return;
735
1106
  }
736
1107
  if (event.type === "error") {
737
- const turn2 = ops.openAgentTurn(record);
1108
+ const turn2 = ops.openSessionTurn(record);
738
1109
  turn2.nodes.push({
739
1110
  key: ops.nextKey(record.id),
740
1111
  kind: "assistant",
741
- text: event.message?.trim() || "error"
1112
+ text: event.message?.trim() || "error",
1113
+ createdAt: record.created_at
742
1114
  });
743
1115
  return;
744
1116
  }
745
1117
  if (event.type !== "item.completed") return;
746
1118
  const item = event.item;
747
- const turn = () => ops.openAgentTurn(record);
1119
+ const turn = () => ops.openSessionTurn(record);
748
1120
  switch (item.type) {
749
1121
  case "agent_message": {
750
- const text = item.text.trim();
751
- if (text)
1122
+ const text2 = (item.text ?? "").trim();
1123
+ if (text2)
752
1124
  turn().nodes.push({
753
1125
  key: ops.nextKey(record.id),
754
1126
  kind: "assistant",
755
- text
1127
+ text: text2,
1128
+ createdAt: record.created_at
756
1129
  });
757
1130
  break;
758
1131
  }
759
1132
  case "reasoning": {
760
1133
  const r = item;
761
- const text = (r.text ?? r.summary ?? "").trim();
762
- if (text)
1134
+ const text2 = (r.text ?? r.summary ?? "").trim();
1135
+ if (text2)
763
1136
  turn().nodes.push({
764
1137
  key: ops.nextKey(record.id),
765
1138
  kind: "thinking",
766
- text
1139
+ text: text2
767
1140
  });
768
1141
  break;
769
1142
  }
@@ -774,7 +1147,7 @@ function codexEventIntoTurns(record, event, ops) {
774
1147
  kind: "tool",
775
1148
  name: "Bash",
776
1149
  input: { command: c.command },
777
- summary: oneLine(c.command, 100),
1150
+ summary: oneLine(c.command ?? "", 100),
778
1151
  result: (c.aggregated_output ?? "").trim() || "(no output)",
779
1152
  isError: typeof c.exit_code === "number" && c.exit_code !== 0,
780
1153
  startedAt: record.created_at,
@@ -979,8 +1352,8 @@ function deriveSandboxState(records, minFeedSeq) {
979
1352
  let configCommitSha = null;
980
1353
  let log = [];
981
1354
  let open = /* @__PURE__ */ new Map();
982
- const push = (record, kind, text) => {
983
- const entry = { key: `${record.feed_seq}:${log.length}`, kind, text };
1355
+ const push = (record, kind, text2) => {
1356
+ const entry = { key: `${record.feed_seq}:${log.length}`, kind, text: text2 };
984
1357
  log.push(entry);
985
1358
  return entry;
986
1359
  };
@@ -1005,8 +1378,8 @@ function deriveSandboxState(records, minFeedSeq) {
1005
1378
  case "session_starting":
1006
1379
  case "session_retrying": {
1007
1380
  seen = true;
1008
- const text = lifecycleText(record.record_type, p);
1009
- headline = !text || text === "Session starting\u2026" ? STARTING_SANDBOX : text;
1381
+ const text2 = lifecycleText(record.record_type, p);
1382
+ headline = !text2 || text2 === "Session starting\u2026" ? STARTING_SANDBOX : text2;
1010
1383
  done = false;
1011
1384
  readySeconds = null;
1012
1385
  reset();
@@ -1035,7 +1408,7 @@ function deriveSandboxState(records, minFeedSeq) {
1035
1408
  const dur = msLabel(p.duration_ms);
1036
1409
  const failed = p.status === "failed";
1037
1410
  const base = failed ? `${label} failed` : label;
1038
- const text = [
1411
+ const text2 = [
1039
1412
  base,
1040
1413
  ...tier ? [tier] : [],
1041
1414
  ...dur ? [dur] : []
@@ -1043,10 +1416,10 @@ function deriveSandboxState(records, minFeedSeq) {
1043
1416
  const line = open.get(key);
1044
1417
  if (line) {
1045
1418
  line.kind = failed ? "failed" : "done";
1046
- line.text = text;
1419
+ line.text = text2;
1047
1420
  open.delete(key);
1048
1421
  } else {
1049
- push(record, failed ? "failed" : "done", text);
1422
+ push(record, failed ? "failed" : "done", text2);
1050
1423
  }
1051
1424
  } else if (!open.has(key)) {
1052
1425
  open.set(key, push(record, "step", `${label}\u2026`));
@@ -1111,8 +1484,8 @@ function startupSettled(sandbox, status) {
1111
1484
  }
1112
1485
  function startupHeadline(sandbox, status) {
1113
1486
  const activity = statusActivityText(status);
1114
- const text = !sandbox || sandbox.done ? activity ?? STARTING_SANDBOX : sandbox.headline;
1115
- return `${text.replace(/…$/, "")}\u2026`;
1487
+ const text2 = !sandbox || sandbox.done ? activity ?? STARTING_SANDBOX : sandbox.headline;
1488
+ return `${text2.replace(/…$/, "")}\u2026`;
1116
1489
  }
1117
1490
  function sandboxSummary(sandbox) {
1118
1491
  const seconds = sandbox?.readySeconds ?? null;
@@ -1157,13 +1530,13 @@ function chatTurnsToItems(turns) {
1157
1530
  settleWake();
1158
1531
  continue;
1159
1532
  }
1160
- const text = sessionLogText(node.recordType, node.payload ?? {});
1161
- if (!text) continue;
1162
- const waking = text === SESSION_WAKING;
1533
+ const text2 = sessionLogText(node.recordType, node.payload ?? {});
1534
+ if (!text2) continue;
1535
+ const waking = text2 === SESSION_WAKING;
1163
1536
  items.push({
1164
1537
  key: node.key,
1165
1538
  kind: "notice",
1166
- text,
1539
+ text: text2,
1167
1540
  spaceBefore: true,
1168
1541
  ...waking ? { loading: true } : {}
1169
1542
  });
@@ -1178,6 +1551,7 @@ function chatTurnsToItems(turns) {
1178
1551
  key: node.key,
1179
1552
  kind: "user",
1180
1553
  text: node.text,
1554
+ createdAt: turn.startedAt,
1181
1555
  spaceBefore: true,
1182
1556
  ...turn.isError ? { isError: true } : {}
1183
1557
  });
@@ -1191,6 +1565,7 @@ function chatTurnsToItems(turns) {
1191
1565
  key: node.key,
1192
1566
  kind: "assistant",
1193
1567
  text: node.text,
1568
+ createdAt: node.createdAt,
1194
1569
  spaceBefore: true
1195
1570
  });
1196
1571
  } else if (node.kind === "thinking") {
@@ -1245,14 +1620,19 @@ function isToolActivity(item) {
1245
1620
  return item.kind === "tool" || item.kind === "tool_result" || isToolFold(item);
1246
1621
  }
1247
1622
  function layOutItems(items) {
1248
- return items.map((item) => ({ item, nested: false, attach: false }));
1623
+ const isActivity = (item) => item.kind === "thinking" || isToolActivity(item);
1624
+ return items.map((item, index) => ({
1625
+ item,
1626
+ nested: false,
1627
+ attach: index > 0 && isActivity(item) && isActivity(items[index - 1])
1628
+ }));
1249
1629
  }
1250
1630
  function foldGutter(nested) {
1251
1631
  return nested ? BRANCH_GLYPH : "\u25CF";
1252
1632
  }
1253
- function tailLines(text, maxLines) {
1254
- const lines = text.split("\n");
1255
- if (lines.length <= maxLines) return { body: text, hidden: 0 };
1633
+ function tailLines(text2, maxLines) {
1634
+ const lines = text2.split("\n");
1635
+ if (lines.length <= maxLines) return { body: text2, hidden: 0 };
1256
1636
  return {
1257
1637
  body: lines.slice(lines.length - maxLines).join("\n"),
1258
1638
  hidden: lines.length - maxLines
@@ -1277,24 +1657,44 @@ function toolRunMembers(items) {
1277
1657
  function splitLedger(turns) {
1278
1658
  const preamble = [];
1279
1659
  const entries = [];
1660
+ const entryByTurn = /* @__PURE__ */ new Map();
1661
+ const entryByMessage = /* @__PURE__ */ new Map();
1280
1662
  for (const turn of turns) {
1281
1663
  if (turn.role === "user") {
1282
1664
  const node = turn.nodes.find((n) => n.kind === "user");
1283
- entries.push({
1665
+ const entry = {
1284
1666
  key: turn.key,
1285
1667
  prompt: node?.text ?? "",
1668
+ createdAt: turn.startedAt,
1286
1669
  cancelled: turn.isError,
1287
- turns: []
1288
- });
1670
+ turns: [],
1671
+ ...turn.progress ? { progress: turn.progress } : {}
1672
+ };
1673
+ entries.push(entry);
1674
+ entryByTurn.set(turn, entry);
1675
+ if (turn.messageId) entryByMessage.set(turn.messageId, entry);
1676
+ }
1677
+ }
1678
+ let last;
1679
+ for (const turn of turns) {
1680
+ if (turn.role === "user") {
1681
+ last = entryByTurn.get(turn);
1289
1682
  continue;
1290
1683
  }
1291
- const last = entries[entries.length - 1];
1292
- if (last) last.turns.push(turn);
1684
+ const owner = turn.messageId ? entryByMessage.get(turn.messageId) : last;
1685
+ if (owner) owner.turns.push(turn);
1293
1686
  else preamble.push(turn);
1294
1687
  }
1295
1688
  return { preamble, entries };
1296
1689
  }
1297
1690
  function entrySeconds(entry, now) {
1691
+ if (entry.progress?.turns.length) {
1692
+ return entry.progress.turns.reduce((ms2, turn) => {
1693
+ if (turn.startedAt == null) return ms2;
1694
+ const end = turn.completedAt == null ? now : Date.parse(turn.completedAt);
1695
+ return ms2 + Math.max(0, end - Date.parse(turn.startedAt));
1696
+ }, 0) / 1e3;
1697
+ }
1298
1698
  let ms = 0;
1299
1699
  for (const turn of entry.turns) {
1300
1700
  if (turn.role !== "assistant" || turn.startedAt == null) continue;
@@ -1306,12 +1706,15 @@ function entrySeconds(entry, now) {
1306
1706
  }
1307
1707
  return ms / 1e3;
1308
1708
  }
1309
- function foldToolTurn(raw, live, members) {
1709
+ function isWorkFold(item) {
1710
+ return item.key.startsWith("work:");
1711
+ }
1712
+ function foldToolTurn(raw, members) {
1310
1713
  const items = [];
1311
1714
  let run = [];
1312
1715
  const flush = () => {
1313
1716
  if (run.length === 0) return;
1314
- const fold = foldRun(run, live && pendingToolCalls(run).length > 0);
1717
+ const fold = foldRun(run, false);
1315
1718
  members.set(fold.key, run);
1316
1719
  items.push(fold);
1317
1720
  run = [];
@@ -1326,70 +1729,224 @@ function foldToolTurn(raw, live, members) {
1326
1729
  flush();
1327
1730
  return items;
1328
1731
  }
1732
+ function finalReplyKeys(entry, live) {
1733
+ const keys = /* @__PURE__ */ new Set();
1734
+ for (const turn of entry.turns) {
1735
+ if (turn.role !== "assistant") continue;
1736
+ for (const node of turn.nodes) {
1737
+ if (node.kind === "assistant" && node.phase === "final_answer") {
1738
+ keys.add(node.key);
1739
+ }
1740
+ }
1741
+ if (entry.cancelled || turn.isError) continue;
1742
+ if (live && turn.completedAt == null && turn.durationMs == null) continue;
1743
+ for (let i = turn.nodes.length - 1; i >= 0; i--) {
1744
+ const node = turn.nodes[i];
1745
+ if (node.kind !== "assistant" || node.phase === "commentary") break;
1746
+ keys.add(node.key);
1747
+ const previous = turn.nodes[i - 1];
1748
+ if (node.createdAt == null || previous?.kind !== "assistant" || previous.createdAt !== node.createdAt)
1749
+ break;
1750
+ }
1751
+ }
1752
+ return keys;
1753
+ }
1754
+ function reasoningSummaryTitle(summary) {
1755
+ const line = summary.trim().split(/\r?\n/, 1)[0] ?? "";
1756
+ const title = line.replace(/^#{1,6}\s+/, "").replace(/^[*_`]+|[*_`]+$/g, "").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").trim();
1757
+ return title.length > 120 ? `${title.slice(0, 119).trimEnd()}\u2026` : title;
1758
+ }
1329
1759
  function entryBody(entry, {
1330
1760
  live,
1331
1761
  now,
1332
- liveText = ""
1762
+ liveText = "",
1763
+ liveSummary = ""
1333
1764
  }) {
1765
+ live = live && (entry.progress === void 0 || entry.progress.phase !== null);
1334
1766
  const raw = chatTurnsToItems(entry.turns);
1767
+ const replies = finalReplyKeys(entry, live);
1335
1768
  const members = /* @__PURE__ */ new Map();
1336
- const toolRan = raw.some((i) => i.kind === "tool");
1337
- let items;
1338
- if (toolRan) {
1339
- items = foldToolTurn(raw, live, members);
1340
- } else {
1341
- const thoughts = raw.filter((i) => i.kind === "thinking");
1342
- items = raw.filter((i) => i.kind !== "thinking");
1343
- const key = `grp:thought:${entry.key}`;
1344
- if (thoughts.length) members.set(key, thoughts);
1345
- if (live && liveText === "") {
1346
- items.push({ key, kind: "notice", text: "Thinking", spaceBefore: true });
1347
- } else if (!live && thoughts.length) {
1348
- items.push({
1349
- key,
1350
- kind: "notice",
1351
- text: `Thought for ${humanDuration(entrySeconds(entry, now))}`,
1769
+ const items = [];
1770
+ const work = [];
1771
+ const wakeKeys = new Set(
1772
+ entry.turns.flatMap(
1773
+ (turn) => turn.nodes.flatMap(
1774
+ (node) => node.kind === "lifecycle" && (node.recordType === "session_resumed" || node.recordType === "session_starting" && typeof node.payload?.wake_index === "number" && node.payload.wake_index > 0) ? [node.key] : []
1775
+ )
1776
+ )
1777
+ );
1778
+ const phase = entry.progress?.phase;
1779
+ const seconds = entry.progress && !entry.progress.turns.some((turn) => turn.startedAt != null) ? null : entrySeconds(entry, now);
1780
+ const key = `work:${entry.key}`;
1781
+ const header = {
1782
+ key,
1783
+ kind: "summary",
1784
+ text: live ? phase === "waking" ? "Waking session" : phase === "starting" ? "Starting agent" : phase === "queued" ? "Queued" : reasoningSummaryTitle(liveSummary) || "Working" : seconds == null ? "Session startup" : `Worked for ${humanDuration(seconds)}`,
1785
+ spaceBefore: true,
1786
+ ...entry.cancelled || entry.turns.some((turn) => turn.isError) ? { isError: true } : {},
1787
+ ...live ? { loading: true } : {}
1788
+ };
1789
+ if (live && (phase === void 0 || phase === "working")) {
1790
+ const visible = raw.filter((item) => !wakeKeys.has(item.key)).map(({ loading: _, ...item }) => item);
1791
+ if (liveText !== "") {
1792
+ visible.push({
1793
+ key: `live:${entry.key}`,
1794
+ kind: "assistant",
1795
+ text: liveText,
1352
1796
  spaceBefore: true
1353
1797
  });
1354
1798
  }
1799
+ return {
1800
+ items: [header, ...foldToolTurn(visible, members)],
1801
+ members,
1802
+ seconds
1803
+ };
1804
+ }
1805
+ const addWork = (item) => {
1806
+ if (work.length === 0) items.push(header);
1807
+ work.push(item);
1808
+ };
1809
+ for (const item of raw) {
1810
+ const { loading: _, ...settled } = item;
1811
+ if (!wakeKeys.has(item.key) && (replies.has(item.key) || !["assistant", "thinking", "tool", "tool_result"].includes(item.kind))) {
1812
+ items.push(settled);
1813
+ } else {
1814
+ addWork(settled);
1815
+ }
1355
1816
  }
1356
1817
  if (live && liveText !== "") {
1357
- items.push({
1818
+ addWork({
1358
1819
  key: `live:${entry.key}`,
1359
1820
  kind: "assistant",
1360
1821
  text: liveText,
1361
1822
  spaceBefore: true
1362
1823
  });
1363
1824
  }
1364
- const chosen = live ? liveRow(items, members) : -1;
1365
- items = items.map((item, i) => {
1366
- const { loading: _, ...rest } = item;
1367
- return i === chosen ? { ...rest, loading: true } : rest;
1368
- });
1369
- return { items, members };
1825
+ if (work.length > 0) {
1826
+ members.set(key, foldToolTurn(work, members));
1827
+ } else if (live) {
1828
+ items.unshift(header);
1829
+ }
1830
+ return { items, members, seconds };
1370
1831
  }
1371
- function liveRow(items, members) {
1372
- if (items.length === 0) return -1;
1373
- const running = items.findIndex((item) => {
1374
- const run = members.get(item.key);
1375
- return run != null && pendingToolCalls(run).length > 0;
1376
- });
1377
- if (running >= 0) return running;
1378
- return items.length - 1;
1832
+
1833
+ // src/store/activity.ts
1834
+ var text = (value) => typeof value === "string" ? value : "";
1835
+ function toolActivities(item) {
1836
+ const input = item.tool?.input ?? {};
1837
+ const name = item.tool?.name ?? item.text;
1838
+ const actions = input.commandActions;
1839
+ if (Array.isArray(actions) && actions.length) {
1840
+ const parsed = actions.flatMap((value) => {
1841
+ if (!value || typeof value !== "object") return [];
1842
+ const action = value;
1843
+ switch (action.type) {
1844
+ case "read":
1845
+ return [
1846
+ {
1847
+ kind: "read",
1848
+ label: "Read",
1849
+ detail: text(action.path) || text(action.name)
1850
+ }
1851
+ ];
1852
+ case "search": {
1853
+ const query = text(action.query);
1854
+ const path2 = text(action.path);
1855
+ return [
1856
+ {
1857
+ kind: "search",
1858
+ label: "Searched",
1859
+ detail: (query ? `for ${query}` : "code") + (path2 ? ` in ${path2}` : "")
1860
+ }
1861
+ ];
1862
+ }
1863
+ case "listFiles":
1864
+ return [
1865
+ {
1866
+ kind: "list",
1867
+ label: "Listed",
1868
+ detail: text(action.path) || "files"
1869
+ }
1870
+ ];
1871
+ default:
1872
+ return [
1873
+ {
1874
+ kind: "command",
1875
+ label: "Ran",
1876
+ detail: text(action.command) || text(input.command)
1877
+ }
1878
+ ];
1879
+ }
1880
+ });
1881
+ if (parsed.length) return parsed;
1882
+ }
1883
+ const path = text(input.file_path) || text(input.path) || text(input.notebook_path);
1884
+ switch (name.toLowerCase()) {
1885
+ case "read":
1886
+ return [{ kind: "read", label: "Read", detail: path }];
1887
+ case "edit":
1888
+ case "write":
1889
+ case "multiedit":
1890
+ if (Array.isArray(input.changes)) {
1891
+ return input.changes.map((change) => ({
1892
+ kind: "edit",
1893
+ label: "Edited",
1894
+ detail: text(change?.path)
1895
+ }));
1896
+ }
1897
+ return [
1898
+ {
1899
+ kind: "edit",
1900
+ label: name.toLowerCase() === "write" ? "Wrote" : "Edited",
1901
+ detail: path
1902
+ }
1903
+ ];
1904
+ case "grep":
1905
+ case "glob":
1906
+ return [
1907
+ {
1908
+ kind: "search",
1909
+ label: "Searched",
1910
+ detail: `for ${text(input.pattern)}` + (path ? ` in ${path}` : "")
1911
+ }
1912
+ ];
1913
+ case "bash":
1914
+ return [{ kind: "command", label: "Ran", detail: text(input.command) }];
1915
+ case "websearch":
1916
+ return [
1917
+ { kind: "web", label: "Searched the web", detail: text(input.query) }
1918
+ ];
1919
+ case "webfetch":
1920
+ return [{ kind: "web", label: "Opened", detail: text(input.url) }];
1921
+ default:
1922
+ return [
1923
+ {
1924
+ kind: "tool",
1925
+ label: name.replace(/^mcp__/, "").replace("__", ":"),
1926
+ detail: item.detail ?? ""
1927
+ }
1928
+ ];
1929
+ }
1930
+ }
1931
+ function activitySummary(activities) {
1932
+ const labels = {
1933
+ read: "read files",
1934
+ search: "searched code",
1935
+ list: "listed files",
1936
+ edit: "edited files",
1937
+ command: "ran commands",
1938
+ web: "browsed the web",
1939
+ tool: "used tools"
1940
+ };
1941
+ const summary = [
1942
+ ...new Set(activities.map((activity) => labels[activity.kind]))
1943
+ ].join(", ");
1944
+ return summary ? summary[0].toUpperCase() + summary.slice(1) : "Activity";
1379
1945
  }
1380
1946
 
1381
1947
  // src/store/index.ts
1382
- var TERMINAL_SESSION_STATUSES = /* @__PURE__ */ new Set([
1383
- "completed",
1384
- "error",
1385
- "cancelled",
1386
- "stopped"
1387
- ]);
1388
1948
  function isConversationOver(session) {
1389
- if (session.session_state != null) {
1390
- return session.session_state === "closed";
1391
- }
1392
- return TERMINAL_SESSION_STATUSES.has(session.status);
1949
+ return session.lifecycle.conversation === "closed";
1393
1950
  }
1394
1951
  function seedTranscriptStore(store, seed) {
1395
1952
  store.ingest({
@@ -1410,6 +1967,7 @@ var EMPTY_SNAPSHOT = {
1410
1967
  acknowledgedMessageIds: /* @__PURE__ */ new Set(),
1411
1968
  historyTruncated: false,
1412
1969
  liveText: "",
1970
+ liveSummary: "",
1413
1971
  liveOutputTokens: null,
1414
1972
  lastEventAt: null,
1415
1973
  conversationOver: false
@@ -1433,7 +1991,10 @@ var SessionTranscriptStore = class {
1433
1991
  cursorSeq = 0;
1434
1992
  // Derived-turn cache, keyed on the records array identity.
1435
1993
  turnsFor = null;
1994
+ turnsSession = null;
1436
1995
  turnsCache = [];
1996
+ summaryTurnId = null;
1997
+ summaryExecutionId = null;
1437
1998
  get cursor() {
1438
1999
  return this.cursorSeq;
1439
2000
  }
@@ -1448,9 +2009,10 @@ var SessionTranscriptStore = class {
1448
2009
  // records_append batch) — what a chat-style transcript renders.
1449
2010
  chatTurns = () => {
1450
2011
  const records = this.snapshot.records;
1451
- if (this.turnsFor !== records) {
1452
- this.turnsCache = groupRecordsToChatTurns(records);
2012
+ if (this.turnsFor !== records || this.turnsSession !== this.snapshot.session) {
2013
+ this.turnsCache = groupRecordsToChatTurns(records, this.snapshot.session);
1453
2014
  this.turnsFor = records;
2015
+ this.turnsSession = this.snapshot.session;
1454
2016
  }
1455
2017
  return this.turnsCache;
1456
2018
  };
@@ -1459,6 +2021,7 @@ var SessionTranscriptStore = class {
1459
2021
  // Fold one lifecycle record into the inbox projection; returns whether the
1460
2022
  // pending view changed. Unknown fields/types are ignored (§3.6).
1461
2023
  applyMessageRecord = (record) => {
2024
+ if (record.kind !== "platform") return false;
1462
2025
  const payload = record.payload;
1463
2026
  const messageId = typeof payload.message_id === "string" ? payload.message_id : null;
1464
2027
  switch (record.record_type) {
@@ -1516,6 +2079,7 @@ var SessionTranscriptStore = class {
1516
2079
  const frame = rawFrame;
1517
2080
  switch (frame.type) {
1518
2081
  case "snapshot": {
2082
+ next.liveSummary = "";
1519
2083
  next.session = frame.session;
1520
2084
  for (const message of frame.messages) {
1521
2085
  this.inbox.set(message.id, message);
@@ -1540,6 +2104,8 @@ var SessionTranscriptStore = class {
1540
2104
  acknowledgedNew = true;
1541
2105
  }
1542
2106
  if (record.source === "lifecycle") {
2107
+ if (record.record_type === "turn_started" && (record.turn_id !== this.summaryTurnId || record.session_execution_id !== this.summaryExecutionId) || (record.record_type === "turn_completed" || record.record_type === "turn_failed") && record.turn_id === this.summaryTurnId && record.session_execution_id === this.summaryExecutionId)
2108
+ next.liveSummary = "";
1543
2109
  inboxChanged = this.applyMessageRecord(record) || inboxChanged;
1544
2110
  if (record.record_type === "message_received" || record.record_type === "message_delivered") {
1545
2111
  const payload = record.payload;
@@ -1561,17 +2127,35 @@ var SessionTranscriptStore = class {
1561
2127
  case "session": {
1562
2128
  next.session = frame.session;
1563
2129
  next.conversationOver = isConversationOver(frame.session);
2130
+ if (next.conversationOver || frame.session.lifecycle.status === "idle") {
2131
+ next.liveSummary = "";
2132
+ }
1564
2133
  break;
1565
2134
  }
1566
2135
  case "delta": {
2136
+ if (frame.kind === "thinking") {
2137
+ if (next.conversationOver) break;
2138
+ this.summaryTurnId = frame.turn_id;
2139
+ this.summaryExecutionId = frame.session_execution_id;
2140
+ if (frame.text != null) next.liveSummary = frame.text;
2141
+ break;
2142
+ }
1567
2143
  if (frame.kind !== "text") break;
1568
- if (frame.text != null) next.liveText = prev.liveText + frame.text;
2144
+ if (frame.text != null) {
2145
+ next.liveText = prev.liveText + frame.text;
2146
+ next.liveSummary = "";
2147
+ }
1569
2148
  if (frame.output_tokens != null)
1570
2149
  next.liveOutputTokens = frame.output_tokens;
1571
2150
  break;
1572
2151
  }
1573
2152
  case "done": {
1574
2153
  next.conversationOver = true;
2154
+ next.liveSummary = "";
2155
+ break;
2156
+ }
2157
+ case "error": {
2158
+ next.liveSummary = "";
1575
2159
  break;
1576
2160
  }
1577
2161
  default:
@@ -1587,10 +2171,12 @@ export {
1587
2171
  SESSION_WAKING,
1588
2172
  SessionTranscriptStore,
1589
2173
  USER_BAR,
2174
+ activitySummary,
1590
2175
  awaitingAgentPhase,
1591
2176
  cacheTierLabel,
1592
2177
  chatTurnsToItems,
1593
2178
  clampLines,
2179
+ claudePayload,
1594
2180
  codexChangedPaths,
1595
2181
  codexEventToItems,
1596
2182
  codexMcpToolName,
@@ -1614,11 +2200,13 @@ export {
1614
2200
  isFoldable,
1615
2201
  isToolActivity,
1616
2202
  isToolFold,
2203
+ isWorkFold,
1617
2204
  lastLines,
1618
2205
  layOutItems,
1619
2206
  lifecycleText,
1620
2207
  oneLine,
1621
2208
  pendingToolCalls,
2209
+ reasoningSummaryTitle,
1622
2210
  recordSlice,
1623
2211
  recordToItems,
1624
2212
  resultCostUsd,
@@ -1635,6 +2223,7 @@ export {
1635
2223
  statusActivityText,
1636
2224
  summarizeToolInput,
1637
2225
  tailLines,
2226
+ toolActivities,
1638
2227
  toolResultText,
1639
2228
  toolRunMembers
1640
2229
  };