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/lines.ts
CHANGED
|
@@ -1,236 +1,342 @@
|
|
|
1
|
-
/** Width-safe styled physical rows for bounded terminal panels. */
|
|
2
|
-
|
|
3
|
-
import type
|
|
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
|
-
/**
|
|
77
|
-
|
|
78
|
-
const width = Math.max(1, Math.floor(columns))
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
1
|
+
/** Width-safe styled physical rows for bounded terminal panels. */
|
|
2
|
+
|
|
3
|
+
import { promptDisplayText, 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, truncateColumns } 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
|
+
/** Prefix every wrapped physical row without exceeding the column budget. */
|
|
77
|
+
function prefixedStyledLines(segments: readonly StyledSegment[], columns: number, prefix: string, prefixStyle: LineStyle = 'plain'): readonly StyledLine[] {
|
|
78
|
+
const width = Math.max(1, Math.floor(columns))
|
|
79
|
+
const prefixWidth = Math.min(width, visibleColumns(prefix))
|
|
80
|
+
const bodyWidth = Math.max(1, width - prefixWidth)
|
|
81
|
+
return styledLines(segments, bodyWidth).map(line => ({
|
|
82
|
+
segments: [lineSegment(prefix, prefixStyle), ...line.segments],
|
|
83
|
+
}))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Text convenience for a tool row whose continuation must keep its gutter. */
|
|
87
|
+
function prefixedTextLines(text: string, columns: number, prefix: string, style: LineStyle = 'plain'): readonly StyledLine[] {
|
|
88
|
+
return prefixedStyledLines([lineSegment(text, style)], columns, prefix, style)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Wrap styled segments with a hanging indent: the first physical row carries
|
|
93
|
+
* `firstPrefix` (often a marker plus gutter) and every wrapped continuation
|
|
94
|
+
* carries the narrower `contPrefix`, so long tool summaries and prompts
|
|
95
|
+
* align under their card instead of falling back to column zero. The first
|
|
96
|
+
* row may hold one prefix-width more than the continuations.
|
|
97
|
+
*/
|
|
98
|
+
function hangingStyledLines(
|
|
99
|
+
segments: readonly StyledSegment[],
|
|
100
|
+
columns: number,
|
|
101
|
+
firstPrefix: string,
|
|
102
|
+
firstStyle: LineStyle,
|
|
103
|
+
contPrefix: string,
|
|
104
|
+
contStyle: LineStyle = firstStyle,
|
|
105
|
+
): readonly StyledLine[] {
|
|
106
|
+
const width = Math.max(2, Math.floor(columns))
|
|
107
|
+
const firstPrefixText = truncateColumns(firstPrefix, Math.max(1, width - 1))
|
|
108
|
+
const contPrefixText = truncateColumns(contPrefix, Math.max(1, width - 1))
|
|
109
|
+
const firstBudget = Math.max(1, width - visibleColumns(firstPrefixText))
|
|
110
|
+
const contBudget = Math.max(1, width - visibleColumns(contPrefixText))
|
|
111
|
+
const lines: StyledLine[] = []
|
|
112
|
+
let current: StyledSegment[] = []
|
|
113
|
+
let used = 0
|
|
114
|
+
let budget = firstBudget
|
|
115
|
+
const flush = (): void => {
|
|
116
|
+
lines.push({ segments: current })
|
|
117
|
+
current = []
|
|
118
|
+
used = 0
|
|
119
|
+
budget = contBudget
|
|
120
|
+
}
|
|
121
|
+
for (const segment of segments) {
|
|
122
|
+
const safe = displayText(segment.text).replaceAll('\t', ' ').replaceAll('\r', '')
|
|
123
|
+
for (const char of safe) {
|
|
124
|
+
if (char === '\n') {
|
|
125
|
+
flush()
|
|
126
|
+
continue
|
|
127
|
+
}
|
|
128
|
+
const cells = visibleColumns(char)
|
|
129
|
+
if (used > 0 && used + cells > budget) flush()
|
|
130
|
+
appendSegment(current, char, segment.style)
|
|
131
|
+
used += cells
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (current.length > 0 || lines.length === 0) flush()
|
|
135
|
+
return lines.map((line, index) => ({
|
|
136
|
+
segments: [
|
|
137
|
+
lineSegment(index === 0 ? firstPrefixText : contPrefixText, index === 0 ? firstStyle : contStyle),
|
|
138
|
+
...line.segments,
|
|
139
|
+
],
|
|
140
|
+
}))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Plain-text convenience over {@link hangingStyledLines}. */
|
|
144
|
+
function hangingTextLines(
|
|
145
|
+
text: string,
|
|
146
|
+
columns: number,
|
|
147
|
+
firstPrefix: string,
|
|
148
|
+
firstStyle: LineStyle = 'plain',
|
|
149
|
+
contPrefix = ' ',
|
|
150
|
+
contStyle: LineStyle = firstStyle,
|
|
151
|
+
): readonly StyledLine[] {
|
|
152
|
+
return hangingStyledLines([lineSegment(text, firstStyle)], columns, firstPrefix, firstStyle, contPrefix, contStyle)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Markdown rows re-hardened so a single long word cannot escape the budget. */
|
|
156
|
+
export function markdownLines(text: string, columns: number): readonly StyledLine[] {
|
|
157
|
+
const width = Math.max(1, Math.floor(columns))
|
|
158
|
+
const parsed = renderMarkdown(displayText(text), Math.max(10, width))
|
|
159
|
+
return parsed.flatMap(line => styledLines(
|
|
160
|
+
line.segments.map(segment => lineSegment(segment.text, segment.style)),
|
|
161
|
+
width,
|
|
162
|
+
))
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Codex-style reasoning rows: the marker occupies the reply gutter and every
|
|
167
|
+
* wrapped or explicit continuation starts with the same two-column indent, so
|
|
168
|
+
* reasoning content and assistant Markdown share one left edge.
|
|
169
|
+
*/
|
|
170
|
+
export function reasoningLines(text: string, columns: number): readonly StyledLine[] {
|
|
171
|
+
const width = Math.max(1, Math.floor(columns))
|
|
172
|
+
if (width < 3) return textLines(text, width, 'dimItalic')
|
|
173
|
+
const contentWidth = width - 2
|
|
174
|
+
const content = displayText(text).replaceAll('\t', ' ').replaceAll('\r', '')
|
|
175
|
+
.split('\n')
|
|
176
|
+
.flatMap(line => styledLines([lineSegment(line, 'dimItalic')], contentWidth))
|
|
177
|
+
return content.map((line, index) => ({
|
|
178
|
+
segments: [
|
|
179
|
+
lineSegment(index === 0 ? '✻ ' : ' ', 'dimItalic'),
|
|
180
|
+
...line.segments,
|
|
181
|
+
],
|
|
182
|
+
}))
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Expanded structured tool detail as scrollable, width-safe rows. */
|
|
186
|
+
/**
|
|
187
|
+
* Detail rows share the tool card's four-column hanging gutter: the summary
|
|
188
|
+
* (⎿) and delegation prompt (└) continuations already sit at four columns, so
|
|
189
|
+
* diff/read/web/raw rows align under them instead of floating two columns
|
|
190
|
+
* shallower.
|
|
191
|
+
*/
|
|
192
|
+
function toolDetailLines(detail: ToolDetail, columns: number): readonly StyledLine[] {
|
|
193
|
+
switch (detail.kind) {
|
|
194
|
+
case 'diff':
|
|
195
|
+
return detail.diffs.flatMap(diff => [
|
|
196
|
+
...prefixedTextLines(`${diff.path}${diff.truncated ? ' (diff truncated)' : ''}`, columns, ' ── ', 'dim'),
|
|
197
|
+
...diff.lines.flatMap(line => prefixedTextLines(
|
|
198
|
+
`${line.mark}${line.text}`,
|
|
199
|
+
columns,
|
|
200
|
+
' ',
|
|
201
|
+
line.mark === '+' ? 'success' : line.mark === '-' ? 'error' : 'dim',
|
|
202
|
+
)),
|
|
203
|
+
])
|
|
204
|
+
case 'read':
|
|
205
|
+
return [
|
|
206
|
+
...prefixedTextLines(
|
|
207
|
+
`${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)' : ''}`,
|
|
208
|
+
columns,
|
|
209
|
+
' ── ',
|
|
210
|
+
'dim',
|
|
211
|
+
),
|
|
212
|
+
...detail.lines.flatMap(line => prefixedTextLines(`${String(line.number).padStart(5, ' ')} | ${line.text}`, columns, ' ', 'dim')),
|
|
213
|
+
]
|
|
214
|
+
case 'web-search':
|
|
215
|
+
return [
|
|
216
|
+
...detail.sources.flatMap(source => [
|
|
217
|
+
...prefixedStyledLines([
|
|
218
|
+
lineSegment(source.title ?? source.url, 'brand'),
|
|
219
|
+
lineSegment(` - ${source.url}`, 'dim'),
|
|
220
|
+
], columns, ' ? '),
|
|
221
|
+
...(source.snippet === '' ? [] : prefixedTextLines(source.snippet, columns, ' ', 'dim')),
|
|
222
|
+
]),
|
|
223
|
+
...prefixedTextLines(`${detail.sources.length} sources${detail.truncated ? ' (capped)' : ''}`, columns, ' ', 'dim'),
|
|
224
|
+
]
|
|
225
|
+
case 'web-fetch':
|
|
226
|
+
return prefixedTextLines(`${detail.url} · HTTP ${detail.statusCode}`, columns, ' ', 'dim')
|
|
227
|
+
case 'raw':
|
|
228
|
+
return [
|
|
229
|
+
...prefixedTextLines(detail.text, columns, ' ', 'dim'),
|
|
230
|
+
...prefixedTextLines(detail.truncated ? '… (output truncated)' : '(end of output)', columns, ' ', 'dim'),
|
|
231
|
+
]
|
|
232
|
+
default: {
|
|
233
|
+
const exhaustive: never = detail
|
|
234
|
+
return exhaustive
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Convert one durable transcript entry to its complete scrollable row model.
|
|
241
|
+
* The source entry stays intact; only the caller's visible slice is rendered.
|
|
242
|
+
* Wrapped continuations keep a hanging indent aligned under each row's
|
|
243
|
+
* content (Codex history-cell alignment) instead of resetting to column 0.
|
|
244
|
+
*/
|
|
245
|
+
export function transcriptEntryLines(
|
|
246
|
+
entry: TranscriptEntry,
|
|
247
|
+
columns: number,
|
|
248
|
+
showReasoning = true,
|
|
249
|
+
reasoningToggleHint = true,
|
|
250
|
+
): readonly StyledLine[] {
|
|
251
|
+
const width = Math.max(1, Math.floor(columns))
|
|
252
|
+
switch (entry.kind) {
|
|
253
|
+
case 'user':
|
|
254
|
+
return entry.notice
|
|
255
|
+
? hangingStyledLines([lineSegment(promptDisplayText(entry), 'dim')], width, '⤷ ', 'dim', ' ', 'dim')
|
|
256
|
+
: hangingStyledLines([lineSegment(promptDisplayText(entry), 'plain')], width, '❯ ', 'brand', ' ', 'plain')
|
|
257
|
+
case 'pending':
|
|
258
|
+
// Codex PendingSteer: a queued prompt renders exactly like an ordinary
|
|
259
|
+
// user row, so the durable user/message retires it without any flicker.
|
|
260
|
+
return hangingStyledLines([lineSegment(promptDisplayText(entry), 'plain')], width, '❯ ', 'brand', ' ', 'plain')
|
|
261
|
+
case 'assistant': {
|
|
262
|
+
const reasoning = entry.reasoning === ''
|
|
263
|
+
? []
|
|
264
|
+
: showReasoning
|
|
265
|
+
? reasoningLines(entry.reasoning, width)
|
|
266
|
+
: textLines(`✻ Thinking (${entry.reasoning.length} chars${reasoningToggleHint ? ', Ctrl+R to expand' : ''})`, width, 'dim')
|
|
267
|
+
// Every reply row carries the composer's two-column gutter, so reply
|
|
268
|
+
// text aligns with the input cursor (Codex LIVE_PREFIX alignment); the
|
|
269
|
+
// wrap budget shrinks by the same amount so no line double-wraps.
|
|
270
|
+
const body = markdownLines(entry.text, Math.max(10, width - 2))
|
|
271
|
+
.map(line => ({ segments: [{ text: ' ', style: 'plain' as const }, ...line.segments] }))
|
|
272
|
+
// A cancelled stream's delivered prefix settles as this entry; one
|
|
273
|
+
// bounded dim marker row distinguishes it from a completed reply.
|
|
274
|
+
const interrupted = entry.interrupted === true ? textLines(' ⏹ interrupted', width, 'dim') : []
|
|
275
|
+
return [...reasoning, ...body, ...interrupted]
|
|
276
|
+
}
|
|
277
|
+
case 'tool': {
|
|
278
|
+
const mark = entry.state === 'running' ? '●' : entry.state === 'error' ? '⨯' : '⏺'
|
|
279
|
+
const markStyle: LineStyle = entry.state === 'running' ? 'brand' : entry.state === 'error' ? 'error' : 'success'
|
|
280
|
+
const summaryStyle: LineStyle = entry.state === 'error' ? 'error' : 'dim'
|
|
281
|
+
return [
|
|
282
|
+
// The invocation row hangs wrapped previews under the call badge.
|
|
283
|
+
...hangingStyledLines([
|
|
284
|
+
// Global call ordinal — the same number an error line references.
|
|
285
|
+
lineSegment(`[${entry.ordinal}] `, 'dim'),
|
|
286
|
+
lineSegment(entry.name, 'brand'),
|
|
287
|
+
lineSegment(entry.preview === '' ? '' : ` ${entry.preview}`, 'dim'),
|
|
288
|
+
], width, `${mark} `, markStyle, ' ', 'plain'),
|
|
289
|
+
// A delegation card carries what the child was asked (Codex's
|
|
290
|
+
// SpawnAgent prompt preview) while it runs, before any result.
|
|
291
|
+
...(entry.prompt === '' ? [] : hangingTextLines(entry.prompt, width, ' └ ', 'dim', ' ')),
|
|
292
|
+
...(entry.summary === '' ? [] : hangingTextLines(
|
|
293
|
+
entry.state === 'error' ? `call ${entry.ordinal}: ${entry.summary}` : entry.summary,
|
|
294
|
+
width, ' ⎿ ', summaryStyle, ' ', summaryStyle,
|
|
295
|
+
)),
|
|
296
|
+
...(entry.detail === undefined ? [] : toolDetailLines(entry.detail, width)),
|
|
297
|
+
]
|
|
298
|
+
}
|
|
299
|
+
case 'command': {
|
|
300
|
+
const mark = entry.state === 'running' ? '●' : entry.state === 'error' ? '⨯' : '⏺'
|
|
301
|
+
const markStyle: LineStyle = entry.state === 'running' ? 'brand' : entry.state === 'error' ? 'error' : 'success'
|
|
302
|
+
const summaryStyle: LineStyle = entry.state === 'error' ? 'error' : 'dim'
|
|
303
|
+
return [
|
|
304
|
+
...hangingStyledLines([
|
|
305
|
+
lineSegment(`/${entry.name}`, 'brand'),
|
|
306
|
+
lineSegment(entry.args === '' ? '' : ` ${entry.args}`, 'dim'),
|
|
307
|
+
], width, `${mark} `, markStyle, ' ', 'plain'),
|
|
308
|
+
...(entry.summary === '' ? [] : hangingTextLines(entry.summary, width, ' ⎿ ', summaryStyle, ' ', summaryStyle)),
|
|
309
|
+
]
|
|
310
|
+
}
|
|
311
|
+
case 'turn-marker':
|
|
312
|
+
return textLines(` ⏹ ${entry.text}`, width, 'dim')
|
|
313
|
+
case 'compaction':
|
|
314
|
+
return textLines(entry.ok
|
|
315
|
+
? ` ⧉ compacted ~${formatTokens(entry.tokens)} tokens`
|
|
316
|
+
: ` ⧉ compaction failed: ${entry.error}`, width, 'dim')
|
|
317
|
+
case 'retry':
|
|
318
|
+
return textLines(
|
|
319
|
+
` ↻ retry ${entry.attempt}/${entry.max} · ${entry.code} · ${Math.round(entry.delayMs / 100) / 10}s`,
|
|
320
|
+
width,
|
|
321
|
+
entry.state === 'running' ? 'warn' : 'dim',
|
|
322
|
+
)
|
|
323
|
+
case 'files':
|
|
324
|
+
return entry.paths.length === 0
|
|
325
|
+
? textLines(' ⎄ no changed files', width, 'dim')
|
|
326
|
+
: [
|
|
327
|
+
...textLines(` ⎄ ${entry.paths.length} changed file${entry.paths.length === 1 ? '' : 's'}`, width, 'dim'),
|
|
328
|
+
...entry.paths.flatMap(path => hangingTextLines(path, width, ' ', 'dim', ' ')),
|
|
329
|
+
]
|
|
330
|
+
case 'error':
|
|
331
|
+
return textLines(entry.text, width, 'error')
|
|
332
|
+
default: {
|
|
333
|
+
const exhaustive: never = entry
|
|
334
|
+
return exhaustive
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** Settled-history variant carrying the Ctrl+R reasoning fold. */
|
|
340
|
+
export function settledEntryLines(entry: TranscriptEntry, columns: number, showReasoning: boolean): readonly StyledLine[] {
|
|
341
|
+
return transcriptEntryLines(entry, columns, showReasoning, false)
|
|
342
|
+
}
|