@ellipsis-dev/sdk 0.28.0 → 0.31.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-G2AEYZW2.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,18 +104,136 @@ 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 Map();
111
+ const turns = /* @__PURE__ */ new Map();
112
+ let ready = false;
113
+ const getTurn = (id) => {
114
+ let turn = turns.get(id);
115
+ if (!turn) {
116
+ turn = {
117
+ timing: { id, startedAt: null, completedAt: null },
118
+ phase: ready ? "working" : "starting",
119
+ messages: /* @__PURE__ */ new Set()
120
+ };
121
+ turns.set(id, turn);
122
+ }
123
+ return turn;
124
+ };
125
+ const attach = (messageId, turnId) => {
126
+ const message = messages.get(messageId);
127
+ if (!message) return;
128
+ const turn = getTurn(turnId);
129
+ if (!turn.messages.has(messageId)) {
130
+ turn.messages.add(messageId);
131
+ message.turns.push(turn.timing);
132
+ }
133
+ pending.delete(messageId);
134
+ message.phase = turn.phase;
135
+ };
136
+ const setPending = (turnId, phase) => {
137
+ for (const [id, turn] of pending) {
138
+ if (turnId == null || turn == null || turn === turnId)
139
+ messages.get(id).phase = phase;
140
+ }
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: "queued", turns: [] });
151
+ if (turnId && turns.has(turnId)) attach(messageId, turnId);
152
+ else pending.set(messageId, turnId ?? null);
153
+ }
154
+ break;
155
+ case "message_delivered":
156
+ if (messageId && turnId) attach(messageId, turnId);
157
+ break;
158
+ case "message_requeued":
159
+ if (messageId && messages.has(messageId)) {
160
+ const requeued = typeof p.requeued_turn_id === "string" ? p.requeued_turn_id : null;
161
+ pending.set(messageId, requeued);
162
+ messages.get(messageId).phase = "queued";
163
+ }
164
+ break;
165
+ case "environment_phase":
166
+ case "environment_output":
167
+ ready = false;
168
+ setPending(turnId ?? null, "preparing");
169
+ break;
170
+ case "environment_ready":
171
+ ready = false;
172
+ setPending(turnId ?? null, "starting");
173
+ break;
174
+ case "turn_started":
175
+ if (turnId) {
176
+ getTurn(turnId).timing.startedAt = record.created_at;
177
+ for (const [id, turn] of pending) {
178
+ if (turn === turnId) attach(id, turnId);
179
+ }
180
+ }
181
+ break;
182
+ case "turn_ended":
183
+ if (turnId) {
184
+ const turn = getTurn(turnId);
185
+ turn.timing.completedAt = record.created_at;
186
+ turn.phase = null;
187
+ for (const id of turn.messages) {
188
+ if (!pending.has(id)) messages.get(id).phase = null;
189
+ }
190
+ for (const [id, pendingTurn] of pending) {
191
+ if (pendingTurn === turnId) {
192
+ pending.delete(id);
193
+ messages.get(id).phase = null;
194
+ }
195
+ }
196
+ }
197
+ break;
198
+ case "session_closed":
199
+ ready = false;
200
+ for (const id of pending.keys()) messages.get(id).phase = null;
201
+ break;
202
+ }
203
+ continue;
204
+ }
205
+ const claude = claudePayload(record);
206
+ 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");
207
+ if (agentStarted) {
208
+ ready = true;
209
+ if (record.turn_id) {
210
+ const turn = getTurn(record.turn_id);
211
+ if (turn.timing.completedAt == null) {
212
+ turn.timing.startedAt ??= record.created_at;
213
+ turn.phase = "working";
214
+ for (const id of turn.messages) messages.get(id).phase = "working";
215
+ }
216
+ }
217
+ }
218
+ if (record.session_message_id && record.turn_id) {
219
+ attach(record.session_message_id, record.turn_id);
220
+ }
221
+ }
222
+ return messages;
223
+ }
224
+
106
225
  // src/store/lifecycle.ts
