praxis-agent 0.34.0 → 0.35.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.md +4 -0
- package/dist/application/session-service.d.ts +0 -1
- package/dist/application/session-service.js +1619 -1617
- package/dist/application/subagent-service.js +27 -16
- package/dist/application/turn-lifecycle.d.ts +38 -0
- package/dist/application/turn-lifecycle.js +36 -0
- package/dist/cli/interactive.js +17 -7
- package/dist/cli/protocol.js +5 -2
- package/dist/cli/tui/claude-style.d.ts +5 -48
- package/dist/cli/tui/claude-style.js +19 -73
- package/dist/cli/tui/transcript-presentation.d.ts +89 -0
- package/dist/cli/tui/transcript-presentation.js +169 -0
- package/dist/cli/tui/transcript-viewport.d.ts +5 -34
- package/dist/cli/tui/transcript-viewport.js +639 -158
- package/dist/cli/tui/tui-view-model.d.ts +6 -2
- package/dist/cli/tui/tui-view-model.js +23 -6
- package/dist/compatibility/claude/context.d.ts +2 -2
- package/dist/compatibility/claude/context.js +2 -14
- package/dist/compatibility/claude/data-plane-adapter.d.ts +9 -0
- package/dist/compatibility/claude/data-plane-adapter.js +44 -0
- package/dist/compatibility/claude/paths.js +2 -2
- package/dist/compatibility/claude/shared-resources.d.ts +7 -38
- package/dist/compatibility/claude/transcript-codec.d.ts +21 -0
- package/dist/compatibility/claude/transcript-codec.js +512 -0
- package/dist/core/context.d.ts +25 -7
- package/dist/core/context.js +46 -0
- package/dist/core/prompt-composer.d.ts +8 -16
- package/dist/core/prompt-composer.js +65 -25
- package/dist/core/resources.d.ts +39 -0
- package/dist/core/resources.js +2 -0
- package/dist/core/runtime.d.ts +1 -1
- package/dist/core/session.d.ts +2 -0
- package/dist/core/session.js +5 -0
- package/dist/core/transcript-codec.d.ts +70 -0
- package/dist/core/transcript-codec.js +60 -0
- package/dist/core/transcript-event.d.ts +33 -0
- package/dist/core/transcript-event.js +198 -0
- package/dist/persistence/data-plane-adapter.d.ts +32 -0
- package/dist/persistence/data-plane-adapter.js +2 -0
- package/dist/persistence/data-plane.d.ts +4 -20
- package/dist/persistence/data-plane.js +10 -47
- package/dist/persistence/native-data-plane-adapter.d.ts +9 -0
- package/dist/persistence/native-data-plane-adapter.js +39 -0
- package/dist/persistence/native-resources.d.ts +4 -4
- package/dist/persistence/native-transcript-codec.d.ts +13 -0
- package/dist/persistence/native-transcript-codec.js +102 -0
- package/dist/tools/claude-user-message.d.ts +0 -1
- package/dist/tools/claude-user-message.js +0 -1
- package/package.json +6 -2
|
@@ -1,184 +1,665 @@
|
|
|
1
|
-
|
|
2
|
-
* Rows reserved outside the shrinkable fullscreen transcript region: the top
|
|
3
|
-
* identity chrome, the composer separator/prompt/footer, and the status line,
|
|
4
|
-
* plus a small headroom so the active streaming tail stays visible below the
|
|
5
|
-
* projected history suffix. Fullscreen computes the transcript budget as
|
|
6
|
-
* `rows - FULLSCREEN_TRANSCRIPT_RESERVED_ROWS`.
|
|
7
|
-
*/
|
|
1
|
+
import { stripVTControlCharacters } from 'node:util';
|
|
8
2
|
export const FULLSCREEN_TRANSCRIPT_RESERVED_ROWS = 12;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
3
|
+
export const TRANSCRIPT_TRUNCATION_MARKER = '…';
|
|
4
|
+
const graphemeSegmenter = new Intl.Segmenter(undefined, {
|
|
5
|
+
granularity: 'grapheme',
|
|
6
|
+
});
|
|
7
|
+
const zeroWidthClusterPattern = /^(?:\p{Default_Ignorable_Code_Point}|\p{Control}|\p{Format}|\p{Nonspacing_Mark}|\p{Enclosing_Mark}|\p{Surrogate})+$/v;
|
|
8
|
+
const leadingNonPrintingPattern = /^[\p{Default_Ignorable_Code_Point}\p{Control}\p{Format}\p{Nonspacing_Mark}\p{Enclosing_Mark}\p{Surrogate}]+/v;
|
|
9
|
+
const rgiEmojiPattern = /^\p{RGI_Emoji}$/v;
|
|
10
|
+
const extendedPictographicPattern = /\p{Extended_Pictographic}/gu;
|
|
11
|
+
const spacingMarkPattern = /\p{Spacing_Mark}/v;
|
|
12
|
+
const unqualifiedKeycapPattern = /^[\d#*]\u20e3$/u;
|
|
13
|
+
function graphemes(text) {
|
|
14
|
+
return Array.from(graphemeSegmenter.segment(text), ({ segment }) => segment);
|
|
15
|
+
}
|
|
16
|
+
function isPrintableAscii(text) {
|
|
17
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
18
|
+
const code = text.charCodeAt(index);
|
|
19
|
+
if (code < 0x20 || code > 0x7e)
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
function isWideCodePoint(codePoint) {
|
|
25
|
+
return (codePoint >= 0x1100 &&
|
|
26
|
+
(codePoint <= 0x115f ||
|
|
27
|
+
codePoint === 0x2329 ||
|
|
28
|
+
codePoint === 0x232a ||
|
|
29
|
+
(codePoint >= 0x2e80 && codePoint <= 0x303e) ||
|
|
30
|
+
(codePoint >= 0x3040 && codePoint <= 0xa4cf) ||
|
|
31
|
+
(codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
|
|
32
|
+
(codePoint >= 0xf900 && codePoint <= 0xfaff) ||
|
|
33
|
+
(codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
|
|
34
|
+
(codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
|
|
35
|
+
(codePoint >= 0xff00 && codePoint <= 0xff60) ||
|
|
36
|
+
(codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
|
|
37
|
+
(codePoint >= 0x1b000 && codePoint <= 0x1b001) ||
|
|
38
|
+
(codePoint >= 0x1f200 && codePoint <= 0x1f251) ||
|
|
39
|
+
(codePoint >= 0x20000 && codePoint <= 0x3fffd)));
|
|
40
|
+
}
|
|
41
|
+
function isDoubleWidthEmoji(cluster) {
|
|
42
|
+
if (rgiEmojiPattern.test(cluster) || unqualifiedKeycapPattern.test(cluster))
|
|
43
|
+
return true;
|
|
44
|
+
if (!cluster.includes('\u200d') || cluster.length > 50)
|
|
45
|
+
return false;
|
|
46
|
+
return (cluster.match(extendedPictographicPattern)?.length ?? 0) >= 2;
|
|
47
|
+
}
|
|
48
|
+
function graphemeWidth(cluster) {
|
|
49
|
+
if (!cluster || zeroWidthClusterPattern.test(cluster))
|
|
50
|
+
return 0;
|
|
51
|
+
if (isDoubleWidthEmoji(cluster))
|
|
52
|
+
return 2;
|
|
53
|
+
const visible = cluster.replace(leadingNonPrintingPattern, '');
|
|
54
|
+
const first = visible.codePointAt(0);
|
|
55
|
+
if (first === undefined)
|
|
56
|
+
return 0;
|
|
57
|
+
let width = isWideCodePoint(first) ? 2 : 1;
|
|
58
|
+
let firstVisible = true;
|
|
59
|
+
for (const character of visible) {
|
|
60
|
+
if (firstVisible) {
|
|
61
|
+
firstVisible = false;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const codePoint = character.codePointAt(0);
|
|
65
|
+
if (codePoint !== undefined &&
|
|
66
|
+
(spacingMarkPattern.test(character) ||
|
|
67
|
+
(codePoint >= 0xff00 && codePoint <= 0xffef)))
|
|
68
|
+
width += isWideCodePoint(codePoint) ? 2 : 1;
|
|
69
|
+
}
|
|
70
|
+
return width;
|
|
71
|
+
}
|
|
72
|
+
function terminalStringWidth(text) {
|
|
73
|
+
if (isPrintableAscii(text))
|
|
74
|
+
return text.length;
|
|
75
|
+
const first = text.codePointAt(0);
|
|
76
|
+
if (first !== undefined && text.length === (first > 0xffff ? 2 : 1)) {
|
|
77
|
+
if (first <= 0x1f || (first >= 0x7f && first <= 0x9f))
|
|
78
|
+
return 0;
|
|
79
|
+
if (isWideCodePoint(first))
|
|
80
|
+
return 2;
|
|
81
|
+
return first <= 0xffff ? 1 : isDoubleWidthEmoji(text) ? 2 : 1;
|
|
82
|
+
}
|
|
83
|
+
return graphemes(text).reduce((width, cluster) => width + graphemeWidth(cluster), 0);
|
|
84
|
+
}
|
|
85
|
+
function expandTabs(line) {
|
|
86
|
+
if (!line.includes('\t'))
|
|
87
|
+
return line;
|
|
88
|
+
let column = 0;
|
|
89
|
+
let expanded = '';
|
|
90
|
+
for (const cluster of graphemes(line)) {
|
|
91
|
+
if (cluster === '\t') {
|
|
92
|
+
const spaces = 8 - (column % 8);
|
|
93
|
+
expanded += ' '.repeat(spaces);
|
|
94
|
+
column += spaces;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
expanded += cluster;
|
|
98
|
+
column += graphemeWidth(cluster);
|
|
99
|
+
}
|
|
100
|
+
return expanded;
|
|
101
|
+
}
|
|
102
|
+
function hardWrappedRows(word, width, startColumn) {
|
|
103
|
+
if (word.length === 0)
|
|
104
|
+
return { rows: 0, column: startColumn };
|
|
105
|
+
if (isPrintableAscii(word)) {
|
|
106
|
+
const occupied = startColumn + word.length;
|
|
107
|
+
return {
|
|
108
|
+
rows: Math.max(0, Math.ceil(occupied / width) - 1),
|
|
109
|
+
column: occupied % width || width,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const clusters = graphemes(word);
|
|
19
113
|
let rows = 0;
|
|
20
|
-
|
|
21
|
-
|
|
114
|
+
let column = startColumn;
|
|
115
|
+
for (const [index, cluster] of clusters.entries()) {
|
|
116
|
+
const clusterWidth = graphemeWidth(cluster);
|
|
117
|
+
if (column + clusterWidth > width) {
|
|
118
|
+
rows += 1;
|
|
119
|
+
column = clusterWidth;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
column += clusterWidth;
|
|
123
|
+
}
|
|
124
|
+
if (column === width && index < clusters.length - 1) {
|
|
125
|
+
rows += 1;
|
|
126
|
+
column = 0;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return { rows, column };
|
|
130
|
+
}
|
|
131
|
+
function wrappedLineRows(line, width) {
|
|
132
|
+
const expanded = expandTabs(line);
|
|
133
|
+
if (isPrintableAscii(expanded) && expanded.length <= width)
|
|
134
|
+
return 1;
|
|
135
|
+
const words = expanded.split(' ');
|
|
136
|
+
let rows = 1;
|
|
137
|
+
let column = 0;
|
|
138
|
+
for (let index = 0; index < words.length; index += 1) {
|
|
139
|
+
const word = words[index] ?? '';
|
|
140
|
+
const wordWidth = isPrintableAscii(word)
|
|
141
|
+
? word.length
|
|
142
|
+
: terminalStringWidth(word);
|
|
143
|
+
if (index > 0) {
|
|
144
|
+
if (column >= width) {
|
|
145
|
+
rows += 1;
|
|
146
|
+
column = 0;
|
|
147
|
+
}
|
|
148
|
+
column += 1;
|
|
149
|
+
}
|
|
150
|
+
if (wordWidth > width) {
|
|
151
|
+
const current = hardWrappedRows(word, width, column);
|
|
152
|
+
const fresh = hardWrappedRows(word, width, 0);
|
|
153
|
+
if (column > 0 && current.rows > fresh.rows) {
|
|
154
|
+
rows += 1;
|
|
155
|
+
column = 0;
|
|
156
|
+
rows += fresh.rows;
|
|
157
|
+
column = fresh.column;
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
rows += current.rows;
|
|
161
|
+
column = current.column;
|
|
162
|
+
}
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (column + wordWidth > width && column > 0 && word.length > 0) {
|
|
166
|
+
rows += 1;
|
|
167
|
+
column = 0;
|
|
168
|
+
}
|
|
169
|
+
column += wordWidth;
|
|
22
170
|
}
|
|
23
171
|
return rows;
|
|
24
172
|
}
|
|
173
|
+
function wrappedLineCount(text, width) {
|
|
174
|
+
const usable = Math.max(1, width);
|
|
175
|
+
if (isPrintableAscii(text))
|
|
176
|
+
return text.length <= usable ? 1 : wrappedLineRows(text, usable);
|
|
177
|
+
const visible = text.includes('\u001b') || text.includes('\u009b')
|
|
178
|
+
? stripVTControlCharacters(text)
|
|
179
|
+
: text;
|
|
180
|
+
return visible
|
|
181
|
+
.normalize()
|
|
182
|
+
.replace(/\r\n?/gu, '\n')
|
|
183
|
+
.split('\n')
|
|
184
|
+
.reduce((rows, line) => rows + wrappedLineRows(line, usable), 0);
|
|
185
|
+
}
|
|
186
|
+
function markdownContentWidth(width) {
|
|
187
|
+
return Math.max(1, width - 4);
|
|
188
|
+
}
|
|
25
189
|
function bounded(text, max) {
|
|
26
190
|
return text.length > max ? text.slice(0, max) : text;
|
|
27
191
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
192
|
+
function contentLines(text) {
|
|
193
|
+
const lines = text.split('\n');
|
|
194
|
+
if (lines.at(-1) === '')
|
|
195
|
+
lines.pop();
|
|
196
|
+
return lines;
|
|
197
|
+
}
|
|
198
|
+
function compactTokens(tokens) {
|
|
199
|
+
if (tokens >= 1_000_000)
|
|
200
|
+
return `${(tokens / 1_000_000).toFixed(1).replace(/\.0$/u, '')}m`;
|
|
201
|
+
if (tokens >= 1_000)
|
|
202
|
+
return `${(tokens / 1_000).toFixed(1).replace(/\.0$/u, '')}k`;
|
|
203
|
+
return String(tokens);
|
|
204
|
+
}
|
|
205
|
+
function percent(tokens, total) {
|
|
206
|
+
return `${Math.round((tokens / Math.max(1, total)) * 100 * 10) / 10}%`;
|
|
207
|
+
}
|
|
208
|
+
function contextUsageLineCount(item, width) {
|
|
209
|
+
const totalTokens = Math.max(1, item.contextWindowTokens);
|
|
210
|
+
const compactBuffer = Math.round(totalTokens * 0.165);
|
|
211
|
+
const usable = Math.max(1, totalTokens - compactBuffer);
|
|
212
|
+
const rightWidth = Math.max(1, width - 16);
|
|
213
|
+
const rightColumnRows = [
|
|
214
|
+
`${item.model ?? 'provider default'} · ${compactTokens(item.usedTokens)}/${compactTokens(totalTokens)} tokens (${percent(item.usedTokens, totalTokens)})`,
|
|
215
|
+
' ',
|
|
216
|
+
'Estimated usage by category',
|
|
217
|
+
`⛁ Messages and other context: ${compactTokens(item.usedTokens)} tokens (${percent(item.usedTokens, totalTokens)})`,
|
|
218
|
+
`⛶ Free space: ${compactTokens(Math.max(0, usable - item.usedTokens))} (${percent(Math.max(0, usable - item.usedTokens), totalTokens)})`,
|
|
219
|
+
`⛝ Autocompact buffer: ${compactTokens(compactBuffer)} tokens (${percent(compactBuffer, totalTokens)})`,
|
|
220
|
+
].reduce((rows, line) => rows + wrappedLineCount(line, rightWidth), 0);
|
|
221
|
+
const gridRows = Math.max(width < 48 ? 10 : 5, rightColumnRows);
|
|
222
|
+
const memoryRows = item.memoryFiles.length
|
|
223
|
+
? item.memoryFiles.reduce((rows, file, index) => rows +
|
|
224
|
+
wrappedLineCount(`${index === item.memoryFiles.length - 1 ? '└' : '├'} ${file.path}: ${file.tokens} tokens`, Math.max(1, width - 2)), 0)
|
|
225
|
+
: wrappedLineCount('└ No memory files', Math.max(1, width - 2));
|
|
226
|
+
const skillRows = item.skills.length
|
|
227
|
+
? item.skills.reduce((rows, skill, index) => rows +
|
|
228
|
+
wrappedLineCount(`${index === item.skills.length - 1 ? '└' : '├'} ${skill.name}: ~${skill.tokens} tokens`, Math.max(1, width - 2)), 0)
|
|
229
|
+
: wrappedLineCount('└ No skills loaded', Math.max(1, width - 2));
|
|
230
|
+
return 8 + gridRows + memoryRows + skillRows;
|
|
231
|
+
}
|
|
232
|
+
function combinedShellOutput(stdout, stderr) {
|
|
233
|
+
return [stdout, stderr]
|
|
234
|
+
.filter(Boolean)
|
|
235
|
+
.join(stdout && stderr && !stdout.endsWith('\n') ? '\n' : '');
|
|
236
|
+
}
|
|
237
|
+
function orphanShellOutput(stdout, stderr) {
|
|
238
|
+
return [stdout, stderr].filter(Boolean).join('\n');
|
|
239
|
+
}
|
|
240
|
+
function inputString(entry, key) {
|
|
241
|
+
const value = entry.item.call.input[key];
|
|
242
|
+
return typeof value === 'string' ? value : '';
|
|
243
|
+
}
|
|
244
|
+
function toolHeading(entry) {
|
|
245
|
+
if (entry.item.call.name === 'Edit')
|
|
246
|
+
return `Update(${inputString(entry, 'file_path')})`;
|
|
247
|
+
if (entry.item.call.name === 'Read')
|
|
248
|
+
return `Read(${inputString(entry, 'file_path')})`;
|
|
249
|
+
if (entry.item.call.name === 'Bash')
|
|
250
|
+
return `Bash(${inputString(entry, 'command')})`;
|
|
251
|
+
return entry.item.call.name;
|
|
252
|
+
}
|
|
253
|
+
function visibleOutputLineCount(text, width, mode) {
|
|
254
|
+
const lines = contentLines(text);
|
|
255
|
+
const visible = mode === 'normal' ? lines.slice(0, 3) : lines;
|
|
256
|
+
const outputWidth = Math.max(1, width - 2);
|
|
257
|
+
const rows = visible.reduce((total, line, index) => total +
|
|
258
|
+
wrappedLineCount(`${index === 0 ? '⎿ ' : ' '}${line || ' '}`, outputWidth), 0);
|
|
259
|
+
return (rows +
|
|
260
|
+
(mode === 'normal' && lines.length > visible.length
|
|
261
|
+
? wrappedLineCount(` … +${lines.length - visible.length} lines (ctrl+o to expand)`, outputWidth)
|
|
262
|
+
: 0));
|
|
263
|
+
}
|
|
264
|
+
function screenReaderContextLineCount(item, width) {
|
|
265
|
+
const totalTokens = Math.max(1, item.contextWindowTokens);
|
|
266
|
+
const compactBuffer = Math.round(totalTokens * 0.165);
|
|
267
|
+
return [
|
|
268
|
+
'Context Usage',
|
|
269
|
+
`${item.model ?? 'provider default'} · ${item.usedTokens.toLocaleString()}/${totalTokens.toLocaleString()} tokens (${percent(item.usedTokens, totalTokens)})`,
|
|
270
|
+
`Autocompact buffer: ${compactBuffer.toLocaleString()} tokens`,
|
|
271
|
+
`Skills: ${item.skills.map(({ name }) => name).join(', ') || 'none'}`,
|
|
272
|
+
].reduce((rows, line) => rows + wrappedLineCount(line, width), 0);
|
|
273
|
+
}
|
|
274
|
+
function textTail(text, budget, width) {
|
|
275
|
+
const usableBudget = Math.max(1, budget);
|
|
276
|
+
const usableWidth = Math.max(1, width);
|
|
277
|
+
if (wrappedLineCount(text, usableWidth) <= usableBudget)
|
|
42
278
|
return text;
|
|
43
279
|
const lines = text.split('\n');
|
|
44
|
-
|
|
45
|
-
// Wrapped rows of every kept line after the current front line (marker-free).
|
|
280
|
+
const kept = [];
|
|
46
281
|
let rowsAfter = 0;
|
|
47
|
-
for (let
|
|
48
|
-
const line = lines[
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
282
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
283
|
+
const line = lines[i] ?? '';
|
|
284
|
+
const markedRows = wrappedLineCount(`${TRANSCRIPT_TRUNCATION_MARKER}${line}`, usableWidth);
|
|
285
|
+
if (rowsAfter + markedRows <= usableBudget) {
|
|
286
|
+
kept.unshift(line);
|
|
287
|
+
rowsAfter += wrappedLineCount(line, usableWidth);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
const remainingRows = usableBudget - rowsAfter;
|
|
291
|
+
if (remainingRows > 0) {
|
|
292
|
+
const clusters = graphemes(line);
|
|
293
|
+
let low = 0;
|
|
294
|
+
let high = clusters.length;
|
|
295
|
+
let suffix = '';
|
|
296
|
+
while (low <= high) {
|
|
297
|
+
const middle = Math.floor((low + high) / 2);
|
|
298
|
+
const candidate = clusters.slice(clusters.length - middle).join('');
|
|
299
|
+
if (wrappedLineCount(`${TRANSCRIPT_TRUNCATION_MARKER}${candidate}`, usableWidth) <= remainingRows) {
|
|
300
|
+
suffix = candidate;
|
|
301
|
+
low = middle + 1;
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
high = middle - 1;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
while (suffix &&
|
|
308
|
+
wrappedLineCount(`${TRANSCRIPT_TRUNCATION_MARKER}${suffix}`, usableWidth) > remainingRows) {
|
|
309
|
+
const suffixClusters = graphemes(suffix);
|
|
310
|
+
suffix = suffixClusters.slice(0, -1).join('');
|
|
311
|
+
}
|
|
312
|
+
kept.unshift(suffix);
|
|
313
|
+
}
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
return `${TRANSCRIPT_TRUNCATION_MARKER}${kept.join('\n')}`;
|
|
317
|
+
}
|
|
318
|
+
export function estimateTranscriptEntryLines(entry, width, mode) {
|
|
319
|
+
if (entry.kind === 'read-summary')
|
|
320
|
+
return wrappedLineCount(` Read ${entry.count} file${entry.count === 1 ? '' : 's'} (ctrl+o to expand)`, width);
|
|
321
|
+
if (entry.kind === 'item') {
|
|
322
|
+
const item = entry.item;
|
|
323
|
+
if (item.kind === 'user')
|
|
324
|
+
return (1 +
|
|
325
|
+
(mode === 'screen-reader' ? 1 : 0) +
|
|
326
|
+
wrappedLineCount(`${mode === 'screen-reader' ? 'You: ' : '❯ '}${item.text}`, width));
|
|
327
|
+
if (item.kind === 'assistant')
|
|
328
|
+
return (1 +
|
|
329
|
+
(mode === 'screen-reader' ? 1 : 0) +
|
|
330
|
+
wrappedLineCount(item.text, mode === 'screen-reader'
|
|
331
|
+
? Math.max(1, width - 7)
|
|
332
|
+
: markdownContentWidth(width)));
|
|
333
|
+
if (item.kind === 'thinking') {
|
|
334
|
+
const summary = item.text.replace(/\s+/gu, ' ').trim();
|
|
335
|
+
if (mode === 'normal')
|
|
336
|
+
return (1 +
|
|
337
|
+
wrappedLineCount(`✻ Thought for a moment${summary ? ` · ${summary.slice(0, 160)}` : ''}`, width) +
|
|
338
|
+
(summary.length > 160
|
|
339
|
+
? wrappedLineCount(' ctrl+o to expand thinking', width)
|
|
340
|
+
: 0));
|
|
341
|
+
const heading = mode === 'screen-reader'
|
|
342
|
+
? 'Thinking:Thought for a moment'
|
|
343
|
+
: `✻ Thought for a moment${summary ? ` · ${summary.slice(0, 160)}` : ''}`;
|
|
344
|
+
return (1 +
|
|
345
|
+
wrappedLineCount(heading, width) +
|
|
346
|
+
wrappedLineCount(item.text, mode === 'screen-reader' ? width : markdownContentWidth(width)));
|
|
347
|
+
}
|
|
348
|
+
if (item.kind === 'compact') {
|
|
349
|
+
if (mode === 'screen-reader')
|
|
350
|
+
return 2 + wrappedLineCount(item.summary, markdownContentWidth(width));
|
|
351
|
+
return (1 +
|
|
352
|
+
wrappedLineCount('✻ Conversation compacted (ctrl+o for history)', width) +
|
|
353
|
+
(mode === 'audit'
|
|
354
|
+
? wrappedLineCount(item.summary, markdownContentWidth(width))
|
|
355
|
+
: 0));
|
|
56
356
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
357
|
+
if (item.kind === 'context')
|
|
358
|
+
return mode === 'screen-reader'
|
|
359
|
+
? screenReaderContextLineCount(item, width)
|
|
360
|
+
: contextUsageLineCount(item, width);
|
|
361
|
+
if (item.kind === 'local-result')
|
|
362
|
+
return wrappedLineCount(`⎿ ${item.text}`, Math.max(1, width - 2));
|
|
363
|
+
if (item.kind === 'notice')
|
|
364
|
+
return wrappedLineCount(`· ${item.text}`, width);
|
|
365
|
+
if (item.kind === 'warning')
|
|
366
|
+
return wrappedLineCount(`⚠ ${item.text}`, width);
|
|
367
|
+
return wrappedLineCount(item.text, width);
|
|
368
|
+
}
|
|
369
|
+
if (entry.kind === 'orphan-tool-result')
|
|
370
|
+
return (1 +
|
|
371
|
+
wrappedLineCount(bounded(entry.item.text, 500), Math.max(1, width - 2)));
|
|
372
|
+
if (entry.kind === 'orphan-shell-result')
|
|
373
|
+
return wrappedLineCount(`⎿ ${orphanShellOutput(entry.item.stdout, entry.item.stderr)}`, width);
|
|
374
|
+
if (entry.kind === 'tool') {
|
|
375
|
+
if (mode === 'normal' &&
|
|
376
|
+
entry.item.call.name === 'Read' &&
|
|
377
|
+
entry.result &&
|
|
378
|
+
!entry.result.isError)
|
|
379
|
+
return 1;
|
|
380
|
+
let rows = 1 + wrappedLineCount(`⏺ ${toolHeading(entry)}`, width);
|
|
381
|
+
if (!['Bash', 'Read', 'Edit'].includes(entry.item.call.name) &&
|
|
382
|
+
entry.item.detail)
|
|
383
|
+
rows += wrappedLineCount(` ${entry.item.detail}`, width);
|
|
384
|
+
if (!entry.result)
|
|
385
|
+
return rows;
|
|
386
|
+
if (entry.result.isError)
|
|
387
|
+
return (rows +
|
|
388
|
+
wrappedLineCount(`⎿ Error: ${bounded(entry.result.text, 500)}`, Math.max(1, width - 2)));
|
|
389
|
+
if (entry.item.call.name === 'Edit') {
|
|
390
|
+
const newLines = contentLines(inputString(entry, 'new_string'));
|
|
391
|
+
const oldLines = contentLines(inputString(entry, 'old_string'));
|
|
392
|
+
rows += wrappedLineCount(`⎿ Added ${newLines.length} line${newLines.length === 1 ? '' : 's'}, removed ${oldLines.length} line${oldLines.length === 1 ? '' : 's'}`, Math.max(1, width - 2));
|
|
393
|
+
if (mode !== 'normal') {
|
|
394
|
+
rows += oldLines.reduce((total, line, index) => total +
|
|
395
|
+
wrappedLineCount(` ${index + 1} -${line}`, Math.max(1, width - 2)), 0);
|
|
396
|
+
rows += newLines.reduce((total, line, index) => total +
|
|
397
|
+
wrappedLineCount(` ${index + 1} +${line}`, Math.max(1, width - 2)), 0);
|
|
398
|
+
}
|
|
399
|
+
return rows;
|
|
400
|
+
}
|
|
401
|
+
return rows + visibleOutputLineCount(entry.result.text, width, mode);
|
|
402
|
+
}
|
|
403
|
+
let rows = 1 +
|
|
404
|
+
wrappedLineCount(`${mode === 'screen-reader' ? 'Shell command: ' : '! '}${entry.item.command}`, width);
|
|
405
|
+
if (!entry.result)
|
|
406
|
+
return rows;
|
|
407
|
+
const output = combinedShellOutput(entry.result.stdout, entry.result.stderr);
|
|
408
|
+
const lines = contentLines(output);
|
|
409
|
+
if (lines.length === 0)
|
|
410
|
+
return rows + wrappedLineCount('⎿ ', Math.max(1, width - 2));
|
|
411
|
+
rows += visibleOutputLineCount(output, width, mode);
|
|
412
|
+
return rows;
|
|
413
|
+
}
|
|
414
|
+
export function transcriptPresentationLineCount(entries, width, mode) {
|
|
415
|
+
return entries.reduce((n, entry) => n + estimateTranscriptEntryLines(entry, width, mode), 0);
|
|
416
|
+
}
|
|
417
|
+
function fitEntryText(entry, value, replace, budget, viewportWidth, contentWidth, mode) {
|
|
418
|
+
if (estimateTranscriptEntryLines(entry, viewportWidth, mode) <= budget ||
|
|
419
|
+
!value)
|
|
420
|
+
return entry;
|
|
421
|
+
const withoutValue = replace(entry, '');
|
|
422
|
+
const remaining = budget - estimateTranscriptEntryLines(withoutValue, viewportWidth, mode);
|
|
423
|
+
if (remaining <= 0)
|
|
424
|
+
return withoutValue;
|
|
425
|
+
const candidate = textTail(value, remaining, contentWidth);
|
|
426
|
+
const projected = replace(entry, candidate);
|
|
427
|
+
if (estimateTranscriptEntryLines(projected, viewportWidth, mode) <= budget)
|
|
428
|
+
return projected;
|
|
429
|
+
const clusters = graphemes(candidate);
|
|
430
|
+
let low = 0;
|
|
431
|
+
let high = clusters.length;
|
|
432
|
+
let suffix = '';
|
|
433
|
+
while (low <= high) {
|
|
434
|
+
const middle = Math.floor((low + high) / 2);
|
|
435
|
+
const next = clusters.slice(clusters.length - middle).join('');
|
|
436
|
+
const nextEntry = replace(entry, next);
|
|
437
|
+
if (estimateTranscriptEntryLines(nextEntry, viewportWidth, mode) <= budget) {
|
|
438
|
+
suffix = next;
|
|
439
|
+
low = middle + 1;
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
high = middle - 1;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return suffix ? replace(entry, suffix) : withoutValue;
|
|
446
|
+
}
|
|
447
|
+
function replaceToolInput(entry, key, value) {
|
|
448
|
+
return {
|
|
449
|
+
...entry,
|
|
450
|
+
item: {
|
|
451
|
+
...entry.item,
|
|
452
|
+
call: {
|
|
453
|
+
...entry.item.call,
|
|
454
|
+
input: { ...entry.item.call.input, [key]: value },
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
function projectOversizedEntry(entry, budget, width, mode) {
|
|
460
|
+
if (entry.kind === 'item') {
|
|
461
|
+
const item = entry.item;
|
|
462
|
+
if (item.kind === 'thinking')
|
|
77
463
|
return {
|
|
78
|
-
...
|
|
79
|
-
|
|
464
|
+
...entry,
|
|
465
|
+
item: {
|
|
466
|
+
...item,
|
|
467
|
+
text: textTail(item.text, Math.max(1, budget - 2), markdownContentWidth(width)),
|
|
468
|
+
},
|
|
80
469
|
};
|
|
81
|
-
|
|
470
|
+
if (item.kind === 'compact')
|
|
82
471
|
return {
|
|
83
|
-
...
|
|
84
|
-
|
|
472
|
+
...entry,
|
|
473
|
+
item: {
|
|
474
|
+
...item,
|
|
475
|
+
summary: textTail(item.summary, Math.max(1, budget - 2), markdownContentWidth(width)),
|
|
476
|
+
},
|
|
85
477
|
};
|
|
86
|
-
|
|
87
|
-
return
|
|
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
|
-
|
|
478
|
+
if ('text' in item)
|
|
479
|
+
return {
|
|
480
|
+
...entry,
|
|
481
|
+
item: {
|
|
482
|
+
...item,
|
|
483
|
+
text: textTail(item.text, Math.max(1, budget -
|
|
484
|
+
(item.kind === 'user'
|
|
485
|
+
? 1
|
|
486
|
+
: item.kind === 'assistant'
|
|
487
|
+
? mode === 'screen-reader'
|
|
488
|
+
? 2
|
|
489
|
+
: 1
|
|
490
|
+
: 0)), item.kind === 'assistant'
|
|
491
|
+
? mode === 'screen-reader'
|
|
492
|
+
? Math.max(1, width - 7)
|
|
493
|
+
: markdownContentWidth(width)
|
|
494
|
+
: item.kind === 'user'
|
|
495
|
+
? Math.max(1, width - (mode === 'screen-reader' ? 5 : 2))
|
|
496
|
+
: Math.max(1, width - (item.kind === 'local-result' ? 4 : 2))),
|
|
497
|
+
},
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
if (entry.kind === 'tool') {
|
|
501
|
+
let projected = entry;
|
|
502
|
+
if (!['Bash', 'Read', 'Edit'].includes(projected.item.call.name)) {
|
|
503
|
+
projected = fitEntryText(projected, projected.item.detail, (source, detail) => ({
|
|
504
|
+
...source,
|
|
505
|
+
item: { ...source.item, detail },
|
|
506
|
+
}), budget, width, Math.max(1, width - 1), mode);
|
|
507
|
+
}
|
|
508
|
+
const inputKeys = projected.item.call.name === 'Edit'
|
|
509
|
+
? ['file_path', 'old_string', 'new_string']
|
|
510
|
+
: projected.item.call.name === 'Read'
|
|
511
|
+
? ['file_path']
|
|
512
|
+
: projected.item.call.name === 'Bash'
|
|
513
|
+
? ['command']
|
|
514
|
+
: [];
|
|
515
|
+
for (const key of inputKeys) {
|
|
516
|
+
const value = inputString(projected, key);
|
|
517
|
+
projected = fitEntryText(projected, value, (source, projectedValue) => replaceToolInput(source, key, projectedValue), budget, width, Math.max(1, width - (key === 'file_path' ? 12 : 8)), mode);
|
|
518
|
+
}
|
|
519
|
+
const result = projected.result;
|
|
520
|
+
if (result) {
|
|
521
|
+
projected = fitEntryText(projected, result.text, (source, text) => ({
|
|
522
|
+
...source,
|
|
523
|
+
result: { ...result, text },
|
|
524
|
+
}), budget, width, Math.max(1, width - 5), mode);
|
|
525
|
+
}
|
|
526
|
+
return projected;
|
|
527
|
+
}
|
|
528
|
+
if (entry.kind === 'shell') {
|
|
529
|
+
let projected = fitEntryText(entry, entry.item.command, (source, command) => ({
|
|
530
|
+
...source,
|
|
531
|
+
item: { ...source.item, command },
|
|
532
|
+
}), budget, width, Math.max(1, width - 4), mode);
|
|
533
|
+
const result = projected.result;
|
|
534
|
+
if (result) {
|
|
535
|
+
const combined = combinedShellOutput(result.stdout, result.stderr);
|
|
536
|
+
projected = fitEntryText(projected, combined, (source, stdout) => ({
|
|
537
|
+
...source,
|
|
538
|
+
result: { ...result, stdout, stderr: '' },
|
|
539
|
+
}), budget, width, Math.max(1, width - 5), mode);
|
|
540
|
+
}
|
|
541
|
+
return projected;
|
|
542
|
+
}
|
|
543
|
+
if (entry.kind === 'orphan-tool-result')
|
|
544
|
+
return {
|
|
545
|
+
...entry,
|
|
546
|
+
item: {
|
|
547
|
+
...entry.item,
|
|
548
|
+
text: textTail(entry.item.text, Math.max(1, budget - 1), Math.max(1, width - 2)),
|
|
549
|
+
},
|
|
550
|
+
};
|
|
551
|
+
if (entry.kind === 'orphan-shell-result') {
|
|
552
|
+
const combined = orphanShellOutput(entry.item.stdout, entry.item.stderr);
|
|
553
|
+
return {
|
|
554
|
+
...entry,
|
|
555
|
+
item: {
|
|
556
|
+
...entry.item,
|
|
557
|
+
stdout: textTail(combined, budget, Math.max(1, width - 2)),
|
|
558
|
+
stderr: '',
|
|
559
|
+
},
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
return entry;
|
|
563
|
+
}
|
|
564
|
+
export function projectTranscriptPresentationTail(entries, budget, width, mode) {
|
|
565
|
+
if (!entries.length)
|
|
136
566
|
return [];
|
|
137
|
-
const
|
|
567
|
+
const limit = Math.max(1, budget);
|
|
138
568
|
let rows = 0;
|
|
139
|
-
let
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
/** Projects a fixed-size transcript window, measured upward from the newest row. */
|
|
160
|
-
export function projectTranscriptWindow(items, budget, width, scrollOffset) {
|
|
161
|
-
if (scrollOffset <= 0)
|
|
162
|
-
return projectTranscriptTail(items, budget, width);
|
|
163
|
-
const endRows = Math.max(0, transcriptLineCount(items, width) - scrollOffset);
|
|
164
|
-
const startRows = Math.max(0, endRows - budget);
|
|
569
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
570
|
+
const entry = entries[i];
|
|
571
|
+
if (!entry)
|
|
572
|
+
continue;
|
|
573
|
+
const count = estimateTranscriptEntryLines(entry, width, mode);
|
|
574
|
+
if (rows + count > limit) {
|
|
575
|
+
if (i !== entries.length - 1)
|
|
576
|
+
return entries.slice(i + 1);
|
|
577
|
+
const projected = projectOversizedEntry(entry, limit, width, mode);
|
|
578
|
+
return estimateTranscriptEntryLines(projected, width, mode) <= limit
|
|
579
|
+
? [projected]
|
|
580
|
+
: [];
|
|
581
|
+
}
|
|
582
|
+
rows += count;
|
|
583
|
+
}
|
|
584
|
+
return entries;
|
|
585
|
+
}
|
|
586
|
+
function projectTranscriptPresentationStart(entries, budget, width, mode) {
|
|
587
|
+
const limit = Math.max(1, budget);
|
|
165
588
|
let rows = 0;
|
|
166
|
-
let start = 0;
|
|
167
589
|
let end = 0;
|
|
168
|
-
for (
|
|
169
|
-
const
|
|
170
|
-
if (
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
590
|
+
for (const entry of entries) {
|
|
591
|
+
const count = estimateTranscriptEntryLines(entry, width, mode);
|
|
592
|
+
if (rows + count > limit) {
|
|
593
|
+
if (end > 0)
|
|
594
|
+
break;
|
|
595
|
+
const projected = projectOversizedEntry(entry, limit, width, mode);
|
|
596
|
+
return estimateTranscriptEntryLines(projected, width, mode) <= limit
|
|
597
|
+
? [projected]
|
|
598
|
+
: [];
|
|
599
|
+
}
|
|
600
|
+
rows += count;
|
|
601
|
+
end += 1;
|
|
602
|
+
}
|
|
603
|
+
return entries.slice(0, end);
|
|
604
|
+
}
|
|
605
|
+
export function projectTranscriptPresentationWindow(entries, budget, width, scrollOffset, mode) {
|
|
606
|
+
if (scrollOffset <= 0)
|
|
607
|
+
return projectTranscriptPresentationTail(entries, budget, width, mode);
|
|
608
|
+
const limit = Math.max(1, budget);
|
|
609
|
+
const totalRows = transcriptPresentationLineCount(entries, width, mode);
|
|
610
|
+
const clampedOffset = Math.min(Math.max(0, scrollOffset), Math.max(0, totalRows - limit));
|
|
611
|
+
const startRows = Math.max(0, totalRows - clampedOffset - limit);
|
|
612
|
+
const endRows = totalRows - clampedOffset;
|
|
613
|
+
let rows = 0;
|
|
614
|
+
let start = entries.length;
|
|
615
|
+
let startBeforeRows = 0;
|
|
616
|
+
let startEntryRows = 0;
|
|
617
|
+
for (let i = 0; i < entries.length; i += 1) {
|
|
618
|
+
const entry = entries[i];
|
|
619
|
+
if (!entry)
|
|
620
|
+
continue;
|
|
621
|
+
const count = estimateTranscriptEntryLines(entry, width, mode);
|
|
622
|
+
const next = rows + count;
|
|
623
|
+
if (start === entries.length && next > startRows) {
|
|
624
|
+
start = i;
|
|
625
|
+
startBeforeRows = rows;
|
|
626
|
+
startEntryRows = count;
|
|
175
627
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
628
|
+
rows = next;
|
|
629
|
+
}
|
|
630
|
+
if (start === entries.length)
|
|
631
|
+
return [];
|
|
632
|
+
let completeEnd = start - 1;
|
|
633
|
+
rows = 0;
|
|
634
|
+
for (let i = 0; i < entries.length; i += 1) {
|
|
635
|
+
const entry = entries[i];
|
|
636
|
+
if (!entry)
|
|
637
|
+
continue;
|
|
638
|
+
rows += estimateTranscriptEntryLines(entry, width, mode);
|
|
639
|
+
if (rows <= endRows)
|
|
640
|
+
completeEnd = i;
|
|
179
641
|
if (rows >= endRows)
|
|
180
642
|
break;
|
|
181
643
|
}
|
|
182
|
-
|
|
644
|
+
const offsetWithinStart = startRows - startBeforeRows;
|
|
645
|
+
const remainingStartRows = startEntryRows - offsetWithinStart;
|
|
646
|
+
if (completeEnd < start) {
|
|
647
|
+
const entry = entries[start];
|
|
648
|
+
if (!entry)
|
|
649
|
+
return [];
|
|
650
|
+
return projectTranscriptPresentationStart([entry], limit, width, mode);
|
|
651
|
+
}
|
|
652
|
+
let candidates = entries.slice(start, completeEnd + 1);
|
|
653
|
+
if (offsetWithinStart > 0) {
|
|
654
|
+
const entry = candidates[0];
|
|
655
|
+
if (entry) {
|
|
656
|
+
const allowance = Math.min(limit, Math.max(1, remainingStartRows));
|
|
657
|
+
candidates = [
|
|
658
|
+
projectOversizedEntry(entry, allowance, width, mode),
|
|
659
|
+
...candidates.slice(1),
|
|
660
|
+
];
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return projectTranscriptPresentationStart(candidates, limit, width, mode);
|
|
183
664
|
}
|
|
184
665
|
//# sourceMappingURL=transcript-viewport.js.map
|