@ellipsis-dev/sdk 0.25.0 → 0.28.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,107 @@
1
1
  import {
2
2
  SESSION_STREAM_PROTOCOL_VERSION
3
- } from "../chunk-RMPAMUAE.js";
3
+ } from "../chunk-YJC53TA7.js";
4
+
5
+ // src/store/claude.ts
6
+ function claudePayload(record) {
7
+ if (record.kind === "claude_sdk") return record.payload;
8
+ if (record.kind !== "claude_code") return null;
9
+ const event = record.payload;
10
+ switch (event.type) {
11
+ case "assistant":
12
+ return {
13
+ ...event.message,
14
+ kind: "assistant",
15
+ message_id: event.message.id,
16
+ parent_tool_use_id: event.parent_tool_use_id
17
+ };
18
+ case "user":
19
+ return {
20
+ kind: "user",
21
+ content: event.message.content,
22
+ uuid: event.uuid,
23
+ parent_tool_use_id: event.parent_tool_use_id
24
+ };
25
+ case "system":
26
+ return {
27
+ kind: "system",
28
+ subtype: event.subtype,
29
+ data: event,
30
+ session_id: event.session_id,
31
+ uuid: event.uuid
32
+ };
33
+ case "result":
34
+ return {
35
+ ...event,
36
+ kind: "result",
37
+ cost_usd: event.total_cost_usd,
38
+ model_usage: event.modelUsage
39
+ };
40
+ case "rate_limit_event":
41
+ return {
42
+ kind: "rate_limit",
43
+ status: event.rate_limit_info.status,
44
+ rate_limit_type: event.rate_limit_info.rateLimitType,
45
+ resets_at: event.rate_limit_info.resetsAt,
46
+ utilization: event.rate_limit_info.utilization
47
+ };
48
+ default:
49
+ return null;
50
+ }
51
+ }
52
+
53
+ // src/store/codexAppServer.ts
54
+ function codexAppServerContent(event) {
55
+ if (event.method === "turn/completed") {
56
+ return { kind: "end", isError: event.params.turn.status !== "completed" };
57
+ }
58
+ if (event.method === "error") {
59
+ return event.params.willRetry ? null : { kind: "error", text: event.params.error.message };
60
+ }
61
+ if (event.method !== "item/completed") return null;
62
+ const item = event.params.item;
63
+ switch (item.type) {
64
+ case "agentMessage":
65
+ case "plan":
66
+ return { kind: "assistant", text: item.text };
67
+ case "reasoning":
68
+ return {
69
+ kind: "thinking",
70
+ text: (item.summary?.length ? item.summary : item.content ?? []).join(
71
+ "\n"
72
+ )
73
+ };
74
+ case "commandExecution":
75
+ return {
76
+ kind: "tool",
77
+ name: "Bash",
78
+ input: { command: item.command },
79
+ summary: item.command,
80
+ result: item.aggregatedOutput?.trim() || "(no output)",
81
+ isError: item.status === "failed" || item.status === "declined" || typeof item.exitCode === "number" && item.exitCode !== 0
82
+ };
83
+ case "fileChange":
84
+ return {
85
+ kind: "tool",
86
+ name: "Edit",
87
+ input: { changes: item.changes },
88
+ summary: item.changes.map((change) => change.path).join(", "),
89
+ result: item.changes.map((change) => change.diff).join("\n"),
90
+ isError: item.status === "failed" || item.status === "declined"
91
+ };
92
+ case "webSearch":
93
+ return {
94
+ kind: "tool",
95
+ name: "WebSearch",
96
+ input: { query: item.query },
97
+ summary: item.query,
98
+ result: null,
99
+ isError: false
100
+ };
101
+ default:
102
+ return null;
103
+ }
104
+ }
4
105
 
5
106
  // src/store/lifecycle.ts
