praxis-agent 0.43.0 → 0.43.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.md +24 -37
- package/dist/application/native-session-projection.d.ts +4 -0
- package/dist/application/native-session-projection.js +135 -0
- package/dist/application/session-service.d.ts +0 -1
- package/dist/application/session-service.js +9 -170
- package/dist/cli/interactive.js +112 -156
- package/dist/cli/tui/ansi-frame-renderer.d.ts +2 -0
- package/dist/cli/tui/ansi-frame-renderer.js +109 -21
- package/dist/cli/tui/ansi-surface.d.ts +6 -22
- package/dist/cli/tui/ansi-surface.js +6 -126
- package/dist/cli/tui/ansi-theme.d.ts +3 -2
- package/dist/cli/tui/ansi-theme.js +22 -1
- package/dist/cli/tui/quiet-choice-rows.d.ts +26 -0
- package/dist/cli/tui/quiet-choice-rows.js +302 -0
- package/dist/cli/tui/quiet-frame-adapter.d.ts +18 -0
- package/dist/cli/tui/quiet-frame-adapter.js +90 -0
- package/dist/cli/tui/quiet-frame.d.ts +43 -0
- package/dist/cli/tui/quiet-frame.js +346 -0
- package/dist/cli/tui/quiet-operational-rows.d.ts +17 -0
- package/dist/cli/tui/quiet-operational-rows.js +413 -0
- package/dist/cli/tui/quiet-screen-projector.d.ts +25 -0
- package/dist/cli/tui/quiet-screen-projector.js +110 -0
- package/dist/cli/tui/quiet-settings-rows.d.ts +17 -0
- package/dist/cli/tui/quiet-settings-rows.js +490 -0
- package/dist/cli/tui/status-line.d.ts +15 -9
- package/dist/cli/tui/status-line.js +15 -6
- package/dist/cli/tui/theme.js +20 -20
- package/dist/cli/tui/transcript-viewport.d.ts +5 -0
- package/dist/cli/tui/transcript-viewport.js +213 -78
- package/dist/cli/tui/tui-row-ir.js +4 -4
- package/dist/persistence/in-memory-transcript-store.js +1 -7
- package/dist/persistence/native-transcript-store.d.ts +0 -9
- package/dist/persistence/native-transcript-store.js +4 -38
- package/package.json +3 -2
|
@@ -10,7 +10,7 @@ const rgiEmojiPattern = /^\p{RGI_Emoji}$/v;
|
|
|
10
10
|
const extendedPictographicPattern = /\p{Extended_Pictographic}/gu;
|
|
11
11
|
const spacingMarkPattern = /\p{Spacing_Mark}/v;
|
|
12
12
|
const unqualifiedKeycapPattern = /^[\d#*]\u20e3$/u;
|
|
13
|
-
function
|
|
13
|
+
export function terminalGraphemes(text) {
|
|
14
14
|
return Array.from(graphemeSegmenter.segment(text), ({ segment }) => segment);
|
|
15
15
|
}
|
|
16
16
|
function isPrintableAscii(text) {
|
|
@@ -45,7 +45,7 @@ function isDoubleWidthEmoji(cluster) {
|
|
|
45
45
|
return false;
|
|
46
46
|
return (cluster.match(extendedPictographicPattern)?.length ?? 0) >= 2;
|
|
47
47
|
}
|
|
48
|
-
function
|
|
48
|
+
export function terminalGraphemeWidth(cluster) {
|
|
49
49
|
if (!cluster || zeroWidthClusterPattern.test(cluster))
|
|
50
50
|
return 0;
|
|
51
51
|
if (isDoubleWidthEmoji(cluster))
|
|
@@ -69,7 +69,7 @@ function graphemeWidth(cluster) {
|
|
|
69
69
|
}
|
|
70
70
|
return width;
|
|
71
71
|
}
|
|
72
|
-
function
|
|
72
|
+
export function terminalTextWidth(text) {
|
|
73
73
|
if (isPrintableAscii(text))
|
|
74
74
|
return text.length;
|
|
75
75
|
const first = text.codePointAt(0);
|
|
@@ -80,14 +80,40 @@ function terminalStringWidth(text) {
|
|
|
80
80
|
return 2;
|
|
81
81
|
return first <= 0xffff ? 1 : isDoubleWidthEmoji(text) ? 2 : 1;
|
|
82
82
|
}
|
|
83
|
-
return
|
|
83
|
+
return terminalGraphemes(text).reduce((width, cluster) => width + terminalGraphemeWidth(cluster), 0);
|
|
84
|
+
}
|
|
85
|
+
export function terminalTextHead(text, budget) {
|
|
86
|
+
const limit = Number.isFinite(budget) ? Math.max(0, Math.floor(budget)) : 0;
|
|
87
|
+
let used = 0;
|
|
88
|
+
const result = [];
|
|
89
|
+
for (const { segment: cluster } of graphemeSegmenter.segment(text)) {
|
|
90
|
+
const width = terminalGraphemeWidth(cluster);
|
|
91
|
+
if (used + width > limit)
|
|
92
|
+
break;
|
|
93
|
+
result.push(cluster);
|
|
94
|
+
used += width;
|
|
95
|
+
}
|
|
96
|
+
return result.join('');
|
|
97
|
+
}
|
|
98
|
+
export function terminalTextTail(text, budget) {
|
|
99
|
+
const limit = Number.isFinite(budget) ? Math.max(0, Math.floor(budget)) : 0;
|
|
100
|
+
let used = 0;
|
|
101
|
+
const result = [];
|
|
102
|
+
for (const cluster of [...terminalGraphemes(text)].reverse()) {
|
|
103
|
+
const width = terminalGraphemeWidth(cluster);
|
|
104
|
+
if (used + width > limit)
|
|
105
|
+
break;
|
|
106
|
+
result.unshift(cluster);
|
|
107
|
+
used += width;
|
|
108
|
+
}
|
|
109
|
+
return result.join('');
|
|
84
110
|
}
|
|
85
111
|
function expandTabs(line) {
|
|
86
112
|
if (!line.includes('\t'))
|
|
87
113
|
return line;
|
|
88
114
|
let column = 0;
|
|
89
115
|
let expanded = '';
|
|
90
|
-
for (const cluster of
|
|
116
|
+
for (const cluster of terminalGraphemes(line)) {
|
|
91
117
|
if (cluster === '\t') {
|
|
92
118
|
const spaces = 8 - (column % 8);
|
|
93
119
|
expanded += ' '.repeat(spaces);
|
|
@@ -95,7 +121,7 @@ function expandTabs(line) {
|
|
|
95
121
|
continue;
|
|
96
122
|
}
|
|
97
123
|
expanded += cluster;
|
|
98
|
-
column +=
|
|
124
|
+
column += terminalGraphemeWidth(cluster);
|
|
99
125
|
}
|
|
100
126
|
return expanded;
|
|
101
127
|
}
|
|
@@ -109,11 +135,11 @@ function hardWrappedRows(word, width, startColumn) {
|
|
|
109
135
|
column: occupied % width || width,
|
|
110
136
|
};
|
|
111
137
|
}
|
|
112
|
-
const clusters =
|
|
138
|
+
const clusters = terminalGraphemes(word);
|
|
113
139
|
let rows = 0;
|
|
114
140
|
let column = startColumn;
|
|
115
141
|
for (const [index, cluster] of clusters.entries()) {
|
|
116
|
-
const clusterWidth =
|
|
142
|
+
const clusterWidth = terminalGraphemeWidth(cluster);
|
|
117
143
|
if (column + clusterWidth > width) {
|
|
118
144
|
rows += 1;
|
|
119
145
|
column = clusterWidth;
|
|
@@ -155,9 +181,9 @@ function wrappedLineLayout(line, width, output) {
|
|
|
155
181
|
column = wrapped.column;
|
|
156
182
|
return;
|
|
157
183
|
}
|
|
158
|
-
const clusters =
|
|
184
|
+
const clusters = terminalGraphemes(word);
|
|
159
185
|
for (const [index, cluster] of clusters.entries()) {
|
|
160
|
-
const clusterWidth =
|
|
186
|
+
const clusterWidth = terminalGraphemeWidth(cluster);
|
|
161
187
|
if (column + clusterWidth > width)
|
|
162
188
|
pushRow();
|
|
163
189
|
row += cluster;
|
|
@@ -170,7 +196,7 @@ function wrappedLineLayout(line, width, output) {
|
|
|
170
196
|
const word = words[index] ?? '';
|
|
171
197
|
const wordWidth = isPrintableAscii(word)
|
|
172
198
|
? word.length
|
|
173
|
-
:
|
|
199
|
+
: terminalTextWidth(word);
|
|
174
200
|
if (index > 0) {
|
|
175
201
|
if (column >= width)
|
|
176
202
|
pushRow();
|
|
@@ -271,14 +297,63 @@ function inputString(entry, key) {
|
|
|
271
297
|
const value = entry.item.call.input[key];
|
|
272
298
|
return typeof value === 'string' ? value : '';
|
|
273
299
|
}
|
|
300
|
+
function oneLine(value, max = 160) {
|
|
301
|
+
const text = stripVTControlCharacters(value)
|
|
302
|
+
.replace(/[\r\n\t]+/gu, ' ')
|
|
303
|
+
.replace(/\s+/gu, ' ')
|
|
304
|
+
.trim();
|
|
305
|
+
return text.length > max ? `${text.slice(0, Math.max(1, max - 1))}…` : text;
|
|
306
|
+
}
|
|
274
307
|
function toolHeading(entry) {
|
|
275
308
|
if (entry.item.call.name === 'Edit')
|
|
276
|
-
return `
|
|
309
|
+
return `Edit ${oneLine(inputString(entry, 'file_path'))}`.trimEnd();
|
|
277
310
|
if (entry.item.call.name === 'Read')
|
|
278
|
-
return `Read
|
|
311
|
+
return `Read ${oneLine(inputString(entry, 'file_path'))}`.trimEnd();
|
|
279
312
|
if (entry.item.call.name === 'Bash')
|
|
280
|
-
return `Bash
|
|
281
|
-
return entry.item.call.name;
|
|
313
|
+
return `Bash ${oneLine(inputString(entry, 'command'))}`.trimEnd();
|
|
314
|
+
return oneLine(entry.item.call.name);
|
|
315
|
+
}
|
|
316
|
+
function toolSummary(entry, mode) {
|
|
317
|
+
const state = !entry.result
|
|
318
|
+
? 'running'
|
|
319
|
+
: entry.result.isError
|
|
320
|
+
? 'failed'
|
|
321
|
+
: 'completed';
|
|
322
|
+
const prefix = mode === 'screen-reader'
|
|
323
|
+
? state === 'running'
|
|
324
|
+
? 'Running tool: '
|
|
325
|
+
: state === 'failed'
|
|
326
|
+
? 'Failed tool: '
|
|
327
|
+
: 'Completed tool: '
|
|
328
|
+
: state === 'running'
|
|
329
|
+
? '… '
|
|
330
|
+
: state === 'failed'
|
|
331
|
+
? '! '
|
|
332
|
+
: '✓ ';
|
|
333
|
+
const detail = !['Bash', 'Read', 'Edit'].includes(entry.item.call.name) &&
|
|
334
|
+
entry.item.detail
|
|
335
|
+
? ` · ${oneLine(entry.item.detail, 120)}`
|
|
336
|
+
: '';
|
|
337
|
+
return `${prefix}${toolHeading(entry)}${detail}`;
|
|
338
|
+
}
|
|
339
|
+
function shellSummary(entry, mode) {
|
|
340
|
+
const state = !entry.result
|
|
341
|
+
? 'running'
|
|
342
|
+
: entry.result.isError
|
|
343
|
+
? 'failed'
|
|
344
|
+
: 'completed';
|
|
345
|
+
const prefix = mode === 'screen-reader'
|
|
346
|
+
? state === 'running'
|
|
347
|
+
? 'Running shell command: '
|
|
348
|
+
: state === 'failed'
|
|
349
|
+
? 'Failed shell command: '
|
|
350
|
+
: 'Completed shell command: '
|
|
351
|
+
: state === 'running'
|
|
352
|
+
? '… Bash '
|
|
353
|
+
: state === 'failed'
|
|
354
|
+
? '! Bash '
|
|
355
|
+
: '✓ Bash ';
|
|
356
|
+
return `${prefix}${oneLine(entry.item.command)}`.trimEnd();
|
|
282
357
|
}
|
|
283
358
|
function visibleOutputLineCount(text, width, mode) {
|
|
284
359
|
const lines = contentLines(text);
|
|
@@ -319,7 +394,7 @@ function textTail(text, budget, width) {
|
|
|
319
394
|
}
|
|
320
395
|
const remainingRows = usableBudget - rowsAfter;
|
|
321
396
|
if (remainingRows > 0) {
|
|
322
|
-
const clusters =
|
|
397
|
+
const clusters = terminalGraphemes(line);
|
|
323
398
|
let low = 0;
|
|
324
399
|
let high = clusters.length;
|
|
325
400
|
let suffix = '';
|
|
@@ -336,7 +411,7 @@ function textTail(text, budget, width) {
|
|
|
336
411
|
}
|
|
337
412
|
while (suffix &&
|
|
338
413
|
wrappedLineCount(`${TRANSCRIPT_TRUNCATION_MARKER}${suffix}`, usableWidth) > remainingRows) {
|
|
339
|
-
const suffixClusters =
|
|
414
|
+
const suffixClusters = terminalGraphemes(suffix);
|
|
340
415
|
suffix = suffixClusters.slice(0, -1).join('');
|
|
341
416
|
}
|
|
342
417
|
kept.unshift(suffix);
|
|
@@ -364,7 +439,7 @@ function textHead(text, budget, width) {
|
|
|
364
439
|
return candidate;
|
|
365
440
|
const remaining = Math.max(0, usableBudget - wrappedLineCount(marker, usableWidth));
|
|
366
441
|
const prefix = kept.join('\n');
|
|
367
|
-
const clusters =
|
|
442
|
+
const clusters = terminalGraphemes(prefix);
|
|
368
443
|
let low = 0;
|
|
369
444
|
let high = clusters.length;
|
|
370
445
|
let best = '';
|
|
@@ -416,19 +491,17 @@ function textRange(text, startRow, budget, width) {
|
|
|
416
491
|
}
|
|
417
492
|
function estimateRenderedTranscriptEntryLines(entry, width, mode) {
|
|
418
493
|
if (entry.kind === 'read-summary')
|
|
419
|
-
return wrappedLineCount(
|
|
494
|
+
return wrappedLineCount(`✓ Read ${entry.count} file${entry.count === 1 ? '' : 's'}`, width);
|
|
420
495
|
if (entry.kind === 'item') {
|
|
421
496
|
const item = entry.item;
|
|
422
497
|
if (item.kind === 'user')
|
|
423
498
|
return (1 +
|
|
424
499
|
(mode === 'screen-reader' ? 1 : 0) +
|
|
425
|
-
wrappedLineCount(`${mode === 'screen-reader' ? 'You: ' : '
|
|
500
|
+
wrappedLineCount(`${mode === 'screen-reader' ? 'You: ' : 'you> '}${item.text}`, width));
|
|
426
501
|
if (item.kind === 'assistant')
|
|
427
502
|
return (1 +
|
|
428
503
|
(mode === 'screen-reader' ? 1 : 0) +
|
|
429
|
-
|
|
430
|
-
? Math.max(1, width - 7)
|
|
431
|
-
: markdownContentWidth(width)));
|
|
504
|
+
assistantMarkdownProjectionRows(item.text, width, mode).length);
|
|
432
505
|
if (item.kind === 'thinking') {
|
|
433
506
|
const summary = item.text.replace(/\s+/gu, ' ').trim();
|
|
434
507
|
if (mode === 'normal')
|
|
@@ -473,15 +546,14 @@ function estimateRenderedTranscriptEntryLines(entry, width, mode) {
|
|
|
473
546
|
if (entry.kind === 'orphan-shell-result')
|
|
474
547
|
return wrappedLineCount(`⎿ ${orphanShellOutput(entry.item.stdout, entry.item.stderr)}`, width);
|
|
475
548
|
if (entry.kind === 'tool') {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
entry.result
|
|
479
|
-
|
|
480
|
-
return
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
rows += wrappedLineCount(` ${entry.item.detail}`, width);
|
|
549
|
+
const summaryRows = wrappedLineCount(toolSummary(entry, mode), width);
|
|
550
|
+
if (mode === 'normal') {
|
|
551
|
+
if (!entry.result || !entry.result.isError)
|
|
552
|
+
return summaryRows;
|
|
553
|
+
return (summaryRows +
|
|
554
|
+
viewportOutputRows(`Error: ${bounded(entry.result.text, 500)}`, width, mode).length);
|
|
555
|
+
}
|
|
556
|
+
let rows = 1 + summaryRows;
|
|
485
557
|
if (!entry.result)
|
|
486
558
|
return rows;
|
|
487
559
|
if (entry.result.isError)
|
|
@@ -491,18 +563,25 @@ function estimateRenderedTranscriptEntryLines(entry, width, mode) {
|
|
|
491
563
|
const newLines = contentLines(inputString(entry, 'new_string'));
|
|
492
564
|
const oldLines = contentLines(inputString(entry, 'old_string'));
|
|
493
565
|
rows += wrappedLineCount(`⎿ Added ${newLines.length} line${newLines.length === 1 ? '' : 's'}, removed ${oldLines.length} line${oldLines.length === 1 ? '' : 's'}`, Math.max(1, width - 2));
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
wrappedLineCount(` ${index + 1} +${line}`, Math.max(1, width - 2)), 0);
|
|
499
|
-
}
|
|
566
|
+
rows += oldLines.reduce((total, line, index) => total +
|
|
567
|
+
wrappedLineCount(` ${index + 1} -${line}`, Math.max(1, width - 2)), 0);
|
|
568
|
+
rows += newLines.reduce((total, line, index) => total +
|
|
569
|
+
wrappedLineCount(` ${index + 1} +${line}`, Math.max(1, width - 2)), 0);
|
|
500
570
|
return rows;
|
|
501
571
|
}
|
|
502
572
|
return rows + visibleOutputLineCount(entry.result.text, width, mode);
|
|
503
573
|
}
|
|
504
|
-
|
|
505
|
-
|
|
574
|
+
const summaryRows = wrappedLineCount(shellSummary(entry, mode), width);
|
|
575
|
+
if (mode === 'normal') {
|
|
576
|
+
if (!entry.result || !entry.result.isError)
|
|
577
|
+
return summaryRows;
|
|
578
|
+
const output = combinedShellOutput(entry.result.stdout, entry.result.stderr);
|
|
579
|
+
return (summaryRows +
|
|
580
|
+
(output
|
|
581
|
+
? viewportOutputRows(output, width, mode).length
|
|
582
|
+
: wrappedLineCount('⎿ ', Math.max(1, width - 2))));
|
|
583
|
+
}
|
|
584
|
+
let rows = 1 + summaryRows;
|
|
506
585
|
if (!entry.result)
|
|
507
586
|
return rows;
|
|
508
587
|
const output = combinedShellOutput(entry.result.stdout, entry.result.stderr);
|
|
@@ -528,18 +607,58 @@ export function transcriptPresentationLineCount(entries, width, mode) {
|
|
|
528
607
|
return entries.reduce((n, entry) => n + estimateTranscriptEntryLines(entry, width, mode), 0);
|
|
529
608
|
}
|
|
530
609
|
function viewportOutputRows(text, width, mode) {
|
|
531
|
-
const lines = contentLines(text);
|
|
532
|
-
const visible = mode === 'normal' ? lines.slice(0, 3) : lines;
|
|
533
610
|
const outputWidth = Math.max(1, width - 2);
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
611
|
+
if (mode !== 'normal') {
|
|
612
|
+
const lines = contentLines(text);
|
|
613
|
+
return lines.flatMap((line, index) => textVisualRows(`${index === 0 ? '⎿ ' : ' '}${line || ' '}`, outputWidth));
|
|
537
614
|
}
|
|
538
|
-
|
|
615
|
+
const rows = [];
|
|
616
|
+
const rowBudget = 8;
|
|
617
|
+
const lines = [];
|
|
618
|
+
let lineStart = 0;
|
|
619
|
+
let omittedLines = 0;
|
|
620
|
+
for (let index = 0; index <= text.length && lines.length < 3; index += 1) {
|
|
621
|
+
if (index !== text.length && text[index] !== '\n')
|
|
622
|
+
continue;
|
|
623
|
+
const line = text.slice(lineStart, index).replace(/\r$/u, '');
|
|
624
|
+
if (!(index === text.length && line === '' && text.endsWith('\n')))
|
|
625
|
+
lines.push(line);
|
|
626
|
+
lineStart = index + 1;
|
|
627
|
+
}
|
|
628
|
+
for (let index = lineStart; index < text.length; index += 1)
|
|
629
|
+
if (text[index] === '\n')
|
|
630
|
+
omittedLines += 1;
|
|
631
|
+
if (lineStart < text.length && !text.endsWith('\n'))
|
|
632
|
+
omittedLines += 1;
|
|
633
|
+
let hidden = omittedLines > 0;
|
|
634
|
+
for (const [index, line] of lines.entries()) {
|
|
635
|
+
const prefix = index === 0 ? '⎿ ' : ' ';
|
|
636
|
+
const remaining = rowBudget - rows.length;
|
|
637
|
+
if (remaining <= 1) {
|
|
638
|
+
hidden = true;
|
|
639
|
+
break;
|
|
640
|
+
}
|
|
641
|
+
const candidate = terminalTextHead(line || ' ', outputWidth * (remaining - 1));
|
|
642
|
+
if (candidate.length < (line || ' ').length)
|
|
643
|
+
hidden = true;
|
|
644
|
+
rows.push(...textVisualRows(`${prefix}${candidate}`, outputWidth).slice(0, remaining - 1));
|
|
645
|
+
if (rows.length >= rowBudget - 1)
|
|
646
|
+
break;
|
|
647
|
+
}
|
|
648
|
+
if (hidden) {
|
|
649
|
+
const marker = textVisualRows(omittedLines > 0
|
|
650
|
+
? ` ${TRANSCRIPT_TRUNCATION_MARKER} +${omittedLines} lines (ctrl+o to expand)`
|
|
651
|
+
: ` ${TRANSCRIPT_TRUNCATION_MARKER}`, outputWidth);
|
|
652
|
+
if (marker.length <= rowBudget - rows.length)
|
|
653
|
+
rows.push(...marker.slice(0, rowBudget - rows.length));
|
|
654
|
+
else
|
|
655
|
+
rows.push(TRANSCRIPT_TRUNCATION_MARKER);
|
|
656
|
+
}
|
|
657
|
+
return rows.slice(0, rowBudget);
|
|
539
658
|
}
|
|
540
659
|
function entryViewportRows(entry, width, mode) {
|
|
541
660
|
if (entry.kind === 'read-summary')
|
|
542
|
-
return textVisualRows(
|
|
661
|
+
return textVisualRows(`✓ Read ${entry.count} file${entry.count === 1 ? '' : 's'}`, width);
|
|
543
662
|
if (entry.kind === 'item') {
|
|
544
663
|
const item = entry.item;
|
|
545
664
|
if (item.kind === 'context')
|
|
@@ -548,15 +667,13 @@ function entryViewportRows(entry, width, mode) {
|
|
|
548
667
|
return [
|
|
549
668
|
'',
|
|
550
669
|
...(mode === 'screen-reader' ? [''] : []),
|
|
551
|
-
...textVisualRows(`${mode === 'screen-reader' ? 'You: ' : '
|
|
670
|
+
...textVisualRows(`${mode === 'screen-reader' ? 'You: ' : 'you> '}${item.text}`, width),
|
|
552
671
|
];
|
|
553
672
|
if (item.kind === 'assistant')
|
|
554
673
|
return [
|
|
555
674
|
'',
|
|
556
675
|
...(mode === 'screen-reader' ? ['Praxis:'] : []),
|
|
557
|
-
...
|
|
558
|
-
? Math.max(1, width - 7)
|
|
559
|
-
: markdownContentWidth(width)),
|
|
676
|
+
...assistantMarkdownProjectionRows(item.text, width, mode).map((row) => row.text),
|
|
560
677
|
];
|
|
561
678
|
if (item.kind === 'thinking') {
|
|
562
679
|
const summary = item.text.replace(/\s+/gu, ' ').trim();
|
|
@@ -605,10 +722,19 @@ function entryViewportRows(entry, width, mode) {
|
|
|
605
722
|
if (entry.kind === 'orphan-shell-result')
|
|
606
723
|
return textVisualRows(`⎿ ${orphanShellOutput(entry.item.stdout, entry.item.stderr)}`, width);
|
|
607
724
|
if (entry.kind === 'shell') {
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
725
|
+
const summaryRows = textVisualRows(shellSummary(entry, mode), width);
|
|
726
|
+
if (mode === 'normal') {
|
|
727
|
+
if (!entry.result || !entry.result.isError)
|
|
728
|
+
return summaryRows;
|
|
729
|
+
const output = combinedShellOutput(entry.result.stdout, entry.result.stderr);
|
|
730
|
+
return [
|
|
731
|
+
...summaryRows,
|
|
732
|
+
...(output
|
|
733
|
+
? viewportOutputRows(output, width, mode)
|
|
734
|
+
: textVisualRows('⎿ ', Math.max(1, width - 2))),
|
|
735
|
+
];
|
|
736
|
+
}
|
|
737
|
+
const rows = ['', ...summaryRows];
|
|
612
738
|
if (!entry.result)
|
|
613
739
|
return rows;
|
|
614
740
|
const output = combinedShellOutput(entry.result.stdout, entry.result.stderr);
|
|
@@ -619,15 +745,16 @@ function entryViewportRows(entry, width, mode) {
|
|
|
619
745
|
: textVisualRows('⎿ ', Math.max(1, width - 2))),
|
|
620
746
|
];
|
|
621
747
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
entry.result
|
|
625
|
-
|
|
626
|
-
return
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
748
|
+
const summaryRows = textVisualRows(toolSummary(entry, mode), width);
|
|
749
|
+
if (mode === 'normal') {
|
|
750
|
+
if (!entry.result || !entry.result.isError)
|
|
751
|
+
return summaryRows;
|
|
752
|
+
return [
|
|
753
|
+
...summaryRows,
|
|
754
|
+
...viewportOutputRows(`Error: ${bounded(entry.result.text, 500)}`, width, mode),
|
|
755
|
+
];
|
|
756
|
+
}
|
|
757
|
+
const rows = ['', ...summaryRows];
|
|
631
758
|
if (!entry.result)
|
|
632
759
|
return rows;
|
|
633
760
|
if (entry.result.isError) {
|
|
@@ -638,12 +765,10 @@ function entryViewportRows(entry, width, mode) {
|
|
|
638
765
|
const oldLines = contentLines(inputString(entry, 'old_string'));
|
|
639
766
|
const newLines = contentLines(inputString(entry, 'new_string'));
|
|
640
767
|
rows.push(...textVisualRows(`⎿ Added ${newLines.length} line${newLines.length === 1 ? '' : 's'}, removed ${oldLines.length} line${oldLines.length === 1 ? '' : 's'}`, Math.max(1, width - 2)));
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
rows.push(...textVisualRows(` ${index + 1} +${line}`, Math.max(1, width - 2)));
|
|
646
|
-
}
|
|
768
|
+
for (const [index, line] of oldLines.entries())
|
|
769
|
+
rows.push(...textVisualRows(` ${index + 1} -${line}`, Math.max(1, width - 2)));
|
|
770
|
+
for (const [index, line] of newLines.entries())
|
|
771
|
+
rows.push(...textVisualRows(` ${index + 1} +${line}`, Math.max(1, width - 2)));
|
|
647
772
|
return rows;
|
|
648
773
|
}
|
|
649
774
|
rows.push(...viewportOutputRows(entry.result.text, width, mode));
|
|
@@ -680,12 +805,25 @@ function markdownViewportRows(text, width) {
|
|
|
680
805
|
}
|
|
681
806
|
return rows;
|
|
682
807
|
}
|
|
808
|
+
function assistantMarkdownProjectionRows(text, width, mode) {
|
|
809
|
+
const contentWidth = mode === 'screen-reader'
|
|
810
|
+
? Math.max(1, width - 7)
|
|
811
|
+
: markdownContentWidth(width);
|
|
812
|
+
const rows = markdownViewportRows(text, contentWidth);
|
|
813
|
+
if (mode === 'screen-reader')
|
|
814
|
+
return rows;
|
|
815
|
+
const first = rows[0] ?? {
|
|
816
|
+
text: '',
|
|
817
|
+
fenceBefore: false,
|
|
818
|
+
fenceAfter: false,
|
|
819
|
+
fenceLabel: 'code',
|
|
820
|
+
};
|
|
821
|
+
const prefixed = textVisualRows(`praxis> ${first.text.trimEnd()}`, width).map((rowText) => ({ ...first, text: rowText }));
|
|
822
|
+
return [...prefixed, ...rows.slice(1)];
|
|
823
|
+
}
|
|
683
824
|
export function createTranscriptEntryViewportIndex(entry, width, mode) {
|
|
684
825
|
if (entry.kind === 'item' && entry.item.kind === 'assistant') {
|
|
685
|
-
const
|
|
686
|
-
? Math.max(1, width - 7)
|
|
687
|
-
: markdownContentWidth(width);
|
|
688
|
-
const assistantMarkdownRows = markdownViewportRows(entry.item.text, contentWidth);
|
|
826
|
+
const assistantMarkdownRows = assistantMarkdownProjectionRows(entry.item.text, width, mode);
|
|
689
827
|
return {
|
|
690
828
|
entry,
|
|
691
829
|
width,
|
|
@@ -712,12 +850,9 @@ function projectAssistantMarkdownSlice(source, sourceText, startRow, budget, wid
|
|
|
712
850
|
const marginTop = startRow <= 0 ? 1 : 0;
|
|
713
851
|
const projectedChromeRows = marginTop + (mode === 'screen-reader' ? 1 : 0);
|
|
714
852
|
const contentBudget = Math.max(1, limit - projectedChromeRows);
|
|
715
|
-
const contentWidth = mode === 'screen-reader'
|
|
716
|
-
? Math.max(1, width - 7)
|
|
717
|
-
: markdownContentWidth(width);
|
|
718
853
|
const sourceRows = matchingViewportIndex(viewportIndex, source, width, mode)
|
|
719
854
|
?.assistantMarkdownRows ??
|
|
720
|
-
|
|
855
|
+
assistantMarkdownProjectionRows(sourceText ?? '', width, mode);
|
|
721
856
|
const contentStart = Math.min(Math.max(0, startRow - sourceChromeRows + (startRow > 0 ? 1 : 0)), sourceRows.length);
|
|
722
857
|
const clippedBefore = contentStart > 0 && contentBudget > 1;
|
|
723
858
|
let remaining = Math.max(1, contentBudget - (clippedBefore ? 1 : 0));
|
|
@@ -15,7 +15,7 @@ function entryText(entry) {
|
|
|
15
15
|
if (entry.kind === 'read-summary')
|
|
16
16
|
return {
|
|
17
17
|
text: `Read ${entry.count} file${entry.count === 1 ? '' : 's'}`,
|
|
18
|
-
role: '
|
|
18
|
+
role: 'success',
|
|
19
19
|
};
|
|
20
20
|
if (entry.kind === 'item') {
|
|
21
21
|
const item = entry.item;
|
|
@@ -28,7 +28,7 @@ function entryText(entry) {
|
|
|
28
28
|
if (item.kind === 'notice')
|
|
29
29
|
return { text: `· ${item.text}`, role: 'muted' };
|
|
30
30
|
if (item.kind === 'warning')
|
|
31
|
-
return { text: `⚠ ${item.text}`, role: '
|
|
31
|
+
return { text: `⚠ ${item.text}`, role: 'warning' };
|
|
32
32
|
if (item.kind === 'local-result')
|
|
33
33
|
return { text: `⎿ ${item.text}`, role: 'muted' };
|
|
34
34
|
if (item.kind === 'compact')
|
|
@@ -51,7 +51,7 @@ function entryText(entry) {
|
|
|
51
51
|
: '';
|
|
52
52
|
return {
|
|
53
53
|
text: `⏺ ${toolHeading(entry.item)}${entry.item.detail ? `\n ${entry.item.detail}` : ''}${output}`,
|
|
54
|
-
role: result
|
|
54
|
+
role: result ? (result.isError ? 'error' : 'success') : 'muted',
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
57
|
if (entry.kind === 'shell') {
|
|
@@ -61,7 +61,7 @@ function entryText(entry) {
|
|
|
61
61
|
: '';
|
|
62
62
|
return {
|
|
63
63
|
text: `! ${entry.item.command}${output}`,
|
|
64
|
-
role: result
|
|
64
|
+
role: result ? (result.isError ? 'error' : 'success') : 'muted',
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
67
|
if (entry.kind === 'orphan-tool-result')
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { projectNativeTranscriptEntries } from './native-transcript-store.js';
|
|
2
1
|
export class InMemoryTranscriptStore {
|
|
3
2
|
records = [];
|
|
4
3
|
reserved = false;
|
|
@@ -47,12 +46,7 @@ export class InMemoryTranscriptStore {
|
|
|
47
46
|
}
|
|
48
47
|
async load() {
|
|
49
48
|
const records = this.records.map((record) => ({ event: record.event }));
|
|
50
|
-
|
|
51
|
-
Object.defineProperty(snapshot, 'entries', {
|
|
52
|
-
value: projectNativeTranscriptEntries(records),
|
|
53
|
-
enumerable: false,
|
|
54
|
-
});
|
|
55
|
-
return snapshot;
|
|
49
|
+
return { records, tail: this.tail() };
|
|
56
50
|
}
|
|
57
51
|
async appendMany(expected, events) {
|
|
58
52
|
if (!events.length)
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { TranscriptEvent } from '../core/transcript-event.js';
|
|
2
|
-
import type { NativeTranscriptEntry } from '../native/schema.js';
|
|
3
2
|
import { type DecodedTranscriptRecord, type TranscriptCodecDiagnostic } from '../core/transcript-codec.js';
|
|
4
3
|
export interface NativeTranscriptTail {
|
|
5
4
|
byteLength: number;
|
|
@@ -10,8 +9,6 @@ export interface NativeTranscriptTail {
|
|
|
10
9
|
}
|
|
11
10
|
export interface NativeTranscriptSnapshot {
|
|
12
11
|
records: readonly DecodedTranscriptRecord[];
|
|
13
|
-
/** Claude-shaped projection for higher-level session helpers; never authoritative. */
|
|
14
|
-
entries: NativeTranscriptEntry[];
|
|
15
12
|
tail: NativeTranscriptTail;
|
|
16
13
|
}
|
|
17
14
|
export interface NativeTranscriptRecovery extends NativeTranscriptSnapshot {
|
|
@@ -50,12 +47,6 @@ export type NativeTranscriptLeaseResult<T> = {
|
|
|
50
47
|
status: 'conflict';
|
|
51
48
|
reason: 'locked';
|
|
52
49
|
};
|
|
53
|
-
/**
|
|
54
|
-
* Projects authoritative native events into the Claude-shaped entry view
|
|
55
|
-
* consumed by higher-level session helpers. The projection is intentionally
|
|
56
|
-
* non-authoritative; callers must persist and validate `records`.
|
|
57
|
-
*/
|
|
58
|
-
export declare function projectNativeTranscriptEntries(records: readonly DecodedTranscriptRecord[]): NativeTranscriptEntry[];
|
|
59
50
|
export declare class NativeTranscriptStore {
|
|
60
51
|
private readonly options;
|
|
61
52
|
private readonly lease;
|
|
@@ -4,40 +4,6 @@ import { dirname } from 'node:path';
|
|
|
4
4
|
import { ExclusiveFileLease } from '../platform/exclusive-file-lease.js';
|
|
5
5
|
import { createNativeTranscriptCodec } from './native-transcript-codec.js';
|
|
6
6
|
import { classifyTranscriptAppend } from './transcript-file-append.js';
|
|
7
|
-
/**
|
|
8
|
-
* Projects authoritative native events into the Claude-shaped entry view
|
|
9
|
-
* consumed by higher-level session helpers. The projection is intentionally
|
|
10
|
-
* non-authoritative; callers must persist and validate `records`.
|
|
11
|
-
*/
|
|
12
|
-
export function projectNativeTranscriptEntries(records) {
|
|
13
|
-
return records.flatMap(({ event }) => {
|
|
14
|
-
const messages = event.kind === 'messages' ? event.messages : [];
|
|
15
|
-
if (messages.length === 0) {
|
|
16
|
-
return [
|
|
17
|
-
{
|
|
18
|
-
...event,
|
|
19
|
-
type: event.kind === 'tool-execution-started' ? 'assistant' : 'system',
|
|
20
|
-
uuid: event.id,
|
|
21
|
-
parentUuid: event.parentId,
|
|
22
|
-
},
|
|
23
|
-
];
|
|
24
|
-
}
|
|
25
|
-
return messages.map((message, index) => ({
|
|
26
|
-
...event,
|
|
27
|
-
type: message.role,
|
|
28
|
-
uuid: index === 0 ? event.id : `${event.id}:${index}`,
|
|
29
|
-
parentUuid: index === 0 ? event.parentId : event.id,
|
|
30
|
-
message,
|
|
31
|
-
}));
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
function attachEntries(snapshot) {
|
|
35
|
-
Object.defineProperty(snapshot, 'entries', {
|
|
36
|
-
value: projectNativeTranscriptEntries(snapshot.records),
|
|
37
|
-
enumerable: false,
|
|
38
|
-
});
|
|
39
|
-
return snapshot;
|
|
40
|
-
}
|
|
41
7
|
const hashLine = (line) => createHash('sha256').update(line).digest('hex');
|
|
42
8
|
const emptyTail = () => ({
|
|
43
9
|
byteLength: 0,
|
|
@@ -197,21 +163,21 @@ export class NativeTranscriptStore {
|
|
|
197
163
|
}
|
|
198
164
|
decode(source, recover) {
|
|
199
165
|
if (!source.length)
|
|
200
|
-
return
|
|
166
|
+
return { records: [], tail: emptyTail(), issue: null };
|
|
201
167
|
const decoded = createNativeTranscriptCodec().decodeDocument(source);
|
|
202
168
|
if (decoded.issue && !recover)
|
|
203
169
|
throw new Error(`Invalid native transcript: ${decoded.issue.message}`);
|
|
204
170
|
if (!recover)
|
|
205
171
|
validateHistory(decoded.records.map((record) => record.event));
|
|
206
|
-
return
|
|
172
|
+
return {
|
|
207
173
|
records: decoded.records,
|
|
208
174
|
tail: tailFrom(source, decoded.records),
|
|
209
175
|
issue: decoded.issue,
|
|
210
|
-
}
|
|
176
|
+
};
|
|
211
177
|
}
|
|
212
178
|
async load() {
|
|
213
179
|
const { records, tail } = this.decode(await this.source(), false);
|
|
214
|
-
return
|
|
180
|
+
return { records, tail };
|
|
215
181
|
}
|
|
216
182
|
async loadReadOnly() {
|
|
217
183
|
return this.decode(await this.source(), true);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "praxis-agent",
|
|
3
|
-
"version": "0.43.
|
|
3
|
+
"version": "0.43.1",
|
|
4
4
|
"description": "Local-first, single-user general agent for the command line.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "wuqisen",
|
|
@@ -48,9 +48,10 @@
|
|
|
48
48
|
"test:tui:pty": "npm run build && node scripts/verify-fullscreen-rendering.mjs && node scripts/verify-interactive-ansi.mjs",
|
|
49
49
|
"test:self-update": "npm run build && node scripts/verify-self-update-contract.mjs",
|
|
50
50
|
"test:core-completion": "npm run build && node scripts/verify-core-completion-audit.mjs",
|
|
51
|
-
"test:performance": "npm run build && node scripts/verify-projection-scaling.mjs && node scripts/verify-projection-regression.mjs",
|
|
51
|
+
"test:performance": "npm run build && node scripts/verify-projection-scaling.mjs && node scripts/verify-projection-regression.mjs && node scripts/verify-quiet-frame-performance.mjs",
|
|
52
52
|
"test:performance:projection": "npm run build && node scripts/verify-projection-scaling.mjs",
|
|
53
53
|
"test:performance:projection-regression": "npm run build && node scripts/verify-projection-regression.mjs",
|
|
54
|
+
"test:performance:quiet-frame": "npm run build && node scripts/verify-quiet-frame-performance.mjs",
|
|
54
55
|
"test:prompt-suggestions": "npm run build && node scripts/verify-prompt-suggestions.mjs",
|
|
55
56
|
"test:mcp-oauth-serve": "npm run build && node scripts/verify-mcp-oauth-serve.mjs",
|
|
56
57
|
"test:package": "npm run build && node scripts/verify-native-release-package.mjs",
|