dsh-code 0.9.1 → 1.0.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.
- package/README.en.md +29 -13
- package/README.md +264 -248
- package/bin/deepseek.mjs +100 -6
- package/cordis.patch.yml +29 -1
- package/lib/index.mjs +2223 -687
- package/lib/startup.mjs +21 -11
- package/lib/{theme-BEi4i_aN.mjs → theme-DCT8Y2xf.mjs} +13 -9
- package/lib/types/app.d.ts +66 -14
- package/lib/types/attachments.d.ts +7 -0
- package/lib/types/editor.d.ts +6 -0
- package/lib/types/fork.d.ts +8 -0
- package/lib/types/git-workflow.d.ts +23 -0
- package/lib/types/index.d.ts +6 -0
- package/lib/types/kernel-panels.d.ts +39 -0
- package/lib/types/keyboard.d.ts +43 -0
- package/lib/types/mentions.d.ts +28 -38
- package/lib/types/presets.d.ts +1 -3
- package/lib/types/provider-settings.d.ts +16 -0
- package/lib/types/render/animations.d.ts +10 -39
- package/lib/types/render/editor.d.ts +137 -0
- package/lib/types/render/export.d.ts +1 -1
- package/lib/types/render/lines.d.ts +6 -2
- package/lib/types/render/markdown.d.ts +3 -1
- package/lib/types/render/projection.d.ts +29 -3
- package/lib/types/render/status.d.ts +5 -12
- package/lib/types/session-directory.d.ts +1 -3
- package/lib/types/startup.d.ts +14 -11
- package/lib/types/store.d.ts +11 -9
- package/lib/types/subagents.d.ts +3 -3
- package/lib/types/theme.d.ts +14 -1
- package/lib/types/version.d.ts +15 -2
- package/package.json +153 -141
- package/src/app.ts +4455 -3917
- package/src/attachments.ts +44 -0
- package/src/editor.ts +51 -0
- package/src/fork.ts +31 -0
- package/src/git-workflow.ts +87 -0
- package/src/index.ts +1510 -1374
- package/src/internals.ts +14 -1
- package/src/kernel-panels.ts +914 -798
- package/src/keyboard.ts +125 -0
- package/src/mentions.ts +72 -117
- package/src/presets.ts +1 -4
- package/src/provider-settings.ts +94 -0
- package/src/render/animations.ts +25 -55
- package/src/render/editor.ts +398 -0
- package/src/render/export.ts +79 -79
- package/src/render/lines.ts +342 -236
- package/src/render/markdown.ts +99 -26
- package/src/render/projection.ts +102 -19
- package/src/render/status.ts +713 -650
- package/src/render/text.ts +150 -150
- package/src/render/tool-detail.ts +3 -1
- package/src/session-directory.ts +3 -3
- package/src/startup.ts +136 -119
- package/src/store.ts +23 -11
- package/src/subagents.ts +13 -5
- package/src/theme.ts +214 -206
- package/src/version.ts +58 -1
package/src/render/markdown.ts
CHANGED
|
@@ -40,34 +40,98 @@ export function visibleColumns(text: string): number {
|
|
|
40
40
|
return columns
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
return lines
|
|
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(
|
|
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
|
-
|
|
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
|
|
package/src/render/projection.ts
CHANGED
|
@@ -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
|
|
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,27 @@ 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
|
+
return `[image: ${name} · ${image.width}×${image.height} · ${image.bytes} B]`
|
|
342
|
+
}).join('\n')
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Prompt text with its durable image labels, without exposing local paths or bytes. */
|
|
346
|
+
export function promptDisplayText(entry: Pick<UserEntry | PendingEntry, 'text' | 'images'>): string {
|
|
347
|
+
const labels = imageLabels(entry.images)
|
|
348
|
+
return entry.text === '' ? labels : labels === '' ? entry.text : `${entry.text}\n${labels}`
|
|
349
|
+
}
|
|
350
|
+
|
|
310
351
|
/** Join the reasoning blocks of a content list; non-reasoning blocks contribute nothing. */
|
|
311
352
|
function reasoningOf(content: readonly ContentBlock[]): string {
|
|
312
353
|
return content.filter(block => block.type === 'reasoning').map(block => block.text).join('')
|
|
@@ -337,6 +378,7 @@ export function createTranscriptView(): TranscriptView {
|
|
|
337
378
|
streaming: '',
|
|
338
379
|
streamingReasoning: '',
|
|
339
380
|
todos: [],
|
|
381
|
+
toolCallOrdinal: 0,
|
|
340
382
|
busy: false,
|
|
341
383
|
busySince: 0,
|
|
342
384
|
model: '',
|
|
@@ -381,11 +423,12 @@ export function projectEvent(view: TranscriptView, event: SessionEvent): Transcr
|
|
|
381
423
|
// to a bounded notice row, exactly like collapsed transcript context
|
|
382
424
|
// elsewhere in the product; only direct human prompts render in full.
|
|
383
425
|
const text = textOf(message.content)
|
|
426
|
+
const images = imagesOf(message.content)
|
|
384
427
|
if (message.source.kind === 'user') {
|
|
385
428
|
return {
|
|
386
429
|
...view,
|
|
387
430
|
pending,
|
|
388
|
-
entries: [...entries, { kind: 'user', text, notice: false }],
|
|
431
|
+
entries: [...entries, { kind: 'user', text, notice: false, ...(images.length === 0 ? {} : { images }) }],
|
|
389
432
|
stats: {
|
|
390
433
|
...view.stats,
|
|
391
434
|
contextSegments: {
|
|
@@ -436,6 +479,7 @@ export function projectEvent(view: TranscriptView, event: SessionEvent): Transcr
|
|
|
436
479
|
messageId: message.id,
|
|
437
480
|
target,
|
|
438
481
|
text: pendingText(message.content),
|
|
482
|
+
...imagesOf(message.content).length === 0 ? {} : { images: imagesOf(message.content) },
|
|
439
483
|
}]
|
|
440
484
|
}
|
|
441
485
|
return { ...view, entries, pending: { ...view.pending, [target]: nextIds } }
|
|
@@ -459,10 +503,18 @@ export function projectEvent(view: TranscriptView, event: SessionEvent): Transcr
|
|
|
459
503
|
}
|
|
460
504
|
}
|
|
461
505
|
if (chunk.type === 'text-delta') {
|
|
462
|
-
return {
|
|
506
|
+
return {
|
|
507
|
+
...view,
|
|
508
|
+
streaming: appendStreamingTail(view.streaming, chunk.text),
|
|
509
|
+
stats,
|
|
510
|
+
}
|
|
463
511
|
}
|
|
464
512
|
if (chunk.type === 'reasoning-delta') {
|
|
465
|
-
return {
|
|
513
|
+
return {
|
|
514
|
+
...view,
|
|
515
|
+
streamingReasoning: appendStreamingTail(view.streamingReasoning, chunk.text),
|
|
516
|
+
stats,
|
|
517
|
+
}
|
|
466
518
|
}
|
|
467
519
|
return view
|
|
468
520
|
}
|
|
@@ -484,11 +536,7 @@ export function projectEvent(view: TranscriptView, event: SessionEvent): Transcr
|
|
|
484
536
|
...view,
|
|
485
537
|
streaming: '',
|
|
486
538
|
streamingReasoning: '',
|
|
487
|
-
entries: [...view.entries, {
|
|
488
|
-
kind: 'assistant',
|
|
489
|
-
text,
|
|
490
|
-
reasoning,
|
|
491
|
-
}],
|
|
539
|
+
entries: [...view.entries, { kind: 'assistant', text, reasoning, interrupted: event.data.interrupted === true ? true : undefined }],
|
|
492
540
|
stats: {
|
|
493
541
|
...view.stats,
|
|
494
542
|
llmMs: view.stats.llmMs + (started === undefined ? 0 : Math.max(0, event.time - started)),
|
|
@@ -519,11 +567,16 @@ export function projectEvent(view: TranscriptView, event: SessionEvent): Transcr
|
|
|
519
567
|
const turnTools = view.anchors.turnTools.get(data.turn) ?? new Set<string>()
|
|
520
568
|
turnTools.add(data.callId)
|
|
521
569
|
view.anchors.turnTools.set(data.turn, turnTools)
|
|
570
|
+
const ordinal = view.toolCallOrdinal + 1
|
|
522
571
|
return {
|
|
523
572
|
...view,
|
|
524
|
-
|
|
525
|
-
|
|
573
|
+
toolCallOrdinal: ordinal,
|
|
574
|
+
entries: [
|
|
575
|
+
...view.entries,
|
|
576
|
+
{
|
|
577
|
+
kind: 'tool',
|
|
526
578
|
callId: data.callId,
|
|
579
|
+
ordinal,
|
|
527
580
|
name: data.name,
|
|
528
581
|
arguments: data.arguments,
|
|
529
582
|
preview: toolArgumentsPreview(data.arguments, data.name),
|
|
@@ -607,7 +660,12 @@ export function projectEvent(view: TranscriptView, event: SessionEvent): Transcr
|
|
|
607
660
|
}
|
|
608
661
|
view.anchors.turnSteps.set(event.data.turn, key)
|
|
609
662
|
view.anchors.stepStart.set(key, event.time)
|
|
610
|
-
return {
|
|
663
|
+
return {
|
|
664
|
+
...view,
|
|
665
|
+
streaming: '',
|
|
666
|
+
streamingReasoning: '',
|
|
667
|
+
stats: { ...view.stats, steps: view.stats.steps + 1 },
|
|
668
|
+
}
|
|
611
669
|
}
|
|
612
670
|
case 'turn/end': {
|
|
613
671
|
const reason = event.data.reason
|
|
@@ -651,13 +709,24 @@ export function projectEvent(view: TranscriptView, event: SessionEvent): Transcr
|
|
|
651
709
|
for (const callId of turnToolSet) view.anchors.toolStart.delete(callId)
|
|
652
710
|
view.anchors.turnTools.delete(event.data.turn)
|
|
653
711
|
}
|
|
654
|
-
if (appended.length === 0)
|
|
655
|
-
|
|
712
|
+
if (appended.length === 0) {
|
|
713
|
+
return { ...view, busy: false, busySince: 0, streaming: '', streamingReasoning: '' }
|
|
714
|
+
}
|
|
715
|
+
return {
|
|
716
|
+
...view,
|
|
717
|
+
busy: false,
|
|
718
|
+
busySince: 0,
|
|
719
|
+
streaming: '',
|
|
720
|
+
streamingReasoning: '',
|
|
721
|
+
entries: [...view.entries, ...appended],
|
|
722
|
+
}
|
|
656
723
|
}
|
|
657
724
|
case 'llm/retry': {
|
|
658
725
|
const data = event.data
|
|
659
726
|
return {
|
|
660
727
|
...view,
|
|
728
|
+
streaming: '',
|
|
729
|
+
streamingReasoning: '',
|
|
661
730
|
entries: [...view.entries, {
|
|
662
731
|
kind: 'retry',
|
|
663
732
|
retryId: data.retryId,
|
|
@@ -842,6 +911,8 @@ export interface ReplayAccumulator {
|
|
|
842
911
|
streaming: string
|
|
843
912
|
streamingReasoning: string
|
|
844
913
|
todos: readonly TodoItem[]
|
|
914
|
+
/** Global tool-call ordinal counter (see `TranscriptView.toolCallOrdinal`). */
|
|
915
|
+
toolCallOrdinal: number
|
|
845
916
|
busy: boolean
|
|
846
917
|
busySince: number
|
|
847
918
|
model: string
|
|
@@ -877,6 +948,7 @@ export function createReplayAccumulator(): ReplayAccumulator {
|
|
|
877
948
|
streaming: '',
|
|
878
949
|
streamingReasoning: '',
|
|
879
950
|
todos: [],
|
|
951
|
+
toolCallOrdinal: 0,
|
|
880
952
|
busy: false,
|
|
881
953
|
busySince: 0,
|
|
882
954
|
model: '',
|
|
@@ -980,8 +1052,9 @@ export function replayProjectEvent(acc: ReplayAccumulator, event: SessionEvent):
|
|
|
980
1052
|
}
|
|
981
1053
|
}
|
|
982
1054
|
const text = textOf(message.content)
|
|
1055
|
+
const images = imagesOf(message.content)
|
|
983
1056
|
if (message.source.kind === 'user') {
|
|
984
|
-
appendReplayEntry(acc, { kind: 'user', text, notice: false })
|
|
1057
|
+
appendReplayEntry(acc, { kind: 'user', text, notice: false, ...(images.length === 0 ? {} : { images }) })
|
|
985
1058
|
acc.stats = {
|
|
986
1059
|
...acc.stats,
|
|
987
1060
|
contextSegments: {
|
|
@@ -1023,7 +1096,8 @@ export function replayProjectEvent(acc: ReplayAccumulator, event: SessionEvent):
|
|
|
1023
1096
|
}
|
|
1024
1097
|
}
|
|
1025
1098
|
for (const message of inserted) {
|
|
1026
|
-
|
|
1099
|
+
const images = imagesOf(message.content)
|
|
1100
|
+
appendReplayEntry(acc, { kind: 'pending', messageId: message.id, target, text: pendingText(message.content), ...(images.length === 0 ? {} : { images }) })
|
|
1027
1101
|
indexList(acc.pendingIndex, message.id).push(acc.entries.length - 1)
|
|
1028
1102
|
ids.push(message.id)
|
|
1029
1103
|
acc.ops += 1
|
|
@@ -1068,7 +1142,7 @@ export function replayProjectEvent(acc: ReplayAccumulator, event: SessionEvent):
|
|
|
1068
1142
|
const reasoning = reasoningOf(event.data.message.content)
|
|
1069
1143
|
acc.streaming = ''
|
|
1070
1144
|
acc.streamingReasoning = ''
|
|
1071
|
-
appendReplayEntry(acc, { kind: 'assistant', text, reasoning })
|
|
1145
|
+
appendReplayEntry(acc, { kind: 'assistant', text, reasoning, interrupted: event.data.interrupted === true ? true : undefined })
|
|
1072
1146
|
acc.stats = {
|
|
1073
1147
|
...acc.stats,
|
|
1074
1148
|
llmMs: acc.stats.llmMs + (started === undefined ? 0 : Math.max(0, event.time - started)),
|
|
@@ -1095,9 +1169,11 @@ export function replayProjectEvent(acc: ReplayAccumulator, event: SessionEvent):
|
|
|
1095
1169
|
const turnTools = acc.turnTools.get(data.turn) ?? new Set<string>()
|
|
1096
1170
|
turnTools.add(data.callId)
|
|
1097
1171
|
acc.turnTools.set(data.turn, turnTools)
|
|
1172
|
+
acc.toolCallOrdinal += 1
|
|
1098
1173
|
appendReplayEntry(acc, {
|
|
1099
1174
|
kind: 'tool',
|
|
1100
1175
|
callId: data.callId,
|
|
1176
|
+
ordinal: acc.toolCallOrdinal,
|
|
1101
1177
|
name: data.name,
|
|
1102
1178
|
arguments: data.arguments,
|
|
1103
1179
|
preview: toolArgumentsPreview(data.arguments, data.name),
|
|
@@ -1173,12 +1249,16 @@ export function replayProjectEvent(acc: ReplayAccumulator, event: SessionEvent):
|
|
|
1173
1249
|
}
|
|
1174
1250
|
acc.turnSteps.set(event.data.turn, key)
|
|
1175
1251
|
acc.stepStart.set(key, event.time)
|
|
1252
|
+
acc.streaming = ''
|
|
1253
|
+
acc.streamingReasoning = ''
|
|
1176
1254
|
acc.stats = { ...acc.stats, steps: acc.stats.steps + 1 }
|
|
1177
1255
|
return
|
|
1178
1256
|
}
|
|
1179
1257
|
case 'turn/end': {
|
|
1180
1258
|
const reason = event.data.reason
|
|
1181
1259
|
const appended: TranscriptEntry[] = []
|
|
1260
|
+
acc.streamingReasoning = ''
|
|
1261
|
+
acc.streaming = ''
|
|
1182
1262
|
if (reason.kind === 'error') {
|
|
1183
1263
|
const recovery = reason.error.code === 'MISSING_CREDENTIAL'
|
|
1184
1264
|
? ' · open /model to add an API key'
|
|
@@ -1217,6 +1297,8 @@ export function replayProjectEvent(acc: ReplayAccumulator, event: SessionEvent):
|
|
|
1217
1297
|
}
|
|
1218
1298
|
case 'llm/retry': {
|
|
1219
1299
|
const data = event.data
|
|
1300
|
+
acc.streaming = ''
|
|
1301
|
+
acc.streamingReasoning = ''
|
|
1220
1302
|
appendReplayEntry(acc, {
|
|
1221
1303
|
kind: 'retry',
|
|
1222
1304
|
retryId: data.retryId,
|
|
@@ -1354,6 +1436,7 @@ export function finishReplay(acc: ReplayAccumulator): TranscriptView {
|
|
|
1354
1436
|
streaming: acc.streaming,
|
|
1355
1437
|
streamingReasoning: acc.streamingReasoning,
|
|
1356
1438
|
todos: acc.todos,
|
|
1439
|
+
toolCallOrdinal: acc.toolCallOrdinal,
|
|
1357
1440
|
busy: acc.busy,
|
|
1358
1441
|
busySince: acc.busySince,
|
|
1359
1442
|
model: acc.model,
|