praxis-agent 0.21.3 → 0.21.4

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.
@@ -16,6 +16,7 @@ import { loadTuiMemoryFiles, openTuiMemoryFolder, } from './tui/memory-files.js'
16
16
  import { loadClaudeReleaseNotes } from './tui/release-notes.js';
17
17
  import { StreamingFrameBuffer } from './tui/streaming-frame-buffer.js';
18
18
  import { createClaudeStatusLineInput, StatusLine } from './tui/status-line.js';
19
+ import { FULLSCREEN_TRANSCRIPT_RESERVED_ROWS, projectTranscriptTail, } from './tui/transcript-viewport.js';
19
20
  import { loadGitDiff, visiblePatchLines, } from './tui/git-diff.js';
20
21
  import { addTuiPermissionRule, loadTuiPermissionRules, removeTuiPermissionRule, } from './tui/permission-settings.js';
21
22
  import { createRecentlyDeniedStore, } from './tui/recently-denied.js';
@@ -447,6 +448,13 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
447
448
  // conversation content appears.
448
449
  const resumed = resume !== undefined && resumedWithTranscript;
449
450
  const freshSession = !resumed && !hasConversationHistory;
451
+ // Fullscreen projects only the newest transcript tail that fits the fixed
452
+ // viewport, leaving the composer/status chrome intact and keeping the active
453
+ // stream visible. Classic and screen-reader modes always render the full
454
+ // history exactly as before.
455
+ const projectedHistory = fixedViewport && !axScreenReader
456
+ ? projectTranscriptTail(history, Math.max(1, (rows ?? 0) - FULLSCREEN_TRANSCRIPT_RESERVED_ROWS), width)
457
+ : history;
450
458
  const sessionLoadRef = useRef(0);
451
459
  const [turnDiffs, setTurnDiffs] = useState([]);
452
460
  const turnNumberRef = useRef(0);
@@ -5683,7 +5691,7 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
5683
5691
  minHeight: 0,
5684
5692
  overflowY: 'hidden',
5685
5693
  }
5686
- : {}), children: _jsx(Transcript, { items: history, activeText: activeText, activeThinking: activeThinking, thinkingExpanded: thinkingExpanded, detailedTranscript: thinkingExpanded || runtimeSettings.verbose, screenReader: axScreenReader }) }), externalEditorRequest !== null ||
5694
+ : {}), children: _jsx(Transcript, { items: projectedHistory, activeText: activeText, activeThinking: activeThinking, thinkingExpanded: thinkingExpanded, detailedTranscript: thinkingExpanded || runtimeSettings.verbose, screenReader: axScreenReader }) }), externalEditorRequest !== null ||
5687
5695
  keybindingsEditing ||
