dsh-code 0.7.0 → 0.9.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 +30 -7
- package/README.md +30 -7
- package/lib/index.mjs +3791 -853
- package/lib/types/app.d.ts +90 -1
- package/lib/types/approval.d.ts +3 -1
- package/lib/types/history.d.ts +15 -4
- package/lib/types/index.d.ts +48 -0
- package/lib/types/kernel-panels.d.ts +65 -8
- package/lib/types/models.d.ts +15 -1
- package/lib/types/permissions.d.ts +37 -0
- package/lib/types/presets.d.ts +2 -0
- package/lib/types/provider-settings.d.ts +144 -0
- package/lib/types/questions.d.ts +2 -0
- package/lib/types/render/animations.d.ts +8 -6
- package/lib/types/render/lines.d.ts +6 -0
- package/lib/types/render/markdown.d.ts +3 -3
- package/lib/types/render/projection.d.ts +97 -3
- package/lib/types/render/status.d.ts +26 -36
- package/lib/types/render/text.d.ts +14 -7
- package/lib/types/render/tool-detail.d.ts +3 -1
- package/lib/types/render/tool-preview.d.ts +14 -1
- package/lib/types/session-directory.d.ts +61 -2
- package/lib/types/store.d.ts +13 -2
- package/lib/types/subagents.d.ts +60 -0
- package/lib/types/version.d.ts +5 -0
- package/package.json +1 -1
- package/src/app.ts +1200 -219
- package/src/approval.ts +161 -126
- package/src/history.ts +20 -5
- package/src/index.ts +577 -167
- package/src/kernel-panels.ts +354 -37
- package/src/models.ts +26 -0
- package/src/permissions.ts +85 -0
- package/src/presets.ts +12 -0
- package/src/provider-settings.ts +520 -0
- package/src/questions.ts +15 -5
- package/src/render/animations.ts +32 -18
- package/src/render/lines.ts +236 -218
- package/src/render/markdown.ts +302 -4
- package/src/render/projection.ts +670 -11
- package/src/render/status.ts +68 -162
- package/src/render/text.ts +28 -9
- package/src/render/tool-detail.ts +81 -40
- package/src/render/tool-preview.ts +77 -34
- package/src/session-directory.ts +171 -10
- package/src/skills.ts +8 -4
- package/src/store.ts +26 -8
- package/src/subagents.ts +165 -0
- package/src/version.ts +16 -0
package/src/render/animations.ts
CHANGED
|
@@ -115,19 +115,33 @@ export const DEEPSEEK_WAVE_BANDS: Readonly<Record<DeepseekWaveStyle, Readonly<Re
|
|
|
115
115
|
},
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
/** Extra display time applied to every Codex ignition style. */
|
|
119
|
+
export const DEEPSEEK_WAVE_DURATION_EXTENSION_MS = 200
|
|
120
|
+
|
|
121
|
+
/** Original Codex duration used as the animation's sampling timeline. */
|
|
122
|
+
function deepseekWaveBaseDuration(tier: DeepseekWaveTier, style: DeepseekWaveStyle): number {
|
|
123
|
+
switch (style) {
|
|
124
|
+
case 'aurora': return tier === 'flash' ? 1300 : 1600
|
|
125
|
+
case 'pulse': return tier === 'flash' ? 900 : 1250
|
|
126
|
+
case 'wave': return tier === 'flash' ? 1000 : 1300
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
118
130
|
/**
|
|
119
|
-
* Total
|
|
120
|
-
*
|
|
131
|
+
* Total visible duration: the Codex ignition duration plus 200ms so its motion
|
|
132
|
+
* remains readable in a busy terminal.
|
|
121
133
|
* @param tier - the active wave tier.
|
|
122
134
|
* @param style - the active ignition style.
|
|
123
135
|
* @returns the duration in milliseconds.
|
|
124
136
|
*/
|
|
125
137
|
export function deepseekWaveDuration(tier: DeepseekWaveTier, style: DeepseekWaveStyle = 'wave'): number {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
138
|
+
return deepseekWaveBaseDuration(tier, style) + DEEPSEEK_WAVE_DURATION_EXTENSION_MS
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Map the extended display timeline back onto the original Codex samples. */
|
|
142
|
+
function deepseekWaveSampleElapsedMs(tick: number, tier: DeepseekWaveTier, style: DeepseekWaveStyle): number {
|
|
143
|
+
const base = deepseekWaveBaseDuration(tier, style)
|
|
144
|
+
return tick * DEEPSEEK_WAVE_TICK_MS * base / deepseekWaveDuration(tier, style)
|
|
131
145
|
}
|
|
132
146
|
|
|
133
147
|
/**
|
|
@@ -282,8 +296,8 @@ export function deepseekWaveColumnBg(
|
|
|
282
296
|
hues: readonly [RgbTriple, RgbTriple, RgbTriple],
|
|
283
297
|
base: RgbTriple,
|
|
284
298
|
): RgbTriple | null {
|
|
285
|
-
const total =
|
|
286
|
-
const elapsed = (tick
|
|
299
|
+
const total = deepseekWaveBaseDuration(tier, style) / 1000
|
|
300
|
+
const elapsed = deepseekWaveSampleElapsedMs(tick, tier, style) / 1000
|
|
287
301
|
const fade = style === 'aurora' ? envelope(elapsed, total, 0.25, 0.40) : 1
|
|
288
302
|
const weights = [0, 0, 0]
|
|
289
303
|
for (const band of DEEPSEEK_WAVE_BANDS[style][tier]) {
|
|
@@ -311,14 +325,14 @@ export function deepseekWaveColumnBg(
|
|
|
311
325
|
}
|
|
312
326
|
|
|
313
327
|
/**
|
|
314
|
-
* The sparkle glyph for a tick — Codex `spark_frame
|
|
315
|
-
*
|
|
316
|
-
*
|
|
328
|
+
* The sparkle glyph for a tick — Codex `spark_frame`, sampled on the same
|
|
329
|
+
* proportionally slowed DeepSeek Wave timeline as the composer background.
|
|
330
|
+
* The Ink layer still must skip occupied cells.
|
|
317
331
|
* @param tick - wave frame at DEEPSEEK_WAVE_TICK_MS.
|
|
318
|
-
* @returns the sparkle glyph, or null outside the
|
|
332
|
+
* @returns the sparkle glyph, or null outside the stretched tail window.
|
|
319
333
|
*/
|
|
320
334
|
export function deepseekWaveSpark(tick: number): string | null {
|
|
321
|
-
const elapsed = tick
|
|
335
|
+
const elapsed = deepseekWaveSampleElapsedMs(tick, 'deepseek', 'wave')
|
|
322
336
|
if (elapsed < SPARK_START_MS) return null
|
|
323
337
|
const frame = Math.floor((elapsed - SPARK_START_MS) / SPARK_FRAME_MS)
|
|
324
338
|
return SPARK_GLYPHS[frame] ?? null
|
|
@@ -342,8 +356,8 @@ export function deepseekWaveBorderColor(
|
|
|
342
356
|
hues: readonly [RgbTriple, RgbTriple, RgbTriple],
|
|
343
357
|
dim: RgbTriple,
|
|
344
358
|
): RgbTriple {
|
|
345
|
-
const total =
|
|
346
|
-
const elapsed = (tick
|
|
359
|
+
const total = deepseekWaveBaseDuration(tier, style) / 1000
|
|
360
|
+
const elapsed = deepseekWaveSampleElapsedMs(tick, tier, style) / 1000
|
|
347
361
|
// The border stays visibly on the tier accent for the whole sweep (a
|
|
348
362
|
// floor keeps it glowing, not just cresting mid-wave): it ramps in as the
|
|
349
363
|
// first band launches and relaxes after the last crest passes.
|
|
@@ -361,8 +375,8 @@ export function deepseekWaveBorderColor(
|
|
|
361
375
|
* @returns true while the wordmark should be visible.
|
|
362
376
|
*/
|
|
363
377
|
export function deepseekWaveWordVisible(tick: number, tier: DeepseekWaveTier, style: DeepseekWaveStyle = 'wave'): boolean {
|
|
364
|
-
const total =
|
|
365
|
-
const elapsed = (tick
|
|
378
|
+
const total = deepseekWaveBaseDuration(tier, style) / 1000
|
|
379
|
+
const elapsed = deepseekWaveSampleElapsedMs(tick, tier, style) / 1000
|
|
366
380
|
return envelope(elapsed, total, total * 0.2, total * 0.35) > 0.25
|
|
367
381
|
}
|
|
368
382
|
|
package/src/render/lines.ts
CHANGED
|
@@ -1,218 +1,236 @@
|
|
|
1
|
-
/** Width-safe styled physical rows for bounded terminal panels. */
|
|
2
|
-
|
|
3
|
-
import type { TranscriptEntry } from './projection.ts'
|
|
4
|
-
import type { ToolDetail } from './tool-detail.ts'
|
|
5
|
-
import { renderMarkdown, visibleColumns, type MdStyle } from './markdown.ts'
|
|
6
|
-
import { formatTokens } from './status.ts'
|
|
7
|
-
import { displayText } from './text.ts'
|
|
8
|
-
|
|
9
|
-
/** Presentation classes mapped to Ink colors by the app boundary. */
|
|
10
|
-
export type LineStyle = MdStyle | 'brand' | 'success' | 'error' | 'warn' | 'dimItalic'
|
|
11
|
-
|
|
12
|
-
/** One styled run within a physical terminal row. */
|
|
13
|
-
export interface StyledSegment {
|
|
14
|
-
text: string
|
|
15
|
-
style: LineStyle
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** One row guaranteed not to exceed the requested terminal width. */
|
|
19
|
-
export interface StyledLine {
|
|
20
|
-
segments: readonly StyledSegment[]
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Construct one segment without leaking mutable objects into cached rows. */
|
|
24
|
-
export function lineSegment(text: string, style: LineStyle = 'plain'): StyledSegment {
|
|
25
|
-
return { text, style }
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Append a character while merging adjacent runs with the same style. */
|
|
29
|
-
function appendSegment(target: StyledSegment[], text: string, style: LineStyle): void {
|
|
30
|
-
const previous = target[target.length - 1]
|
|
31
|
-
if (previous?.style === style) {
|
|
32
|
-
target[target.length - 1] = { text: previous.text + text, style }
|
|
33
|
-
return
|
|
34
|
-
}
|
|
35
|
-
target.push({ text, style })
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Sanitize and hard-wrap styled content into exact physical rows.
|
|
40
|
-
* Tabs become two visible spaces because terminal tab stops are contextual
|
|
41
|
-
* and therefore cannot participate in a deterministic row budget.
|
|
42
|
-
*/
|
|
43
|
-
export function styledLines(segments: readonly StyledSegment[], columns: number): readonly StyledLine[] {
|
|
44
|
-
const width = Math.max(1, Math.floor(columns))
|
|
45
|
-
const lines: StyledLine[] = []
|
|
46
|
-
let current: StyledSegment[] = []
|
|
47
|
-
let used = 0
|
|
48
|
-
const flush = (): void => {
|
|
49
|
-
lines.push({ segments: current })
|
|
50
|
-
current = []
|
|
51
|
-
used = 0
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
for (const segment of segments) {
|
|
55
|
-
const safe = displayText(segment.text).replaceAll('\t', ' ').replaceAll('\r', '')
|
|
56
|
-
for (const char of safe) {
|
|
57
|
-
if (char === '\n') {
|
|
58
|
-
flush()
|
|
59
|
-
continue
|
|
60
|
-
}
|
|
61
|
-
const cells = visibleColumns(char)
|
|
62
|
-
if (used > 0 && used + cells > width) flush()
|
|
63
|
-
appendSegment(current, char, segment.style)
|
|
64
|
-
used += cells
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
if (current.length > 0 || lines.length === 0) flush()
|
|
68
|
-
return lines
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Plain/dim text convenience over {@link styledLines}. */
|
|
72
|
-
export function textLines(text: string, columns: number, style: LineStyle = 'plain'): readonly StyledLine[] {
|
|
73
|
-
return styledLines([lineSegment(text, style)], columns)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Markdown rows re-hardened so a single long word cannot escape the budget. */
|
|
77
|
-
export function markdownLines(text: string, columns: number): readonly StyledLine[] {
|
|
78
|
-
const width = Math.max(1, Math.floor(columns))
|
|
79
|
-
const parsed = renderMarkdown(displayText(text), Math.max(10, width))
|
|
80
|
-
return parsed.flatMap(line => styledLines(
|
|
81
|
-
line.segments.map(segment => lineSegment(segment.text, segment.style)),
|
|
82
|
-
width,
|
|
83
|
-
))
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
case '
|
|
110
|
-
return [
|
|
111
|
-
...
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
]
|
|
120
|
-
case '
|
|
121
|
-
return
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
lineSegment(
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
case '
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
:
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
1
|
+
/** Width-safe styled physical rows for bounded terminal panels. */
|
|
2
|
+
|
|
3
|
+
import type { TranscriptEntry } from './projection.ts'
|
|
4
|
+
import type { ToolDetail } from './tool-detail.ts'
|
|
5
|
+
import { renderMarkdown, visibleColumns, type MdStyle } from './markdown.ts'
|
|
6
|
+
import { formatTokens } from './status.ts'
|
|
7
|
+
import { displayText } from './text.ts'
|
|
8
|
+
|
|
9
|
+
/** Presentation classes mapped to Ink colors by the app boundary. */
|
|
10
|
+
export type LineStyle = MdStyle | 'brand' | 'success' | 'error' | 'warn' | 'dimItalic'
|
|
11
|
+
|
|
12
|
+
/** One styled run within a physical terminal row. */
|
|
13
|
+
export interface StyledSegment {
|
|
14
|
+
text: string
|
|
15
|
+
style: LineStyle
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** One row guaranteed not to exceed the requested terminal width. */
|
|
19
|
+
export interface StyledLine {
|
|
20
|
+
segments: readonly StyledSegment[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Construct one segment without leaking mutable objects into cached rows. */
|
|
24
|
+
export function lineSegment(text: string, style: LineStyle = 'plain'): StyledSegment {
|
|
25
|
+
return { text, style }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Append a character while merging adjacent runs with the same style. */
|
|
29
|
+
function appendSegment(target: StyledSegment[], text: string, style: LineStyle): void {
|
|
30
|
+
const previous = target[target.length - 1]
|
|
31
|
+
if (previous?.style === style) {
|
|
32
|
+
target[target.length - 1] = { text: previous.text + text, style }
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
target.push({ text, style })
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Sanitize and hard-wrap styled content into exact physical rows.
|
|
40
|
+
* Tabs become two visible spaces because terminal tab stops are contextual
|
|
41
|
+
* and therefore cannot participate in a deterministic row budget.
|
|
42
|
+
*/
|
|
43
|
+
export function styledLines(segments: readonly StyledSegment[], columns: number): readonly StyledLine[] {
|
|
44
|
+
const width = Math.max(1, Math.floor(columns))
|
|
45
|
+
const lines: StyledLine[] = []
|
|
46
|
+
let current: StyledSegment[] = []
|
|
47
|
+
let used = 0
|
|
48
|
+
const flush = (): void => {
|
|
49
|
+
lines.push({ segments: current })
|
|
50
|
+
current = []
|
|
51
|
+
used = 0
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const segment of segments) {
|
|
55
|
+
const safe = displayText(segment.text).replaceAll('\t', ' ').replaceAll('\r', '')
|
|
56
|
+
for (const char of safe) {
|
|
57
|
+
if (char === '\n') {
|
|
58
|
+
flush()
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
const cells = visibleColumns(char)
|
|
62
|
+
if (used > 0 && used + cells > width) flush()
|
|
63
|
+
appendSegment(current, char, segment.style)
|
|
64
|
+
used += cells
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (current.length > 0 || lines.length === 0) flush()
|
|
68
|
+
return lines
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Plain/dim text convenience over {@link styledLines}. */
|
|
72
|
+
export function textLines(text: string, columns: number, style: LineStyle = 'plain'): readonly StyledLine[] {
|
|
73
|
+
return styledLines([lineSegment(text, style)], columns)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Markdown rows re-hardened so a single long word cannot escape the budget. */
|
|
77
|
+
export function markdownLines(text: string, columns: number): readonly StyledLine[] {
|
|
78
|
+
const width = Math.max(1, Math.floor(columns))
|
|
79
|
+
const parsed = renderMarkdown(displayText(text), Math.max(10, width))
|
|
80
|
+
return parsed.flatMap(line => styledLines(
|
|
81
|
+
line.segments.map(segment => lineSegment(segment.text, segment.style)),
|
|
82
|
+
width,
|
|
83
|
+
))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Codex-style reasoning rows: the marker occupies the reply gutter and every
|
|
88
|
+
* wrapped or explicit continuation starts with the same two-column indent, so
|
|
89
|
+
* reasoning content and assistant Markdown share one left edge.
|
|
90
|
+
*/
|
|
91
|
+
export function reasoningLines(text: string, columns: number): readonly StyledLine[] {
|
|
92
|
+
const width = Math.max(1, Math.floor(columns))
|
|
93
|
+
if (width < 3) return textLines(text, width, 'dimItalic')
|
|
94
|
+
const contentWidth = width - 2
|
|
95
|
+
const content = displayText(text).replaceAll('\t', ' ').replaceAll('\r', '')
|
|
96
|
+
.split('\n')
|
|
97
|
+
.flatMap(line => styledLines([lineSegment(line, 'dimItalic')], contentWidth))
|
|
98
|
+
return content.map((line, index) => ({
|
|
99
|
+
segments: [
|
|
100
|
+
lineSegment(index === 0 ? '✻ ' : ' ', 'dimItalic'),
|
|
101
|
+
...line.segments,
|
|
102
|
+
],
|
|
103
|
+
}))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Expanded structured tool detail as scrollable, width-safe rows. */
|
|
107
|
+
function toolDetailLines(detail: ToolDetail, columns: number): readonly StyledLine[] {
|
|
108
|
+
switch (detail.kind) {
|
|
109
|
+
case 'diff':
|
|
110
|
+
return detail.diffs.flatMap(diff => [
|
|
111
|
+
...styledLines([
|
|
112
|
+
lineSegment(' ── ', 'dim'),
|
|
113
|
+
lineSegment(diff.path, 'dim'),
|
|
114
|
+
lineSegment(diff.truncated ? ' (diff truncated)' : '', 'dim'),
|
|
115
|
+
], columns),
|
|
116
|
+
...diff.lines.flatMap(line => styledLines([
|
|
117
|
+
lineSegment(` ${line.mark}${line.text}`, line.mark === '+' ? 'success' : line.mark === '-' ? 'error' : 'dim'),
|
|
118
|
+
], columns)),
|
|
119
|
+
])
|
|
120
|
+
case 'read':
|
|
121
|
+
return [
|
|
122
|
+
...textLines(
|
|
123
|
+
` ── ${detail.path} · lines ${detail.offset}-${detail.lines.length > 0 ? detail.lines[detail.lines.length - 1]!.number : detail.offset - 1} of ${detail.totalLines}${detail.truncated ? ' (window truncated)' : ''}`,
|
|
124
|
+
columns,
|
|
125
|
+
'dim',
|
|
126
|
+
),
|
|
127
|
+
...detail.lines.flatMap(line => textLines(` ${String(line.number).padStart(5, ' ')} | ${line.text}`, columns, 'dim')),
|
|
128
|
+
]
|
|
129
|
+
case 'web-search':
|
|
130
|
+
return [
|
|
131
|
+
...detail.sources.flatMap(source => [
|
|
132
|
+
...styledLines([
|
|
133
|
+
lineSegment(` ? ${source.title ?? source.url}`, 'brand'),
|
|
134
|
+
lineSegment(` - ${source.url}`, 'dim'),
|
|
135
|
+
], columns),
|
|
136
|
+
...(source.snippet === '' ? [] : textLines(` ${source.snippet}`, columns, 'dim')),
|
|
137
|
+
]),
|
|
138
|
+
...textLines(` ${detail.sources.length} sources${detail.truncated ? ' (capped)' : ''}`, columns, 'dim'),
|
|
139
|
+
]
|
|
140
|
+
case 'web-fetch':
|
|
141
|
+
return textLines(` ${detail.url} · HTTP ${detail.statusCode}`, columns, 'dim')
|
|
142
|
+
case 'raw':
|
|
143
|
+
return [
|
|
144
|
+
...textLines(` ${detail.text}`, columns, 'dim'),
|
|
145
|
+
...textLines(detail.truncated ? ' … (output truncated)' : ' (end of output)', columns, 'dim'),
|
|
146
|
+
]
|
|
147
|
+
default: {
|
|
148
|
+
const exhaustive: never = detail
|
|
149
|
+
return exhaustive
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Convert one durable transcript entry to its complete scrollable row model.
|
|
156
|
+
* The source entry stays intact; only the caller's visible slice is rendered.
|
|
157
|
+
*/
|
|
158
|
+
export function transcriptEntryLines(entry: TranscriptEntry, columns: number): readonly StyledLine[] {
|
|
159
|
+
const width = Math.max(1, Math.floor(columns))
|
|
160
|
+
switch (entry.kind) {
|
|
161
|
+
case 'user':
|
|
162
|
+
return styledLines([
|
|
163
|
+
lineSegment(entry.notice ? '⤷ ' : '❯ ', entry.notice ? 'dim' : 'brand'),
|
|
164
|
+
lineSegment(entry.text, entry.notice ? 'dim' : 'plain'),
|
|
165
|
+
], width)
|
|
166
|
+
case 'pending':
|
|
167
|
+
// Codex PendingSteer: a queued prompt renders exactly like an ordinary
|
|
168
|
+
// user row, so the durable user/message retires it without any flicker.
|
|
169
|
+
return styledLines([
|
|
170
|
+
lineSegment('❯ ', 'brand'),
|
|
171
|
+
lineSegment(entry.text, 'plain'),
|
|
172
|
+
], width)
|
|
173
|
+
case 'assistant': {
|
|
174
|
+
const reasoning = entry.reasoning === '' ? [] : reasoningLines(entry.reasoning, width)
|
|
175
|
+
// Every reply row carries the composer's two-column gutter, so reply
|
|
176
|
+
// text aligns with the input cursor (Codex LIVE_PREFIX alignment); the
|
|
177
|
+
// wrap budget shrinks by the same amount so no line double-wraps.
|
|
178
|
+
const body = markdownLines(entry.text, Math.max(10, width - 2))
|
|
179
|
+
.map(line => ({ segments: [{ text: ' ', style: 'plain' as const }, ...line.segments] }))
|
|
180
|
+
return [...reasoning, ...body]
|
|
181
|
+
}
|
|
182
|
+
case 'tool': {
|
|
183
|
+
const mark = entry.state === 'running' ? '●' : entry.state === 'error' ? '⨯' : '⏺'
|
|
184
|
+
const markStyle: LineStyle = entry.state === 'running' ? 'brand' : entry.state === 'error' ? 'error' : 'success'
|
|
185
|
+
return [
|
|
186
|
+
...styledLines([
|
|
187
|
+
lineSegment(`${mark} `, markStyle),
|
|
188
|
+
lineSegment(entry.name, 'brand'),
|
|
189
|
+
lineSegment(entry.preview === '' ? '' : ` ${entry.preview}`, 'dim'),
|
|
190
|
+
], width),
|
|
191
|
+
// A delegation card carries what the child was asked (Codex's
|
|
192
|
+
// SpawnAgent prompt preview) while it runs, before any result.
|
|
193
|
+
...(entry.prompt === '' ? [] : textLines(` └ ${entry.prompt}`, width, 'dim')),
|
|
194
|
+
...(entry.summary === '' ? [] : textLines(` ⎿ ${entry.summary}`, width, entry.state === 'error' ? 'error' : 'dim')),
|
|
195
|
+
...(entry.detail === undefined ? [] : toolDetailLines(entry.detail, width)),
|
|
196
|
+
]
|
|
197
|
+
}
|
|
198
|
+
case 'command': {
|
|
199
|
+
const mark = entry.state === 'running' ? '●' : entry.state === 'error' ? '⨯' : '⏺'
|
|
200
|
+
const markStyle: LineStyle = entry.state === 'running' ? 'brand' : entry.state === 'error' ? 'error' : 'success'
|
|
201
|
+
return [
|
|
202
|
+
...styledLines([
|
|
203
|
+
lineSegment(`${mark} `, markStyle),
|
|
204
|
+
lineSegment(`/${entry.name}`, 'brand'),
|
|
205
|
+
lineSegment(entry.args === '' ? '' : ` ${entry.args}`, 'dim'),
|
|
206
|
+
], width),
|
|
207
|
+
...(entry.summary === '' ? [] : textLines(` ⎿ ${entry.summary}`, width, entry.state === 'error' ? 'error' : 'dim')),
|
|
208
|
+
]
|
|
209
|
+
}
|
|
210
|
+
case 'turn-marker':
|
|
211
|
+
return textLines(` ⏹ ${entry.text}`, width, 'dim')
|
|
212
|
+
case 'compaction':
|
|
213
|
+
return textLines(entry.ok
|
|
214
|
+
? ` ⧉ compacted ~${formatTokens(entry.tokens)} tokens`
|
|
215
|
+
: ` ⧉ compaction failed: ${entry.error}`, width, 'dim')
|
|
216
|
+
case 'retry':
|
|
217
|
+
return textLines(
|
|
218
|
+
` ↻ retry ${entry.attempt}/${entry.max} · ${entry.code} · ${Math.round(entry.delayMs / 100) / 10}s`,
|
|
219
|
+
width,
|
|
220
|
+
entry.state === 'running' ? 'warn' : 'dim',
|
|
221
|
+
)
|
|
222
|
+
case 'files':
|
|
223
|
+
return entry.paths.length === 0
|
|
224
|
+
? textLines(' ⎄ no changed files', width, 'dim')
|
|
225
|
+
: [
|
|
226
|
+
...textLines(` ⎄ ${entry.paths.length} changed file${entry.paths.length === 1 ? '' : 's'}`, width, 'dim'),
|
|
227
|
+
...entry.paths.flatMap(path => textLines(` ${path}`, width, 'dim')),
|
|
228
|
+
]
|
|
229
|
+
case 'error':
|
|
230
|
+
return textLines(entry.text, width, 'error')
|
|
231
|
+
default: {
|
|
232
|
+
const exhaustive: never = entry
|
|
233
|
+
return exhaustive
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|