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.
- package/README.en.md +278 -249
- package/README.md +131 -102
- package/bin/deepseek.mjs +100 -6
- package/cordis.patch.yml +36 -1
- package/lib/index.mjs +3055 -819
- package/lib/startup.mjs +21 -11
- package/lib/{theme-BEi4i_aN.mjs → theme-DCT8Y2xf.mjs} +13 -9
- package/lib/types/app.d.ts +84 -16
- package/lib/types/attachments.d.ts +20 -0
- package/lib/types/authorization-panel.d.ts +22 -0
- package/lib/types/authorization.d.ts +36 -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 +41 -0
- package/lib/types/mentions.d.ts +30 -38
- package/lib/types/models.d.ts +3 -1
- package/lib/types/permissions.d.ts +4 -14
- package/lib/types/presets.d.ts +5 -20
- 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 +6 -13
- 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 +159 -141
- package/src/app.ts +1490 -663
- package/src/attachments.ts +128 -0
- package/src/authorization-panel.ts +285 -0
- package/src/authorization.ts +147 -0
- package/src/editor.ts +51 -0
- package/src/fork.ts +31 -0
- package/src/git-workflow.ts +87 -0
- package/src/index.ts +1523 -1374
- package/src/internals.ts +14 -1
- package/src/kernel-panels.ts +914 -798
- package/src/keyboard.ts +126 -0
- package/src/mentions.ts +78 -117
- package/src/models.ts +20 -14
- package/src/permissions.ts +5 -13
- package/src/presets.ts +6 -22
- package/src/provider-settings.ts +95 -1
- package/src/render/animations.ts +420 -450
- 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 +106 -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 +4 -4
- 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,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 {
|
|
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 {
|
|
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
|
-
|
|
525
|
-
|
|
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 {
|
|
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)
|
|
655
|
-
|
|
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
|
-
|
|
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,
|