dsh-code 0.9.1 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.en.md +278 -249
  2. package/README.md +131 -102
  3. package/bin/deepseek.mjs +100 -6
  4. package/cordis.patch.yml +36 -1
  5. package/lib/index.mjs +3055 -819
  6. package/lib/startup.mjs +21 -11
  7. package/lib/{theme-BEi4i_aN.mjs → theme-DCT8Y2xf.mjs} +13 -9
  8. package/lib/types/app.d.ts +84 -16
  9. package/lib/types/attachments.d.ts +20 -0
  10. package/lib/types/authorization-panel.d.ts +22 -0
  11. package/lib/types/authorization.d.ts +36 -0
  12. package/lib/types/editor.d.ts +6 -0
  13. package/lib/types/fork.d.ts +8 -0
  14. package/lib/types/git-workflow.d.ts +23 -0
  15. package/lib/types/index.d.ts +6 -0
  16. package/lib/types/kernel-panels.d.ts +39 -0
  17. package/lib/types/keyboard.d.ts +41 -0
  18. package/lib/types/mentions.d.ts +30 -38
  19. package/lib/types/models.d.ts +3 -1
  20. package/lib/types/permissions.d.ts +4 -14
  21. package/lib/types/presets.d.ts +5 -20
  22. package/lib/types/provider-settings.d.ts +16 -0
  23. package/lib/types/render/animations.d.ts +10 -39
  24. package/lib/types/render/editor.d.ts +137 -0
  25. package/lib/types/render/export.d.ts +1 -1
  26. package/lib/types/render/lines.d.ts +6 -2
  27. package/lib/types/render/markdown.d.ts +3 -1
  28. package/lib/types/render/projection.d.ts +29 -3
  29. package/lib/types/render/status.d.ts +6 -13
  30. package/lib/types/session-directory.d.ts +1 -3
  31. package/lib/types/startup.d.ts +14 -11
  32. package/lib/types/store.d.ts +11 -9
  33. package/lib/types/subagents.d.ts +3 -3
  34. package/lib/types/theme.d.ts +14 -1
  35. package/lib/types/version.d.ts +15 -2
  36. package/package.json +159 -141
  37. package/src/app.ts +1490 -663
  38. package/src/attachments.ts +128 -0
  39. package/src/authorization-panel.ts +285 -0
  40. package/src/authorization.ts +147 -0
  41. package/src/editor.ts +51 -0
  42. package/src/fork.ts +31 -0
  43. package/src/git-workflow.ts +87 -0
  44. package/src/index.ts +1523 -1374
  45. package/src/internals.ts +14 -1
  46. package/src/kernel-panels.ts +914 -798
  47. package/src/keyboard.ts +126 -0
  48. package/src/mentions.ts +78 -117
  49. package/src/models.ts +20 -14
  50. package/src/permissions.ts +5 -13
  51. package/src/presets.ts +6 -22
  52. package/src/provider-settings.ts +95 -1
  53. package/src/render/animations.ts +420 -450
  54. package/src/render/editor.ts +398 -0
  55. package/src/render/export.ts +79 -79
  56. package/src/render/lines.ts +342 -236
  57. package/src/render/markdown.ts +99 -26
  58. package/src/render/projection.ts +106 -19
  59. package/src/render/status.ts +713 -650
  60. package/src/render/text.ts +150 -150
  61. package/src/render/tool-detail.ts +3 -1
  62. package/src/session-directory.ts +4 -4
  63. package/src/startup.ts +136 -119
  64. package/src/store.ts +23 -11
  65. package/src/subagents.ts +13 -5
  66. package/src/theme.ts +214 -206
  67. package/src/version.ts +58 -1
@@ -40,34 +40,98 @@ export function visibleColumns(text: string): number {
40
40
  return columns
41
41
  }
42
42
 
