@ellipsis-dev/sdk 0.28.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,6 @@
1
1
  import {
2
2
  SESSION_STREAM_PROTOCOL_VERSION
3
- } from "../chunk-YJC53TA7.js";
3
+ } from "../chunk-K252XHLI.js";
4
4
 
5
5
  // src/store/claude.ts
6
6
  function claudePayload(record) {
@@ -62,8 +62,9 @@ function codexAppServerContent(event) {
62
62
  const item = event.params.item;
63
63
  switch (item.type) {
64
64
  case "agentMessage":
65
+ return { kind: "assistant", text: item.text, phase: item.phase };
65
66
  case "plan":
66
- return { kind: "assistant", text: item.text };
67
+ return { kind: "assistant", text: item.text, phase: "commentary" };
67
68
  case "reasoning":
68
69
  return {
69
70
  kind: "thinking",
@@ -75,7 +76,7 @@ function codexAppServerContent(event) {
75
76
  return {
76
77
  kind: "tool",
77
78
  name: "Bash",
78
- input: { command: item.command },
79
+ input: { command: item.command, commandActions: item.commandActions },
79
80
  summary: item.command,
80
81
  result: item.aggregatedOutput?.trim() || "(no output)",
81
82
  isError: item.status === "failed" || item.status === "declined" || typeof item.exitCode === "number" && item.exitCode !== 0
@@ -103,6 +104,117 @@ function codexAppServerContent(event) {
103
104
  }
104
105
  }
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
+ }
217
+
106
218
  // src/store/lifecycle.ts
107
219
  function sandboxOutputStep(payload) {
108
220
  if (typeof payload.step === "string" && payload.step) return payload.step;
@@ -221,8 +333,8 @@ function lifecycleText(recordType, payload) {
221
333
  }
222
334
 
223
335
  // src/store/transcript.ts
224
- function oneLine(text, max) {
225
- const collapsed = text.replace(/\s+/g, " ").trim();
336
+ function oneLine(text2, max) {
337
+ const collapsed = text2.replace(/\s+/g, " ").trim();
226
338
  return collapsed.length <= max ? collapsed : `${collapsed.slice(0, max - 3)}...`;
227
339
  }
228
340
  function summarizeToolInput(name, input) {
@@ -377,15 +489,15 @@ function codexEventToItems(event, keyBase) {
377
489
  const item = event.item;
378
490
  switch (item.type) {
379
491
  case "agent_message": {
380
- const text = (item.text ?? "").trim();
381
- if (text) push({ kind: "assistant", text, spaceBefore: true });
492
+ const text2 = (item.text ?? "").trim();
493
+ if (text2) push({ kind: "assistant", text: text2, spaceBefore: true });
382
494
  break;
383
495
  }
384
496
  case "reasoning": {
385
497
  const r = item;
386
- const text = (r.text ?? r.summary ?? "").trim();
387
- if (text)
388
- 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 });
389
501
  break;
390
502
  }
391
503
  case "command_execution": {
@@ -517,8 +629,8 @@ function recordToItems(record, keyBase, options = {}) {
517
629
  ] : [];
518
630
  }
519
631
  case "platform": {
520
- const text = lifecycleText(record.record_type, record.payload);
521
- return text ? [{ key: keyBase, kind: "notice", text, spaceBefore: true }] : [];
632
+ const text2 = lifecycleText(record.record_type, record.payload);
633
+ return text2 ? [{ key: keyBase, kind: "notice", text: text2, spaceBefore: true }] : [];
522
634
  }
523
635
  default:
524
636
  return [];
@@ -589,9 +701,9 @@ function collapseToolRuns(items, { working = false } = {}) {
589
701
  flush(true);
590
702
  return out;
591
703
  }
592
- function clampLines(text, maxLines) {
593
- const lines = text.split("\n");
594
- 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 };
595
707
  return {
596
708
  body: lines.slice(0, maxLines).join("\n"),
597
709
  more: lines.length - maxLines
@@ -633,20 +745,29 @@ var SANDBOX_RECORD_TYPES = /* @__PURE__ */ new Set([
633
745
  "sandbox_output",
634
746
  "sandbox_ready"
635
747
  ]);
636
- function groupRecordsToChatTurns(records) {
748
+ function groupRecordsToChatTurns(records, session) {
637
749
  const turns = [];
638
750
  const toolNodeByUseId = /* @__PURE__ */ new Map();
639
751
  const userTurnByMessageId = /* @__PURE__ */ new Map();
640
- 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;
641
756
  let seq = 0;
642
757
  const nextKey = (base) => `${base}:${seq++}`;
643
758
  let openIdx = -1;
644
759
  let initCount = 0;
645
760
  let pendingResume = false;
646
- 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
+ }
647
766
  if (openIdx < 0) {
648
767
  turns.push({
649
768
  key: nextKey(record.id),
769
+ messageId,
770
+ turnId: record.turn_id ?? void 0,
650
771
  role: "assistant",
651
772
  nodes: [],
652
773
  startedAt: record.created_at,
@@ -672,6 +793,7 @@ function groupRecordsToChatTurns(records) {
672
793
  resumed: false,
673
794
  isError: false,
674
795
  key: `msg:${messageId}`,
796
+ messageId,
675
797
  role: "user",
676
798
  nodes: [{ key: `msg:${messageId}`, kind: "user", text: body.trim() }],
677
799
  startedAt: record.created_at,
@@ -685,19 +807,24 @@ function groupRecordsToChatTurns(records) {
685
807
  continue;
686
808
  }
687
809
  if (record.record_type === "message_delivered" && messageId) {
688
- if (typeof record.payload.turn_id === "string")
689
- 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);
690
819
  continue;
691
820
  }
692
821
  if (record.record_type === "turn_failed") {
693
- const turnId = typeof record.payload.turn_id === "string" ? record.payload.turn_id : null;
694
- const owner = turnId ? messageIdByTurnId.get(turnId) : void 0;
695
- const userTurn = owner ? userTurnByMessageId.get(owner) : void 0;
696
- if (userTurn) userTurn.isError = true;
822
+ if (typeof record.payload.turn_id === "string")
823
+ failedTurnIds.add(record.payload.turn_id);
697
824
  continue;
698
825
  }
699
- const text = lifecycleText(record.record_type, record.payload);
700
- if (!text) continue;
826
+ const text2 = lifecycleText(record.record_type, record.payload);
827
+ if (!text2) continue;
701
828
  const isSandbox = SANDBOX_RECORD_TYPES.has(record.record_type);
702
829
  const isSandboxOutput = record.record_type === "sandbox_output";
703
830
  const step = isSandboxOutput ? sandboxOutputStep(record.payload) : void 0;
@@ -718,13 +845,13 @@ function groupRecordsToChatTurns(records) {
718
845
  if (foldTarget) {
719
846
  const last = foldTarget.nodes[foldTarget.nodes.length - 1];
720
847
  if (isSandboxOutput && last && last.kind === "lifecycle" && last.recordType === "sandbox_output" && last.step === step) {
721
- last.text = text;
848
+ last.text = text2;
722
849
  last.lines = [...last.lines ?? [], ...lines ?? []];
723
850
  } else {
724
851
  foldTarget.nodes.push({
725
852
  key: nextKey(record.id),
726
853
  kind: "lifecycle",
727
- text,
854
+ text: text2,
728
855
  recordType: record.record_type,
729
856
  step,
730
857
  lines,
@@ -741,7 +868,7 @@ function groupRecordsToChatTurns(records) {
741
868
  {
742
869
  key: nextKey(record.id),
743
870
  kind: "lifecycle",
744
- text,
871
+ text: text2,
745
872
  recordType: record.record_type,
746
873
  step,
747
874
  lines,
@@ -758,9 +885,14 @@ function groupRecordsToChatTurns(records) {
758
885
  });
759
886
  continue;
760
887
  }
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
+ }
761
893
  if (record.kind === "codex") {
762
894
  codexEventIntoTurns(record, record.payload, {
763
- openAgentTurn,
895
+ openSessionTurn,
764
896
  closeTurn: (completedAt, isError) => {
765
897
  if (openIdx >= 0) {
766
898
  turns[openIdx].completedAt = completedAt;
@@ -782,7 +914,7 @@ function groupRecordsToChatTurns(records) {
782
914
  openIdx = -1;
783
915
  }
784
916
  } else if (content.kind === "tool") {
785
- openAgentTurn(record).nodes.push({
917
+ openSessionTurn(record).nodes.push({
786
918
  key: nextKey(record.id),
787
919
  kind: "tool",
788
920
  name: content.name,
@@ -794,12 +926,14 @@ function groupRecordsToChatTurns(records) {
794
926
  completedAt: record.created_at
795
927
  });
796
928
  } else if (content.text.trim()) {
797
- const turn2 = openAgentTurn(record);
929
+ const turn2 = openSessionTurn(record);
798
930
  if (content.kind === "error") turn2.isError = true;
799
931
  turn2.nodes.push({
800
932
  key: nextKey(record.id),
801
933
  kind: content.kind === "error" ? "assistant" : content.kind,
802
- text: content.text.trim()
934
+ text: content.text.trim(),
935
+ createdAt: record.created_at,
936
+ ...content.kind === "assistant" && content.phase != null ? { phase: content.phase } : {}
803
937
  });
804
938
  }
805
939
  continue;
@@ -856,6 +990,9 @@ function groupRecordsToChatTurns(records) {
856
990
  const node = toolNodeByUseId.get(useId);
857
991
  if (node) {
858
992
  node.result = body;
993
+ if (record.kind === "claude_code" || record.kind === "claude_sdk") {
994
+ node.resultDetails = record.payload.tool_use_result;
995
+ }
859
996
  node.isError = !!block.is_error;
860
997
  node.completedAt = record.created_at;
861
998
  } else {
@@ -870,14 +1007,14 @@ function groupRecordsToChatTurns(records) {
870
1007
  startedAt: null,
871
1008
  completedAt: record.created_at
872
1009
  };
873
- openAgentTurn(record).nodes.push(orphan);
1010
+ openSessionTurn(record).nodes.push(orphan);
874
1011
  toolNodeByUseId.set(useId, orphan);
875
1012
  }
876
1013
  }
877
1014
  continue;
878
1015
  }
879
1016
  if (data.kind !== "assistant") continue;
880
- const turn = openAgentTurn(record);
1017
+ const turn = openSessionTurn(record);
881
1018
  if (data.usage && typeof data.usage.output_tokens === "number") {
882
1019
  turn.tokens = (turn.tokens ?? 0) + data.usage.output_tokens;
883
1020
  }
@@ -906,11 +1043,60 @@ function groupRecordsToChatTurns(records) {
906
1043
  turn.nodes.push({
907
1044
  key: nextKey(record.id),
908
1045
  kind: "assistant",
909
- text: block.text.trim()
1046
+ text: block.text.trim(),
1047
+ createdAt: record.created_at
910
1048
  });
911
1049
  }
912
1050
  }
913
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
+ }
914
1100
  return turns;
915
1101
  }
916
1102
  function codexEventIntoTurns(record, event, ops) {
@@ -919,36 +1105,38 @@ function codexEventIntoTurns(record, event, ops) {
919
1105
  return;
920
1106
  }
921
1107
  if (event.type === "error") {
922
- const turn2 = ops.openAgentTurn(record);
1108
+ const turn2 = ops.openSessionTurn(record);
923
1109
  turn2.nodes.push({
924
1110
  key: ops.nextKey(record.id),
925
1111
  kind: "assistant",
926
- text: event.message?.trim() || "error"
1112
+ text: event.message?.trim() || "error",
1113
+ createdAt: record.created_at
927
1114
  });
928
1115
  return;
929
1116
  }
930
1117
  if (event.type !== "item.completed") return;
931
1118
  const item = event.item;
932
- const turn = () => ops.openAgentTurn(record);
1119
+ const turn = () => ops.openSessionTurn(record);
933
1120
  switch (item.type) {
934
1121
  case "agent_message": {
935
- const text = (item.text ?? "").trim();
936
- if (text)
1122
+ const text2 = (item.text ?? "").trim();
1123
+ if (text2)
937
1124
  turn().nodes.push({
938
1125
  key: ops.nextKey(record.id),
939
1126
  kind: "assistant",
940
- text
1127
+ text: text2,
1128
+ createdAt: record.created_at
941
1129
  });
942
1130
  break;
943
1131
  }
944
1132
  case "reasoning": {
945
1133
  const r = item;
946
- const text = (r.text ?? r.summary ?? "").trim();
947
- if (text)
1134
+ const text2 = (r.text ?? r.summary ?? "").trim();
1135
+ if (text2)
948
1136
  turn().nodes.push({
949
1137
  key: ops.nextKey(record.id),
950
1138
  kind: "thinking",
951
- text
1139
+ text: text2
952
1140
  });
953
1141
  break;
954
1142
  }
@@ -1164,8 +1352,8 @@ function deriveSandboxState(records, minFeedSeq) {
1164
1352
  let configCommitSha = null;
1165
1353
  let log = [];
1166
1354
  let open = /* @__PURE__ */ new Map();
1167
- const push = (record, kind, text) => {
1168
- 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 };
1169
1357
  log.push(entry);
1170
1358
  return entry;
1171
1359
  };
@@ -1190,8 +1378,8 @@ function deriveSandboxState(records, minFeedSeq) {
1190
1378
  case "session_starting":
1191
1379
  case "session_retrying": {
1192
1380
  seen = true;
1193
- const text = lifecycleText(record.record_type, p);
1194
- 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;
1195
1383
  done = false;
1196
1384
  readySeconds = null;
1197
1385
  reset();
@@ -1220,7 +1408,7 @@ function deriveSandboxState(records, minFeedSeq) {
1220
1408
  const dur = msLabel(p.duration_ms);
1221
1409
  const failed = p.status === "failed";
1222
1410
  const base = failed ? `${label} failed` : label;
1223
- const text = [
1411
+ const text2 = [
1224
1412
  base,
1225
1413
  ...tier ? [tier] : [],
1226
1414
  ...dur ? [dur] : []
@@ -1228,10 +1416,10 @@ function deriveSandboxState(records, minFeedSeq) {
1228
1416
  const line = open.get(key);
1229
1417
  if (line) {
1230
1418
  line.kind = failed ? "failed" : "done";
1231
- line.text = text;
1419
+ line.text = text2;
1232
1420
  open.delete(key);
1233
1421
  } else {
1234
- push(record, failed ? "failed" : "done", text);
1422
+ push(record, failed ? "failed" : "done", text2);
1235
1423
  }
1236
1424
  } else if (!open.has(key)) {
1237
1425
  open.set(key, push(record, "step", `${label}\u2026`));
@@ -1296,8 +1484,8 @@ function startupSettled(sandbox, status) {
1296
1484
  }
1297
1485
  function startupHeadline(sandbox, status) {
1298
1486
  const activity = statusActivityText(status);
1299
- const text = !sandbox || sandbox.done ? activity ?? STARTING_SANDBOX : sandbox.headline;
1300
- return `${text.replace(/…$/, "")}\u2026`;
1487
+ const text2 = !sandbox || sandbox.done ? activity ?? STARTING_SANDBOX : sandbox.headline;
1488
+ return `${text2.replace(/…$/, "")}\u2026`;
1301
1489
  }
1302
1490
  function sandboxSummary(sandbox) {
1303
1491
  const seconds = sandbox?.readySeconds ?? null;
@@ -1342,13 +1530,13 @@ function chatTurnsToItems(turns) {
1342
1530
  settleWake();
1343
1531
  continue;
1344
1532
  }
1345
- const text = sessionLogText(node.recordType, node.payload ?? {});
1346
- if (!text) continue;
1347
- const waking = text === SESSION_WAKING;
1533
+ const text2 = sessionLogText(node.recordType, node.payload ?? {});
1534
+ if (!text2) continue;
1535
+ const waking = text2 === SESSION_WAKING;
1348
1536
  items.push({
1349
1537
  key: node.key,
1350
1538
  kind: "notice",
1351
- text,
1539
+ text: text2,
1352
1540
  spaceBefore: true,
1353
1541
  ...waking ? { loading: true } : {}
1354
1542
  });
@@ -1363,6 +1551,7 @@ function chatTurnsToItems(turns) {
1363
1551
  key: node.key,
1364
1552
  kind: "user",
1365
1553
  text: node.text,
1554
+ createdAt: turn.startedAt,
1366
1555
  spaceBefore: true,
1367
1556
  ...turn.isError ? { isError: true } : {}
1368
1557
  });
@@ -1376,6 +1565,7 @@ function chatTurnsToItems(turns) {
1376
1565
  key: node.key,
1377
1566
  kind: "assistant",
1378
1567
  text: node.text,
1568
+ createdAt: node.createdAt,
1379
1569
  spaceBefore: true
1380
1570
  });
1381
1571
  } else if (node.kind === "thinking") {
@@ -1430,14 +1620,19 @@ function isToolActivity(item) {
1430
1620
  return item.kind === "tool" || item.kind === "tool_result" || isToolFold(item);
1431
1621
  }
1432
1622
  function layOutItems(items) {
1433
- 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
+ }));
1434
1629
  }
1435
1630
  function foldGutter(nested) {
1436
1631
  return nested ? BRANCH_GLYPH : "\u25CF";
1437
1632
  }
1438
- function tailLines(text, maxLines) {
1439
- const lines = text.split("\n");
1440
- 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 };
1441
1636
  return {
1442
1637
  body: lines.slice(lines.length - maxLines).join("\n"),
1443
1638
  hidden: lines.length - maxLines
@@ -1462,24 +1657,44 @@ function toolRunMembers(items) {
1462
1657
  function splitLedger(turns) {
1463
1658
  const preamble = [];
1464
1659
  const entries = [];
1660
+ const entryByTurn = /* @__PURE__ */ new Map();
1661
+ const entryByMessage = /* @__PURE__ */ new Map();
1465
1662
  for (const turn of turns) {
1466
1663
  if (turn.role === "user") {
1467
1664
  const node = turn.nodes.find((n) => n.kind === "user");
1468
- entries.push({
1665
+ const entry = {
1469
1666
  key: turn.key,
1470
1667
  prompt: node?.text ?? "",
1668
+ createdAt: turn.startedAt,
1471
1669
  cancelled: turn.isError,
1472
- turns: []
1473
- });
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);
1474
1682
  continue;
1475
1683
  }
1476
- const last = entries[entries.length - 1];
1477
- if (last) last.turns.push(turn);
1684
+ const owner = turn.messageId ? entryByMessage.get(turn.messageId) : last;
1685
+ if (owner) owner.turns.push(turn);
1478
1686
  else preamble.push(turn);
1479
1687
  }
1480
1688
  return { preamble, entries };
1481
1689
  }
1482
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
+ }
1483
1698
  let ms = 0;
1484
1699
  for (const turn of entry.turns) {
1485
1700
  if (turn.role !== "assistant" || turn.startedAt == null) continue;
@@ -1491,12 +1706,15 @@ function entrySeconds(entry, now) {
1491
1706
  }
1492
1707
  return ms / 1e3;
1493
1708
  }
1494
- function foldToolTurn(raw, live, members) {
1709
+ function isWorkFold(item) {
1710
+ return item.key.startsWith("work:");
1711
+ }
1712
+ function foldToolTurn(raw, members) {
1495
1713
  const items = [];
1496
1714
  let run = [];
1497
1715
  const flush = () => {
1498
1716
  if (run.length === 0) return;
1499
- const fold = foldRun(run, live && pendingToolCalls(run).length > 0);
1717
+ const fold = foldRun(run, false);
1500
1718
  members.set(fold.key, run);
1501
1719
  items.push(fold);
1502
1720
  run = [];
@@ -1511,77 +1729,224 @@ function foldToolTurn(raw, live, members) {
1511
1729
  flush();
1512
1730
  return items;
1513
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
+ }
1514
1759
  function entryBody(entry, {
1515
1760
  live,
1516
1761
  now,
1517
- liveText = ""
1762
+ liveText = "",
1763
+ liveSummary = ""
1518
1764
  }) {
1765
+ live = live && (entry.progress === void 0 || entry.progress.phase !== null);
1519
1766
  const raw = chatTurnsToItems(entry.turns);
1767
+ const replies = finalReplyKeys(entry, live);
1520
1768
  const members = /* @__PURE__ */ new Map();
1521
- const toolRan = raw.some((i) => i.kind === "tool");
1522
- let items;
1523
- if (toolRan) {
1524
- items = foldToolTurn(raw, live, members);
1525
- } else {
1526
- const thoughts = raw.filter((i) => i.kind === "thinking");
1527
- items = raw.filter((i) => i.kind !== "thinking");
1528
- const key = `grp:thought:${entry.key}`;
1529
- if (thoughts.length) members.set(key, thoughts);
1530
- if (live && liveText === "") {
1531
- items.push({
1532
- key,
1533
- kind: "thinking",
1534
- text: "Thinking",
1535
- summary: true,
1536
- spaceBefore: true
1537
- });
1538
- } else if (!live && thoughts.length) {
1539
- items.push({
1540
- key,
1541
- kind: "thinking",
1542
- text: `Thought for ${humanDuration(entrySeconds(entry, now))}`,
1543
- summary: true,
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,
1544
1796
  spaceBefore: true
1545
1797
  });
1546
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
+ }
1547
1816
  }
1548
1817
  if (live && liveText !== "") {
1549
- items.push({
1818
+ addWork({
1550
1819
  key: `live:${entry.key}`,
1551
1820
  kind: "assistant",
1552
1821
  text: liveText,
1553
1822
  spaceBefore: true
1554
1823
  });
1555
1824
  }
1556
- const chosen = live ? liveRow(items, members) : -1;
1557
- items = items.map((item, i) => {
1558
- const { loading: _, ...rest } = item;
1559
- return i === chosen ? { ...rest, loading: true } : rest;
1560
- });
1561
- 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 };
1562
1831
  }
1563
- function liveRow(items, members) {
1564
- if (items.length === 0) return -1;
1565
- const running = items.findIndex((item) => {
1566
- const run = members.get(item.key);
1567
- return run != null && pendingToolCalls(run).length > 0;
1568
- });
1569
- if (running >= 0) return running;
1570
- 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";
1571
1945
  }
1572
1946
 
1573
1947
  // src/store/index.ts
1574
- var TERMINAL_SESSION_STATUSES = /* @__PURE__ */ new Set([
1575
- "completed",
1576
- "error",
1577
- "cancelled",
1578
- "stopped"
1579
- ]);
1580
1948
  function isConversationOver(session) {
1581
- if (session.session_state != null) {
1582
- return session.session_state === "closed";
1583
- }
1584
- return TERMINAL_SESSION_STATUSES.has(session.status);
1949
+ return session.lifecycle.conversation === "closed";
1585
1950
  }
1586
1951
  function seedTranscriptStore(store, seed) {
1587
1952
  store.ingest({
@@ -1602,6 +1967,7 @@ var EMPTY_SNAPSHOT = {
1602
1967
  acknowledgedMessageIds: /* @__PURE__ */ new Set(),
1603
1968
  historyTruncated: false,
1604
1969
  liveText: "",
1970
+ liveSummary: "",
1605
1971
  liveOutputTokens: null,
1606
1972
  lastEventAt: null,
1607
1973
  conversationOver: false
@@ -1625,7 +1991,10 @@ var SessionTranscriptStore = class {
1625
1991
  cursorSeq = 0;
1626
1992
  // Derived-turn cache, keyed on the records array identity.
1627
1993
  turnsFor = null;
1994
+ turnsSession = null;
1628
1995
  turnsCache = [];
1996
+ summaryTurnId = null;
1997
+ summaryExecutionId = null;
1629
1998
  get cursor() {
1630
1999
  return this.cursorSeq;
1631
2000
  }
@@ -1640,9 +2009,10 @@ var SessionTranscriptStore = class {
1640
2009
  // records_append batch) — what a chat-style transcript renders.
1641
2010
  chatTurns = () => {
1642
2011
  const records = this.snapshot.records;
1643
- if (this.turnsFor !== records) {
1644
- this.turnsCache = groupRecordsToChatTurns(records);
2012
+ if (this.turnsFor !== records || this.turnsSession !== this.snapshot.session) {
2013
+ this.turnsCache = groupRecordsToChatTurns(records, this.snapshot.session);
1645
2014
  this.turnsFor = records;
2015
+ this.turnsSession = this.snapshot.session;
1646
2016
  }
1647
2017
  return this.turnsCache;
1648
2018
  };
@@ -1709,6 +2079,7 @@ var SessionTranscriptStore = class {
1709
2079
  const frame = rawFrame;
1710
2080
  switch (frame.type) {
1711
2081
  case "snapshot": {
2082
+ next.liveSummary = "";
1712
2083
  next.session = frame.session;
1713
2084
  for (const message of frame.messages) {
1714
2085
  this.inbox.set(message.id, message);
@@ -1733,6 +2104,8 @@ var SessionTranscriptStore = class {
1733
2104
  acknowledgedNew = true;
1734
2105
  }
1735
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 = "";
1736
2109
  inboxChanged = this.applyMessageRecord(record) || inboxChanged;
1737
2110
  if (record.record_type === "message_received" || record.record_type === "message_delivered") {
1738
2111
  const payload = record.payload;
@@ -1754,17 +2127,35 @@ var SessionTranscriptStore = class {
1754
2127
  case "session": {
1755
2128
  next.session = frame.session;
1756
2129
  next.conversationOver = isConversationOver(frame.session);
2130
+ if (next.conversationOver || frame.session.lifecycle.status === "idle") {
2131
+ next.liveSummary = "";
2132
+ }
1757
2133
  break;
1758
2134
  }
1759
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
+ }
1760
2143
  if (frame.kind !== "text") break;
1761
- 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
+ }
1762
2148
  if (frame.output_tokens != null)
1763
2149
  next.liveOutputTokens = frame.output_tokens;
1764
2150
  break;
1765
2151
  }
1766
2152
  case "done": {
1767
2153
  next.conversationOver = true;
2154
+ next.liveSummary = "";
2155
+ break;
2156
+ }
2157
+ case "error": {
2158
+ next.liveSummary = "";
1768
2159
  break;
1769
2160
  }
1770
2161
  default:
@@ -1780,6 +2171,7 @@ export {
1780
2171
  SESSION_WAKING,
1781
2172
  SessionTranscriptStore,
1782
2173
  USER_BAR,
2174
+ activitySummary,
1783
2175
  awaitingAgentPhase,
1784
2176
  cacheTierLabel,
1785
2177
  chatTurnsToItems,
@@ -1808,11 +2200,13 @@ export {
1808
2200
  isFoldable,
1809
2201
  isToolActivity,
1810
2202
  isToolFold,
2203
+ isWorkFold,
1811
2204
  lastLines,
1812
2205
  layOutItems,
1813
2206
  lifecycleText,
1814
2207
  oneLine,
1815
2208
  pendingToolCalls,
2209
+ reasoningSummaryTitle,
1816
2210
  recordSlice,
1817
2211
  recordToItems,
1818
2212
  resultCostUsd,
@@ -1829,6 +2223,7 @@ export {
1829
2223
  statusActivityText,
1830
2224
  summarizeToolInput,
1831
2225
  tailLines,
2226
+ toolActivities,
1832
2227
  toolResultText,
1833
2228
  toolRunMembers
1834
2229
  };