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.
Files changed (34) hide show
  1. package/README.md +24 -37
  2. package/dist/application/native-session-projection.d.ts +4 -0
  3. package/dist/application/native-session-projection.js +135 -0
  4. package/dist/application/session-service.d.ts +0 -1
  5. package/dist/application/session-service.js +9 -170
  6. package/dist/cli/interactive.js +112 -156
  7. package/dist/cli/tui/ansi-frame-renderer.d.ts +2 -0
  8. package/dist/cli/tui/ansi-frame-renderer.js +109 -21
  9. package/dist/cli/tui/ansi-surface.d.ts +6 -22
  10. package/dist/cli/tui/ansi-surface.js +6 -126
  11. package/dist/cli/tui/ansi-theme.d.ts +3 -2
  12. package/dist/cli/tui/ansi-theme.js +22 -1
  13. package/dist/cli/tui/quiet-choice-rows.d.ts +26 -0
  14. package/dist/cli/tui/quiet-choice-rows.js +302 -0
  15. package/dist/cli/tui/quiet-frame-adapter.d.ts +18 -0
  16. package/dist/cli/tui/quiet-frame-adapter.js +90 -0
  17. package/dist/cli/tui/quiet-frame.d.ts +43 -0
  18. package/dist/cli/tui/quiet-frame.js +346 -0
  19. package/dist/cli/tui/quiet-operational-rows.d.ts +17 -0
  20. package/dist/cli/tui/quiet-operational-rows.js +413 -0
  21. package/dist/cli/tui/quiet-screen-projector.d.ts +25 -0
  22. package/dist/cli/tui/quiet-screen-projector.js +110 -0
  23. package/dist/cli/tui/quiet-settings-rows.d.ts +17 -0
  24. package/dist/cli/tui/quiet-settings-rows.js +490 -0
  25. package/dist/cli/tui/status-line.d.ts +15 -9
  26. package/dist/cli/tui/status-line.js +15 -6
  27. package/dist/cli/tui/theme.js +20 -20
  28. package/dist/cli/tui/transcript-viewport.d.ts +5 -0
  29. package/dist/cli/tui/transcript-viewport.js +213 -78
  30. package/dist/cli/tui/tui-row-ir.js +4 -4
  31. package/dist/persistence/in-memory-transcript-store.js +1 -7
  32. package/dist/persistence/native-transcript-store.d.ts +0 -9
  33. package/dist/persistence/native-transcript-store.js +4 -38
  34. 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 graphemes(text) {
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 graphemeWidth(cluster) {
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 terminalStringWidth(text) {
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 graphemes(text).reduce((width, cluster) => width + graphemeWidth(cluster), 0);
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 graphemes(line)) {
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 += graphemeWidth(cluster);
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 = graphemes(word);
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 = graphemeWidth(cluster);
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 = graphemes(word);
184
+ const clusters = terminalGraphemes(word);
159
185
  for (const [index, cluster] of clusters.entries()) {
160
- const clusterWidth = graphemeWidth(cluster);
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
- : terminalStringWidth(word);
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 `Update(${inputString(entry, 'file_path')})`;
309
+ return `Edit ${oneLine(inputString(entry, 'file_path'))}`.trimEnd();
277
310
  if (entry.item.call.name === 'Read')
278
- return `Read(${inputString(entry, 'file_path')})`;
311
+ return `Read ${oneLine(inputString(entry, 'file_path'))}`.trimEnd();
279
312
  if (entry.item.call.name === 'Bash')
280
- return `Bash(${inputString(entry, 'command')})`;
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 = graphemes(line);
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 = graphemes(suffix);
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 = graphemes(prefix);
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(` Read ${entry.count} file${entry.count === 1 ? '' : 's'} (ctrl+o to expand)`, width);
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: ' : '❯ '}${item.text}`, width));
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
- wrappedLineCount(item.text, mode === 'screen-reader'
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
- if (mode === 'normal' &&
477
- entry.item.call.name === 'Read' &&
478
- entry.result &&
479
- !entry.result.isError)
480
- return 1;
481
- let rows = 1 + wrappedLineCount(`⏺ ${toolHeading(entry)}`, width);
482
- if (!['Bash', 'Read', 'Edit'].includes(entry.item.call.name) &&
483
- entry.item.detail)
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
- if (mode !== 'normal') {
495
- rows += oldLines.reduce((total, line, index) => total +
496
- wrappedLineCount(` ${index + 1} -${line}`, Math.max(1, width - 2)), 0);
497
- rows += newLines.reduce((total, line, index) => total +
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
- let rows = 1 +
505
- wrappedLineCount(`${mode === 'screen-reader' ? 'Shell command: ' : '! '}${entry.item.command}`, width);
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
- const rows = visible.flatMap((line, index) => textVisualRows(`${index === 0 ? '⎿ ' : ' '}${line || ' '}`, outputWidth));
535
- if (mode === 'normal' && lines.length > visible.length) {
536
- rows.push(...textVisualRows(` ${TRANSCRIPT_TRUNCATION_MARKER} +${lines.length - visible.length} lines (ctrl+o to expand)`, outputWidth));
611
+ if (mode !== 'normal') {
612
+ const lines = contentLines(text);
613
+ return lines.flatMap((line, index) => textVisualRows(`${index === 0 ? '⎿ ' : ' '}${line || ' '}`, outputWidth));
537
614
  }
538
- return rows;
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(` Read ${entry.count} file${entry.count === 1 ? '' : 's'} (ctrl+o to expand)`, width);
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: ' : '❯ '}${item.text}`, width),
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
- ...textVisualRows(item.text, mode === 'screen-reader'
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 rows = [
609
- '',
610
- ...textVisualRows(`${mode === 'screen-reader' ? 'Shell command: ' : '! '}${entry.item.command}`, width),
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
- if (mode === 'normal' &&
623
- entry.item.call.name === 'Read' &&
624
- entry.result &&
625
- !entry.result.isError)
626
- return textVisualRows(' Read 1 file (ctrl+o to expand)', width);
627
- const rows = ['', ...textVisualRows(`⏺ ${toolHeading(entry)}`, width)];
628
- if (!['Bash', 'Read', 'Edit'].includes(entry.item.call.name) &&
629
- entry.item.detail)
630
- rows.push(...textVisualRows(` ${entry.item.detail}`, width));
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
- if (mode !== 'normal') {
642
- for (const [index, line] of oldLines.entries())
643
- rows.push(...textVisualRows(` ${index + 1} -${line}`, Math.max(1, width - 2)));
644
- for (const [index, line] of newLines.entries())
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 contentWidth = mode === 'screen-reader'
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
- markdownViewportRows(sourceText ?? '', contentWidth);
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: 'muted',
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: 'error' };
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?.isError ? 'error' : 'tool',
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?.isError ? 'error' : 'tool',
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
- const snapshot = { records, tail: this.tail() };
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 attachEntries({ records: [], tail: emptyTail(), issue: null });
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 attachEntries({
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 attachEntries({ records, tail });
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.0",
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",