praxis-agent 0.21.3 → 0.21.5
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/dist/cli/interactive.js
CHANGED
|
@@ -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, projectTranscriptWindow, transcriptLineCount, } 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';
|
|
@@ -430,6 +431,7 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
430
431
|
const [costUsd, setCostUsd] = useState();
|
|
431
432
|
const [contextWindowTokens, setContextWindowTokens] = useState(display.contextWindowTokens);
|
|
432
433
|
const [history, setHistory] = useState([...initialHistory]);
|
|
434
|
+
const [transcriptScrollOffset, setTranscriptScrollOffset] = useState(0);
|
|
433
435
|
// Startup diagnostics are useful before the first prompt, but they are not
|
|
434
436
|
// conversation history and must not suppress the new-session welcome panel.
|
|
435
437
|
// Only real user/assistant transcript entries start a conversation; every
|
|
@@ -447,6 +449,15 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
447
449
|
// conversation content appears.
|
|
448
450
|
const resumed = resume !== undefined && resumedWithTranscript;
|
|
449
451
|
const freshSession = !resumed && !hasConversationHistory;
|
|
452
|
+
// Fullscreen projects only the newest transcript tail that fits the fixed
|
|
453
|
+
// viewport, leaving the composer/status chrome intact and keeping the active
|
|
454
|
+
// stream visible. Classic and screen-reader modes always render the full
|
|
455
|
+
// history exactly as before.
|
|
456
|
+
const projectedHistory = fixedViewport && !axScreenReader
|
|
457
|
+
? transcriptScrollOffset > 0
|
|
458
|
+
? projectTranscriptWindow(history, Math.max(1, (rows ?? 0) - FULLSCREEN_TRANSCRIPT_RESERVED_ROWS), width, transcriptScrollOffset)
|
|
459
|
+
: projectTranscriptTail(history, Math.max(1, (rows ?? 0) - FULLSCREEN_TRANSCRIPT_RESERVED_ROWS), width)
|
|
460
|
+
: history;
|
|
450
461
|
const sessionLoadRef = useRef(0);
|
|
451
462
|
const [turnDiffs, setTurnDiffs] = useState([]);
|
|
452
463
|
const turnNumberRef = useRef(0);
|
|
@@ -713,6 +724,7 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
713
724
|
// transcript state so the active stream never renders below a tool,
|
|
714
725
|
// thinking, or completion entry that it textually precedes.
|
|
715
726
|
streamingFrameRef.current?.flush();
|
|
727
|
+
setTranscriptScrollOffset(0);
|
|
716
728
|
setHistory((current) => [...current, line]);
|
|
717
729
|
};
|
|
718
730
|
useEffect(() => {
|
|
@@ -2582,6 +2594,7 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
2582
2594
|
void restoring.finally(() => onTurnChange?.(null));
|
|
2583
2595
|
};
|
|
2584
2596
|
const submit = async (prompt, shellCommand, images = []) => {
|
|
2597
|
+
setTranscriptScrollOffset(0);
|
|
2585
2598
|
const turnNumber = turnNumberRef.current + 1;
|
|
2586
2599
|
const turnStartedAt = Date.now();
|
|
2587
2600
|
turnNumberRef.current = turnNumber;
|
|
@@ -2890,6 +2903,33 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
2890
2903
|
return;
|
|
2891
2904
|
}
|
|
2892
2905
|
const isKeybinding = (action) => keybindingAction === action;
|
|
2906
|
+
if (fixedViewport &&
|
|
2907
|
+
menuRef.current === null &&
|
|
2908
|
+
!permission &&
|
|
2909
|
+
!planApproval &&
|
|
2910
|
+
!question &&
|
|
2911
|
+
!elicitation &&
|
|
2912
|
+
!selectingSession) {
|
|
2913
|
+
const page = Math.max(1, (rows ?? 0) - FULLSCREEN_TRANSCRIPT_RESERVED_ROWS);
|
|
2914
|
+
const scrollDelta = key.pageUp
|
|
2915
|
+
? page
|
|
2916
|
+
: key.pageDown
|
|
2917
|
+
? -page
|
|
2918
|
+
: controlKey('u') || controlKey('b')
|
|
2919
|
+
? page / 2
|
|
2920
|
+
: controlKey('f') || controlKey('n')
|
|
2921
|
+
? -page / 2
|
|
2922
|
+
: inputRef.current.length === 0 && key.upArrow
|
|
2923
|
+
? 1
|
|
2924
|
+
: inputRef.current.length === 0 && key.downArrow
|
|
2925
|
+
? -1
|
|
2926
|
+
: 0;
|
|
2927
|
+
if (scrollDelta !== 0) {
|
|
2928
|
+
setTranscriptScrollOffset((current) => Math.min(Math.max(0, transcriptLineCount(history, width) -
|
|
2929
|
+
Math.max(1, (rows ?? 0) - FULLSCREEN_TRANSCRIPT_RESERVED_ROWS)), Math.max(0, current + Math.trunc(scrollDelta))));
|
|
2930
|
+
return;
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2893
2933
|
if (runtimeSettingsRef.current.editor === 'vim' &&
|
|
2894
2934
|
!permission &&
|
|
2895
2935
|
!planApproval &&
|
|
@@ -5683,7 +5723,7 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
5683
5723
|
minHeight: 0,
|
|
5684
5724
|
overflowY: 'hidden',
|
|
5685
5725
|
}
|
|
5686
|
-
: {}), children: _jsx(Transcript, { items:
|
|
5726
|
+
: {}), children: _jsx(Transcript, { items: projectedHistory, activeText: activeText, activeThinking: activeThinking, thinkingExpanded: thinkingExpanded, detailedTranscript: thinkingExpanded || runtimeSettings.verbose, screenReader: axScreenReader }) }), externalEditorRequest !== null ||
|
|
5687
5727
|
keybindingsEditing ||
|
|
5688
5728
|
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
5729
|
(permissionSelection === 0
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
export declare function transcriptLineCount(items: readonly TranscriptItem[], width: number): number;
|
|
23
|
+
/**
|
|
24
|
+
* Pure, deterministic suffix projector for the fullscreen transcript region.
|
|
25
|
+
*
|
|
26
|
+
* Given transcript items, a terminal row budget, and the usable width, it
|
|
27
|
+
* returns the longest suffix of items whose conservative line estimate fits
|
|
28
|
+
* the budget. The newest user/assistant content is always retained; if even
|
|
29
|
+
* the newest item exceeds the budget, that item is projected as a bounded tail
|
|
30
|
+
* clone rather than rendering an empty transcript (or clipping the tail below
|
|
31
|
+
* the viewport). Ordering and item object identity are preserved for items
|
|
32
|
+
* that fit and the input is never mutated.
|
|
33
|
+
*/
|
|
34
|
+
export declare function projectTranscriptTail(items: readonly TranscriptItem[], budget: number, width: number): readonly TranscriptItem[];
|
|
35
|
+
/** Projects a fixed-size transcript window, measured upward from the newest row. */
|
|
36
|
+
export declare function projectTranscriptWindow(items: readonly TranscriptItem[], budget: number, width: number, scrollOffset: number): readonly TranscriptItem[];
|
|
37
|
+
//# sourceMappingURL=transcript-viewport.d.ts.map
|
|
@@ -0,0 +1,184 @@
|
|
|
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
|
+
export function transcriptLineCount(items, width) {
|
|
121
|
+
return items.reduce((total, item) => total + estimateTranscriptLines(item, width), 0);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Pure, deterministic suffix projector for the fullscreen transcript region.
|
|
125
|
+
*
|
|
126
|
+
* Given transcript items, a terminal row budget, and the usable width, it
|
|
127
|
+
* returns the longest suffix of items whose conservative line estimate fits
|
|
128
|
+
* the budget. The newest user/assistant content is always retained; if even
|
|
129
|
+
* the newest item exceeds the budget, that item is projected as a bounded tail
|
|
130
|
+
* clone rather than rendering an empty transcript (or clipping the tail below
|
|
131
|
+
* the viewport). Ordering and item object identity are preserved for items
|
|
132
|
+
* that fit and the input is never mutated.
|
|
133
|
+
*/
|
|
134
|
+
export function projectTranscriptTail(items, budget, width) {
|
|
135
|
+
if (items.length === 0)
|
|
136
|
+
return [];
|
|
137
|
+
const usableBudget = Math.max(1, budget);
|
|
138
|
+
let rows = 0;
|
|
139
|
+
let start = items.length;
|
|
140
|
+
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
141
|
+
const item = items[index];
|
|
142
|
+
if (item === undefined)
|
|
143
|
+
break;
|
|
144
|
+
const estimate = estimateTranscriptLines(item, width);
|
|
145
|
+
if (rows + estimate > usableBudget) {
|
|
146
|
+
// Stop at the first item that no longer fits. When no item has been
|
|
147
|
+
// added yet, even the newest item exceeds the budget: returning it whole
|
|
148
|
+
// would let Ink clip the oversized tail below the viewport, so project a
|
|
149
|
+
// bounded suffix clone that keeps the newest content visible instead.
|
|
150
|
+
return start === items.length
|
|
151
|
+
? [projectOversizedItem(item, usableBudget, width)]
|
|
152
|
+
: items.slice(start);
|
|
153
|
+
}
|
|
154
|
+
rows += estimate;
|
|
155
|
+
start = index;
|
|
156
|
+
}
|
|
157
|
+
return items.slice(start);
|
|
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);
|
|
165
|
+
let rows = 0;
|
|
166
|
+
let start = 0;
|
|
167
|
+
let end = 0;
|
|
168
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
169
|
+
const item = items[index];
|
|
170
|
+
if (!item)
|
|
171
|
+
break;
|
|
172
|
+
const nextRows = rows + estimateTranscriptLines(item, width);
|
|
173
|
+
if (nextRows <= startRows) {
|
|
174
|
+
start = index + 1;
|
|
175
|
+
}
|
|
176
|
+
if (rows < endRows)
|
|
177
|
+
end = index + 1;
|
|
178
|
+
rows = nextRows;
|
|
179
|
+
if (rows >= endRows)
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
return items.slice(start, end);
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=transcript-viewport.js.map
|