praxis-agent 0.45.2 → 0.45.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.
- package/README.md +2 -1
- package/dist/application/session-service.js +6 -0
- package/dist/cli/interactive.js +148 -1
- package/dist/cli/tui/ansi-frame-renderer.d.ts +1 -0
- package/dist/cli/tui/ansi-frame-renderer.js +78 -3
- package/dist/cli/tui/ansi-surface.d.ts +1 -0
- package/dist/cli/tui/ansi-surface.js +6 -0
- package/dist/cli/tui/ansi-theme.js +4 -1
- package/dist/cli/tui/quiet-frame-adapter.js +1 -0
- package/dist/cli/tui/terminal-selection.d.ts +51 -0
- package/dist/cli/tui/terminal-selection.js +263 -0
- package/dist/cli/tui/tui-row-ir.d.ts +1 -1
- package/dist/tools/claude-workflow-tools.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -126,7 +126,8 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
|
|
|
126
126
|
support, and no-color output,
|
|
127
127
|
cursor/history composer, provider-free `/cost` usage and pricing summaries,
|
|
128
128
|
with a hermetic PTY smoke covering real `runInteractive` ANSI entry,
|
|
129
|
-
resize-safe lifecycle,
|
|
129
|
+
resize-safe lifecycle, Ctrl-C restoration, fullscreen `Ctrl+L` redraw, and
|
|
130
|
+
mouse-wheel/drag selection with edge autoscroll and OSC 52 copy,
|
|
130
131
|
interactive `/doctor` diagnostics, per-session model/effort/permission controls,
|
|
131
132
|
context/status/skill/task dashboards, prompt stash and continuation shortcuts,
|
|
132
133
|
filterable `@` file and agent references, composer undo, `Ctrl+G` external
|
|
@@ -2990,11 +2990,13 @@ export class ClaudeSessionService {
|
|
|
2990
2990
|
let projectMemoryRecallMessages = [];
|
|
2991
2991
|
let observeModelRequestUsage = () => undefined;
|
|
2992
2992
|
const durableFollowUps = new DurableFollowUpTracker();
|
|
2993
|
+
const claimedToolCallIds = new Set();
|
|
2993
2994
|
const observer = {
|
|
2994
2995
|
...(nativeLease
|
|
2995
2996
|
? {
|
|
2996
2997
|
toolExecutionStarted: async (call) => {
|
|
2997
2998
|
await nativeLease.beginToolExecution(call.id);
|
|
2999
|
+
claimedToolCallIds.add(call.id);
|
|
2998
3000
|
},
|
|
2999
3001
|
}
|
|
3000
3002
|
: {}),
|
|
@@ -3087,6 +3089,10 @@ export class ClaudeSessionService {
|
|
|
3087
3089
|
await refreshRuntimeContext();
|
|
3088
3090
|
}
|
|
3089
3091
|
if (nativeLease) {
|
|
3092
|
+
if (!claimedToolCallIds.has(call.id) && !signal?.aborted) {
|
|
3093
|
+
await nativeLease.beginToolExecution(call.id);
|
|
3094
|
+
claimedToolCallIds.add(call.id);
|
|
3095
|
+
}
|
|
3090
3096
|
await nativeLease.appendToolCompletion({
|
|
3091
3097
|
callId: call.id,
|
|
3092
3098
|
result: toolResult,
|
package/dist/cli/interactive.js
CHANGED
|
@@ -28,6 +28,7 @@ import { createTuiAppendHistoryChange } from './tui/transcript-window-model.js';
|
|
|
28
28
|
import { createTuiHistoryChange, resolveTuiRenderer, } from './tui/tui-view-model.js';
|
|
29
29
|
import { projectTuiScreen, } from './tui/tui-screen-model.js';
|
|
30
30
|
import { TuiAnsiSurface } from './tui/ansi-surface.js';
|
|
31
|
+
import { clampTranscriptScrollOffset, createTerminalSelectionContext, createTerminalSelectionState, parseTerminalMouseReport, projectTerminalSelection, refreshTerminalSelection, releaseTerminalSelection, startTerminalSelection, updateTerminalSelection, } from './tui/terminal-selection.js';
|
|
31
32
|
import { QuietInkFrame } from './tui/quiet-frame-adapter.js';
|
|
32
33
|
import { projectQuietScreenFrame, } from './tui/quiet-screen-projector.js';
|
|
33
34
|
import { projectTuiHelpSurface } from './tui/help-surface-model.js';
|
|
@@ -487,6 +488,11 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
487
488
|
transcriptScrollOffsetRef.current = offset;
|
|
488
489
|
setTranscriptScrollOffsetState(offset);
|
|
489
490
|
};
|
|
491
|
+
const [terminalSelection, setTerminalSelection] = useState(createTerminalSelectionState);
|
|
492
|
+
const terminalSelectionRef = useRef(createTerminalSelectionState());
|
|
493
|
+
const terminalSelectionContextRef = useRef(null);
|
|
494
|
+
const selectionEdgeTimerRef = useRef(null);
|
|
495
|
+
const [clearRevision, setClearRevision] = useState(0);
|
|
490
496
|
const sessionLoadRef = useRef(0);
|
|
491
497
|
const [turnDiffs, setTurnDiffs] = useState([]);
|
|
492
498
|
const turnNumberRef = useRef(0);
|
|
@@ -1191,6 +1197,56 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
1191
1197
|
const conversationScreen = screen.body.kind === 'conversation' ? screen.body : undefined;
|
|
1192
1198
|
const transcriptPageRows = conversationScreen?.transcript.pageRows ?? 2;
|
|
1193
1199
|
const maxTranscriptScrollOffset = conversationScreen?.transcript.maxScrollOffset ?? 0;
|
|
1200
|
+
const stopSelectionEdgeScroll = () => {
|
|
1201
|
+
if (selectionEdgeTimerRef.current !== null) {
|
|
1202
|
+
clearInterval(selectionEdgeTimerRef.current);
|
|
1203
|
+
selectionEdgeTimerRef.current = null;
|
|
1204
|
+
}
|
|
1205
|
+
};
|
|
1206
|
+
const selectionContextRef = terminalSelectionContextRef;
|
|
1207
|
+
const copyTerminalSelection = (text) => {
|
|
1208
|
+
if (!text)
|
|
1209
|
+
return;
|
|
1210
|
+
void sideQuestionClipboardWriter(text).catch((error) => {
|
|
1211
|
+
append({
|
|
1212
|
+
kind: 'warning',
|
|
1213
|
+
text: `Clipboard unavailable: ${error instanceof Error ? error.message : String(error)}`,
|
|
1214
|
+
});
|
|
1215
|
+
});
|
|
1216
|
+
};
|
|
1217
|
+
const beginSelectionEdgeScroll = (direction) => {
|
|
1218
|
+
if (selectionEdgeTimerRef.current !== null)
|
|
1219
|
+
return;
|
|
1220
|
+
selectionEdgeTimerRef.current = setInterval(() => {
|
|
1221
|
+
const context = selectionContextRef.current;
|
|
1222
|
+
const current = terminalSelectionRef.current;
|
|
1223
|
+
if (!context ||
|
|
1224
|
+
current.phase !== 'dragging' ||
|
|
1225
|
+
current.edge !== direction) {
|
|
1226
|
+
stopSelectionEdgeScroll();
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
const offset = transcriptScrollOffsetRef.current;
|
|
1230
|
+
const next = clampTranscriptScrollOffset(offset + (direction === 'older' ? 1 : -1), context.maxTranscriptScrollOffset);
|
|
1231
|
+
if (next === offset) {
|
|
1232
|
+
stopSelectionEdgeScroll();
|
|
1233
|
+
setTerminalSelection((state) => ({ ...state, edge: 'none' }));
|
|
1234
|
+
terminalSelectionRef.current = { ...current, edge: 'none' };
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
setTranscriptScrollOffset(next);
|
|
1238
|
+
}, 80);
|
|
1239
|
+
};
|
|
1240
|
+
useEffect(() => {
|
|
1241
|
+
if (!ansiActive) {
|
|
1242
|
+
stopSelectionEdgeScroll();
|
|
1243
|
+
terminalSelectionRef.current = createTerminalSelectionState();
|
|
1244
|
+
setTerminalSelection(terminalSelectionRef.current);
|
|
1245
|
+
}
|
|
1246
|
+
return () => {
|
|
1247
|
+
stopSelectionEdgeScroll();
|
|
1248
|
+
};
|
|
1249
|
+
}, [ansiActive]);
|
|
1194
1250
|
const permissionOptions = useMemo(() => [
|
|
1195
1251
|
...PERMISSION_OPTIONS,
|
|
1196
1252
|
...(allowDangerouslySkipPermissions
|
|
@@ -3360,6 +3416,53 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
3360
3416
|
return () => controller.abort();
|
|
3361
3417
|
}, [busy, initialPromptPending, permission, selectingSession, sessionId]);
|
|
3362
3418
|
useInput((value, key) => {
|
|
3419
|
+
if (ansiActive) {
|
|
3420
|
+
const mouse = parseTerminalMouseReport(value);
|
|
3421
|
+
if (mouse !== null) {
|
|
3422
|
+
const context = terminalSelectionContextRef.current;
|
|
3423
|
+
if (context === null)
|
|
3424
|
+
return;
|
|
3425
|
+
if (mouse.kind === 'wheel') {
|
|
3426
|
+
const offset = transcriptScrollOffsetRef.current;
|
|
3427
|
+
setTranscriptScrollOffset(clampTranscriptScrollOffset(offset + (mouse.direction === 'older' ? 1 : -1), context.maxTranscriptScrollOffset));
|
|
3428
|
+
return;
|
|
3429
|
+
}
|
|
3430
|
+
if (mouse.kind === 'press') {
|
|
3431
|
+
stopSelectionEdgeScroll();
|
|
3432
|
+
const next = startTerminalSelection(context, mouse);
|
|
3433
|
+
terminalSelectionRef.current = next;
|
|
3434
|
+
setTerminalSelection(next);
|
|
3435
|
+
return;
|
|
3436
|
+
}
|
|
3437
|
+
if (mouse.kind === 'drag') {
|
|
3438
|
+
const current = terminalSelectionRef.current;
|
|
3439
|
+
if (current.phase !== 'dragging')
|
|
3440
|
+
return;
|
|
3441
|
+
const previousEdge = current.edge;
|
|
3442
|
+
const next = updateTerminalSelection(current, context, mouse);
|
|
3443
|
+
terminalSelectionRef.current = next;
|
|
3444
|
+
setTerminalSelection(next);
|
|
3445
|
+
if (next.edge !== 'none') {
|
|
3446
|
+
if (next.edge !== previousEdge) {
|
|
3447
|
+
const offset = transcriptScrollOffsetRef.current;
|
|
3448
|
+
const moved = clampTranscriptScrollOffset(offset + (next.edge === 'older' ? 1 : -1), context.maxTranscriptScrollOffset);
|
|
3449
|
+
if (moved !== offset)
|
|
3450
|
+
setTranscriptScrollOffset(moved);
|
|
3451
|
+
}
|
|
3452
|
+
beginSelectionEdgeScroll(next.edge);
|
|
3453
|
+
}
|
|
3454
|
+
else
|
|
3455
|
+
stopSelectionEdgeScroll();
|
|
3456
|
+
return;
|
|
3457
|
+
}
|
|
3458
|
+
const released = releaseTerminalSelection(terminalSelectionRef.current, context, mouse);
|
|
3459
|
+
stopSelectionEdgeScroll();
|
|
3460
|
+
terminalSelectionRef.current = released.state;
|
|
3461
|
+
setTerminalSelection(released.state);
|
|
3462
|
+
copyTerminalSelection(released.text);
|
|
3463
|
+
return;
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3363
3466
|
const lower = value.toLowerCase();
|
|
3364
3467
|
const controlKey = (letter) => (key.ctrl && lower === letter) ||
|
|
3365
3468
|
value === String.fromCharCode(letter.charCodeAt(0) - 96);
|
|
@@ -3547,6 +3650,32 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
3547
3650
|
if (!keybindingAction) {
|
|
3548
3651
|
keybindingAction = resolveTuiKeybinding(keybindings, keybindingContexts, inputChord);
|
|
3549
3652
|
}
|
|
3653
|
+
const clearScreenBinding = ansiActive &&
|
|
3654
|
+
(keybindingAction === 'chat:clearScreen' ||
|
|
3655
|
+
(inputChord === 'ctrl+l' && keybindingAction === 'chat:clearInput'));
|
|
3656
|
+
if (clearScreenBinding) {
|
|
3657
|
+
stopSelectionEdgeScroll();
|
|
3658
|
+
const cleared = createTerminalSelectionState();
|
|
3659
|
+
terminalSelectionRef.current = cleared;
|
|
3660
|
+
setTerminalSelection(cleared);
|
|
3661
|
+
setClearRevision((revision) => revision + 1);
|
|
3662
|
+
return;
|
|
3663
|
+
}
|
|
3664
|
+
if (ansiActive && terminalSelectionRef.current.phase !== 'idle') {
|
|
3665
|
+
const selectionAction = resolveTuiKeybinding(keybindings, ['Scroll'], inputChord);
|
|
3666
|
+
if (selectionAction === 'selection:copy') {
|
|
3667
|
+
const context = terminalSelectionContextRef.current;
|
|
3668
|
+
if (context === null)
|
|
3669
|
+
return;
|
|
3670
|
+
const copied = releaseTerminalSelection(terminalSelectionRef.current, context);
|
|
3671
|
+
copyTerminalSelection(copied.text);
|
|
3672
|
+
return;
|
|
3673
|
+
}
|
|
3674
|
+
stopSelectionEdgeScroll();
|
|
3675
|
+
const cleared = createTerminalSelectionState();
|
|
3676
|
+
terminalSelectionRef.current = cleared;
|
|
3677
|
+
setTerminalSelection(cleared);
|
|
3678
|
+
}
|
|
3550
3679
|
const scrollIntent = key.pageUp
|
|
3551
3680
|
? 'page-older'
|
|
3552
3681
|
: key.pageDown
|
|
@@ -6545,7 +6674,25 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
6545
6674
|
conciseStatus,
|
|
6546
6675
|
runtimeDisplay,
|
|
6547
6676
|
]);
|
|
6548
|
-
|
|
6677
|
+
const terminalSelectionContext = useMemo(() => createTerminalSelectionContext(quietFrame, conversationScreen?.transcript.rows.length ?? 0, transcriptScrollOffset, maxTranscriptScrollOffset), [
|
|
6678
|
+
quietFrame,
|
|
6679
|
+
conversationScreen?.transcript.rows.length,
|
|
6680
|
+
transcriptScrollOffset,
|
|
6681
|
+
maxTranscriptScrollOffset,
|
|
6682
|
+
]);
|
|
6683
|
+
terminalSelectionContextRef.current = terminalSelectionContext;
|
|
6684
|
+
terminalSelectionRef.current = terminalSelection;
|
|
6685
|
+
useEffect(() => {
|
|
6686
|
+
if (!ansiActive || terminalSelection.phase !== 'dragging')
|
|
6687
|
+
return;
|
|
6688
|
+
const refreshed = refreshTerminalSelection(terminalSelection, terminalSelectionContext);
|
|
6689
|
+
if (refreshed !== terminalSelection) {
|
|
6690
|
+
terminalSelectionRef.current = refreshed;
|
|
6691
|
+
setTerminalSelection(refreshed);
|
|
6692
|
+
}
|
|
6693
|
+
}, [ansiActive, terminalSelection, terminalSelectionContext]);
|
|
6694
|
+
const selectedQuietFrame = useMemo(() => projectTerminalSelection(quietFrame, terminalSelection, terminalSelectionContext), [quietFrame, terminalSelection, terminalSelectionContext]);
|
|
6695
|
+
return (_jsx(TuiThemeProvider, { settings: themeSettings, screenReader: axScreenReader, children: ansiActive ? (_jsx(TuiAnsiSurface, { frame: selectedQuietFrame, clearRevision: clearRevision, onError: () => setAnsiRendererFailed(true) })) : (_jsx(QuietInkFrame, { frame: quietFrame, screenReader: axScreenReader })) }));
|
|
6549
6696
|
}
|
|
6550
6697
|
/**
|
|
6551
6698
|
* Whether the user explicitly saved a `tui` renderer value in configuration.
|
|
@@ -4,6 +4,18 @@ const ALTERNATE_SCREEN_ENTER = '\u001b[?1049h';
|
|
|
4
4
|
const ALTERNATE_SCREEN_LEAVE = '\u001b[?1049l';
|
|
5
5
|
const HIDE_CURSOR = '\u001b[?25l';
|
|
6
6
|
const SHOW_CURSOR = '\u001b[?25h';
|
|
7
|
+
const MOUSE_MODE_ENABLE = [
|
|
8
|
+
'\u001b[?1000h',
|
|
9
|
+
'\u001b[?1002h',
|
|
10
|
+
'\u001b[?1003h',
|
|
11
|
+
'\u001b[?1006h',
|
|
12
|
+
];
|
|
13
|
+
const MOUSE_MODE_DISABLE = [
|
|
14
|
+
'\u001b[?1006l',
|
|
15
|
+
'\u001b[?1003l',
|
|
16
|
+
'\u001b[?1002l',
|
|
17
|
+
'\u001b[?1000l',
|
|
18
|
+
];
|
|
7
19
|
const SYNCHRONIZED_BEGIN = '\u001b[?2026h';
|
|
8
20
|
const SYNCHRONIZED_END = '\u001b[?2026l';
|
|
9
21
|
const RESET = '\u001b[0m';
|
|
@@ -91,11 +103,16 @@ export class AnsiFullscreenRenderer {
|
|
|
91
103
|
let alternateScreenEntered = false;
|
|
92
104
|
let cursorHidden = false;
|
|
93
105
|
let synchronizedOutputBegun = false;
|
|
106
|
+
const mouseModesEnabled = [];
|
|
94
107
|
try {
|
|
95
108
|
alternateScreenEntered = true;
|
|
96
109
|
this.#writer.write(ALTERNATE_SCREEN_ENTER);
|
|
97
110
|
cursorHidden = true;
|
|
98
111
|
this.#writer.write(HIDE_CURSOR);
|
|
112
|
+
for (const [index, mode] of MOUSE_MODE_ENABLE.entries()) {
|
|
113
|
+
mouseModesEnabled[index] = true;
|
|
114
|
+
this.#writer.write(mode);
|
|
115
|
+
}
|
|
99
116
|
if (this.#synchronizedOutput) {
|
|
100
117
|
synchronizedOutputBegun = true;
|
|
101
118
|
this.#writer.write(SYNCHRONIZED_BEGIN);
|
|
@@ -106,6 +123,16 @@ export class AnsiFullscreenRenderer {
|
|
|
106
123
|
this.#mounted = false;
|
|
107
124
|
this.#previousLines = [];
|
|
108
125
|
this.#previousCursor = undefined;
|
|
126
|
+
for (let index = MOUSE_MODE_ENABLE.length - 1; index >= 0; index -= 1) {
|
|
127
|
+
if (!mouseModesEnabled[index])
|
|
128
|
+
continue;
|
|
129
|
+
try {
|
|
130
|
+
this.#writer.write(MOUSE_MODE_DISABLE[MOUSE_MODE_ENABLE.length - 1 - index]);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// Rollback must continue if one restoration write fails.
|
|
134
|
+
}
|
|
135
|
+
}
|
|
109
136
|
if (synchronizedOutputBegun) {
|
|
110
137
|
try {
|
|
111
138
|
this.#writer.write(SYNCHRONIZED_END);
|
|
@@ -133,6 +160,40 @@ export class AnsiFullscreenRenderer {
|
|
|
133
160
|
throw error;
|
|
134
161
|
}
|
|
135
162
|
}
|
|
163
|
+
clear() {
|
|
164
|
+
if (!this.#mounted)
|
|
165
|
+
throw new Error('ANSI fullscreen renderer is not mounted');
|
|
166
|
+
let firstError;
|
|
167
|
+
if (this.#synchronizedOutput) {
|
|
168
|
+
try {
|
|
169
|
+
this.#writer.write(SYNCHRONIZED_BEGIN);
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
firstError = error;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (firstError === undefined) {
|
|
176
|
+
try {
|
|
177
|
+
this.#writer.write('\u001b[2J\u001b[H');
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
firstError = error;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (this.#synchronizedOutput) {
|
|
184
|
+
try {
|
|
185
|
+
this.#writer.write(SYNCHRONIZED_END);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
if (firstError === undefined)
|
|
189
|
+
firstError = error;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (firstError !== undefined)
|
|
193
|
+
throw firstError;
|
|
194
|
+
this.#previousLines = [];
|
|
195
|
+
this.#previousCursor = undefined;
|
|
196
|
+
}
|
|
136
197
|
draw(frame) {
|
|
137
198
|
if (!this.#mounted)
|
|
138
199
|
throw new Error('ANSI fullscreen renderer is not mounted');
|
|
@@ -192,17 +253,31 @@ export class AnsiFullscreenRenderer {
|
|
|
192
253
|
dispose() {
|
|
193
254
|
if (!this.#mounted)
|
|
194
255
|
return;
|
|
256
|
+
let firstError;
|
|
257
|
+
const attempt = (write) => {
|
|
258
|
+
try {
|
|
259
|
+
this.#writer.write(write);
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
if (firstError === undefined)
|
|
263
|
+
firstError = error;
|
|
264
|
+
}
|
|
265
|
+
};
|
|
195
266
|
try {
|
|
267
|
+
for (const mode of MOUSE_MODE_DISABLE)
|
|
268
|
+
attempt(mode);
|
|
196
269
|
if (this.#synchronizedOutput)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
270
|
+
attempt(SYNCHRONIZED_END);
|
|
271
|
+
attempt(SHOW_CURSOR);
|
|
272
|
+
attempt(ALTERNATE_SCREEN_LEAVE);
|
|
200
273
|
}
|
|
201
274
|
finally {
|
|
202
275
|
this.#mounted = false;
|
|
203
276
|
this.#previousLines = [];
|
|
204
277
|
this.#previousCursor = undefined;
|
|
205
278
|
}
|
|
279
|
+
if (firstError !== undefined)
|
|
280
|
+
throw firstError;
|
|
206
281
|
}
|
|
207
282
|
}
|
|
208
283
|
//# sourceMappingURL=ansi-frame-renderer.js.map
|
|
@@ -3,6 +3,7 @@ import type { QuietFrame } from './quiet-frame.js';
|
|
|
3
3
|
export interface TuiAnsiSurfaceFrameProps {
|
|
4
4
|
readonly frame: QuietFrame;
|
|
5
5
|
readonly onError: (error: unknown) => void;
|
|
6
|
+
readonly clearRevision?: number;
|
|
6
7
|
}
|
|
7
8
|
export type TuiAnsiSurfaceProps = TuiAnsiSurfaceFrameProps;
|
|
8
9
|
export declare function projectAnsiQuietFrame(frame: QuietFrame): AnsiFrame;
|
|
@@ -17,6 +17,7 @@ export function TuiAnsiSurface(props) {
|
|
|
17
17
|
const styles = useMemo(() => resolveAnsiTextStyles(theme), [theme]);
|
|
18
18
|
const rendererRef = useRef(null);
|
|
19
19
|
const failedRef = useRef(false);
|
|
20
|
+
const clearRevisionRef = useRef(undefined);
|
|
20
21
|
if (rendererRef.current === null) {
|
|
21
22
|
rendererRef.current = new AnsiFullscreenRenderer({
|
|
22
23
|
writer: { write: (chunk) => stdout.write(chunk) },
|
|
@@ -31,6 +32,7 @@ export function TuiAnsiSurface(props) {
|
|
|
31
32
|
return;
|
|
32
33
|
try {
|
|
33
34
|
renderer.mount();
|
|
35
|
+
clearRevisionRef.current = props.clearRevision;
|
|
34
36
|
}
|
|
35
37
|
catch (error) {
|
|
36
38
|
failedRef.current = true;
|
|
@@ -55,6 +57,10 @@ export function TuiAnsiSurface(props) {
|
|
|
55
57
|
if (renderer === null)
|
|
56
58
|
return;
|
|
57
59
|
try {
|
|
60
|
+
if (clearRevisionRef.current !== props.clearRevision) {
|
|
61
|
+
renderer.clear();
|
|
62
|
+
clearRevisionRef.current = props.clearRevision;
|
|
63
|
+
}
|
|
58
64
|
renderer.draw(projectAnsiQuietFrame(props.frame));
|
|
59
65
|
}
|
|
60
66
|
catch (error) {
|
|
@@ -70,8 +70,10 @@ function styleSequence(style) {
|
|
|
70
70
|
return codes.length === 0 ? undefined : `\u001b[${codes.join(';')}m`;
|
|
71
71
|
}
|
|
72
72
|
export function resolveAnsiTextStyles(theme) {
|
|
73
|
-
if (theme.
|
|
73
|
+
if (theme.screenReader)
|
|
74
74
|
return {};
|
|
75
|
+
if (theme.noColor)
|
|
76
|
+
return { textSelection: '\u001b[7m' };
|
|
75
77
|
const styles = {};
|
|
76
78
|
const roleMap = {
|
|
77
79
|
body: 'body',
|
|
@@ -83,6 +85,7 @@ export function resolveAnsiTextStyles(theme) {
|
|
|
83
85
|
error: 'error',
|
|
84
86
|
tool: 'info',
|
|
85
87
|
selection: 'focusMarker',
|
|
88
|
+
textSelection: 'inputCursor',
|
|
86
89
|
input: 'inputMarker',
|
|
87
90
|
diffAdded: 'diffAdded',
|
|
88
91
|
diffRemoved: 'diffRemoved',
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { QuietFrame, QuietFrameRow } from './quiet-frame.js';
|
|
2
|
+
export type TerminalMouseEvent = {
|
|
3
|
+
readonly kind: 'press' | 'drag' | 'release';
|
|
4
|
+
readonly column: number;
|
|
5
|
+
readonly row: number;
|
|
6
|
+
} | {
|
|
7
|
+
readonly kind: 'wheel';
|
|
8
|
+
readonly direction: 'older' | 'newer';
|
|
9
|
+
readonly column: number;
|
|
10
|
+
readonly row: number;
|
|
11
|
+
};
|
|
12
|
+
export interface TerminalSelectionPoint {
|
|
13
|
+
readonly row: number;
|
|
14
|
+
readonly column: number;
|
|
15
|
+
}
|
|
16
|
+
export type TerminalSelectionEdge = 'none' | 'older' | 'newer';
|
|
17
|
+
export interface TerminalSelectionVisibleRow {
|
|
18
|
+
readonly physicalRow: number;
|
|
19
|
+
readonly logicalRow: number;
|
|
20
|
+
readonly row: QuietFrameRow;
|
|
21
|
+
readonly text: string;
|
|
22
|
+
}
|
|
23
|
+
export interface TerminalSelectionContext {
|
|
24
|
+
readonly frame: QuietFrame;
|
|
25
|
+
readonly transcriptRowCount: number;
|
|
26
|
+
readonly transcriptScrollOffset: number;
|
|
27
|
+
readonly maxTranscriptScrollOffset: number;
|
|
28
|
+
readonly visibleTranscriptRows: readonly TerminalSelectionVisibleRow[];
|
|
29
|
+
}
|
|
30
|
+
export interface TerminalSelectionState {
|
|
31
|
+
readonly phase: 'idle' | 'dragging' | 'selected';
|
|
32
|
+
readonly anchor: TerminalSelectionPoint | null;
|
|
33
|
+
readonly focus: TerminalSelectionPoint | null;
|
|
34
|
+
readonly rows: ReadonlyMap<number, string>;
|
|
35
|
+
readonly edge: TerminalSelectionEdge;
|
|
36
|
+
}
|
|
37
|
+
export interface TerminalSelectionRelease {
|
|
38
|
+
readonly state: TerminalSelectionState;
|
|
39
|
+
readonly text: string | null;
|
|
40
|
+
}
|
|
41
|
+
/** Parse one complete SGR mouse report. Coordinates returned here are zero based. */
|
|
42
|
+
export declare function parseTerminalMouseReport(value: string): TerminalMouseEvent | null;
|
|
43
|
+
export declare function clampTranscriptScrollOffset(offset: number, maxOffset: number): number;
|
|
44
|
+
export declare function createTerminalSelectionContext(frame: QuietFrame, transcriptRowCount: number, transcriptScrollOffset: number, maxTranscriptScrollOffset: number): TerminalSelectionContext;
|
|
45
|
+
export declare function createTerminalSelectionState(): TerminalSelectionState;
|
|
46
|
+
export declare function startTerminalSelection(context: TerminalSelectionContext, point: TerminalSelectionPoint): TerminalSelectionState;
|
|
47
|
+
export declare function updateTerminalSelection(state: TerminalSelectionState, context: TerminalSelectionContext, point: TerminalSelectionPoint): TerminalSelectionState;
|
|
48
|
+
export declare function refreshTerminalSelection(state: TerminalSelectionState, context: TerminalSelectionContext, edge?: TerminalSelectionEdge): TerminalSelectionState;
|
|
49
|
+
export declare function releaseTerminalSelection(state: TerminalSelectionState, context: TerminalSelectionContext, point?: TerminalSelectionPoint): TerminalSelectionRelease;
|
|
50
|
+
export declare function projectTerminalSelection(frame: QuietFrame, state: TerminalSelectionState, context: TerminalSelectionContext): QuietFrame;
|
|
51
|
+
//# sourceMappingURL=terminal-selection.d.ts.map
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { stripVTControlCharacters } from 'node:util';
|
|
2
|
+
import { terminalGraphemeWidth, terminalGraphemes, terminalTextWidth, } from './transcript-viewport.js';
|
|
3
|
+
function safeCoordinate(value) {
|
|
4
|
+
if (!/^[0-9]+$/u.test(value))
|
|
5
|
+
return null;
|
|
6
|
+
const number = Number(value);
|
|
7
|
+
return Number.isSafeInteger(number) && number > 0 ? number - 1 : null;
|
|
8
|
+
}
|
|
9
|
+
/** Parse one complete SGR mouse report. Coordinates returned here are zero based. */
|
|
10
|
+
export function parseTerminalMouseReport(value) {
|
|
11
|
+
if (typeof value !== 'string')
|
|
12
|
+
return null;
|
|
13
|
+
const report = value.codePointAt(0) === 0x1b ? value.slice(1) : value;
|
|
14
|
+
const match = /^\[<([0-9]+);([0-9]+);([0-9]+)([Mm])$/u.exec(report);
|
|
15
|
+
if (!match)
|
|
16
|
+
return null;
|
|
17
|
+
const code = Number(match[1]);
|
|
18
|
+
const column = safeCoordinate(match[2] ?? '');
|
|
19
|
+
const row = safeCoordinate(match[3] ?? '');
|
|
20
|
+
if (!Number.isSafeInteger(code) || column === null || row === null)
|
|
21
|
+
return null;
|
|
22
|
+
const final = match[4];
|
|
23
|
+
if (code === 0 && final === 'M')
|
|
24
|
+
return { kind: 'press', column, row };
|
|
25
|
+
if (code === 0 && final === 'm')
|
|
26
|
+
return { kind: 'release', column, row };
|
|
27
|
+
if (final === 'M' && code === 32)
|
|
28
|
+
return { kind: 'drag', column, row };
|
|
29
|
+
if (final === 'M' && code === 64)
|
|
30
|
+
return { kind: 'wheel', direction: 'older', column, row };
|
|
31
|
+
if (final === 'M' && code === 65)
|
|
32
|
+
return { kind: 'wheel', direction: 'newer', column, row };
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
function cleanRowText(row) {
|
|
36
|
+
return stripVTControlCharacters(row.segments.map((segment) => segment.text).join(''));
|
|
37
|
+
}
|
|
38
|
+
function finiteOffset(value) {
|
|
39
|
+
return Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : 0;
|
|
40
|
+
}
|
|
41
|
+
export function clampTranscriptScrollOffset(offset, maxOffset) {
|
|
42
|
+
const maximum = finiteOffset(maxOffset);
|
|
43
|
+
return Math.min(maximum, finiteOffset(offset));
|
|
44
|
+
}
|
|
45
|
+
export function createTerminalSelectionContext(frame, transcriptRowCount, transcriptScrollOffset, maxTranscriptScrollOffset) {
|
|
46
|
+
const rows = frame.lines
|
|
47
|
+
.map((row, physicalRow) => ({ row, physicalRow }))
|
|
48
|
+
.filter(({ row }) => row.region === 'transcript' && cleanRowText(row) !== '…');
|
|
49
|
+
const count = finiteOffset(transcriptRowCount);
|
|
50
|
+
const offset = clampTranscriptScrollOffset(transcriptScrollOffset, maxTranscriptScrollOffset);
|
|
51
|
+
const first = finiteOffset(maxTranscriptScrollOffset) - offset;
|
|
52
|
+
const droppedLeading = Math.max(0, count - rows.length);
|
|
53
|
+
return {
|
|
54
|
+
frame,
|
|
55
|
+
transcriptRowCount: count,
|
|
56
|
+
transcriptScrollOffset: offset,
|
|
57
|
+
maxTranscriptScrollOffset: finiteOffset(maxTranscriptScrollOffset),
|
|
58
|
+
visibleTranscriptRows: rows.map(({ row, physicalRow }, index) => ({
|
|
59
|
+
physicalRow,
|
|
60
|
+
logicalRow: first + droppedLeading + index,
|
|
61
|
+
row,
|
|
62
|
+
text: cleanRowText(row),
|
|
63
|
+
})),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export function createTerminalSelectionState() {
|
|
67
|
+
return {
|
|
68
|
+
phase: 'idle',
|
|
69
|
+
anchor: null,
|
|
70
|
+
focus: null,
|
|
71
|
+
rows: new Map(),
|
|
72
|
+
edge: 'none',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function rowAtPhysical(context, point) {
|
|
76
|
+
return context.visibleTranscriptRows.find((row) => row.physicalRow === point.row);
|
|
77
|
+
}
|
|
78
|
+
function clampColumn(column, text) {
|
|
79
|
+
const width = terminalTextWidth(text);
|
|
80
|
+
if (!Number.isFinite(column))
|
|
81
|
+
return 0;
|
|
82
|
+
return Math.max(0, Math.min(Math.trunc(column), Math.max(0, width - 1)));
|
|
83
|
+
}
|
|
84
|
+
function snapshotRows(state, context) {
|
|
85
|
+
const rows = new Map(state.rows);
|
|
86
|
+
let changed = false;
|
|
87
|
+
for (const visible of context.visibleTranscriptRows) {
|
|
88
|
+
if (state.rows.get(visible.logicalRow) === visible.text)
|
|
89
|
+
continue;
|
|
90
|
+
rows.set(visible.logicalRow, visible.text);
|
|
91
|
+
changed = true;
|
|
92
|
+
}
|
|
93
|
+
return changed ? rows : state.rows;
|
|
94
|
+
}
|
|
95
|
+
function pointFor(context, point) {
|
|
96
|
+
const row = rowAtPhysical(context, point);
|
|
97
|
+
if (!row)
|
|
98
|
+
return null;
|
|
99
|
+
return { row: row.logicalRow, column: clampColumn(point.column, row.text) };
|
|
100
|
+
}
|
|
101
|
+
function edgeFor(context, physicalRow) {
|
|
102
|
+
const first = context.visibleTranscriptRows[0]?.physicalRow;
|
|
103
|
+
const last = context.visibleTranscriptRows.at(-1)?.physicalRow;
|
|
104
|
+
if (first === undefined || last === undefined)
|
|
105
|
+
return 'none';
|
|
106
|
+
if (physicalRow <= first)
|
|
107
|
+
return context.transcriptScrollOffset < context.maxTranscriptScrollOffset
|
|
108
|
+
? 'older'
|
|
109
|
+
: 'none';
|
|
110
|
+
if (physicalRow >= last)
|
|
111
|
+
return context.transcriptScrollOffset > 0 ? 'newer' : 'none';
|
|
112
|
+
return 'none';
|
|
113
|
+
}
|
|
114
|
+
export function startTerminalSelection(context, point) {
|
|
115
|
+
const logical = pointFor(context, point);
|
|
116
|
+
if (!logical)
|
|
117
|
+
return createTerminalSelectionState();
|
|
118
|
+
return {
|
|
119
|
+
phase: 'dragging',
|
|
120
|
+
anchor: logical,
|
|
121
|
+
focus: logical,
|
|
122
|
+
rows: snapshotRows(createTerminalSelectionState(), context),
|
|
123
|
+
edge: 'none',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
export function updateTerminalSelection(state, context, point) {
|
|
127
|
+
if (state.phase !== 'dragging')
|
|
128
|
+
return state;
|
|
129
|
+
const logical = pointFor(context, point) ??
|
|
130
|
+
(context.visibleTranscriptRows.length > 0
|
|
131
|
+
? (() => {
|
|
132
|
+
const first = context.visibleTranscriptRows[0];
|
|
133
|
+
const last = context.visibleTranscriptRows.at(-1);
|
|
134
|
+
if (!first || !last)
|
|
135
|
+
return null;
|
|
136
|
+
const target = point.row <= first.physicalRow ? first : last;
|
|
137
|
+
return {
|
|
138
|
+
row: target.logicalRow,
|
|
139
|
+
column: clampColumn(point.column, target.text),
|
|
140
|
+
};
|
|
141
|
+
})()
|
|
142
|
+
: null);
|
|
143
|
+
if (!logical)
|
|
144
|
+
return state;
|
|
145
|
+
const rows = snapshotRows(state, context);
|
|
146
|
+
const edge = edgeFor(context, point.row);
|
|
147
|
+
if (state.focus?.row === logical.row &&
|
|
148
|
+
state.focus.column === logical.column &&
|
|
149
|
+
state.edge === edge &&
|
|
150
|
+
rows === state.rows)
|
|
151
|
+
return state;
|
|
152
|
+
return { ...state, focus: logical, rows, edge };
|
|
153
|
+
}
|
|
154
|
+
export function refreshTerminalSelection(state, context, edge = state.edge) {
|
|
155
|
+
if (state.phase !== 'dragging')
|
|
156
|
+
return state;
|
|
157
|
+
const visible = context.visibleTranscriptRows;
|
|
158
|
+
if (visible.length === 0)
|
|
159
|
+
return state;
|
|
160
|
+
const target = edge === 'older'
|
|
161
|
+
? visible[0]
|
|
162
|
+
: edge === 'newer'
|
|
163
|
+
? visible.at(-1)
|
|
164
|
+
: undefined;
|
|
165
|
+
const rows = snapshotRows(state, context);
|
|
166
|
+
if (!target)
|
|
167
|
+
return rows === state.rows ? state : { ...state, rows };
|
|
168
|
+
const focus = {
|
|
169
|
+
row: target.logicalRow,
|
|
170
|
+
column: clampColumn(state.focus?.column ?? 0, target.text),
|
|
171
|
+
};
|
|
172
|
+
const nextEdge = edgeFor(context, target.physicalRow);
|
|
173
|
+
if (state.focus?.row === focus.row &&
|
|
174
|
+
state.focus.column === focus.column &&
|
|
175
|
+
state.edge === nextEdge &&
|
|
176
|
+
rows === state.rows)
|
|
177
|
+
return state;
|
|
178
|
+
return { ...state, focus, rows, edge: nextEdge };
|
|
179
|
+
}
|
|
180
|
+
function normalizedPoints(state) {
|
|
181
|
+
if (!state.anchor || !state.focus)
|
|
182
|
+
return null;
|
|
183
|
+
return state.anchor.row < state.focus.row ||
|
|
184
|
+
(state.anchor.row === state.focus.row &&
|
|
185
|
+
state.anchor.column <= state.focus.column)
|
|
186
|
+
? [state.anchor, state.focus]
|
|
187
|
+
: [state.focus, state.anchor];
|
|
188
|
+
}
|
|
189
|
+
function selectedText(text, start, end) {
|
|
190
|
+
const output = [];
|
|
191
|
+
let cell = 0;
|
|
192
|
+
for (const grapheme of terminalGraphemes(text)) {
|
|
193
|
+
const width = terminalGraphemeWidth(grapheme);
|
|
194
|
+
const clusterEnd = cell + Math.max(1, width) - 1;
|
|
195
|
+
if (clusterEnd >= start && cell <= end)
|
|
196
|
+
output.push(grapheme);
|
|
197
|
+
cell += width;
|
|
198
|
+
}
|
|
199
|
+
return output.join('');
|
|
200
|
+
}
|
|
201
|
+
export function releaseTerminalSelection(state, context, point) {
|
|
202
|
+
const updated = point && state.phase === 'dragging'
|
|
203
|
+
? updateTerminalSelection(state, context, point)
|
|
204
|
+
: state;
|
|
205
|
+
const selected = {
|
|
206
|
+
...updated,
|
|
207
|
+
phase: updated.anchor && updated.focus
|
|
208
|
+
? 'selected'
|
|
209
|
+
: 'idle',
|
|
210
|
+
edge: 'none',
|
|
211
|
+
};
|
|
212
|
+
const points = normalizedPoints(selected);
|
|
213
|
+
if (!points)
|
|
214
|
+
return { state: selected, text: null };
|
|
215
|
+
const [start, end] = points;
|
|
216
|
+
const text = [];
|
|
217
|
+
for (let row = start.row; row <= end.row; row += 1) {
|
|
218
|
+
const value = selected.rows.get(row);
|
|
219
|
+
if (value === undefined)
|
|
220
|
+
return { state: selected, text: null };
|
|
221
|
+
text.push(selectedText(value, row === start.row ? start.column : 0, row === end.row ? end.column : Number.MAX_SAFE_INTEGER));
|
|
222
|
+
}
|
|
223
|
+
return { state: selected, text: text.join('\n') };
|
|
224
|
+
}
|
|
225
|
+
export function projectTerminalSelection(frame, state, context) {
|
|
226
|
+
const points = normalizedPoints(state);
|
|
227
|
+
if (!points || state.phase === 'idle')
|
|
228
|
+
return frame;
|
|
229
|
+
const [start, end] = points;
|
|
230
|
+
const lines = frame.lines.map((row) => {
|
|
231
|
+
const visible = context.visibleTranscriptRows.find((item) => item.row.key === row.key);
|
|
232
|
+
if (!visible ||
|
|
233
|
+
visible.logicalRow < start.row ||
|
|
234
|
+
visible.logicalRow > end.row)
|
|
235
|
+
return row;
|
|
236
|
+
const lower = visible.logicalRow === start.row ? start.column : 0;
|
|
237
|
+
const upper = visible.logicalRow === end.row ? end.column : Number.MAX_SAFE_INTEGER;
|
|
238
|
+
const segments = [];
|
|
239
|
+
let position = 0;
|
|
240
|
+
for (const segment of row.segments) {
|
|
241
|
+
const plain = stripVTControlCharacters(segment.text);
|
|
242
|
+
for (const grapheme of terminalGraphemes(plain)) {
|
|
243
|
+
const width = terminalGraphemeWidth(grapheme);
|
|
244
|
+
const clusterEnd = position + Math.max(1, width) - 1;
|
|
245
|
+
const role = clusterEnd >= lower && position <= upper
|
|
246
|
+
? 'textSelection'
|
|
247
|
+
: segment.role;
|
|
248
|
+
const previous = segments.at(-1);
|
|
249
|
+
if (previous?.role === role)
|
|
250
|
+
segments[segments.length - 1] = {
|
|
251
|
+
...previous,
|
|
252
|
+
text: previous.text + grapheme,
|
|
253
|
+
};
|
|
254
|
+
else
|
|
255
|
+
segments.push({ text: grapheme, role });
|
|
256
|
+
position += width;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return { ...row, segments };
|
|
260
|
+
});
|
|
261
|
+
return { ...frame, lines };
|
|
262
|
+
}
|
|
263
|
+
//# sourceMappingURL=terminal-selection.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { TranscriptPresentationEntry, TranscriptPresentationMode } from './transcript-presentation.js';
|
|
2
|
-
export type TuiTextRole = 'body' | 'heading' | 'muted' | 'accent' | 'success' | 'warning' | 'error' | 'tool' | 'selection' | 'input' | 'diffAdded' | 'diffRemoved';
|
|
2
|
+
export type TuiTextRole = 'body' | 'heading' | 'muted' | 'accent' | 'success' | 'warning' | 'error' | 'tool' | 'selection' | 'textSelection' | 'input' | 'diffAdded' | 'diffRemoved';
|
|
3
3
|
export interface TuiRowSegment {
|
|
4
4
|
readonly text: string;
|
|
5
5
|
readonly role: TuiTextRole;
|
|
@@ -9,7 +9,7 @@ function workflowDefinition() {
|
|
|
9
9
|
name: 'Workflow',
|
|
10
10
|
description: `Run an explicitly requested, sandboxed JavaScript workflow in the background. Use only when the user asks for a workflow or named saved workflow; for ordinary delegation use Agent.
|
|
11
11
|
|
|
12
|
-
Every script starts with a pure-literal \`export const meta = { name, description, phases? }\`. Its async body may use \`args\`, \`agent(prompt, options?)\`, \`parallel(thunks)\`, \`pipeline(items, ...stages)\`, \`workflow(nameOrScriptPath, args)\`, \`phase(title)\`, \`log(message)\`, and \`budget.{total,spent(),remaining()}\`. Agent options are \`label\`, \`phase\`, \`model\`, \`effort\`, \`agentType\`, \`schema\`, and \`isolation: 'worktree'\`. Pipeline stages receive \`(previousResult, originalItem, index)\`; failed parallel or pipeline items become null. Nested workflows are limited to one level.
|
|
12
|
+
Every script starts with a pure-literal \`export const meta = { name, description, phases? }\`. \`phases\` is optional and must be an array of objects shaped \`{ title: string, detail?: string, model?: string }\`, never an array of strings. Its async body may use \`args\`, \`agent(prompt, options?)\`, \`parallel(thunks)\`, \`pipeline(items, ...stages)\`, \`workflow(nameOrScriptPath, args)\`, \`phase(title)\`, \`log(message)\`, and \`budget.{total,spent(),remaining()}\`. Agent options are \`label\`, \`phase\`, \`model\`, \`effort\`, \`agentType\`, \`schema\`, and \`isolation: 'worktree'\`. Pipeline stages receive \`(previousResult, originalItem, index)\`; failed parallel or pipeline items become null. Nested workflows are limited to one level.
|
|
13
13
|
|
|
14
14
|
Provide one source: \`scriptPath\` takes precedence over \`script\`, which takes precedence over \`name\`. Saved names resolve from project \`${projectDirectory}/workflows\`, then user workflows, followed by built-ins. The call returns a task ID immediately; use TaskOutput/TaskStop for lifecycle. Resume a terminal or interrupted run with its script path and \`resumeFromRunId\`; completed matching agents replay from journal. Workflows allow at most 1000 agents, 4096 collection items, and bounded concurrency. Scripts have no Node.js, filesystem, network, ambient time, or randomness.`,
|
|
15
15
|
inputSchema: {
|