43
- /** Walk a segment list, breaking it into lines that fit `width` columns. */
44
- function wrapSegments(segments: readonly MdSegment[], width: number): readonly MdSegment[][] {
45
- const lines: MdSegment[][] = []
46
- let current: MdSegment[] = []
47
- let used = 0
43
+ interface WrapUnit {
44
+ text: string
45
+ style: MdStyle
46
+ }
47
+
48
+ /** Punctuation that should not become the first visible glyph of a row. */
49
+ function isClosingPunctuation(text: string): boolean {
50
+ return /^[,.;:!?%、。,.!?:;%)》」』】〕〉》”’]/u.test(text)
51
+ }
52
+
53
+ /**
54
+ * Split prose into soft-wrap units: ASCII words stay intact, while wide CJK
55
+ * glyphs become individual break opportunities. This keeps ordinary prose
56
+ * readable without allowing a Chinese paragraph to become one overlong word.
57
+ */
58
+ function wrapUnits(segments: readonly MdSegment[]): readonly WrapUnit[] {
59
+ const units: WrapUnit[] = []
48
60
  for (const segment of segments) {
49
- // Break the segment at spaces into words so long runs wrap mid-text.
50
- const words = segment.text.split(/( )/u)
51
- for (const word of words) {
52
- if (word === '') continue
53
- const columns = visibleColumns(word)
54
- if (used + columns > width && used > 0) {
55
- lines.push(current)
56
- current = []
57
- used = 0
61
+ let word = ''
62
+ const flushWord = (): void => {
63
+ if (word !== '') units.push({ text: word, style: segment.style })
64
+ word = ''
65
+ }
66
+ for (const char of segment.text) {
67
+ if (char === ' ') {
68
+ flushWord()
69
+ units.push({ text: char, style: segment.style })
70
+ } else if (visibleColumns(char) > 1) {
71
+ flushWord()
72
+ units.push({ text: char, style: segment.style })
73
+ } else {
74
+ word += char
58
75
  }
59
- // A single word wider than the line still goes on its own line.
60
- current.push({ text: word, style: segment.style })
76
+ }
77
+ flushWord()
78
+ }
79
+ return units
80
+ }
81
+
82
+ /** Walk styled units, keeping logical rows within the physical column budget. */
83
+ function wrapSegments(segments: readonly MdSegment[], width: number): readonly (readonly MdSegment[])[] {
84
+ const limit = Math.max(1, Math.floor(width))
85
+ const lines: (readonly MdSegment[])[] = []
86
+ let current: WrapUnit[] = []
87
+ let used = 0
88
+ const flush = (): void => {
89
+ while (current.at(-1)?.text === ' ') {
90
+ used -= visibleColumns(current.pop()!.text)
91
+ }
92
+ if (current.length > 0) lines.push(merge(current))
93
+ current = []
94
+ used = 0
95
+ }
96
+ const appendAtom = (unit: WrapUnit): void => {
97
+ const columns = visibleColumns(unit.text)
98
+ if (used > 0 && used + columns > limit) flush()
99
+ current.push(unit)
100
+ used += columns
101
+ }
102
+ const append = (unit: WrapUnit): void => {
103
+ const columns = visibleColumns(unit.text)
104
+ if (used === 0 && columns > limit) {
105
+ for (const char of unit.text) appendAtom({ text: char, style: unit.style })
106
+ return
107
+ }
108
+ if (used + columns <= limit || current.length === 0) {
109
+ current.push(unit)
61
110
  used += columns
111
+ return
112
+ }
113
+ // Keep full-width punctuation attached to the preceding CJK glyph. If
114
+ // the row is full, move that glyph with the punctuation instead of
115
+ // producing a visually orphaned line beginning with ':' or '。'.
116
+ const previous = current.at(-1)
117
+ if (isClosingPunctuation(unit.text) && previous !== undefined && visibleColumns(previous.text) > 1) {
118
+ current.pop()
119
+ used -= visibleColumns(previous.text)
120
+ flush()
121
+ appendAtom(previous)
122
+ appendAtom(unit)
123
+ return
124
+ }
125
+ flush()
126
+ if (columns > limit) {
127
+ for (const char of unit.text) appendAtom({ text: char, style: unit.style })
128
+ } else {
129
+ appendAtom(unit)
62
130
  }
63
131
  }
64
- if (current.length > 0) lines.push(current)
65
- // Drop the trailing space a wrapped line picked up before the break.
66
- return lines.map(line => {
67
- const last = line[line.length - 1]
68
- if (last !== undefined && last.text === ' ' && line.length > 1) return line.slice(0, -1)
69
- return line
70
- })
132
+ for (const unit of wrapUnits(segments)) append(unit)
133
+ flush()
134
+ return lines
71
135
  }
72
136
 
73
137
  /** Join adjacent same-style runs so the app renders fewer elements. */
@@ -448,10 +512,18 @@ function renderTableRecords(table: ParsedTable, width: number): readonly MdLine[
448
512
  }
449
513
 
450
514
  /** Render markdown text into styled lines of at most `width` columns. */
451
- export function renderMarkdown(text: string, width: number): readonly MdLine[] {
515
+ export function renderMarkdown(
516
+ text: string,
517
+ width: number,
518
+ options: { physicalWrap?: boolean } = {},
519
+ ): readonly MdLine[] {
452
520
  const lines: MdLine[] = []
453
521
  let separatorPending = false
454
522
  const push = (segments: readonly MdSegment[]): void => {
523
+ if (options.physicalWrap === false) {
524
+ lines.push({ segments: merge(segments) })
525
+ return
526
+ }
455
527
  for (const wrapped of wrapSegments(segments, Math.max(10, width))) {
456
528
  lines.push({ segments: merge(wrapped) })
457
529
  }
@@ -520,12 +592,13 @@ export function renderMarkdown(text: string, width: number): readonly MdLine[] {
520
592
  }
521
593
  const ordered = ORDERED.exec(line)
522
594
  if (ordered !== null) {
523
- push([seg(` ${ordered[1] ?? ''}. `, 'accent'), ...parseInline(ordered[2] ?? '').map(run => seg(run.text, run.style))])
595
+ const prefix = options.physicalWrap === false ? `${ordered[1] ?? ''}. ` : ` ${ordered[1] ?? ''}. `
596
+ push([seg(prefix, 'accent'), ...parseInline(ordered[2] ?? '').map(run => seg(run.text, run.style))])
524
597
  continue
525
598
  }
526
599
  const unordered = UNORDERED.exec(line)
527
600
  if (unordered !== null) {
528
- push([seg(' • ', 'accent'), ...parseInline(unordered[1] ?? '').map(run => seg(run.text, run.style))])
601
+ push([seg(options.physicalWrap === false ? '• ' : ' • ', 'accent'), ...parseInline(unordered[1] ?? '').map(run => seg(run.text, run.style))])
529
602
  continue
530
603
  }
531
604
 
@@ -7,7 +7,7 @@
7
7
  * @module @deepseek-ai/dsh-tui/render/projection
8
8
  */
9
9
 
10
- import { boundContextSummary, type ContentBlock, type MessageId } from '@deepseek-ai/dsh-llm'
10
+ import { boundContextSummary, type ContentBlock, type ImageBlock, type MessageId } from '@deepseek-ai/dsh-llm'
11
11
  import type { SessionEvent, TodoItem } from '@deepseek-ai/dsh-session'
12
12
  // Type-only imports merge the plugin-owned SessionEventMap variants
13
13
  // (agent/inbox/spliced, command/*, compaction/*, goal/change, llm/retry*,
@@ -52,6 +52,8 @@ export interface UserEntry {
52
52
  /** True for collapsed injected context (plugin/continuation notices), which
53
53
  * the renderer marks with a dim ↳ instead of the user ❯ prompt. */
54
54
  notice: boolean
55
+ /** Durable image references carried by this prompt. */
56
+ images?: readonly ImageBlock['attachment'][]
55
57
  }
56
58
 
57
59
  /** One user message waiting in the agent inbox (the web's queued-message row). */
@@ -63,15 +65,20 @@ export interface PendingEntry {
63
65
  target: 'next-turn' | 'next-step'
64
66
  /** Full message text — Codex PendingSteer renders queued prompts exactly like user rows. */
65
67
  text: string
68
+ /** Durable image references queued with this prompt. */
69
+ images?: readonly ImageBlock['attachment'][]
66
70
  }
67
71
 
68
- /** One assembled assistant reply. */
72
+ /** One authoritative assembled assistant reply. */
69
73
  export interface AssistantEntry {
70
74
  kind: 'assistant'
71
75
  /** Joined text blocks of the assistant message. */
72
76
  text: string
73
- /** Joined reasoning blocks of the same message, empty when the model thought out loud. */
77
+ /** Joined reasoning blocks from the same assembled message. */
74
78
  reasoning: string
79
+ /** True when a cancelled stream's delivered prefix was finalized as this
80
+ * entry (rc.8 `assistant/message.interrupted`) — rendered with a marker. */
81
+ interrupted?: true
75
82
  }
76
83
 
77
84
  /** One model-requested tool invocation and its settled state. */
@@ -79,6 +86,13 @@ export interface ToolEntry {
79
86
  kind: 'tool'
80
87
  /** Correlation id shared with the matching `tool/result`. */
81
88
  callId: string
89
+ /**
90
+ * Global tool-call ordinal across the whole transcript (1, 2, 3…, never
91
+ * reset between turns). The tool-card badge and every error line that
92
+ * references the failed call share this number, so "call N" in an error
93
+ * always names the exact card the badge shows.
94
+ */
95
+ ordinal: number
82
96
  /** Tool name as the model addressed it. */
83
97
  name: string
84
98
  /** Raw arguments JSON string exactly as the model produced it. */
@@ -253,6 +267,12 @@ export interface TranscriptView {
253
267
  streamingReasoning: string
254
268
  /** Latest whole-list todo snapshot from `todo/write`, empty when none. */
255
269
  todos: readonly TodoItem[]
270
+ /**
271
+ * Global tool-call ordinal counter: the number the NEXT `tool/call` lands
272
+ * with (1-based). Never reset, so the counter and the badges/error lines
273
+ * stay consistent across turns and resumed sessions.
274
+ */
275
+ toolCallOrdinal: number
256
276
  /** True while a durable turn is open (`turn/start` … `turn/end`). */
257
277
  busy: boolean
258
278
  /** `turn/start` time of the open turn (0 while idle) — the web TurnStatus clock anchor. */
@@ -307,6 +327,31 @@ function textOf(content: readonly ContentBlock[]): string {
307
327
  return content.filter(block => block.type === 'text').map(block => block.text).join('')
308
328
  }
309
329
 
330
+ /** Durable image references in their model-visible order. */
331
+ function imagesOf(content: readonly ContentBlock[]): readonly ImageBlock['attachment'][] {
332
+ return content.filter((block): block is ImageBlock => block.type === 'image').map(block => block.attachment)
333
+ }
334
+
335
+ /** Human-readable bounded image labels for transcript, inspector, and export surfaces. */
336
+ export function imageLabels(images: readonly ImageBlock['attachment'][] | undefined): string {
337
+ if (images === undefined || images.length === 0) return ''
338
+ return images.map((image, index) => {
339
+ const rawName = image.name?.trim() || `image ${index + 1}`
340
+ const name = rawName.length <= 80 ? rawName : `${rawName.slice(0, 79)}…`
341
+ const original = image.originalDimensions
342
+ const dimensions = original === undefined
343
+ ? `${image.width}×${image.height}`
344
+ : `${image.width}×${image.height} · original ${original.width}×${original.height}`
345
+ return `[image: ${name} · ${dimensions} · ${image.bytes} B]`
346
+ }).join('\n')
347
+ }
348
+
349
+ /** Prompt text with its durable image labels, without exposing local paths or bytes. */
350
+ export function promptDisplayText(entry: Pick<UserEntry | PendingEntry, 'text' | 'images'>): string {
351
+ const labels = imageLabels(entry.images)
352
+ return entry.text === '' ? labels : labels === '' ? entry.text : `${entry.text}\n${labels}`
353
+ }
354
+
310
355
  /** Join the reasoning blocks of a content list; non-reasoning blocks contribute nothing. */
311
356
  function reasoningOf(content: readonly ContentBlock[]): string {
312
357
  return content.filter(block => block.type === 'reasoning').map(block => block.text).join('')
@@ -337,6 +382,7 @@ export function createTranscriptView(): TranscriptView {
337
382
  streaming: '',
338
383
  streamingReasoning: '',
339
384
  todos: [],
385
+ toolCallOrdinal: 0,
340
386
  busy: false,
341
387
  busySince: 0,
342
388
  model: '',
@@ -381,11 +427,12 @@ export function projectEvent(view: TranscriptView, event: SessionEvent): Transcr
381
427
  // to a bounded notice row, exactly like collapsed transcript context
382
428
  // elsewhere in the product; only direct human prompts render in full.
383
429
  const text = textOf(message.content)
430
+ const images = imagesOf(message.content)
384
431
  if (message.source.kind === 'user') {
385
432
  return {
386
433
  ...view,
387
434
  pending,
388
- entries: [...entries, { kind: 'user', text, notice: false }],
435
+ entries: [...entries, { kind: 'user', text, notice: false, ...(images.length === 0 ? {} : { images }) }],
389
436
  stats: {
390
437
  ...view.stats,
391
438
  contextSegments: {
@@ -436,6 +483,7 @@ export function projectEvent(view: TranscriptView, event: SessionEvent): Transcr
436
483
  messageId: message.id,
437
484
  target,
438
485
  text: pendingText(message.content),
486
+ ...imagesOf(message.content).length === 0 ? {} : { images: imagesOf(message.content) },
439
487
  }]
440
488
  }
441
489
  return { ...view, entries, pending: { ...view.pending, [target]: nextIds } }
@@ -459,10 +507,18 @@ export function projectEvent(view: TranscriptView, event: SessionEvent): Transcr
459
507
  }
460
508
  }
461
509
  if (chunk.type === 'text-delta') {
462
- return { ...view, streaming: appendStreamingTail(view.streaming, chunk.text), stats }
510
+ return {
511
+ ...view,
512
+ streaming: appendStreamingTail(view.streaming, chunk.text),
513
+ stats,
514
+ }
463
515
  }
464
516
  if (chunk.type === 'reasoning-delta') {
465
- return { ...view, streamingReasoning: appendStreamingTail(view.streamingReasoning, chunk.text), stats }
517
+ return {
518
+ ...view,
519
+ streamingReasoning: appendStreamingTail(view.streamingReasoning, chunk.text),
520
+ stats,
521
+ }
466
522
  }
467
523
  return view
468
524
  }
@@ -484,11 +540,7 @@ export function projectEvent(view: TranscriptView, event: SessionEvent): Transcr
484
540
  ...view,
485
541
  streaming: '',
486
542
  streamingReasoning: '',
487
- entries: [...view.entries, {
488
- kind: 'assistant',
489
- text,
490
- reasoning,
491
- }],
543
+ entries: [...view.entries, { kind: 'assistant', text, reasoning, interrupted: event.data.interrupted === true ? true : undefined }],
492
544
  stats: {
493
545
  ...view.stats,
494
546
  llmMs: view.stats.llmMs + (started === undefined ? 0 : Math.max(0, event.time - started)),
@@ -519,11 +571,16 @@ export function projectEvent(view: TranscriptView, event: SessionEvent): Transcr
519
571
  const turnTools = view.anchors.turnTools.get(data.turn) ?? new Set<string>()
520
572
  turnTools.add(data.callId)
521
573
  view.anchors.turnTools.set(data.turn, turnTools)
574
+ const ordinal = view.toolCallOrdinal + 1
522
575
  return {
523
576
  ...view,
524
- entries: [...view.entries, {
525
- kind: 'tool',
577
+ toolCallOrdinal: ordinal,
578
+ entries: [
579
+ ...view.entries,
580
+ {
581
+ kind: 'tool',
526
582
  callId: data.callId,
583
+ ordinal,
527
584
  name: data.name,
528
585
  arguments: data.arguments,
529
586
  preview: toolArgumentsPreview(data.arguments, data.name),
@@ -607,7 +664,12 @@ export function projectEvent(view: TranscriptView, event: SessionEvent): Transcr
607
664
  }
608
665
  view.anchors.turnSteps.set(event.data.turn, key)
609
666
  view.anchors.stepStart.set(key, event.time)
610
- return { ...view, stats: { ...view.stats, steps: view.stats.steps + 1 } }
667
+ return {
668
+ ...view,
669
+ streaming: '',
670
+ streamingReasoning: '',
671
+ stats: { ...view.stats, steps: view.stats.steps + 1 },
672
+ }
611
673
  }
612
674
  case 'turn/end': {
613
675
  const reason = event.data.reason
@@ -651,13 +713,24 @@ export function projectEvent(view: TranscriptView, event: SessionEvent): Transcr
651
713
  for (const callId of turnToolSet) view.anchors.toolStart.delete(callId)
652
714
  view.anchors.turnTools.delete(event.data.turn)
653
715
  }
654
- if (appended.length === 0) return { ...view, busy: false, busySince: 0 }
655
- return { ...view, busy: false, busySince: 0, entries: [...view.entries, ...appended] }
716
+ if (appended.length === 0) {
717
+ return { ...view, busy: false, busySince: 0, streaming: '', streamingReasoning: '' }
718
+ }
719
+ return {
720
+ ...view,
721
+ busy: false,
722
+ busySince: 0,
723
+ streaming: '',
724
+ streamingReasoning: '',
725
+ entries: [...view.entries, ...appended],
726
+ }
656
727
  }
657
728
  case 'llm/retry': {
658
729
  const data = event.data
659
730
  return {
660
731
  ...view,
732
+ streaming: '',
733
+ streamingReasoning: '',
661
734
  entries: [...view.entries, {
662
735
  kind: 'retry',
663
736
  retryId: data.retryId,
@@ -842,6 +915,8 @@ export interface ReplayAccumulator {
842
915
  streaming: string
843
916
  streamingReasoning: string
844
917
  todos: readonly TodoItem[]
918
+ /** Global tool-call ordinal counter (see `TranscriptView.toolCallOrdinal`). */
919
+ toolCallOrdinal: number
845
920
  busy: boolean
846
921
  busySince: number
847
922
  model: string
@@ -877,6 +952,7 @@ export function createReplayAccumulator(): ReplayAccumulator {
877
952
  streaming: '',
878
953
  streamingReasoning: '',
879
954
  todos: [],
955
+ toolCallOrdinal: 0,
880
956
  busy: false,
881
957
  busySince: 0,
882
958
  model: '',
@@ -980,8 +1056,9 @@ export function replayProjectEvent(acc: ReplayAccumulator, event: SessionEvent):
980
1056
  }
981
1057
  }
982
1058
  const text = textOf(message.content)
1059
+ const images = imagesOf(message.content)
983
1060
  if (message.source.kind === 'user') {
984
- appendReplayEntry(acc, { kind: 'user', text, notice: false })
1061
+ appendReplayEntry(acc, { kind: 'user', text, notice: false, ...(images.length === 0 ? {} : { images }) })
985
1062
  acc.stats = {
986
1063
  ...acc.stats,
987
1064
  contextSegments: {
@@ -1023,7 +1100,8 @@ export function replayProjectEvent(acc: ReplayAccumulator, event: SessionEvent):
1023
1100
  }
1024
1101
  }
1025
1102
  for (const message of inserted) {
1026
- appendReplayEntry(acc, { kind: 'pending', messageId: message.id, target, text: pendingText(message.content) })
1103
+ const images = imagesOf(message.content)
1104
+ appendReplayEntry(acc, { kind: 'pending', messageId: message.id, target, text: pendingText(message.content), ...(images.length === 0 ? {} : { images }) })
1027
1105
  indexList(acc.pendingIndex, message.id).push(acc.entries.length - 1)
1028
1106
  ids.push(message.id)
1029
1107
  acc.ops += 1
@@ -1068,7 +1146,7 @@ export function replayProjectEvent(acc: ReplayAccumulator, event: SessionEvent):
1068
1146
  const reasoning = reasoningOf(event.data.message.content)
1069
1147
  acc.streaming = ''
1070
1148
  acc.streamingReasoning = ''
1071
- appendReplayEntry(acc, { kind: 'assistant', text, reasoning })
1149
+ appendReplayEntry(acc, { kind: 'assistant', text, reasoning, interrupted: event.data.interrupted === true ? true : undefined })
1072
1150
  acc.stats = {
1073
1151
  ...acc.stats,
1074
1152
  llmMs: acc.stats.llmMs + (started === undefined ? 0 : Math.max(0, event.time - started)),
@@ -1095,9 +1173,11 @@ export function replayProjectEvent(acc: ReplayAccumulator, event: SessionEvent):
1095
1173
  const turnTools = acc.turnTools.get(data.turn) ?? new Set<string>()
1096
1174
  turnTools.add(data.callId)
1097
1175
  acc.turnTools.set(data.turn, turnTools)
1176
+ acc.toolCallOrdinal += 1
1098
1177
  appendReplayEntry(acc, {
1099
1178
  kind: 'tool',
1100
1179
  callId: data.callId,
1180
+ ordinal: acc.toolCallOrdinal,
1101
1181
  name: data.name,
1102
1182
  arguments: data.arguments,
1103
1183
  preview: toolArgumentsPreview(data.arguments, data.name),
@@ -1173,12 +1253,16 @@ export function replayProjectEvent(acc: ReplayAccumulator, event: SessionEvent):
1173
1253
  }
1174
1254
  acc.turnSteps.set(event.data.turn, key)
1175
1255
  acc.stepStart.set(key, event.time)
1256
+ acc.streaming = ''
1257
+ acc.streamingReasoning = ''
1176
1258
  acc.stats = { ...acc.stats, steps: acc.stats.steps + 1 }
1177
1259
  return
1178
1260
  }
1179
1261
  case 'turn/end': {
1180
1262
  const reason = event.data.reason
1181
1263
  const appended: TranscriptEntry[] = []
1264
+ acc.streamingReasoning = ''
1265
+ acc.streaming = ''
1182
1266
  if (reason.kind === 'error') {
1183
1267
  const recovery = reason.error.code === 'MISSING_CREDENTIAL'
1184
1268
  ? ' · open /model to add an API key'
@@ -1217,6 +1301,8 @@ export function replayProjectEvent(acc: ReplayAccumulator, event: SessionEvent):
1217
1301
  }
1218
1302
  case 'llm/retry': {
1219
1303
  const data = event.data
1304
+ acc.streaming = ''
1305
+ acc.streamingReasoning = ''
1220
1306
  appendReplayEntry(acc, {
1221
1307
  kind: 'retry',
1222
1308
  retryId: data.retryId,
@@ -1354,6 +1440,7 @@ export function finishReplay(acc: ReplayAccumulator): TranscriptView {
1354
1440
  streaming: acc.streaming,
1355
1441
  streamingReasoning: acc.streamingReasoning,
1356
1442
  todos: acc.todos,
1443
+ toolCallOrdinal: acc.toolCallOrdinal,
1357
1444
  busy: acc.busy,
1358
1445
  busySince: acc.busySince,
1359
1446
  model: acc.model,