6
107
  function sandboxOutputStep(payload) {
@@ -62,11 +163,11 @@ function sandboxPhaseText(payload) {
62
163
  if (tier) parts.push(tier);
63
164
  const duration = durationLabel(payload.duration_ms);
64
165
  if (duration) parts.push(duration);
65
- return parts.join(" \xB7 ");
166
+ return parts.join(", ");
66
167
  }
67
168
  case "failed": {
68
169
  const duration = durationLabel(payload.duration_ms);
69
- return duration ? `${label} failed \xB7 ${duration}` : `${label} failed`;
170
+ return duration ? `${label} failed, ${duration}` : `${label} failed`;
70
171
  }
71
172
  default:
72
173
  return label;
@@ -76,7 +177,7 @@ function lifecycleText(recordType, payload) {
76
177
  switch (recordType) {
77
178
  case "session_scheduled": {
78
179
  const config = typeof payload.config_name === "string" ? payload.config_name : null;
79
- return config ? `Session scheduled \xB7 ${config}` : "Session scheduled";
180
+ return config ? `Session scheduled, ${config}` : "Session scheduled";
80
181
  }
81
182
  case "session_starting": {
82
183
  const attempt = typeof payload.attempt === "number" ? payload.attempt : 0;
@@ -85,14 +186,14 @@ function lifecycleText(recordType, payload) {
85
186
  return wakeIndex > 0 ? "Waking the session\u2026" : "Session starting\u2026";
86
187
  }
87
188
  case "session_retrying":
88
- return typeof payload.reason === "string" && payload.reason ? `Retrying \xB7 ${payload.reason}` : "Retrying after a transient error\u2026";
189
+ return typeof payload.reason === "string" && payload.reason ? `Retrying, ${payload.reason}` : "Retrying after a transient error\u2026";
89
190
  case "sandbox_starting":
90
191
  return "Starting sandbox\u2026";
91
192
  case "sandbox_phase":
92
193
  return sandboxPhaseText(payload);
93
194
  case "sandbox_output": {
94
195
  const last = sandboxOutputLine(payload);
95
- return last ? `${sandboxOutputStep(payload)} \xB7 ${last}` : null;
196
+ return last ? `${sandboxOutputStep(payload)}, ${last}` : null;
96
197
  }
97
198
  case "sandbox_ready": {
98
199
  const repos = Array.isArray(payload.repositories) ? payload.repositories.filter(
@@ -102,7 +203,7 @@ function lifecycleText(recordType, payload) {
102
203
  if (repos.length) parts.push(repos.join(", "));
103
204
  const tier = cacheTierLabel(payload.cache_tier);
104
205
  if (tier) parts.push(tier);
105
- return parts.join(" \xB7 ");
206
+ return parts.join(", ");
106
207
  }
107
208
  case "session_resumed":
108
209
  return "Resumed the conversation";
@@ -112,7 +213,7 @@ function lifecycleText(recordType, payload) {
112
213
  return "Conversation closed";
113
214
  case "session_cancelled": {
114
215
  const reason = typeof payload.reason === "string" ? payload.reason : null;
115
- return reason ? `Session cancelled \xB7 ${reason}` : "Session cancelled";
216
+ return reason ? `Session cancelled, ${reason}` : "Session cancelled";
116
217
  }
117
218
  default:
118
219
  return null;
@@ -182,7 +283,7 @@ function eventToItems(event, keyBase, options = {}) {
182
283
  const label = event.is_error ? "turn ended with an error" : "turn complete";
183
284
  push({
184
285
  kind: "summary",
185
- text: bits.length ? `${label} \xB7 ${bits.join(" \xB7 ")}` : label,
286
+ text: bits.length ? `${label}, ${bits.join(", ")}` : label,
186
287
  spaceBefore: true,
187
288
  isError: event.is_error
188
289
  });
@@ -196,17 +297,17 @@ function eventToItems(event, keyBase, options = {}) {
196
297
  if (typeof data.cwd === "string") bits.push(data.cwd);
197
298
  push({
198
299
  kind: "system",
199
- text: bits.length ? `session started \xB7 ${bits.join(" \xB7 ")}` : "session started",
300
+ text: bits.length ? `session started, ${bits.join(", ")}` : "session started",
200
301
  spaceBefore: true
201
302
  });
202
303
  return items;
203
304
  }
204
305
  if (event.kind === "rate_limit") return items;
205
- const blocks = blocksOf(event.content);
306
+ const blocks = blocksOf(event.content ?? []);
206
307
  if (event.kind === "user") {
207
308
  for (const block of blocks) {
208
309
  if (block.type === "tool_result") {
209
- const body = toolResultText(block.content);
310
+ const body = toolResultText(block.content ?? null);
210
311
  push({
211
312
  kind: "tool_result",
212
313
  gutter: "\u23BF",
@@ -234,14 +335,14 @@ function eventToItems(event, keyBase, options = {}) {
234
335
  spaceBefore: true
235
336
  });
236
337
  } else if (block.type === "tool_use" || block.type === "server_tool_use") {
237
- const summary = summarizeToolInput(block.name, block.input);
338
+ const summary = summarizeToolInput(block.name, block.input ?? {});
238
339
  push({
239
340
  kind: "tool",
240
341
  gutter: "\u25CF",
241
342
  text: block.name,
242
343
  detail: summary ? `(${summary})` : void 0,
243
344
  spaceBefore: true,
244
- tool: { name: block.name, input: block.input }
345
+ tool: { name: block.name, input: block.input ?? {} }
245
346
  });
246
347
  } else if (block.type === "text" && block.text.trim()) {
247
348
  push({ kind: "assistant", text: block.text.trim(), spaceBefore: true });
@@ -276,7 +377,7 @@ function codexEventToItems(event, keyBase) {
276
377
  const item = event.item;
277
378
  switch (item.type) {
278
379
  case "agent_message": {
279
- const text = item.text.trim();
380
+ const text = (item.text ?? "").trim();
280
381
  if (text) push({ kind: "assistant", text, spaceBefore: true });
281
382
  break;
282
383
  }
@@ -289,7 +390,7 @@ function codexEventToItems(event, keyBase) {
289
390
  }
290
391
  case "command_execution": {
291
392
  const c = item;
292
- const summary = oneLine(c.command, 100);
393
+ const summary = oneLine(c.command ?? "", 100);
293
394
  push({
294
395
  kind: "tool",
295
396
  gutter: "\u25CF",
@@ -361,12 +462,61 @@ function codexMcpToolName(item) {
361
462
  return item.server && item.tool ? `mcp__${item.server}__${item.tool}` : item.tool ?? "mcp_tool_call";
362
463
  }
363
464
  function recordToItems(record, keyBase, options = {}) {
364
- switch (record.record_format) {
365
- case "claude_sdk@1":
366
- return eventToItems(record.payload, keyBase, options);
367
- case "codex_jsonl@1":
465
+ switch (record.kind) {
466
+ case "claude_sdk":
467
+ case "claude_code": {
468
+ const payload = claudePayload(record);
469
+ return payload ? eventToItems(payload, keyBase, options) : [];
470
+ }
471
+ case "codex":
368
472
  return codexEventToItems(record.payload, keyBase);
369
- case "ellipsis_lifecycle@1": {
473
+ case "codex_app_server": {
474
+ const content = codexAppServerContent(record.payload);
475
+ if (!content) return [];
476
+ if (content.kind === "end") {
477
+ return content.isError ? [
478
+ {
479
+ key: keyBase,
480
+ kind: "summary",
481
+ text: "turn ended with an error",
482
+ isError: true,
483
+ spaceBefore: true
484
+ }
485
+ ] : [];
486
+ }
487
+ if (content.kind === "tool") {
488
+ const summary = oneLine(content.summary, 100);
489
+ return [
490
+ {
491
+ key: `${keyBase}:0`,
492
+ kind: "tool",
493
+ gutter: "\u25CF",
494
+ text: content.name,
495
+ detail: summary ? `(${summary})` : void 0,
496
+ spaceBefore: true,
497
+ tool: { name: content.name, input: content.input ?? void 0 }
498
+ },
499
+ {
500
+ key: `${keyBase}:1`,
501
+ kind: "tool_result",
502
+ gutter: "\u23BF",
503
+ text: content.result || "(completed)",
504
+ isError: content.isError,
505
+ spaceBefore: false
506
+ }
507
+ ];
508
+ }
509
+ return content.text.trim() ? [
510
+ {
511
+ key: keyBase,
512
+ kind: content.kind,
513
+ text: content.text.trim(),
514
+ isError: content.kind === "error",
515
+ spaceBefore: true
516
+ }
517
+ ] : [];
518
+ }
519
+ case "platform": {
370
520
  const text = lifecycleText(record.record_type, record.payload);
371
521
  return text ? [{ key: keyBase, kind: "notice", text, spaceBefore: true }] : [];
372
522
  }
@@ -386,42 +536,57 @@ function pendingToolCalls(items) {
386
536
  }
387
537
  return pending;
388
538
  }
389
- function collapseToolRuns(items) {
539
+ function isFoldable(item) {
540
+ return item.kind === "tool" || item.kind === "tool_result" || item.kind === "thinking";
541
+ }
542
+ function foldLabel(group, settled) {
543
+ const work = group.filter((i) => i.kind !== "thinking");
544
+ if (work.length === 0) return settled ? "Thinking\u2026" : "Thinking";
545
+ const names = [
546
+ ...new Set(work.filter((i) => i.kind === "tool").map((i) => i.text))
547
+ ];
548
+ const n = work.filter((i) => i.kind === "tool").length || work.length;
549
+ const plural = n === 1 ? "" : "s";
550
+ if (names.length === 1 && names[0] === "Bash")
551
+ return `Ran ${n} shell command${plural}`;
552
+ if (names.length === 1 && names[0] === "Read")
553
+ return `Read ${n} file${plural}`;
554
+ if (names.length === 1)
555
+ return n === 1 ? `Ran ${names[0]}` : `Ran ${names[0]} ${n} times`;
556
+ if (names.length === 0) return `Ran ${n} tool call${plural}`;
557
+ const shown = names.slice(0, 3).join(", ") + (names.length > 3 ? ", \u2026" : "");
558
+ return `Ran ${n} tool call${plural} (${shown})`;
559
+ }
560
+ function foldRun(group, loading) {
561
+ const thinkingOnly = group.every((i) => i.kind === "thinking");
562
+ return {
563
+ key: `grp:${group[0].key}`,
564
+ kind: thinkingOnly ? "thinking" : "notice",
565
+ ...thinkingOnly ? { gutter: "\u273B" } : {},
566
+ text: foldLabel(group, !loading),
567
+ spaceBefore: true,
568
+ ...loading ? { loading } : {}
569
+ };
570
+ }
571
+ function collapseToolRuns(items, { working = false } = {}) {
390
572
  const out = [];
391
573
  let group = [];
392
- const flush = () => {
574
+ const flush = (trailing) => {
393
575
  if (group.length === 0) return;
394
- const names = [
395
- ...new Set(group.filter((i) => i.kind === "tool").map((i) => i.text))
396
- ];
397
- const n = group.filter((i) => i.kind === "tool").length || group.length;
398
- const plural = n === 1 ? "" : "s";
399
- let label;
400
- if (names.length === 1 && names[0] === "Bash")
401
- label = `Ran ${n} shell command${plural}`;
402
- else if (names.length === 1 && names[0] === "Read")
403
- label = `Read ${n} file${plural}`;
404
- else if (names.length === 0) label = `Ran ${n} tool call${plural}`;
405
- else {
406
- const shown = names.slice(0, 3).join(", ") + (names.length > 3 ? ", \u2026" : "");
407
- label = `Ran ${n} tool call${plural} (${shown})`;
408
- }
409
- out.push({
410
- key: `grp:${group[0].key}`,
411
- kind: "notice",
412
- text: label,
413
- spaceBefore: true
414
- });
576
+ const pending = pendingToolCalls(group).length > 0;
577
+ const thinkingOnly = group.every((i) => i.kind === "thinking");
578
+ const loading = pending || trailing && working && thinkingOnly;
579
+ out.push(foldRun(group, loading));
415
580
  group = [];
416
581
  };
417
582
  for (const item of items) {
418
- if (item.kind === "tool" || item.kind === "tool_result") group.push(item);
583
+ if (isFoldable(item)) group.push(item);
419
584
  else {
420
- flush();
585
+ flush(false);
421
586
  out.push(item);
422
587
  }
423
588
  }
424
- flush();
589
+ flush(true);
425
590
  return out;
426
591
  }
427
592
  function clampLines(text, maxLines) {
@@ -471,6 +636,8 @@ var SANDBOX_RECORD_TYPES = /* @__PURE__ */ new Set([
471
636
  function groupRecordsToChatTurns(records) {
472
637
  const turns = [];
473
638
  const toolNodeByUseId = /* @__PURE__ */ new Map();
639
+ const userTurnByMessageId = /* @__PURE__ */ new Map();
640
+ const messageIdByTurnId = /* @__PURE__ */ new Map();
474
641
  let seq = 0;
475
642
  const nextKey = (base) => `${base}:${seq++}`;
476
643
  let openIdx = -1;
@@ -496,7 +663,39 @@ function groupRecordsToChatTurns(records) {
496
663
  return turns[openIdx];
497
664
  };
498
665
  for (const record of records) {
499
- if (record.record_format === "ellipsis_lifecycle@1") {
666
+ if (record.kind === "platform") {
667
+ const messageId = typeof record.payload.message_id === "string" ? record.payload.message_id : null;
668
+ if (record.record_type === "message_received" && messageId) {
669
+ const body = typeof record.payload.body === "string" ? record.payload.body : "";
670
+ if (!body.trim() || userTurnByMessageId.has(messageId)) continue;
671
+ const turn2 = {
672
+ resumed: false,
673
+ isError: false,
674
+ key: `msg:${messageId}`,
675
+ role: "user",
676
+ nodes: [{ key: `msg:${messageId}`, kind: "user", text: body.trim() }],
677
+ startedAt: record.created_at,
678
+ completedAt: record.created_at,
679
+ durationMs: null,
680
+ costUsd: null,
681
+ tokens: null
682
+ };
683
+ userTurnByMessageId.set(messageId, turn2);
684
+ turns.push(turn2);
685
+ continue;
686
+ }
687
+ if (record.record_type === "message_delivered" && messageId) {
688
+ if (typeof record.payload.turn_id === "string")
689
+ messageIdByTurnId.set(record.payload.turn_id, messageId);
690
+ continue;
691
+ }
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;
697
+ continue;
698
+ }
500
699
  const text = lifecycleText(record.record_type, record.payload);
501
700
  if (!text) continue;
502
701
  const isSandbox = SANDBOX_RECORD_TYPES.has(record.record_type);
@@ -559,7 +758,7 @@ function groupRecordsToChatTurns(records) {
559
758
  });
560
759
  continue;
561
760
  }
562
- if (record.record_format === "codex_jsonl@1") {
761
+ if (record.kind === "codex") {
563
762
  codexEventIntoTurns(record, record.payload, {
564
763
  openAgentTurn,
565
764
  closeTurn: (completedAt, isError) => {
@@ -573,7 +772,40 @@ function groupRecordsToChatTurns(records) {
573
772
  });
574
773
  continue;
575
774
  }
576
- const data = record.payload;
775
+ if (record.kind === "codex_app_server") {
776
+ const content = codexAppServerContent(record.payload);
777
+ if (!content) continue;
778
+ if (content.kind === "end") {
779
+ if (openIdx >= 0) {
780
+ turns[openIdx].completedAt = record.created_at;
781
+ if (content.isError) turns[openIdx].isError = true;
782
+ openIdx = -1;
783
+ }
784
+ } else if (content.kind === "tool") {
785
+ openAgentTurn(record).nodes.push({
786
+ key: nextKey(record.id),
787
+ kind: "tool",
788
+ name: content.name,
789
+ input: content.input,
790
+ summary: oneLine(content.summary, 100),
791
+ result: content.result,
792
+ isError: content.isError,
793
+ startedAt: record.created_at,
794
+ completedAt: record.created_at
795
+ });
796
+ } else if (content.text.trim()) {
797
+ const turn2 = openAgentTurn(record);
798
+ if (content.kind === "error") turn2.isError = true;
799
+ turn2.nodes.push({
800
+ key: nextKey(record.id),
801
+ kind: content.kind === "error" ? "assistant" : content.kind,
802
+ text: content.text.trim()
803
+ });
804
+ }
805
+ continue;
806
+ }
807
+ const data = claudePayload(record);
808
+ if (!data) continue;
577
809
  if (data.kind === "system") {
578
810
  if (isInitEvent(data)) {
579
811
  initCount += 1;
@@ -598,6 +830,8 @@ function groupRecordsToChatTurns(records) {
598
830
  if (typeof content === "string") {
599
831
  if (content.trim()) {
600
832
  openIdx = -1;
833
+ if (record.session_message_id != null && userTurnByMessageId.has(record.session_message_id))
834
+ continue;
601
835
  turns.push({
602
836
  resumed: false,
603
837
  isError: false,
@@ -617,7 +851,7 @@ function groupRecordsToChatTurns(records) {
617
851
  }
618
852
  for (const block of content) {
619
853
  if (block.type !== "tool_result") continue;
620
- const body = toolResultText(block.content) || "(no output)";
854
+ const body = toolResultText(block.content ?? null) || "(no output)";
621
855
  const useId = block.tool_use_id;
622
856
  const node = toolNodeByUseId.get(useId);
623
857
  if (node) {
@@ -647,7 +881,7 @@ function groupRecordsToChatTurns(records) {
647
881
  if (data.usage && typeof data.usage.output_tokens === "number") {
648
882
  turn.tokens = (turn.tokens ?? 0) + data.usage.output_tokens;
649
883
  }
650
- for (const block of data.content) {
884
+ for (const block of data.content ?? []) {
651
885
  if (block.type === "thinking" && block.thinking.trim()) {
652
886
  turn.nodes.push({
653
887
  key: nextKey(record.id),
@@ -659,7 +893,7 @@ function groupRecordsToChatTurns(records) {
659
893
  key: nextKey(record.id),
660
894
  kind: "tool",
661
895
  name: block.name,
662
- input: block.input,
896
+ input: block.input ?? null,
663
897
  summary: summarizeToolInput(block.name, block.input),
664
898
  result: null,
665
899
  isError: false,
@@ -698,7 +932,7 @@ function codexEventIntoTurns(record, event, ops) {
698
932
  const turn = () => ops.openAgentTurn(record);
699
933
  switch (item.type) {
700
934
  case "agent_message": {
701
- const text = item.text.trim();
935
+ const text = (item.text ?? "").trim();
702
936
  if (text)
703
937
  turn().nodes.push({
704
938
  key: ops.nextKey(record.id),
@@ -725,7 +959,7 @@ function codexEventIntoTurns(record, event, ops) {
725
959
  kind: "tool",
726
960
  name: "Bash",
727
961
  input: { command: c.command },
728
- summary: oneLine(c.command, 100),
962
+ summary: oneLine(c.command ?? "", 100),
729
963
  result: (c.aggregated_output ?? "").trim() || "(no output)",
730
964
  isError: typeof c.exit_code === "number" && c.exit_code !== 0,
731
965
  startedAt: record.created_at,
@@ -801,6 +1035,7 @@ function humanDuration(seconds) {
801
1035
  if (s > 0 || bits.length === 0) bits.push(`${s}s`);
802
1036
  return bits.join(" ");
803
1037
  }
1038
+ var SESSION_WAKING = "Session waking";
804
1039
  function sessionLogText(recordType, payload) {
805
1040
  const p = payload;
806
1041
  switch (recordType) {
@@ -810,14 +1045,14 @@ function sessionLogText(recordType, payload) {
810
1045
  const wake = typeof p.wake_index === "number" ? p.wake_index : 0;
811
1046
  const attempt = typeof p.attempt === "number" ? p.attempt : 0;
812
1047
  if (attempt > 0) return "Restarting the sandbox after a transient error\u2026";
813
- return wake > 0 ? "Waking the session\u2026" : null;
1048
+ return wake > 0 ? SESSION_WAKING : null;
814
1049
  }
815
1050
  case "session_retrying":
816
- return typeof p.reason === "string" && p.reason ? `Retrying \xB7 ${p.reason}` : "Retrying after a transient error\u2026";
1051
+ return typeof p.reason === "string" && p.reason ? `Retrying, ${p.reason}` : "Retrying after a transient error\u2026";
817
1052
  case "session_resumed":
818
1053
  return "Session awake";
819
1054
  case "session_cancelled": {
820
- const reason = typeof p.reason === "string" && p.reason ? ` \xB7 ${p.reason}` : "";
1055
+ const reason = typeof p.reason === "string" && p.reason ? `, ${p.reason}` : "";
821
1056
  return `Session cancelled${reason}`;
822
1057
  }
823
1058
  default:
@@ -879,6 +1114,7 @@ function deliveredUnechoedSends(records) {
879
1114
  }
880
1115
  return out;
881
1116
  }
1117
+ var STARTING_SANDBOX = "Starting sandbox\u2026";
882
1118
  function msLabel(ms) {
883
1119
  if (typeof ms !== "number" || !isFinite(ms) || ms < 0) return null;
884
1120
  return humanDuration(ms / 1e3);
@@ -920,7 +1156,7 @@ function stepLabel(phase, step) {
920
1156
  }
921
1157
  function deriveSandboxState(records, minFeedSeq) {
922
1158
  let seen = false;
923
- let headline = "Starting cloud agent\u2026";
1159
+ let headline = STARTING_SANDBOX;
924
1160
  let done = false;
925
1161
  let sandboxDone = false;
926
1162
  let readySeconds = null;
@@ -955,7 +1191,7 @@ function deriveSandboxState(records, minFeedSeq) {
955
1191
  case "session_retrying": {
956
1192
  seen = true;
957
1193
  const text = lifecycleText(record.record_type, p);
958
- headline = !text || text === "Session starting\u2026" ? "Starting cloud agent\u2026" : text;
1194
+ headline = !text || text === "Session starting\u2026" ? STARTING_SANDBOX : text;
959
1195
  done = false;
960
1196
  readySeconds = null;
961
1197
  reset();
@@ -970,7 +1206,6 @@ function deriveSandboxState(records, minFeedSeq) {
970
1206
  case "sandbox_starting": {
971
1207
  seen = true;
972
1208
  reset();
973
- push(record, "step", "Starting sandbox\u2026");
974
1209
  break;
975
1210
  }
976
1211
  case "sandbox_phase": {
@@ -989,7 +1224,7 @@ function deriveSandboxState(records, minFeedSeq) {
989
1224
  base,
990
1225
  ...tier ? [tier] : [],
991
1226
  ...dur ? [dur] : []
992
- ].join(" \xB7 ");
1227
+ ].join(", ");
993
1228
  const line = open.get(key);
994
1229
  if (line) {
995
1230
  line.kind = failed ? "failed" : "done";
@@ -1025,7 +1260,7 @@ function deriveSandboxState(records, minFeedSeq) {
1025
1260
  "Sandbox ready",
1026
1261
  ...tier ? [tier] : [],
1027
1262
  ...totalSeconds > 0 ? [humanDuration(totalSeconds)] : []
1028
- ].join(" \xB7 ")
1263
+ ].join(", ")
1029
1264
  );
1030
1265
  sandboxDone = true;
1031
1266
  readySeconds = totalSeconds > 0 ? totalSeconds : null;
@@ -1036,6 +1271,8 @@ function deriveSandboxState(records, minFeedSeq) {
1036
1271
  break;
1037
1272
  }
1038
1273
  }
1274
+ const openLines = [...open.values()];
1275
+ if (openLines.length) openLines[openLines.length - 1].loading = true;
1039
1276
  const full = configName ? [
1040
1277
  {
1041
1278
  key: "config",
@@ -1054,6 +1291,14 @@ function deriveSandboxState(records, minFeedSeq) {
1054
1291
  log: full
1055
1292
  } : null;
1056
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
+ }
1057
1302
  function sandboxSummary(sandbox) {
1058
1303
  const seconds = sandbox?.readySeconds ?? null;
1059
1304
  return seconds ? `Sandbox ready in ${humanDuration(seconds)}` : "Sandbox started";
@@ -1081,20 +1326,33 @@ var SANDBOX_RECORD_TYPES2 = /* @__PURE__ */ new Set([
1081
1326
  function chatTurnsToItems(turns) {
1082
1327
  const items = [];
1083
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
+ };
1084
1335
  for (const turn of turns) {
1336
+ if (turn.role === "assistant") settleWake();
1085
1337
  if (turn.role === "lifecycle") {
1086
1338
  for (const node of turn.nodes) {
1087
1339
  if (node.kind !== "lifecycle") continue;
1088
1340
  if (SANDBOX_RECORD_TYPES2.has(node.recordType)) continue;
1089
- if (node.recordType === "session_resumed" && wakeAt >= 0) {
1090
- items[wakeAt] = { ...items[wakeAt], text: "Session awake" };
1091
- wakeAt = -1;
1341
+ if (node.recordType === "session_resumed") {
1342
+ settleWake();
1092
1343
  continue;
1093
1344
  }
1094
1345
  const text = sessionLogText(node.recordType, node.payload ?? {});
1095
1346
  if (!text) continue;
1096
- items.push({ key: node.key, kind: "notice", text, spaceBefore: true });
1097
- wakeAt = text === "Waking the session\u2026" ? items.length - 1 : -1;
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;
1098
1356
  }
1099
1357
  continue;
1100
1358
  }
@@ -1105,7 +1363,8 @@ function chatTurnsToItems(turns) {
1105
1363
  key: node.key,
1106
1364
  kind: "user",
1107
1365
  text: node.text,
1108
- spaceBefore: true
1366
+ spaceBefore: true,
1367
+ ...turn.isError ? { isError: true } : {}
1109
1368
  });
1110
1369
  }
1111
1370
  }
@@ -1170,21 +1429,8 @@ function isToolFold(item) {
1170
1429
  function isToolActivity(item) {
1171
1430
  return item.kind === "tool" || item.kind === "tool_result" || isToolFold(item);
1172
1431
  }
1173
- function isAgentSpeech(item) {
1174
- return item.kind === "assistant" || item.kind === "thinking";
1175
- }
1176
1432
  function layOutItems(items) {
1177
- const out = [];
1178
- let nestUnder = false;
1179
- for (const item of items) {
1180
- if (!isToolActivity(item)) {
1181
- out.push({ item, nested: false, attach: false });
1182
- nestUnder = isAgentSpeech(item);
1183
- continue;
1184
- }
1185
- out.push({ item, nested: nestUnder, attach: nestUnder });
1186
- }
1187
- return out;
1433
+ return items.map((item) => ({ item, nested: false, attach: false }));
1188
1434
  }
1189
1435
  function foldGutter(nested) {
1190
1436
  return nested ? BRANCH_GLYPH : "\u25CF";
@@ -1205,13 +1451,125 @@ function toolRunMembers(items) {
1205
1451
  group = [];
1206
1452
  };
1207
1453
  for (const item of items) {
1208
- if (item.kind === "tool" || item.kind === "tool_result") group.push(item);
1454
+ if (isFoldable(item)) group.push(item);
1209
1455
  else flush();
1210
1456
  }
1211
1457
  flush();
1212
1458
  return out;
1213
1459
  }
1214
1460
 
1461
+ // src/store/ledger.ts
1462
+ function splitLedger(turns) {
1463
+ const preamble = [];
1464
+ const entries = [];
1465
+ for (const turn of turns) {
1466
+ if (turn.role === "user") {
1467
+ const node = turn.nodes.find((n) => n.kind === "user");
1468
+ entries.push({
1469
+ key: turn.key,
1470
+ prompt: node?.text ?? "",
1471
+ cancelled: turn.isError,
1472
+ turns: []
1473
+ });
1474
+ continue;
1475
+ }
1476
+ const last = entries[entries.length - 1];
1477
+ if (last) last.turns.push(turn);
1478
+ else preamble.push(turn);
1479
+ }
1480
+ return { preamble, entries };
1481
+ }
1482
+ function entrySeconds(entry, now) {
1483
+ let ms = 0;
1484
+ for (const turn of entry.turns) {
1485
+ if (turn.role !== "assistant" || turn.startedAt == null) continue;
1486
+ if (turn.durationMs != null) ms += turn.durationMs;
1487
+ else {
1488
+ const end = turn.completedAt != null ? Date.parse(turn.completedAt) : now;
1489
+ ms += Math.max(0, end - Date.parse(turn.startedAt));
1490
+ }
1491
+ }
1492
+ return ms / 1e3;
1493
+ }
1494
+ function foldToolTurn(raw, live, members) {
1495
+ const items = [];
1496
+ let run = [];
1497
+ const flush = () => {
1498
+ if (run.length === 0) return;
1499
+ const fold = foldRun(run, live && pendingToolCalls(run).length > 0);
1500
+ members.set(fold.key, run);
1501
+ items.push(fold);
1502
+ run = [];
1503
+ };
1504
+ for (const item of raw) {
1505
+ if (item.kind === "tool" || item.kind === "tool_result") run.push(item);
1506
+ else {
1507
+ flush();
1508
+ items.push(item);
1509
+ }
1510
+ }
1511
+ flush();
1512
+ return items;
1513
+ }
1514
+ function entryBody(entry, {
1515
+ live,
1516
+ now,
1517
+ liveText = ""
1518
+ }) {
1519
+ const raw = chatTurnsToItems(entry.turns);
1520
+ 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,
1544
+ spaceBefore: true
1545
+ });
1546
+ }
1547
+ }
1548
+ if (live && liveText !== "") {
1549
+ items.push({
1550
+ key: `live:${entry.key}`,
1551
+ kind: "assistant",
1552
+ text: liveText,
1553
+ spaceBefore: true
1554
+ });
1555
+ }
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 };
1562
+ }
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;
1571
+ }
1572
+
1215
1573
  // src/store/index.ts
1216
1574
  var TERMINAL_SESSION_STATUSES = /* @__PURE__ */ new Set([
1217
1575
  "completed",
@@ -1293,6 +1651,7 @@ var SessionTranscriptStore = class {
1293
1651
  // Fold one lifecycle record into the inbox projection; returns whether the
1294
1652
  // pending view changed. Unknown fields/types are ignored (§3.6).
1295
1653
  applyMessageRecord = (record) => {
1654
+ if (record.kind !== "platform") return false;
1296
1655
  const payload = record.payload;
1297
1656
  const messageId = typeof payload.message_id === "string" ? payload.message_id : null;
1298
1657
  switch (record.record_type) {
@@ -1418,12 +1777,14 @@ var SessionTranscriptStore = class {
1418
1777
  export {
1419
1778
  BRANCH_GLYPH,
1420
1779
  LIVE_GLYPH,
1780
+ SESSION_WAKING,
1421
1781
  SessionTranscriptStore,
1422
1782
  USER_BAR,
1423
1783
  awaitingAgentPhase,
1424
1784
  cacheTierLabel,
1425
1785
  chatTurnsToItems,
1426
1786
  clampLines,
1787
+ claudePayload,
1427
1788
  codexChangedPaths,
1428
1789
  codexEventToItems,
1429
1790
  codexMcpToolName,
@@ -1431,17 +1792,20 @@ export {
1431
1792
  deliveredUnechoedSends,
1432
1793
  deriveSandboxState,
1433
1794
  emptySessionTranscriptSnapshot,
1795
+ entryBody,
1796
+ entrySeconds,
1434
1797
  eventToItems,
1435
1798
  foldCosts,
1436
1799
  foldGutter,
1800
+ foldRun,
1437
1801
  formatDuration,
1438
1802
  groupRecordsToChatTurns,
1439
1803
  gutterFor,
1440
1804
  hookPhrase,
1441
1805
  humanDuration,
1442
- isAgentSpeech,
1443
1806
  isConnectVisibleRecord,
1444
1807
  isConversationOver,
1808
+ isFoldable,
1445
1809
  isToolActivity,
1446
1810
  isToolFold,
1447
1811
  lastLines,
@@ -1459,6 +1823,9 @@ export {
1459
1823
  sandboxSummary,
1460
1824
  seedTranscriptStore,
1461
1825
  sessionLogText,
1826
+ splitLedger,
1827
+ startupHeadline,
1828
+ startupSettled,
1462
1829
  statusActivityText,
1463
1830
  summarizeToolInput,
1464
1831
  tailLines,