107
- function sandboxOutputStep(payload) {
226
+ function environmentOutputStep(payload) {
108
227
  if (typeof payload.step === "string" && payload.step) return payload.step;
109
228
  return typeof payload.phase === "string" ? payload.phase : "setup";
110
229
  }
111
- function sandboxOutputLines(payload) {
230
+ function environmentOutputLines(payload) {
112
231
  return Array.isArray(payload.lines) ? payload.lines.filter(
113
232
  (l) => typeof l === "string" && l.trim().length > 0
114
233
  ) : [];
115
234
  }
116
- function sandboxOutputLine(payload) {
117
- const lines = sandboxOutputLines(payload);
235
+ function environmentOutputLine(payload) {
236
+ const lines = environmentOutputLines(payload);
118
237
  return lines.length ? lines[lines.length - 1].trim() : null;
119
238
  }
120
239
  function cacheTierLabel(tier) {
@@ -129,7 +248,7 @@ function cacheTierLabel(tier) {
129
248
  return null;
130
249
  }
131
250
  }
132
- function sandboxPhaseLabel(phase) {
251
+ function environmentPhaseLabel(phase) {
133
252
  switch (phase) {
134
253
  case "image":
135
254
  return "Preparing image";
@@ -151,8 +270,8 @@ function durationLabel(ms) {
151
270
  if (typeof ms !== "number" || !isFinite(ms) || ms < 0) return null;
152
271
  return ms >= 1e3 ? `${(ms / 1e3).toFixed(1)}s` : `${Math.round(ms)}ms`;
153
272
  }
154
- function sandboxPhaseText(payload) {
155
- const label = sandboxPhaseLabel(payload.phase);
273
+ function environmentPhaseText(payload) {
274
+ const label = environmentPhaseLabel(payload.phase);
156
275
  const detail = payload.detail && typeof payload.detail === "object" ? payload.detail : {};
157
276
  switch (payload.status) {
158
277
  case "started":
@@ -173,56 +292,42 @@ function sandboxPhaseText(payload) {
173
292
  return label;
174
293
  }
175
294
  }
295
+ function turnEndedText(payload) {
296
+ const status = typeof payload.status === "string" ? payload.status : "";
297
+ if (!status || status === "completed") return null;
298
+ const detail = typeof payload.detail === "string" ? payload.detail.trim() : "";
299
+ return detail || `Turn ${status}`;
300
+ }
176
301
  function lifecycleText(recordType, payload) {
177
302
  switch (recordType) {
178
- case "session_scheduled": {
179
- const config = typeof payload.config_name === "string" ? payload.config_name : null;
180
- return config ? `Session scheduled, ${config}` : "Session scheduled";
181
- }
182
- case "session_starting": {
183
- const attempt = typeof payload.attempt === "number" ? payload.attempt : 0;
184
- if (attempt > 0) return "Retrying\u2026";
185
- const wakeIndex = typeof payload.wake_index === "number" ? payload.wake_index : 0;
186
- return wakeIndex > 0 ? "Waking the session\u2026" : "Session starting\u2026";
303
+ case "environment_phase":
304
+ return environmentPhaseText(payload);
305
+ case "environment_output": {
306
+ const last = environmentOutputLine(payload);
307
+ return last ? `${environmentOutputStep(payload)}, ${last}` : null;
187
308
  }
188
- case "session_retrying":
189
- return typeof payload.reason === "string" && payload.reason ? `Retrying, ${payload.reason}` : "Retrying after a transient error\u2026";
190
- case "sandbox_starting":
191
- return "Starting sandbox\u2026";
192
- case "sandbox_phase":
193
- return sandboxPhaseText(payload);
194
- case "sandbox_output": {
195
- const last = sandboxOutputLine(payload);
196
- return last ? `${sandboxOutputStep(payload)}, ${last}` : null;
197
- }
198
- case "sandbox_ready": {
309
+ case "environment_ready": {
199
310
  const repos = Array.isArray(payload.repositories) ? payload.repositories.filter(
200
311
  (r) => typeof r === "string"
201
312
  ) : [];
202
- const parts = ["Sandbox ready"];
313
+ const parts = ["Environment ready"];
203
314
  if (repos.length) parts.push(repos.join(", "));
204
- const tier = cacheTierLabel(payload.cache_tier);
205
- if (tier) parts.push(tier);
315
+ const duration = durationLabel(payload.duration_ms);
316
+ if (duration) parts.push(duration);
206
317
  return parts.join(", ");
207
318
  }
208
- case "session_resumed":
209
- return "Resumed the conversation";
210
- case "session_idle":
211
- return "Idle \u2014 your next message wakes it";
319
+ case "turn_ended":
320
+ return turnEndedText(payload);
212
321
  case "session_closed":
213
322
  return "Conversation closed";
214
- case "session_cancelled": {
215
- const reason = typeof payload.reason === "string" ? payload.reason : null;
216
- return reason ? `Session cancelled, ${reason}` : "Session cancelled";
217
- }
218
323
  default:
219
324
  return null;
220
325
  }
221
326
  }
222
327
 
223
328
  // src/store/transcript.ts
224
- function oneLine(text, max) {
225
- const collapsed = text.replace(/\s+/g, " ").trim();
329
+ function oneLine(text2, max) {
330
+ const collapsed = text2.replace(/\s+/g, " ").trim();
226
331
  return collapsed.length <= max ? collapsed : `${collapsed.slice(0, max - 3)}...`;
227
332
  }
228
333
  function summarizeToolInput(name, input) {
@@ -377,15 +482,15 @@ function codexEventToItems(event, keyBase) {
377
482
  const item = event.item;
378
483
  switch (item.type) {
379
484
  case "agent_message": {
380
- const text = (item.text ?? "").trim();
381
- if (text) push({ kind: "assistant", text, spaceBefore: true });
485
+ const text2 = (item.text ?? "").trim();
486
+ if (text2) push({ kind: "assistant", text: text2, spaceBefore: true });
382
487
  break;
383
488
  }
384
489
  case "reasoning": {
385
490
  const r = item;
386
- const text = (r.text ?? r.summary ?? "").trim();
387
- if (text)
388
- push({ kind: "thinking", gutter: "\u273B", text, spaceBefore: true });
491
+ const text2 = (r.text ?? r.summary ?? "").trim();
492
+ if (text2)
493
+ push({ kind: "thinking", gutter: "\u273B", text: text2, spaceBefore: true });
389
494
  break;
390
495
  }
391
496
  case "command_execution": {
@@ -517,15 +622,15 @@ function recordToItems(record, keyBase, options = {}) {
517
622
  ] : [];
518
623
  }
519
624
  case "platform": {
520
- const text = lifecycleText(record.record_type, record.payload);
521
- return text ? [{ key: keyBase, kind: "notice", text, spaceBefore: true }] : [];
625
+ const text2 = lifecycleText(record.record_type, record.payload);
626
+ return text2 ? [{ key: keyBase, kind: "notice", text: text2, spaceBefore: true }] : [];
522
627
  }
523
628
  default:
524
629
  return [];
525
630
  }
526
631
  }
527
632
  function isConnectVisibleRecord(record) {
528
- return record.source !== "lifecycle" || record.record_type === "sandbox_ready";
633
+ return record.source !== "lifecycle" || record.record_type === "environment_ready";
529
634
  }
530
635
  function pendingToolCalls(items) {
531
636
  let pending = [];
@@ -589,9 +694,9 @@ function collapseToolRuns(items, { working = false } = {}) {
589
694
  flush(true);
590
695
  return out;
591
696
  }
592
- function clampLines(text, maxLines) {
593
- const lines = text.split("\n");
594
- if (lines.length <= maxLines) return { body: text, more: 0 };
697
+ function clampLines(text2, maxLines) {
698
+ const lines = text2.split("\n");
699
+ if (lines.length <= maxLines) return { body: text2, more: 0 };
595
700
  return {
596
701
  body: lines.slice(0, maxLines).join("\n"),
597
702
  more: lines.length - maxLines
@@ -614,12 +719,8 @@ function foldCosts(events) {
614
719
  }
615
720
  function statusActivityText(status) {
616
721
  switch (status) {
617
- case "scheduled":
618
- return "Waiting for a worker";
619
- case "starting":
620
- return "Starting sandbox";
621
- case "retrying":
622
- return "Retrying after a transient error";
722
+ case "pending":
723
+ return "Waiting to start";
623
724
  default:
624
725
  return null;
625
726
  }
@@ -627,26 +728,34 @@ function statusActivityText(status) {
627
728
 
628
729
  // src/store/chatTurns.ts
629
730
  var isInitEvent = (data) => data.kind === "system" && data.subtype === "init";
630
- var SANDBOX_RECORD_TYPES = /* @__PURE__ */ new Set([
631
- "sandbox_starting",
632
- "sandbox_phase",
633
- "sandbox_output",
634
- "sandbox_ready"
731
+ var ENVIRONMENT_RECORD_TYPES = /* @__PURE__ */ new Set([
732
+ "environment_phase",
733
+ "environment_output",
734
+ "environment_ready"
635
735
  ]);
636
- function groupRecordsToChatTurns(records) {
736
+ function groupRecordsToChatTurns(records, session) {
637
737
  const turns = [];
638
738
  const toolNodeByUseId = /* @__PURE__ */ new Map();
639
739
  const userTurnByMessageId = /* @__PURE__ */ new Map();
640
- const messageIdByTurnId = /* @__PURE__ */ new Map();
740
+ const turnByMessageId = /* @__PURE__ */ new Map();
741
+ const unansweredTurnIds = /* @__PURE__ */ new Set();
742
+ const activeMessageByTurnId = /* @__PURE__ */ new Map();
743
+ let lastDeliveredMessageId;
641
744
  let seq = 0;
642
745
  const nextKey = (base) => `${base}:${seq++}`;
643
746
  let openIdx = -1;
644
747
  let initCount = 0;
645
748
  let pendingResume = false;
646
- const openAgentTurn = (record) => {
749
+ const openSessionTurn = (record) => {
750
+ const messageId = (record.turn_id ? activeMessageByTurnId.get(record.turn_id) : void 0) ?? lastDeliveredMessageId;
751
+ if (openIdx >= 0 && (turns[openIdx].turnId !== (record.turn_id ?? void 0) || turns[openIdx].messageId !== messageId)) {
752
+ openIdx = -1;
753
+ }
647
754
  if (openIdx < 0) {
648
755
  turns.push({
649
756
  key: nextKey(record.id),
757
+ messageId,
758
+ turnId: record.turn_id ?? void 0,
650
759
  role: "assistant",
651
760
  nodes: [],
652
761
  startedAt: record.created_at,
@@ -666,12 +775,15 @@ function groupRecordsToChatTurns(records) {
666
775
  if (record.kind === "platform") {
667
776
  const messageId = typeof record.payload.message_id === "string" ? record.payload.message_id : null;
668
777
  if (record.record_type === "message_received" && messageId) {
778
+ if (typeof record.payload.turn_id === "string")
779
+ turnByMessageId.set(messageId, record.payload.turn_id);
669
780
  const body = typeof record.payload.body === "string" ? record.payload.body : "";
670
781
  if (!body.trim() || userTurnByMessageId.has(messageId)) continue;
671
782
  const turn2 = {
672
783
  resumed: false,
673
784
  isError: false,
674
785
  key: `msg:${messageId}`,
786
+ messageId,
675
787
  role: "user",
676
788
  nodes: [{ key: `msg:${messageId}`, kind: "user", text: body.trim() }],
677
789
  startedAt: record.created_at,
@@ -685,46 +797,51 @@ function groupRecordsToChatTurns(records) {
685
797
  continue;
686
798
  }
687
799
  if (record.record_type === "message_delivered" && messageId) {
688
- if (typeof record.payload.turn_id === "string")
689
- messageIdByTurnId.set(record.payload.turn_id, messageId);
800
+ if (typeof record.payload.turn_id === "string") {
801
+ turnByMessageId.set(messageId, record.payload.turn_id);
802
+ activeMessageByTurnId.set(record.payload.turn_id, messageId);
803
+ lastDeliveredMessageId = messageId;
804
+ }
690
805
  continue;
691
806
  }
692
- 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;
807
+ if (record.record_type === "message_requeued" && messageId) {
808
+ if (typeof record.payload.requeued_turn_id === "string")
809
+ turnByMessageId.set(messageId, record.payload.requeued_turn_id);
810
+ else turnByMessageId.delete(messageId);
697
811
  continue;
698
812
  }
699
- const text = lifecycleText(record.record_type, record.payload);
700
- if (!text) continue;
701
- const isSandbox = SANDBOX_RECORD_TYPES.has(record.record_type);
702
- const isSandboxOutput = record.record_type === "sandbox_output";
703
- const step = isSandboxOutput ? sandboxOutputStep(record.payload) : void 0;
704
- const lines = isSandboxOutput ? sandboxOutputLines(record.payload) : void 0;
813
+ if (record.record_type === "turn_ended" && typeof record.payload.turn_id === "string" && record.payload.status !== "completed")
814
+ unansweredTurnIds.add(record.payload.turn_id);
815
+ const text2 = lifecycleText(record.record_type, record.payload);
816
+ if (!text2) continue;
817
+ const isEnvironment = ENVIRONMENT_RECORD_TYPES.has(record.record_type);
818
+ const isOutput = record.record_type === "environment_output";
819
+ const step = isOutput ? environmentOutputStep(record.payload) : void 0;
820
+ const lines = isOutput ? environmentOutputLines(record.payload) : void 0;
821
+ const turnId = record.turn_id ?? void 0;
705
822
  let foldTarget = null;
706
- if (isSandbox && record.record_type !== "sandbox_starting") {
823
+ if (isEnvironment) {
707
824
  for (let i = turns.length - 1; i >= 0; i--) {
708
825
  const candidate = turns[i];
709
826
  if (candidate.role !== "lifecycle") break;
710
827
  if (candidate.nodes.some(
711
- (n) => n.kind === "lifecycle" && SANDBOX_RECORD_TYPES.has(n.recordType)
828
+ (n) => n.kind === "lifecycle" && ENVIRONMENT_RECORD_TYPES.has(n.recordType)
712
829
  )) {
713
- foldTarget = candidate;
830
+ if (candidate.turnId === turnId) foldTarget = candidate;
714
831
  break;
715
832
  }
716
833
  }
717
834
  }
718
835
  if (foldTarget) {
719
836
  const last = foldTarget.nodes[foldTarget.nodes.length - 1];
720
- if (isSandboxOutput && last && last.kind === "lifecycle" && last.recordType === "sandbox_output" && last.step === step) {
721
- last.text = text;
837
+ if (isOutput && last && last.kind === "lifecycle" && last.recordType === "environment_output" && last.step === step) {
838
+ last.text = text2;
722
839
  last.lines = [...last.lines ?? [], ...lines ?? []];
723
840
  } else {
724
841
  foldTarget.nodes.push({
725
842
  key: nextKey(record.id),
726
843
  kind: "lifecycle",
727
- text,
844
+ text: text2,
728
845
  recordType: record.record_type,
729
846
  step,
730
847
  lines,
@@ -736,12 +853,13 @@ function groupRecordsToChatTurns(records) {
736
853
  }
737
854
  turns.push({
738
855
  key: nextKey(record.id),
856
+ turnId,
739
857
  role: "lifecycle",
740
858
  nodes: [
741
859
  {
742
860
  key: nextKey(record.id),
743
861
  kind: "lifecycle",
744
- text,
862
+ text: text2,
745
863
  recordType: record.record_type,
746
864
  step,
747
865
  lines,
@@ -758,9 +876,14 @@ function groupRecordsToChatTurns(records) {
758
876
  });
759
877
  continue;
760
878
  }
879
+ if (record.session_message_id) {
880
+ lastDeliveredMessageId = record.session_message_id;
881
+ if (record.turn_id)
882
+ activeMessageByTurnId.set(record.turn_id, record.session_message_id);
883
+ }
761
884
  if (record.kind === "codex") {
762
885
  codexEventIntoTurns(record, record.payload, {
763
- openAgentTurn,
886
+ openSessionTurn,
764
887
  closeTurn: (completedAt, isError) => {
765
888
  if (openIdx >= 0) {
766
889
  turns[openIdx].completedAt = completedAt;
@@ -782,7 +905,7 @@ function groupRecordsToChatTurns(records) {
782
905
  openIdx = -1;
783
906
  }
784
907
  } else if (content.kind === "tool") {
785
- openAgentTurn(record).nodes.push({
908
+ openSessionTurn(record).nodes.push({
786
909
  key: nextKey(record.id),
787
910
  kind: "tool",
788
911
  name: content.name,
@@ -794,12 +917,14 @@ function groupRecordsToChatTurns(records) {
794
917
  completedAt: record.created_at
795
918
  });
796
919
  } else if (content.text.trim()) {
797
- const turn2 = openAgentTurn(record);
920
+ const turn2 = openSessionTurn(record);
798
921
  if (content.kind === "error") turn2.isError = true;
799
922
  turn2.nodes.push({
800
923
  key: nextKey(record.id),
801
924
  kind: content.kind === "error" ? "assistant" : content.kind,
802
- text: content.text.trim()
925
+ text: content.text.trim(),
926
+ createdAt: record.created_at,
927
+ ...content.kind === "assistant" && content.phase != null ? { phase: content.phase } : {}
803
928
  });
804
929
  }
805
930
  continue;
@@ -856,6 +981,9 @@ function groupRecordsToChatTurns(records) {
856
981
  const node = toolNodeByUseId.get(useId);
857
982
  if (node) {
858
983
  node.result = body;
984
+ if (record.kind === "claude_code" || record.kind === "claude_sdk") {
985
+ node.resultDetails = record.payload.tool_use_result;
986
+ }
859
987
  node.isError = !!block.is_error;
860
988
  node.completedAt = record.created_at;
861
989
  } else {
@@ -870,14 +998,14 @@ function groupRecordsToChatTurns(records) {
870
998
  startedAt: null,
871
999
  completedAt: record.created_at
872
1000
  };
873
- openAgentTurn(record).nodes.push(orphan);
1001
+ openSessionTurn(record).nodes.push(orphan);
874
1002
  toolNodeByUseId.set(useId, orphan);
875
1003
  }
876
1004
  }
877
1005
  continue;
878
1006
  }
879
1007
  if (data.kind !== "assistant") continue;
880
- const turn = openAgentTurn(record);
1008
+ const turn = openSessionTurn(record);
881
1009
  if (data.usage && typeof data.usage.output_tokens === "number") {
882
1010
  turn.tokens = (turn.tokens ?? 0) + data.usage.output_tokens;
883
1011
  }
@@ -906,11 +1034,60 @@ function groupRecordsToChatTurns(records) {
906
1034
  turn.nodes.push({
907
1035
  key: nextKey(record.id),
908
1036
  kind: "assistant",
909
- text: block.text.trim()
1037
+ text: block.text.trim(),
1038
+ createdAt: record.created_at
910
1039
  });
911
1040
  }
912
1041
  }
913
1042
  }
1043
+ for (const [messageId, turnId] of turnByMessageId) {
1044
+ const turn = userTurnByMessageId.get(messageId);
1045
+ if (turn) turn.isError = unansweredTurnIds.has(turnId);
1046
+ }
1047
+ for (const [messageId, progress] of messageProgress(records)) {
1048
+ const turn = userTurnByMessageId.get(messageId);
1049
+ if (turn) turn.progress = progress;
1050
+ }
1051
+ const initialTurn = records.find(
1052
+ (r) => r.kind === "platform" && r.record_type === "turn_started" && r.payload.turn_index === 0
1053
+ );
1054
+ const openingTurnId = initialTurn?.turn_id ?? initialTurn?.payload.turn_id ?? (session?.turn?.index === 0 ? session.turn.id : void 0);
1055
+ const initialReceived = openingTurnId == null ? void 0 : records.find(
1056
+ (r) => r.kind === "platform" && r.record_type === "message_received" && (r.turn_id === openingTurnId || r.payload.turn_id === openingTurnId)
1057
+ );
1058
+ const initialDelivery = openingTurnId == null ? void 0 : records.find(
1059
+ (r) => r.kind === "platform" && r.record_type === "message_delivered" && r.payload.turn_id === openingTurnId
1060
+ );
1061
+ const initialId = (initialReceived ?? initialDelivery)?.payload.message_id;
1062
+ const opening = initialId ? userTurnByMessageId.get(initialId) : void 0;
1063
+ const includesStart = records.length === 0 || initialTurn != null;
1064
+ const prompt = (session?.claude_code ?? session?.codex)?.prompt;
1065
+ if (prompt?.trim() && session && (opening || includesStart)) {
1066
+ const first = opening ?? {
1067
+ key: "prompt",
1068
+ role: "user",
1069
+ nodes: [{ key: "prompt", kind: "user", text: prompt }],
1070
+ startedAt: session.created_at,
1071
+ completedAt: session.created_at,
1072
+ durationMs: null,
1073
+ costUsd: null,
1074
+ tokens: null,
1075
+ resumed: false,
1076
+ isError: false,
1077
+ progress: { phase: "preparing", turns: [] }
1078
+ };
1079
+ if (opening) turns.splice(turns.indexOf(opening), 1);
1080
+ first.key = "prompt";
1081
+ first.startedAt = session.created_at;
1082
+ turns.unshift(first);
1083
+ if (!opening) {
1084
+ for (const turn of turns) {
1085
+ if (turn !== first && turn.role === "user" && turn.progress && !turn.progress.turns.length) {
1086
+ turn.progress.phase = turn.progress.phase === null ? null : "queued";
1087
+ }
1088
+ }
1089
+ }
1090
+ }
914
1091
  return turns;
915
1092
  }
916
1093
  function codexEventIntoTurns(record, event, ops) {
@@ -919,36 +1096,38 @@ function codexEventIntoTurns(record, event, ops) {
919
1096
  return;
920
1097
  }
921
1098
  if (event.type === "error") {
922
- const turn2 = ops.openAgentTurn(record);
1099
+ const turn2 = ops.openSessionTurn(record);
923
1100
  turn2.nodes.push({
924
1101
  key: ops.nextKey(record.id),
925
1102
  kind: "assistant",
926
- text: event.message?.trim() || "error"
1103
+ text: event.message?.trim() || "error",
1104
+ createdAt: record.created_at
927
1105
  });
928
1106
  return;
929
1107
  }
930
1108
  if (event.type !== "item.completed") return;
931
1109
  const item = event.item;
932
- const turn = () => ops.openAgentTurn(record);
1110
+ const turn = () => ops.openSessionTurn(record);
933
1111
  switch (item.type) {
934
1112
  case "agent_message": {
935
- const text = (item.text ?? "").trim();
936
- if (text)
1113
+ const text2 = (item.text ?? "").trim();
1114
+ if (text2)
937
1115
  turn().nodes.push({
938
1116
  key: ops.nextKey(record.id),
939
1117
  kind: "assistant",
940
- text
1118
+ text: text2,
1119
+ createdAt: record.created_at
941
1120
  });
942
1121
  break;
943
1122
  }
944
1123
  case "reasoning": {
945
1124
  const r = item;
946
- const text = (r.text ?? r.summary ?? "").trim();
947
- if (text)
1125
+ const text2 = (r.text ?? r.summary ?? "").trim();
1126
+ if (text2)
948
1127
  turn().nodes.push({
949
1128
  key: ops.nextKey(record.id),
950
1129
  kind: "thinking",
951
- text
1130
+ text: text2
952
1131
  });
953
1132
  break;
954
1133
  }
@@ -1017,6 +1196,10 @@ function codexEventIntoTurns(record, event, ops) {
1017
1196
  function recordSlice(records) {
1018
1197
  return records;
1019
1198
  }
1199
+ function turnOf(record) {
1200
+ if (record.turn_id != null) return record.turn_id;
1201
+ return typeof record.payload.turn_id === "string" ? record.payload.turn_id : null;
1202
+ }
1020
1203
  function humanDuration(seconds) {
1021
1204
  const clamped = Math.max(0, seconds);
1022
1205
  if (clamped === 0) return "0s";
@@ -1035,26 +1218,12 @@ function humanDuration(seconds) {
1035
1218
  if (s > 0 || bits.length === 0) bits.push(`${s}s`);
1036
1219
  return bits.join(" ");
1037
1220
  }
1038
- var SESSION_WAKING = "Session waking";
1039
1221
  function sessionLogText(recordType, payload) {
1040
- const p = payload;
1041
1222
  switch (recordType) {
1042
- case "session_idle":
1043
- return "Session asleep";
1044
- case "session_starting": {
1045
- const wake = typeof p.wake_index === "number" ? p.wake_index : 0;
1046
- const attempt = typeof p.attempt === "number" ? p.attempt : 0;
1047
- if (attempt > 0) return "Restarting the sandbox after a transient error\u2026";
1048
- return wake > 0 ? SESSION_WAKING : null;
1049
- }
1050
- case "session_retrying":
1051
- return typeof p.reason === "string" && p.reason ? `Retrying, ${p.reason}` : "Retrying after a transient error\u2026";
1052
- case "session_resumed":
1053
- return "Session awake";
1054
- case "session_cancelled": {
1055
- const reason = typeof p.reason === "string" && p.reason ? `, ${p.reason}` : "";
1056
- return `Session cancelled${reason}`;
1057
- }
1223
+ case "turn_ended":
1224
+ return turnEndedText(payload);
1225
+ case "session_closed":
1226
+ return "Conversation closed";
1058
1227
  default:
1059
1228
  return null;
1060
1229
  }
@@ -1065,14 +1234,20 @@ function awaitingAgentPhase(records) {
1065
1234
  for (const r of records) {
1066
1235
  if (r.source !== "lifecycle") {
1067
1236
  sawAgent = true;
1068
- } else {
1069
- if (r.record_type === "turn_started") inFlight = true;
1070
- else if (r.record_type === "turn_completed" || r.record_type === "turn_failed") {
1071
- inFlight = false;
1072
- } else if (r.record_type === "session_starting" || r.record_type === "session_retrying") {
1237
+ continue;
1238
+ }
1239
+ switch (r.record_type) {
1240
+ case "turn_started":
1241
+ inFlight = true;
1242
+ break;
1243
+ case "turn_ended":
1073
1244
  inFlight = false;
1245
+ break;
1246
+ case "environment_phase":
1247
+ case "environment_output":
1248
+ case "environment_ready":
1074
1249
  sawAgent = false;
1075
- }
1250
+ break;
1076
1251
  }
1077
1252
  }
1078
1253
  if (!inFlight) return null;
@@ -1081,14 +1256,14 @@ function awaitingAgentPhase(records) {
1081
1256
  function deliveredUnechoedSends(records) {
1082
1257
  const received = /* @__PURE__ */ new Map();
1083
1258
  const delivered = /* @__PURE__ */ new Map();
1084
- const failedTurns = /* @__PURE__ */ new Set();
1259
+ const unanswered = /* @__PURE__ */ new Set();
1085
1260
  const echoed = /* @__PURE__ */ new Set();
1086
1261
  for (const r of records) {
1087
1262
  if (r.session_message_id != null) echoed.add(r.session_message_id);
1088
1263
  if (r.source !== "lifecycle") continue;
1089
- if (r.record_type === "turn_failed") {
1090
- if (typeof r.payload.turn_id === "string")
1091
- failedTurns.add(r.payload.turn_id);
1264
+ if (r.record_type === "turn_ended") {
1265
+ if (typeof r.payload.turn_id === "string" && r.payload.status !== "completed")
1266
+ unanswered.add(r.payload.turn_id);
1092
1267
  continue;
1093
1268
  }
1094
1269
  const id = typeof r.payload.message_id === "string" ? r.payload.message_id : null;
@@ -1110,11 +1285,11 @@ function deliveredUnechoedSends(records) {
1110
1285
  for (const [id, body] of received) {
1111
1286
  const turnId = delivered.get(id);
1112
1287
  if (turnId === void 0 || echoed.has(id)) continue;
1113
- out.push({ id, body, cancelled: failedTurns.has(turnId) });
1288
+ out.push({ id, body, cancelled: unanswered.has(turnId) });
1114
1289
  }
1115
1290
  return out;
1116
1291
  }
1117
- var STARTING_SANDBOX = "Starting sandbox\u2026";
1292
+ var PREPARING_ENVIRONMENT = "Preparing environment";
1118
1293
  function msLabel(ms) {
1119
1294
  if (typeof ms !== "number" || !isFinite(ms) || ms < 0) return null;
1120
1295
  return humanDuration(ms / 1e3);
@@ -1152,64 +1327,38 @@ function stepLabel(phase, step) {
1152
1327
  if (phase === "image") return imageStepLabel(step);
1153
1328
  return step;
1154
1329
  }
1155
- return sandboxPhaseLabel(phase);
1330
+ return environmentPhaseLabel(phase);
1156
1331
  }
1157
- function deriveSandboxState(records, minFeedSeq) {
1332
+ function deriveEnvironmentState(records, minFeedSeq) {
1158
1333
  let seen = false;
1159
- let headline = STARTING_SANDBOX;
1160
1334
  let done = false;
1161
- let sandboxDone = false;
1162
1335
  let readySeconds = null;
1163
- let configName = null;
1164
- let configCommitSha = null;
1165
1336
  let log = [];
1166
1337
  let open = /* @__PURE__ */ new Map();
1167
- const push = (record, kind, text) => {
1168
- const entry = { key: `${record.feed_seq}:${log.length}`, kind, text };
1338
+ let storyTurn = null;
1339
+ const push = (record, kind, text2) => {
1340
+ const entry = { key: `${record.feed_seq}:${log.length}`, kind, text: text2 };
1169
1341
  log.push(entry);
1170
1342
  return entry;
1171
1343
  };
1172
- const reset = () => {
1173
- log = [];
1174
- open = /* @__PURE__ */ new Map();
1175
- sandboxDone = false;
1344
+ const begin = (record) => {
1345
+ const turn = turnOf(record);
1346
+ if (seen && turn !== storyTurn) {
1347
+ log = [];
1348
+ open = /* @__PURE__ */ new Map();
1349
+ done = false;
1350
+ readySeconds = null;
1351
+ }
1352
+ seen = true;
1353
+ storyTurn = turn;
1176
1354
  };
1177
1355
  for (const record of records) {
1178
1356
  if (record.feed_seq <= minFeedSeq || record.source !== "lifecycle")
1179
1357
  continue;
1180
1358
  const p = record.payload;
1181
1359
  switch (record.record_type) {
1182
- case "session_scheduled": {
1183
- seen = true;
1184
- headline = "Session scheduled\u2026";
1185
- done = false;
1186
- configName = typeof p.config_name === "string" && p.config_name ? p.config_name : null;
1187
- configCommitSha = typeof p.config_commit_sha === "string" && p.config_commit_sha ? p.config_commit_sha : null;
1188
- break;
1189
- }
1190
- case "session_starting":
1191
- case "session_retrying": {
1192
- seen = true;
1193
- const text = lifecycleText(record.record_type, p);
1194
- headline = !text || text === "Session starting\u2026" ? STARTING_SANDBOX : text;
1195
- done = false;
1196
- readySeconds = null;
1197
- reset();
1198
- break;
1199
- }
1200
- case "session_resumed":
1201
- case "session_idle": {
1202
- seen = true;
1203
- done = true;
1204
- break;
1205
- }
1206
- case "sandbox_starting": {
1207
- seen = true;
1208
- reset();
1209
- break;
1210
- }
1211
- case "sandbox_phase": {
1212
- seen = true;
1360
+ case "environment_phase": {
1361
+ begin(record);
1213
1362
  const phase = typeof p.phase === "string" && p.phase ? p.phase : "setup";
1214
1363
  const step = typeof p.step === "string" && p.step ? p.step : null;
1215
1364
  const key = step ? `${phase}:${step}` : phase;
@@ -1220,7 +1369,7 @@ function deriveSandboxState(records, minFeedSeq) {
1220
1369
  const dur = msLabel(p.duration_ms);
1221
1370
  const failed = p.status === "failed";
1222
1371
  const base = failed ? `${label} failed` : label;
1223
- const text = [
1372
+ const text2 = [
1224
1373
  base,
1225
1374
  ...tier ? [tier] : [],
1226
1375
  ...dur ? [dur] : []
@@ -1228,80 +1377,60 @@ function deriveSandboxState(records, minFeedSeq) {
1228
1377
  const line = open.get(key);
1229
1378
  if (line) {
1230
1379
  line.kind = failed ? "failed" : "done";
1231
- line.text = text;
1380
+ line.text = text2;
1232
1381
  open.delete(key);
1233
1382
  } else {
1234
- push(record, failed ? "failed" : "done", text);
1383
+ push(record, failed ? "failed" : "done", text2);
1235
1384
  }
1236
1385
  } else if (!open.has(key)) {
1237
1386
  open.set(key, push(record, "step", `${label}\u2026`));
1238
1387
  }
1239
1388
  break;
1240
1389
  }
1241
- case "sandbox_output": {
1242
- seen = true;
1243
- for (const l of sandboxOutputLines(p)) push(record, "output", l);
1390
+ case "environment_output": {
1391
+ begin(record);
1392
+ for (const l of environmentOutputLines(p)) push(record, "output", l);
1244
1393
  break;
1245
1394
  }
1246
- case "sandbox_ready": {
1247
- seen = true;
1395
+ case "environment_ready": {
1396
+ begin(record);
1248
1397
  for (const [, line] of open) line.kind = "done";
1249
1398
  open = /* @__PURE__ */ new Map();
1250
- const timings = p.phase_timings && typeof p.phase_timings === "object" ? Object.values(p.phase_timings) : [];
1251
- const totalSeconds = timings.reduce(
1252
- (acc, v) => typeof v === "number" && isFinite(v) ? acc + v : acc,
1253
- 0
1254
- );
1255
- const tier = cacheTierLabel(p.cache_tier);
1399
+ const seconds = typeof p.duration_ms === "number" && isFinite(p.duration_ms) && p.duration_ms > 0 ? p.duration_ms / 1e3 : null;
1256
1400
  push(
1257
1401
  record,
1258
1402
  "done",
1259
- [
1260
- "Sandbox ready",
1261
- ...tier ? [tier] : [],
1262
- ...totalSeconds > 0 ? [humanDuration(totalSeconds)] : []
1263
- ].join(", ")
1403
+ seconds == null ? "Environment ready" : `Environment ready, ${humanDuration(seconds)}`
1264
1404
  );
1265
- sandboxDone = true;
1266
- readySeconds = totalSeconds > 0 ? totalSeconds : null;
1405
+ readySeconds = seconds;
1267
1406
  done = true;
1268
1407
  break;
1269
1408
  }
1409
+ case "turn_started":
1410
+ case "turn_ended":
1411
+ if (seen && turnOf(record) === storyTurn) done = true;
1412
+ break;
1413
+ case "session_closed":
1414
+ if (seen) done = true;
1415
+ break;
1270
1416
  default:
1271
1417
  break;
1272
1418
  }
1273
1419
  }
1274
1420
  const openLines = [...open.values()];
1275
1421
  if (openLines.length) openLines[openLines.length - 1].loading = true;
1276
- const full = configName ? [
1277
- {
1278
- key: "config",
1279
- kind: "done",
1280
- text: `Using ${configName}${configCommitSha ? ` @ ${configCommitSha.slice(0, 7)}` : ""}`
1281
- },
1282
- ...log
1283
- ] : log;
1284
- return seen ? {
1285
- headline,
1286
- done,
1287
- readySeconds,
1288
- sandboxDone,
1289
- configName,
1290
- configCommitSha,
1291
- log: full
1292
- } : null;
1293
- }
1294
- function startupSettled(sandbox, status) {
1295
- return (sandbox?.done ?? false) && statusActivityText(status) == null;
1296
- }
1297
- function startupHeadline(sandbox, status) {
1298
- const activity = statusActivityText(status);
1299
- const text = !sandbox || sandbox.done ? activity ?? STARTING_SANDBOX : sandbox.headline;
1300
- return `${text.replace(/…$/, "")}\u2026`;
1301
- }
1302
- function sandboxSummary(sandbox) {
1303
- const seconds = sandbox?.readySeconds ?? null;
1304
- return seconds ? `Sandbox ready in ${humanDuration(seconds)}` : "Sandbox started";
1422
+ return seen ? { done, readySeconds, log } : null;
1423
+ }
1424
+ function startupSettled(environment, status) {
1425
+ return (environment?.done ?? false) && statusActivityText(status) == null;
1426
+ }
1427
+ function startupHeadline(environment, status) {
1428
+ const text2 = !environment || environment.done ? statusActivityText(status) ?? PREPARING_ENVIRONMENT : PREPARING_ENVIRONMENT;
1429
+ return `${text2}\u2026`;
1430
+ }
1431
+ function environmentSummary(environment) {
1432
+ const seconds = environment?.readySeconds ?? null;
1433
+ return seconds ? `Environment ready in ${humanDuration(seconds)}` : "Environment ready";
1305
1434
  }
1306
1435
  function lastLines(log, max) {
1307
1436
  return log.length <= max ? [...log] : log.slice(log.length - max);
@@ -1317,42 +1446,21 @@ function gutterFor(item) {
1317
1446
  if (item.kind === "system" || item.kind === "notice") return "\u2726";
1318
1447
  return item.gutter ?? "";
1319
1448
  }
1320
- var SANDBOX_RECORD_TYPES2 = /* @__PURE__ */ new Set([
1321
- "sandbox_starting",
1322
- "sandbox_phase",
1323
- "sandbox_output",
1324
- "sandbox_ready"
1449
+ var ENVIRONMENT_RECORD_TYPES2 = /* @__PURE__ */ new Set([
1450
+ "environment_phase",
1451
+ "environment_output",
1452
+ "environment_ready"
1325
1453
  ]);
1326
1454
  function chatTurnsToItems(turns) {
1327
1455
  const items = [];
1328
- let wakeAt = -1;
1329
- const settleWake = () => {
1330
- if (wakeAt < 0) return;
1331
- const { loading: _, ...settled } = items[wakeAt];
1332
- items[wakeAt] = { ...settled, text: "Session awake" };
1333
- wakeAt = -1;
1334
- };
1335
1456
  for (const turn of turns) {
1336
- if (turn.role === "assistant") settleWake();
1337
1457
  if (turn.role === "lifecycle") {
1338
1458
  for (const node of turn.nodes) {
1339
1459
  if (node.kind !== "lifecycle") continue;
1340
- if (SANDBOX_RECORD_TYPES2.has(node.recordType)) continue;
1341
- if (node.recordType === "session_resumed") {
1342
- settleWake();
1343
- continue;
1344
- }
1345
- const text = sessionLogText(node.recordType, node.payload ?? {});
1346
- if (!text) continue;
1347
- const waking = text === SESSION_WAKING;
1348
- items.push({
1349
- key: node.key,
1350
- kind: "notice",
1351
- text,
1352
- spaceBefore: true,
1353
- ...waking ? { loading: true } : {}
1354
- });
1355
- wakeAt = waking ? items.length - 1 : -1;
1460
+ if (ENVIRONMENT_RECORD_TYPES2.has(node.recordType)) continue;
1461
+ const text2 = sessionLogText(node.recordType, node.payload ?? {});
1462
+ if (!text2) continue;
1463
+ items.push({ key: node.key, kind: "notice", text: text2, spaceBefore: true });
1356
1464
  }
1357
1465
  continue;
1358
1466
  }
@@ -1363,6 +1471,7 @@ function chatTurnsToItems(turns) {
1363
1471
  key: node.key,
1364
1472
  kind: "user",
1365
1473
  text: node.text,
1474
+ createdAt: turn.startedAt,
1366
1475
  spaceBefore: true,
1367
1476
  ...turn.isError ? { isError: true } : {}
1368
1477
  });
@@ -1376,6 +1485,7 @@ function chatTurnsToItems(turns) {
1376
1485
  key: node.key,
1377
1486
  kind: "assistant",
1378
1487
  text: node.text,
1488
+ createdAt: node.createdAt,
1379
1489
  spaceBefore: true
1380
1490
  });
1381
1491
  } else if (node.kind === "thinking") {
@@ -1430,14 +1540,19 @@ function isToolActivity(item) {
1430
1540
  return item.kind === "tool" || item.kind === "tool_result" || isToolFold(item);
1431
1541
  }
1432
1542
  function layOutItems(items) {
1433
- return items.map((item) => ({ item, nested: false, attach: false }));
1543
+ const isActivity = (item) => item.kind === "thinking" || isToolActivity(item);
1544
+ return items.map((item, index) => ({
1545
+ item,
1546
+ nested: false,
1547
+ attach: index > 0 && isActivity(item) && isActivity(items[index - 1])
1548
+ }));
1434
1549
  }
1435
1550
  function foldGutter(nested) {
1436
1551
  return nested ? BRANCH_GLYPH : "\u25CF";
1437
1552
  }
1438
- function tailLines(text, maxLines) {
1439
- const lines = text.split("\n");
1440
- if (lines.length <= maxLines) return { body: text, hidden: 0 };
1553
+ function tailLines(text2, maxLines) {
1554
+ const lines = text2.split("\n");
1555
+ if (lines.length <= maxLines) return { body: text2, hidden: 0 };
1441
1556
  return {
1442
1557
  body: lines.slice(lines.length - maxLines).join("\n"),
1443
1558
  hidden: lines.length - maxLines
@@ -1462,24 +1577,44 @@ function toolRunMembers(items) {
1462
1577
  function splitLedger(turns) {
1463
1578
  const preamble = [];
1464
1579
  const entries = [];
1580
+ const entryByTurn = /* @__PURE__ */ new Map();
1581
+ const entryByMessage = /* @__PURE__ */ new Map();
1465
1582
  for (const turn of turns) {
1466
1583
  if (turn.role === "user") {
1467
1584
  const node = turn.nodes.find((n) => n.kind === "user");
1468
- entries.push({
1585
+ const entry = {
1469
1586
  key: turn.key,
1470
1587
  prompt: node?.text ?? "",
1588
+ createdAt: turn.startedAt,
1471
1589
  cancelled: turn.isError,
1472
- turns: []
1473
- });
1590
+ turns: [],
1591
+ ...turn.progress ? { progress: turn.progress } : {}
1592
+ };
1593
+ entries.push(entry);
1594
+ entryByTurn.set(turn, entry);
1595
+ if (turn.messageId) entryByMessage.set(turn.messageId, entry);
1596
+ }
1597
+ }
1598
+ let last;
1599
+ for (const turn of turns) {
1600
+ if (turn.role === "user") {
1601
+ last = entryByTurn.get(turn);
1474
1602
  continue;
1475
1603
  }
1476
- const last = entries[entries.length - 1];
1477
- if (last) last.turns.push(turn);
1604
+ const owner = turn.messageId ? entryByMessage.get(turn.messageId) : last;
1605
+ if (owner) owner.turns.push(turn);
1478
1606
  else preamble.push(turn);
1479
1607
  }
1480
1608
  return { preamble, entries };
1481
1609
  }
1482
1610
  function entrySeconds(entry, now) {
1611
+ if (entry.progress?.turns.length) {
1612
+ return entry.progress.turns.reduce((ms2, turn) => {
1613
+ if (turn.startedAt == null) return ms2;
1614
+ const end = turn.completedAt == null ? now : Date.parse(turn.completedAt);
1615
+ return ms2 + Math.max(0, end - Date.parse(turn.startedAt));
1616
+ }, 0) / 1e3;
1617
+ }
1483
1618
  let ms = 0;
1484
1619
  for (const turn of entry.turns) {
1485
1620
  if (turn.role !== "assistant" || turn.startedAt == null) continue;
@@ -1491,12 +1626,15 @@ function entrySeconds(entry, now) {
1491
1626
  }
1492
1627
  return ms / 1e3;
1493
1628
  }
1494
- function foldToolTurn(raw, live, members) {
1629
+ function isWorkFold(item) {
1630
+ return item.key.startsWith("work:");
1631
+ }
1632
+ function foldToolTurn(raw, members) {
1495
1633
  const items = [];
1496
1634
  let run = [];
1497
1635
  const flush = () => {
1498
1636
  if (run.length === 0) return;
1499
- const fold = foldRun(run, live && pendingToolCalls(run).length > 0);
1637
+ const fold = foldRun(run, false);
1500
1638
  members.set(fold.key, run);
1501
1639
  items.push(fold);
1502
1640
  run = [];
@@ -1511,77 +1649,217 @@ function foldToolTurn(raw, live, members) {
1511
1649
  flush();
1512
1650
  return items;
1513
1651
  }
1652
+ function finalReplyKeys(entry, live) {
1653
+ const keys = /* @__PURE__ */ new Set();
1654
+ for (const turn of entry.turns) {
1655
+ if (turn.role !== "assistant") continue;
1656
+ for (const node of turn.nodes) {
1657
+ if (node.kind === "assistant" && node.phase === "final_answer") {
1658
+ keys.add(node.key);
1659
+ }
1660
+ }
1661
+ if (entry.cancelled || turn.isError) continue;
1662
+ if (live && turn.completedAt == null && turn.durationMs == null) continue;
1663
+ for (let i = turn.nodes.length - 1; i >= 0; i--) {
1664
+ const node = turn.nodes[i];
1665
+ if (node.kind !== "assistant" || node.phase === "commentary") break;
1666
+ keys.add(node.key);
1667
+ const previous = turn.nodes[i - 1];
1668
+ if (node.createdAt == null || previous?.kind !== "assistant" || previous.createdAt !== node.createdAt)
1669
+ break;
1670
+ }
1671
+ }
1672
+ return keys;
1673
+ }
1674
+ function reasoningSummaryTitle(summary) {
1675
+ const line = summary.trim().split(/\r?\n/, 1)[0] ?? "";
1676
+ const title = line.replace(/^#{1,6}\s+/, "").replace(/^[*_`]+|[*_`]+$/g, "").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").trim();
1677
+ return title.length > 120 ? `${title.slice(0, 119).trimEnd()}\u2026` : title;
1678
+ }
1514
1679
  function entryBody(entry, {
1515
1680
  live,
1516
1681
  now,
1517
- liveText = ""
1682
+ liveText = "",
1683
+ liveSummary = ""
1518
1684
  }) {
1685
+ live = live && (entry.progress === void 0 || entry.progress.phase !== null);
1519
1686
  const raw = chatTurnsToItems(entry.turns);
1687
+ const replies = finalReplyKeys(entry, live);
1520
1688
  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,
1689
+ const items = [];
1690
+ const work = [];
1691
+ const phase = entry.progress?.phase;
1692
+ const seconds = entry.progress && !entry.progress.turns.some((turn) => turn.startedAt != null) ? null : entrySeconds(entry, now);
1693
+ const key = `work:${entry.key}`;
1694
+ const header = {
1695
+ key,
1696
+ kind: "summary",
1697
+ text: live ? phase === "preparing" ? "Preparing environment" : phase === "starting" ? "Starting agent" : phase === "queued" ? "Queued" : reasoningSummaryTitle(liveSummary) || "Working" : seconds == null ? "Session startup" : `Worked for ${humanDuration(seconds)}`,
1698
+ spaceBefore: true,
1699
+ ...entry.cancelled || entry.turns.some((turn) => turn.isError) ? { isError: true } : {},
1700
+ ...live ? { loading: true } : {}
1701
+ };
1702
+ if (live && (phase === void 0 || phase === "working")) {
1703
+ const visible = raw.map(({ loading: _, ...item }) => item);
1704
+ if (liveText !== "") {
1705
+ visible.push({
1706
+ key: `live:${entry.key}`,
1707
+ kind: "assistant",
1708
+ text: liveText,
1544
1709
  spaceBefore: true
1545
1710
  });
1546
1711
  }
1712
+ return {
1713
+ items: [header, ...foldToolTurn(visible, members)],
1714
+ members,
1715
+ seconds
1716
+ };
1717
+ }
1718
+ const addWork = (item) => {
1719
+ if (work.length === 0) items.push(header);
1720
+ work.push(item);
1721
+ };
1722
+ for (const item of raw) {
1723
+ const { loading: _, ...settled } = item;
1724
+ if (replies.has(item.key) || !["assistant", "thinking", "tool", "tool_result"].includes(item.kind)) {
1725
+ items.push(settled);
1726
+ } else {
1727
+ addWork(settled);
1728
+ }
1547
1729
  }
1548
1730
  if (live && liveText !== "") {
1549
- items.push({
1731
+ addWork({
1550
1732
  key: `live:${entry.key}`,
1551
1733
  kind: "assistant",
1552
1734
  text: liveText,
1553
1735
  spaceBefore: true
1554
1736
  });
1555
1737
  }
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 };
1738
+ if (work.length > 0) {
1739
+ members.set(key, foldToolTurn(work, members));
1740
+ } else if (live) {
1741
+ items.unshift(header);
1742
+ }
1743
+ return { items, members, seconds };
1562
1744
  }
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;
1745
+
1746
+ // src/store/activity.ts
1747
+ var text = (value) => typeof value === "string" ? value : "";
1748
+ function toolActivities(item) {
1749
+ const input = item.tool?.input ?? {};
1750
+ const name = item.tool?.name ?? item.text;
1751
+ const actions = input.commandActions;
1752
+ if (Array.isArray(actions) && actions.length) {
1753
+ const parsed = actions.flatMap((value) => {
1754
+ if (!value || typeof value !== "object") return [];
1755
+ const action = value;
1756
+ switch (action.type) {
1757
+ case "read":
1758
+ return [
1759
+ {
1760
+ kind: "read",
1761
+ label: "Read",
1762
+ detail: text(action.path) || text(action.name)
1763
+ }
1764
+ ];
1765
+ case "search": {
1766
+ const query = text(action.query);
1767
+ const path2 = text(action.path);
1768
+ return [
1769
+ {
1770
+ kind: "search",
1771
+ label: "Searched",
1772
+ detail: (query ? `for ${query}` : "code") + (path2 ? ` in ${path2}` : "")
1773
+ }
1774
+ ];
1775
+ }
1776
+ case "listFiles":
1777
+ return [
1778
+ {
1779
+ kind: "list",
1780
+ label: "Listed",
1781
+ detail: text(action.path) || "files"
1782
+ }
1783
+ ];
1784
+ default:
1785
+ return [
1786
+ {
1787
+ kind: "command",
1788
+ label: "Ran",
1789
+ detail: text(action.command) || text(input.command)
1790
+ }
1791
+ ];
1792
+ }
1793
+ });
1794
+ if (parsed.length) return parsed;
1795
+ }
1796
+ const path = text(input.file_path) || text(input.path) || text(input.notebook_path);
1797
+ switch (name.toLowerCase()) {
1798
+ case "read":
1799
+ return [{ kind: "read", label: "Read", detail: path }];
1800
+ case "edit":
1801
+ case "write":
1802
+ case "multiedit":
1803
+ if (Array.isArray(input.changes)) {
1804
+ return input.changes.map((change) => ({
1805
+ kind: "edit",
1806
+ label: "Edited",
1807
+ detail: text(change?.path)
1808
+ }));
1809
+ }
1810
+ return [
1811
+ {
1812
+ kind: "edit",
1813
+ label: name.toLowerCase() === "write" ? "Wrote" : "Edited",
1814
+ detail: path
1815
+ }
1816
+ ];
1817
+ case "grep":
1818
+ case "glob":
1819
+ return [
1820
+ {
1821
+ kind: "search",
1822
+ label: "Searched",
1823
+ detail: `for ${text(input.pattern)}` + (path ? ` in ${path}` : "")
1824
+ }
1825
+ ];
1826
+ case "bash":
1827
+ return [{ kind: "command", label: "Ran", detail: text(input.command) }];
1828
+ case "websearch":
1829
+ return [
1830
+ { kind: "web", label: "Searched the web", detail: text(input.query) }
1831
+ ];
1832
+ case "webfetch":
1833
+ return [{ kind: "web", label: "Opened", detail: text(input.url) }];
1834
+ default:
1835
+ return [
1836
+ {
1837
+ kind: "tool",
1838
+ label: name.replace(/^mcp__/, "").replace("__", ":"),
1839
+ detail: item.detail ?? ""
1840
+ }
1841
+ ];
1842
+ }
1843
+ }
1844
+ function activitySummary(activities) {
1845
+ const labels = {
1846
+ read: "read files",
1847
+ search: "searched code",
1848
+ list: "listed files",
1849
+ edit: "edited files",
1850
+ command: "ran commands",
1851
+ web: "browsed the web",
1852
+ tool: "used tools"
1853
+ };
1854
+ const summary = [
1855
+ ...new Set(activities.map((activity) => labels[activity.kind]))
1856
+ ].join(", ");
1857
+ return summary ? summary[0].toUpperCase() + summary.slice(1) : "Activity";
1571
1858
  }
1572
1859
 
1573
1860
  // src/store/index.ts
1574
- var TERMINAL_SESSION_STATUSES = /* @__PURE__ */ new Set([
1575
- "completed",
1576
- "error",
1577
- "cancelled",
1578
- "stopped"
1579
- ]);
1580
1861
  function isConversationOver(session) {
1581
- if (session.session_state != null) {
1582
- return session.session_state === "closed";
1583
- }
1584
- return TERMINAL_SESSION_STATUSES.has(session.status);
1862
+ return session.conversation.state === "closed";
1585
1863
  }
1586
1864
  function seedTranscriptStore(store, seed) {
1587
1865
  store.ingest({
@@ -1602,6 +1880,7 @@ var EMPTY_SNAPSHOT = {
1602
1880
  acknowledgedMessageIds: /* @__PURE__ */ new Set(),
1603
1881
  historyTruncated: false,
1604
1882
  liveText: "",
1883
+ liveSummary: "",
1605
1884
  liveOutputTokens: null,
1606
1885
  lastEventAt: null,
1607
1886
  conversationOver: false
@@ -1625,7 +1904,9 @@ var SessionTranscriptStore = class {
1625
1904
  cursorSeq = 0;
1626
1905
  // Derived-turn cache, keyed on the records array identity.
1627
1906
  turnsFor = null;
1907
+ turnsSession = null;
1628
1908
  turnsCache = [];
1909
+ summaryTurnId = null;
1629
1910
  get cursor() {
1630
1911
  return this.cursorSeq;
1631
1912
  }
@@ -1640,9 +1921,10 @@ var SessionTranscriptStore = class {
1640
1921
  // records_append batch) — what a chat-style transcript renders.
1641
1922
  chatTurns = () => {
1642
1923
  const records = this.snapshot.records;
1643
- if (this.turnsFor !== records) {
1644
- this.turnsCache = groupRecordsToChatTurns(records);
1924
+ if (this.turnsFor !== records || this.turnsSession !== this.snapshot.session) {
1925
+ this.turnsCache = groupRecordsToChatTurns(records, this.snapshot.session);
1645
1926
  this.turnsFor = records;
1927
+ this.turnsSession = this.snapshot.session;
1646
1928
  }
1647
1929
  return this.turnsCache;
1648
1930
  };
@@ -1668,7 +1950,8 @@ var SessionTranscriptStore = class {
1668
1950
  sender_attribution_id: typeof payload.sender_attribution_id === "string" ? payload.sender_attribution_id : null,
1669
1951
  created_at: record.created_at,
1670
1952
  delivered_at: null,
1671
- delivered_turn_id: null
1953
+ images: [],
1954
+ turn_id: typeof payload.turn_id === "string" ? payload.turn_id : null
1672
1955
  });
1673
1956
  return true;
1674
1957
  }
@@ -1679,7 +1962,7 @@ var SessionTranscriptStore = class {
1679
1962
  ...existing,
1680
1963
  status: "delivered",
1681
1964
  delivered_at: record.created_at,
1682
- delivered_turn_id: typeof payload.turn_id === "string" ? payload.turn_id : null
1965
+ turn_id: typeof payload.turn_id === "string" ? payload.turn_id : existing.turn_id
1683
1966
  });
1684
1967
  return true;
1685
1968
  }
@@ -1690,7 +1973,7 @@ var SessionTranscriptStore = class {
1690
1973
  ...existing,
1691
1974
  status: "pending",
1692
1975
  delivered_at: null,
1693
- delivered_turn_id: null
1976
+ turn_id: typeof payload.requeued_turn_id === "string" ? payload.requeued_turn_id : null
1694
1977
  });
1695
1978
  return true;
1696
1979
  }
@@ -1709,6 +1992,7 @@ var SessionTranscriptStore = class {
1709
1992
  const frame = rawFrame;
1710
1993
  switch (frame.type) {
1711
1994
  case "snapshot": {
1995
+ next.liveSummary = "";
1712
1996
  next.session = frame.session;
1713
1997
  for (const message of frame.messages) {
1714
1998
  this.inbox.set(message.id, message);
@@ -1733,6 +2017,8 @@ var SessionTranscriptStore = class {
1733
2017
  acknowledgedNew = true;
1734
2018
  }
1735
2019
  if (record.source === "lifecycle") {
2020
+ if (record.record_type === "turn_started" && record.turn_id !== this.summaryTurnId || record.record_type === "turn_ended" && record.turn_id === this.summaryTurnId)
2021
+ next.liveSummary = "";
1736
2022
  inboxChanged = this.applyMessageRecord(record) || inboxChanged;
1737
2023
  if (record.record_type === "message_received" || record.record_type === "message_delivered") {
1738
2024
  const payload = record.payload;
@@ -1754,17 +2040,34 @@ var SessionTranscriptStore = class {
1754
2040
  case "session": {
1755
2041
  next.session = frame.session;
1756
2042
  next.conversationOver = isConversationOver(frame.session);
2043
+ if (next.conversationOver || frame.session.turn?.status !== "running") {
2044
+ next.liveSummary = "";
2045
+ }
1757
2046
  break;
1758
2047
  }
1759
2048
  case "delta": {
2049
+ if (frame.kind === "thinking") {
2050
+ if (next.conversationOver) break;
2051
+ this.summaryTurnId = frame.turn_id;
2052
+ if (frame.text != null) next.liveSummary = frame.text;
2053
+ break;
2054
+ }
1760
2055
  if (frame.kind !== "text") break;
1761
- if (frame.text != null) next.liveText = prev.liveText + frame.text;
2056
+ if (frame.text != null) {
2057
+ next.liveText = prev.liveText + frame.text;
2058
+ next.liveSummary = "";
2059
+ }
1762
2060
  if (frame.output_tokens != null)
1763
2061
  next.liveOutputTokens = frame.output_tokens;
1764
2062
  break;
1765
2063
  }
1766
2064
  case "done": {
1767
2065
  next.conversationOver = true;
2066
+ next.liveSummary = "";
2067
+ break;
2068
+ }
2069
+ case "error": {
2070
+ next.liveSummary = "";
1768
2071
  break;
1769
2072
  }
1770
2073
  default:
@@ -1777,9 +2080,9 @@ var SessionTranscriptStore = class {
1777
2080
  export {
1778
2081
  BRANCH_GLYPH,
1779
2082
  LIVE_GLYPH,
1780
- SESSION_WAKING,
1781
2083
  SessionTranscriptStore,
1782
2084
  USER_BAR,
2085
+ activitySummary,
1783
2086
  awaitingAgentPhase,
1784
2087
  cacheTierLabel,
1785
2088
  chatTurnsToItems,
@@ -1790,10 +2093,15 @@ export {
1790
2093
  codexMcpToolName,
1791
2094
  collapseToolRuns,
1792
2095
  deliveredUnechoedSends,
1793
- deriveSandboxState,
2096
+ deriveEnvironmentState,
1794
2097
  emptySessionTranscriptSnapshot,
1795
2098
  entryBody,
1796
2099
  entrySeconds,
2100
+ environmentOutputLine,
2101
+ environmentOutputLines,
2102
+ environmentOutputStep,
2103
+ environmentPhaseLabel,
2104
+ environmentSummary,
1797
2105
  eventToItems,
1798
2106
  foldCosts,
1799
2107
  foldGutter,
@@ -1808,19 +2116,16 @@ export {
1808
2116
  isFoldable,
1809
2117
  isToolActivity,
1810
2118
  isToolFold,
2119
+ isWorkFold,
1811
2120
  lastLines,
1812
2121
  layOutItems,
1813
2122
  lifecycleText,
1814
2123
  oneLine,
1815
2124
  pendingToolCalls,
2125
+ reasoningSummaryTitle,
1816
2126
  recordSlice,
1817
2127
  recordToItems,
1818
2128
  resultCostUsd,
1819
- sandboxOutputLine,
1820
- sandboxOutputLines,
1821
- sandboxOutputStep,
1822
- sandboxPhaseLabel,
1823
- sandboxSummary,
1824
2129
  seedTranscriptStore,
1825
2130
  sessionLogText,
1826
2131
  splitLedger,
@@ -1829,6 +2134,8 @@ export {
1829
2134
  statusActivityText,
1830
2135
  summarizeToolInput,
1831
2136
  tailLines,
2137
+ toolActivities,
1832
2138
  toolResultText,
1833
- toolRunMembers
2139
+ toolRunMembers,
2140
+ turnEndedText
1834
2141
  };