5688
5696
  memoryEditorRequest !== null ? (_jsx(ExternalEditorWait, { screenReader: axScreenReader })) : permission ? (permission.kind === 'tool' && toolPermissionModel ? (_jsx(ToolPermissionDialog, { model: toolPermissionModel, selection: permissionSelection, feedbackMode: permissionFeedbackMode, feedback: input, ruleEditor: permissionRuleEditor, screenReader: axScreenReader })) : (_jsxs(DialogFrame, { title: `Retry interrupted ${permission.call.name}?`, screenReader: axScreenReader, children: [_jsx(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: _jsx(Text, { bold: true, children: describeTool(permission.call, sensitiveValues) }) }), _jsx(Text, { children: "Do you want to proceed?" }), _jsxs(Text, { inverse: !axScreenReader && permissionSelection === 0, children: [selectionPrefix(permissionSelection === 0, axScreenReader), "1. Yes"] }), _jsxs(Text, { inverse: !axScreenReader && permissionSelection === 1, children: [selectionPrefix(permissionSelection === 1, axScreenReader), "2. No"] }), permissionFeedbackMode ? (_jsxs(Text, { children: ["\u203A", ' ', input ||
5689
5697
  (permissionSelection === 0
@@ -0,0 +1,34 @@
1
+ import type { TranscriptItem } from './claude-style.js';
2
+ /**
3
+ * Rows reserved outside the shrinkable fullscreen transcript region: the top
4
+ * identity chrome, the composer separator/prompt/footer, and the status line,
5
+ * plus a small headroom so the active streaming tail stays visible below the
6
+ * projected history suffix. Fullscreen computes the transcript budget as
7
+ * `rows - FULLSCREEN_TRANSCRIPT_RESERVED_ROWS`.
8
+ */
9
+ export declare const FULLSCREEN_TRANSCRIPT_RESERVED_ROWS = 12;
10
+ /**
11
+ * Compact marker prepended to the projected tail of an oversized newest item so
12
+ * the truncation is visually explicit while the newest lines remain visible.
13
+ */
14
+ export declare const TRANSCRIPT_TRUNCATION_MARKER = "\u2026";
15
+ /**
16
+ * Deterministic estimate of the terminal rows one transcript item occupies in
17
+ * the TUI presentation. It is intentionally conservative (leans toward
18
+ * overestimation) so the suffix projector keeps the newest content visible
19
+ * rather than dropping items that would actually render.
20
+ */
21
+ export declare function estimateTranscriptLines(item: TranscriptItem, width: number): number;
22
+ /**
23
+ * Pure, deterministic suffix projector for the fullscreen transcript region.
24
+ *
25
+ * Given transcript items, a terminal row budget, and the usable width, it
26
+ * returns the longest suffix of items whose conservative line estimate fits
27
+ * the budget. The newest user/assistant content is always retained; if even
28
+ * the newest item exceeds the budget, that item is projected as a bounded tail
29
+ * clone rather than rendering an empty transcript (or clipping the tail below
30
+ * the viewport). Ordering and item object identity are preserved for items
31
+ * that fit and the input is never mutated.
32
+ */
33
+ export declare function projectTranscriptTail(items: readonly TranscriptItem[], budget: number, width: number): readonly TranscriptItem[];
34
+ //# sourceMappingURL=transcript-viewport.d.ts.map
@@ -0,0 +1,156 @@
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
+ */
8
+ export const FULLSCREEN_TRANSCRIPT_RESERVED_ROWS = 12;
9
+ /**
10
+ * Conservative width-aware row estimate for a block of text. Blank lines still
11
+ * occupy a row, and every logical line wraps across `Math.ceil(length / width)`
12
+ * rows so the estimate leans high and never drops content that would actually
13
+ * fit.
14
+ */
15
+ function wrappedLineCount(text, width) {
16
+ if (text === '')
17
+ return 1;
18
+ const usable = Math.max(1, width);
19
+ let rows = 0;
20
+ for (const line of text.split('\n')) {
21
+ rows += Math.max(1, Math.ceil(line.length / usable));
22
+ }
23
+ return rows;
24
+ }
25
+ function bounded(text, max) {
26
+ return text.length > max ? text.slice(0, max) : text;
27
+ }
28
+ /**
29
+ * Compact marker prepended to the projected tail of an oversized newest item so
30
+ * the truncation is visually explicit while the newest lines remain visible.
31
+ */
32
+ export const TRANSCRIPT_TRUNCATION_MARKER = '…';
33
+ /**
34
+ * Projects a text block to the longest trailing suffix (newest content) that
35
+ * fits `budget` terminal rows, prefixed with the truncation marker. The marker
36
+ * merges into the first kept line, so the result is exactly
37
+ * `${TRANSCRIPT_TRUNCATION_MARKER}${suffix}`. A text that already fits is
38
+ * returned unchanged.
39
+ */
40
+ function projectTextTail(text, budget, width) {
41
+ if (wrappedLineCount(text, width) <= budget)
42
+ return text;
43
+ const lines = text.split('\n');
44
+ let start = lines.length;
45
+ // Wrapped rows of every kept line after the current front line (marker-free).
46
+ let rowsAfter = 0;
47
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
48
+ const line = lines[index];
49
+ if (line === undefined)
50
+ break;
51
+ // This line becomes the front of the tail and carries the marker.
52
+ if (rowsAfter +
53
+ wrappedLineCount(`${TRANSCRIPT_TRUNCATION_MARKER}${line}`, width) >
54
+ budget) {
55
+ break;
56
+ }
57
+ start = index;
58
+ rowsAfter += wrappedLineCount(line, width);
59
+ }
60
+ return `${TRANSCRIPT_TRUNCATION_MARKER}${lines.slice(start).join('\n')}`;
61
+ }
62
+ /**
63
+ * Returns a cloned oversized text-bearing item whose text/summary is a bounded
64
+ * trailing suffix that fits `budget` rows, preserving the newest content and
65
+ * the item kind without mutating the input. Non-text display items (context,
66
+ * tool, tool-result, shell, shell-result) are returned unchanged.
67
+ */
68
+ function projectOversizedItem(item, budget, width) {
69
+ switch (item.kind) {
70
+ case 'user':
71
+ case 'assistant':
72
+ case 'notice':
73
+ case 'warning':
74
+ case 'local-result':
75
+ return { ...item, text: projectTextTail(item.text, budget, width) };
76
+ case 'thinking':
77
+ return {
78
+ ...item,
79
+ text: projectTextTail(item.text, Math.max(1, budget - 1), width),
80
+ };
81
+ case 'compact':
82
+ return {
83
+ ...item,
84
+ summary: projectTextTail(item.summary, Math.max(1, budget - 1), width),
85
+ };
86
+ default:
87
+ return item;
88
+ }
89
+ }
90
+ /**
91
+ * Deterministic estimate of the terminal rows one transcript item occupies in
92
+ * the TUI presentation. It is intentionally conservative (leans toward
93
+ * overestimation) so the suffix projector keeps the newest content visible
94
+ * rather than dropping items that would actually render.
95
+ */
96
+ export function estimateTranscriptLines(item, width) {
97
+ switch (item.kind) {
98
+ case 'user':
99
+ case 'assistant':
100
+ case 'notice':
101
+ case 'warning':
102
+ case 'local-result':
103
+ return wrappedLineCount(item.text, width);
104
+ case 'thinking':
105
+ return 1 + wrappedLineCount(item.text, width);
106
+ case 'compact':
107
+ return 1 + wrappedLineCount(item.summary, width);
108
+ case 'context':
109
+ return 12 + item.skills.length + item.memoryFiles.length;
110
+ case 'tool':
111
+ return 1 + (item.detail ? 1 : 0);
112
+ case 'tool-result':
113
+ return 1 + wrappedLineCount(bounded(item.text, 500), width);
114
+ case 'shell':
115
+ return 2 + wrappedLineCount(item.command, width);
116
+ case 'shell-result':
117
+ return wrappedLineCount(bounded(`${item.stdout}\n${item.stderr}`, 500), width);
118
+ }
119
+ }
120
+ /**
121
+ * Pure, deterministic suffix projector for the fullscreen transcript region.
122
+ *
123
+ * Given transcript items, a terminal row budget, and the usable width, it
124
+ * returns the longest suffix of items whose conservative line estimate fits
125
+ * the budget. The newest user/assistant content is always retained; if even
126
+ * the newest item exceeds the budget, that item is projected as a bounded tail
127
+ * clone rather than rendering an empty transcript (or clipping the tail below
128
+ * the viewport). Ordering and item object identity are preserved for items
129
+ * that fit and the input is never mutated.
130
+ */
131
+ export function projectTranscriptTail(items, budget, width) {
132
+ if (items.length === 0)
133
+ return [];
134
+ const usableBudget = Math.max(1, budget);
135
+ let rows = 0;
136
+ let start = items.length;
137
+ for (let index = items.length - 1; index >= 0; index -= 1) {
138
+ const item = items[index];
139
+ if (item === undefined)
140
+ break;
141
+ const estimate = estimateTranscriptLines(item, width);
142
+ if (rows + estimate > usableBudget) {
143
+ // Stop at the first item that no longer fits. When no item has been
144
+ // added yet, even the newest item exceeds the budget: returning it whole
145
+ // would let Ink clip the oversized tail below the viewport, so project a
146
+ // bounded suffix clone that keeps the newest content visible instead.
147
+ return start === items.length
148
+ ? [projectOversizedItem(item, usableBudget, width)]
149
+ : items.slice(start);
150
+ }
151
+ rows += estimate;
152
+ start = index;
153
+ }
154
+ return items.slice(start);
155
+ }
156
+ //# sourceMappingURL=transcript-viewport.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "praxis-agent",
3
- "version": "0.21.3",
3
+ "version": "0.21.4",
4
4
  "description": "Local-first, single-user general agent for the command line.",
5
5
  "license": "MIT",
6
6
  "author": "wuqisen",