praxis-agent 0.45.3 → 0.46.0
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 +6 -2
- package/dist/application/session-service.d.ts +4 -0
- package/dist/application/session-service.js +83 -0
- package/dist/cli/interactive.d.ts +3 -0
- package/dist/cli/interactive.js +370 -5
- 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/quiet-frame.d.ts +6 -1
- package/dist/cli/tui/quiet-frame.js +4 -0
- package/dist/cli/tui/quiet-screen-projector.d.ts +1 -0
- package/dist/cli/tui/quiet-screen-projector.js +3 -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-interaction-router.js +1 -1
- package/dist/cli/tui/tui-row-ir.d.ts +1 -1
- package/dist/core/active-turn-input.d.ts +63 -0
- package/dist/core/active-turn-input.js +53 -0
- package/dist/core/runtime.d.ts +12 -0
- package/dist/core/runtime.js +66 -7
- package/package.json +1 -1
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';
|
|
@@ -374,6 +375,15 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
374
375
|
const inputHistoryRef = useRef([]);
|
|
375
376
|
const inputHistoryIndexRef = useRef(null);
|
|
376
377
|
const inputHistoryDraftRef = useRef('');
|
|
378
|
+
const [pendingInputs, setPendingInputs] = useState([]);
|
|
379
|
+
const pendingInputsRef = useRef(pendingInputs);
|
|
380
|
+
pendingInputsRef.current = pendingInputs;
|
|
381
|
+
const followUpQueueRef = useRef([]);
|
|
382
|
+
const updatePendingInputs = (update) => {
|
|
383
|
+
const next = typeof update === 'function' ? update(pendingInputsRef.current) : update;
|
|
384
|
+
pendingInputsRef.current = next;
|
|
385
|
+
setPendingInputs(next);
|
|
386
|
+
};
|
|
377
387
|
const undoStackRef = useRef([]);
|
|
378
388
|
const composerImagesRef = useRef(new Map());
|
|
379
389
|
const nextImageIdRef = useRef(1);
|
|
@@ -487,6 +497,11 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
487
497
|
transcriptScrollOffsetRef.current = offset;
|
|
488
498
|
setTranscriptScrollOffsetState(offset);
|
|
489
499
|
};
|
|
500
|
+
const [terminalSelection, setTerminalSelection] = useState(createTerminalSelectionState);
|
|
501
|
+
const terminalSelectionRef = useRef(createTerminalSelectionState());
|
|
502
|
+
const terminalSelectionContextRef = useRef(null);
|
|
503
|
+
const selectionEdgeTimerRef = useRef(null);
|
|
504
|
+
const [clearRevision, setClearRevision] = useState(0);
|
|
490
505
|
const sessionLoadRef = useRef(0);
|
|
491
506
|
const [turnDiffs, setTurnDiffs] = useState([]);
|
|
492
507
|
const turnNumberRef = useRef(0);
|
|
@@ -1191,6 +1206,56 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
1191
1206
|
const conversationScreen = screen.body.kind === 'conversation' ? screen.body : undefined;
|
|
1192
1207
|
const transcriptPageRows = conversationScreen?.transcript.pageRows ?? 2;
|
|
1193
1208
|
const maxTranscriptScrollOffset = conversationScreen?.transcript.maxScrollOffset ?? 0;
|
|
1209
|
+
const stopSelectionEdgeScroll = () => {
|
|
1210
|
+
if (selectionEdgeTimerRef.current !== null) {
|
|
1211
|
+
clearInterval(selectionEdgeTimerRef.current);
|
|
1212
|
+
selectionEdgeTimerRef.current = null;
|
|
1213
|
+
}
|
|
1214
|
+
};
|
|
1215
|
+
const selectionContextRef = terminalSelectionContextRef;
|
|
1216
|
+
const copyTerminalSelection = (text) => {
|
|
1217
|
+
if (!text)
|
|
1218
|
+
return;
|
|
1219
|
+
void sideQuestionClipboardWriter(text).catch((error) => {
|
|
1220
|
+
append({
|
|
1221
|
+
kind: 'warning',
|
|
1222
|
+
text: `Clipboard unavailable: ${error instanceof Error ? error.message : String(error)}`,
|
|
1223
|
+
});
|
|
1224
|
+
});
|
|
1225
|
+
};
|
|
1226
|
+
const beginSelectionEdgeScroll = (direction) => {
|
|
1227
|
+
if (selectionEdgeTimerRef.current !== null)
|
|
1228
|
+
return;
|
|
1229
|
+
selectionEdgeTimerRef.current = setInterval(() => {
|
|
1230
|
+
const context = selectionContextRef.current;
|
|
1231
|
+
const current = terminalSelectionRef.current;
|
|
1232
|
+
if (!context ||
|
|
1233
|
+
current.phase !== 'dragging' ||
|
|
1234
|
+
current.edge !== direction) {
|
|
1235
|
+
stopSelectionEdgeScroll();
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
const offset = transcriptScrollOffsetRef.current;
|
|
1239
|
+
const next = clampTranscriptScrollOffset(offset + (direction === 'older' ? 1 : -1), context.maxTranscriptScrollOffset);
|
|
1240
|
+
if (next === offset) {
|
|
1241
|
+
stopSelectionEdgeScroll();
|
|
1242
|
+
setTerminalSelection((state) => ({ ...state, edge: 'none' }));
|
|
1243
|
+
terminalSelectionRef.current = { ...current, edge: 'none' };
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
setTranscriptScrollOffset(next);
|
|
1247
|
+
}, 80);
|
|
1248
|
+
};
|
|
1249
|
+
useEffect(() => {
|
|
1250
|
+
if (!ansiActive) {
|
|
1251
|
+
stopSelectionEdgeScroll();
|
|
1252
|
+
terminalSelectionRef.current = createTerminalSelectionState();
|
|
1253
|
+
setTerminalSelection(terminalSelectionRef.current);
|
|
1254
|
+
}
|
|
1255
|
+
return () => {
|
|
1256
|
+
stopSelectionEdgeScroll();
|
|
1257
|
+
};
|
|
1258
|
+
}, [ansiActive]);
|
|
1194
1259
|
const permissionOptions = useMemo(() => [
|
|
1195
1260
|
...PERMISSION_OPTIONS,
|
|
1196
1261
|
...(allowDangerouslySkipPermissions
|
|
@@ -1672,6 +1737,30 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
1672
1737
|
inputHistoryIndexRef.current = nextIndex;
|
|
1673
1738
|
updateComposerInput(history[nextIndex] ?? '');
|
|
1674
1739
|
};
|
|
1740
|
+
const withdrawLatestPending = () => {
|
|
1741
|
+
if (inputRef.current.trim().length !== 0)
|
|
1742
|
+
return false;
|
|
1743
|
+
const pending = pendingInputsRef.current.at(-1);
|
|
1744
|
+
if (!pending)
|
|
1745
|
+
return false;
|
|
1746
|
+
if (pending.kind === 'follow-up') {
|
|
1747
|
+
followUpQueueRef.current = followUpQueueRef.current.filter((item) => item.id !== pending.id);
|
|
1748
|
+
updatePendingInputs((current) => current.filter((item) => item.id !== pending.id));
|
|
1749
|
+
updateComposerInput(pending.text);
|
|
1750
|
+
return true;
|
|
1751
|
+
}
|
|
1752
|
+
const activeSessionId = sessionIdRef.current;
|
|
1753
|
+
const result = activeSessionId
|
|
1754
|
+
? serviceRef.current?.withdrawSteering?.(activeSessionId, pending.id)
|
|
1755
|
+
: undefined;
|
|
1756
|
+
if (result?.kind === 'withdrawn') {
|
|
1757
|
+
updatePendingInputs((current) => current.filter((item) => item.id !== pending.id));
|
|
1758
|
+
updateComposerInput(result.item.content);
|
|
1759
|
+
return true;
|
|
1760
|
+
}
|
|
1761
|
+
append({ kind: 'warning', text: 'Input is already delivered.' });
|
|
1762
|
+
return true;
|
|
1763
|
+
};
|
|
1675
1764
|
const dismissExitConfirmation = () => {
|
|
1676
1765
|
if (!exitConfirmation)
|
|
1677
1766
|
return;
|
|
@@ -1725,6 +1814,33 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
1725
1814
|
case 'user-message':
|
|
1726
1815
|
append({ kind: 'assistant', text: event.message });
|
|
1727
1816
|
break;
|
|
1817
|
+
case 'user-input-delivered':
|
|
1818
|
+
// Steering is delivered after the preceding assistant batch has been
|
|
1819
|
+
// persisted. Commit that batch to the visible transcript before the
|
|
1820
|
+
// user row, then start the next provider continuation with a fresh
|
|
1821
|
+
// active buffer. Otherwise the active row would appear after the
|
|
1822
|
+
// steering message and be discarded when the turn finally completes.
|
|
1823
|
+
if ((streamingFrameRef.current?.text.trim().length ?? 0) > 0) {
|
|
1824
|
+
append({
|
|
1825
|
+
kind: 'assistant',
|
|
1826
|
+
text: streamingFrameRef.current?.text ?? '',
|
|
1827
|
+
});
|
|
1828
|
+
}
|
|
1829
|
+
streamingFrameRef.current?.resetText();
|
|
1830
|
+
streamingFrameRef.current?.resetThinking();
|
|
1831
|
+
streamingFrameRef.current?.flush();
|
|
1832
|
+
updatePendingInputs((current) => current.filter((item) => item.id !== event.id));
|
|
1833
|
+
append({ kind: 'user', text: event.content });
|
|
1834
|
+
break;
|
|
1835
|
+
case 'user-input-rejected':
|
|
1836
|
+
updatePendingInputs((current) => current.filter((item) => item.id !== event.id));
|
|
1837
|
+
if (inputRef.current.trim().length === 0)
|
|
1838
|
+
updateComposerInput(event.content);
|
|
1839
|
+
append({
|
|
1840
|
+
kind: 'warning',
|
|
1841
|
+
text: `Input not delivered · ${redactSensitiveText(event.content, sensitiveValues)}`,
|
|
1842
|
+
});
|
|
1843
|
+
break;
|
|
1728
1844
|
case 'state':
|
|
1729
1845
|
if (event.state === 'awaiting-model') {
|
|
1730
1846
|
activeAttemptThinkingItemsRef.current = [];
|
|
@@ -3156,7 +3272,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
3156
3272
|
onTurnChange?.(restoring);
|
|
3157
3273
|
void restoring.finally(() => onTurnChange?.(null));
|
|
3158
3274
|
};
|
|
3159
|
-
const
|
|
3275
|
+
const submitTurn = async (prompt, shellCommand, images = [], followUpId, internal = false) => {
|
|
3160
3276
|
setTranscriptScrollOffset(0);
|
|
3161
3277
|
const turnNumber = turnNumberRef.current + 1;
|
|
3162
3278
|
const turnStartedAt = Date.now();
|
|
@@ -3171,6 +3287,9 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
3171
3287
|
? AbortSignal.any([signal, turnController.signal])
|
|
3172
3288
|
: turnController.signal;
|
|
3173
3289
|
setBusy(true);
|
|
3290
|
+
if (followUpId !== undefined) {
|
|
3291
|
+
updatePendingInputs((current) => current.filter((item) => item.id !== followUpId));
|
|
3292
|
+
}
|
|
3174
3293
|
setTurnDuration(undefined);
|
|
3175
3294
|
setCommandPaletteOpen(false);
|
|
3176
3295
|
const submittedCommandName = /^\/([^\s]+)/u.exec(prompt)?.[1]?.toLowerCase();
|
|
@@ -3189,9 +3308,10 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
3189
3308
|
else
|
|
3190
3309
|
turnMutatedFilesRef.current = true;
|
|
3191
3310
|
let commands;
|
|
3311
|
+
let turnSucceeded = false;
|
|
3192
3312
|
try {
|
|
3193
3313
|
commands = await service();
|
|
3194
|
-
let activeSessionId =
|
|
3314
|
+
let activeSessionId = sessionIdRef.current;
|
|
3195
3315
|
const startedNewSession = activeSessionId === null;
|
|
3196
3316
|
if (activeSessionId === null) {
|
|
3197
3317
|
activeSessionId = randomUUID();
|
|
@@ -3260,7 +3380,9 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
3260
3380
|
streamingFrameRef.current?.flush();
|
|
3261
3381
|
setStatus('ready');
|
|
3262
3382
|
setTurnDuration(Date.now() - turnStartedAt);
|
|
3263
|
-
if (
|
|
3383
|
+
if (!internal &&
|
|
3384
|
+
followUpQueueRef.current.length === 0 &&
|
|
3385
|
+
runtimeSettingsRef.current.notifChannel !== 'notifications_disabled') {
|
|
3264
3386
|
notifyTerminal({
|
|
3265
3387
|
channel: runtimeSettingsRef.current.notifChannel,
|
|
3266
3388
|
title: 'Praxis',
|
|
@@ -3270,6 +3392,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
3270
3392
|
: { write: notificationWriter }),
|
|
3271
3393
|
});
|
|
3272
3394
|
}
|
|
3395
|
+
turnSucceeded = true;
|
|
3273
3396
|
}
|
|
3274
3397
|
catch (error) {
|
|
3275
3398
|
if (turnController.signal.aborted && !signal?.aborted) {
|
|
@@ -3287,7 +3410,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
3287
3410
|
}
|
|
3288
3411
|
}
|
|
3289
3412
|
finally {
|
|
3290
|
-
if (!factory.scheduledPrompts && commands) {
|
|
3413
|
+
if (!internal && !factory.scheduledPrompts && commands) {
|
|
3291
3414
|
try {
|
|
3292
3415
|
await commands.close?.();
|
|
3293
3416
|
}
|
|
@@ -3304,6 +3427,62 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
3304
3427
|
}
|
|
3305
3428
|
if (turnControllerRef.current === turnController)
|
|
3306
3429
|
turnControllerRef.current = null;
|
|
3430
|
+
if (!internal) {
|
|
3431
|
+
setBusy(false);
|
|
3432
|
+
}
|
|
3433
|
+
}
|
|
3434
|
+
return turnSucceeded;
|
|
3435
|
+
};
|
|
3436
|
+
const submit = async (prompt, shellCommand, images = [], followUpId) => {
|
|
3437
|
+
let currentPrompt = prompt;
|
|
3438
|
+
let currentShellCommand = shellCommand;
|
|
3439
|
+
let currentImages = images;
|
|
3440
|
+
let currentFollowUpId = followUpId;
|
|
3441
|
+
let completed = false;
|
|
3442
|
+
try {
|
|
3443
|
+
while (true) {
|
|
3444
|
+
completed = await submitTurn(currentPrompt, currentShellCommand, currentImages, currentFollowUpId, true);
|
|
3445
|
+
if (!completed)
|
|
3446
|
+
break;
|
|
3447
|
+
const next = followUpQueueRef.current.shift();
|
|
3448
|
+
if (!next)
|
|
3449
|
+
break;
|
|
3450
|
+
currentPrompt = next.text;
|
|
3451
|
+
currentShellCommand = undefined;
|
|
3452
|
+
currentImages = [];
|
|
3453
|
+
currentFollowUpId = next.id;
|
|
3454
|
+
}
|
|
3455
|
+
if (completed &&
|
|
3456
|
+
runtimeSettingsRef.current.notifChannel !== 'notifications_disabled') {
|
|
3457
|
+
notifyTerminal({
|
|
3458
|
+
channel: runtimeSettingsRef.current.notifChannel,
|
|
3459
|
+
title: 'Praxis',
|
|
3460
|
+
message: 'Turn complete',
|
|
3461
|
+
...(notificationWriter === undefined
|
|
3462
|
+
? {}
|
|
3463
|
+
: { write: notificationWriter }),
|
|
3464
|
+
});
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
finally {
|
|
3468
|
+
if (!factory.scheduledPrompts) {
|
|
3469
|
+
const commands = serviceRef.current;
|
|
3470
|
+
if (commands) {
|
|
3471
|
+
try {
|
|
3472
|
+
await commands.close?.();
|
|
3473
|
+
}
|
|
3474
|
+
catch (error) {
|
|
3475
|
+
append({
|
|
3476
|
+
kind: 'warning',
|
|
3477
|
+
text: redactSensitiveText(error instanceof Error ? error.message : String(error), sensitiveValues),
|
|
3478
|
+
});
|
|
3479
|
+
}
|
|
3480
|
+
finally {
|
|
3481
|
+
if (serviceRef.current === commands)
|
|
3482
|
+
serviceRef.current = null;
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3485
|
+
}
|
|
3307
3486
|
setBusy(false);
|
|
3308
3487
|
}
|
|
3309
3488
|
};
|
|
@@ -3360,6 +3539,53 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
3360
3539
|
return () => controller.abort();
|
|
3361
3540
|
}, [busy, initialPromptPending, permission, selectingSession, sessionId]);
|
|
3362
3541
|
useInput((value, key) => {
|
|
3542
|
+
if (ansiActive) {
|
|
3543
|
+
const mouse = parseTerminalMouseReport(value);
|
|
3544
|
+
if (mouse !== null) {
|
|
3545
|
+
const context = terminalSelectionContextRef.current;
|
|
3546
|
+
if (context === null)
|
|
3547
|
+
return;
|
|
3548
|
+
if (mouse.kind === 'wheel') {
|
|
3549
|
+
const offset = transcriptScrollOffsetRef.current;
|
|
3550
|
+
setTranscriptScrollOffset(clampTranscriptScrollOffset(offset + (mouse.direction === 'older' ? 1 : -1), context.maxTranscriptScrollOffset));
|
|
3551
|
+
return;
|
|
3552
|
+
}
|
|
3553
|
+
if (mouse.kind === 'press') {
|
|
3554
|
+
stopSelectionEdgeScroll();
|
|
3555
|
+
const next = startTerminalSelection(context, mouse);
|
|
3556
|
+
terminalSelectionRef.current = next;
|
|
3557
|
+
setTerminalSelection(next);
|
|
3558
|
+
return;
|
|
3559
|
+
}
|
|
3560
|
+
if (mouse.kind === 'drag') {
|
|
3561
|
+
const current = terminalSelectionRef.current;
|
|
3562
|
+
if (current.phase !== 'dragging')
|
|
3563
|
+
return;
|
|
3564
|
+
const previousEdge = current.edge;
|
|
3565
|
+
const next = updateTerminalSelection(current, context, mouse);
|
|
3566
|
+
terminalSelectionRef.current = next;
|
|
3567
|
+
setTerminalSelection(next);
|
|
3568
|
+
if (next.edge !== 'none') {
|
|
3569
|
+
if (next.edge !== previousEdge) {
|
|
3570
|
+
const offset = transcriptScrollOffsetRef.current;
|
|
3571
|
+
const moved = clampTranscriptScrollOffset(offset + (next.edge === 'older' ? 1 : -1), context.maxTranscriptScrollOffset);
|
|
3572
|
+
if (moved !== offset)
|
|
3573
|
+
setTranscriptScrollOffset(moved);
|
|
3574
|
+
}
|
|
3575
|
+
beginSelectionEdgeScroll(next.edge);
|
|
3576
|
+
}
|
|
3577
|
+
else
|
|
3578
|
+
stopSelectionEdgeScroll();
|
|
3579
|
+
return;
|
|
3580
|
+
}
|
|
3581
|
+
const released = releaseTerminalSelection(terminalSelectionRef.current, context, mouse);
|
|
3582
|
+
stopSelectionEdgeScroll();
|
|
3583
|
+
terminalSelectionRef.current = released.state;
|
|
3584
|
+
setTerminalSelection(released.state);
|
|
3585
|
+
copyTerminalSelection(released.text);
|
|
3586
|
+
return;
|
|
3587
|
+
}
|
|
3588
|
+
}
|
|
3363
3589
|
const lower = value.toLowerCase();
|
|
3364
3590
|
const controlKey = (letter) => (key.ctrl && lower === letter) ||
|
|
3365
3591
|
value === String.fromCharCode(letter.charCodeAt(0) - 96);
|
|
@@ -3547,6 +3773,32 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
3547
3773
|
if (!keybindingAction) {
|
|
3548
3774
|
keybindingAction = resolveTuiKeybinding(keybindings, keybindingContexts, inputChord);
|
|
3549
3775
|
}
|
|
3776
|
+
const clearScreenBinding = ansiActive &&
|
|
3777
|
+
(keybindingAction === 'chat:clearScreen' ||
|
|
3778
|
+
(inputChord === 'ctrl+l' && keybindingAction === 'chat:clearInput'));
|
|
3779
|
+
if (clearScreenBinding) {
|
|
3780
|
+
stopSelectionEdgeScroll();
|
|
3781
|
+
const cleared = createTerminalSelectionState();
|
|
3782
|
+
terminalSelectionRef.current = cleared;
|
|
3783
|
+
setTerminalSelection(cleared);
|
|
3784
|
+
setClearRevision((revision) => revision + 1);
|
|
3785
|
+
return;
|
|
3786
|
+
}
|
|
3787
|
+
if (ansiActive && terminalSelectionRef.current.phase !== 'idle') {
|
|
3788
|
+
const selectionAction = resolveTuiKeybinding(keybindings, ['Scroll'], inputChord);
|
|
3789
|
+
if (selectionAction === 'selection:copy') {
|
|
3790
|
+
const context = terminalSelectionContextRef.current;
|
|
3791
|
+
if (context === null)
|
|
3792
|
+
return;
|
|
3793
|
+
const copied = releaseTerminalSelection(terminalSelectionRef.current, context);
|
|
3794
|
+
copyTerminalSelection(copied.text);
|
|
3795
|
+
return;
|
|
3796
|
+
}
|
|
3797
|
+
stopSelectionEdgeScroll();
|
|
3798
|
+
const cleared = createTerminalSelectionState();
|
|
3799
|
+
terminalSelectionRef.current = cleared;
|
|
3800
|
+
setTerminalSelection(cleared);
|
|
3801
|
+
}
|
|
3550
3802
|
const scrollIntent = key.pageUp
|
|
3551
3803
|
? 'page-older'
|
|
3552
3804
|
: key.pageDown
|
|
@@ -5761,6 +6013,94 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
5761
6013
|
void backgrounding;
|
|
5762
6014
|
return;
|
|
5763
6015
|
}
|
|
6016
|
+
const pendingText = inputRef.current.trim();
|
|
6017
|
+
const addPending = (item) => updatePendingInputs((current) => [...current, item]);
|
|
6018
|
+
if (key.upArrow && pendingText.length === 0) {
|
|
6019
|
+
withdrawLatestPending();
|
|
6020
|
+
return;
|
|
6021
|
+
}
|
|
6022
|
+
if (key.return && key.shift) {
|
|
6023
|
+
updateComposerEditor(insertComposerText(editor(), '\n'));
|
|
6024
|
+
return;
|
|
6025
|
+
}
|
|
6026
|
+
if (isKeybinding('chat:submit') && !key.shift && !key.meta && !key.ctrl) {
|
|
6027
|
+
if (!pendingText)
|
|
6028
|
+
return;
|
|
6029
|
+
const controller = turnControllerRef.current;
|
|
6030
|
+
if (!controller) {
|
|
6031
|
+
append({
|
|
6032
|
+
kind: 'warning',
|
|
6033
|
+
text: 'Turn is completing; input was retained.',
|
|
6034
|
+
});
|
|
6035
|
+
return;
|
|
6036
|
+
}
|
|
6037
|
+
const restoreSteeringText = () => {
|
|
6038
|
+
const current = inputRef.current;
|
|
6039
|
+
updateComposerInput(current.trim().length === 0
|
|
6040
|
+
? pendingText
|
|
6041
|
+
: `${pendingText}\n${current}`);
|
|
6042
|
+
};
|
|
6043
|
+
// Service construction may still be in flight while the busy composer
|
|
6044
|
+
// is already visible. Clear now so newly typed text becomes a distinct
|
|
6045
|
+
// draft; restore both drafts if the active-turn command loses a race.
|
|
6046
|
+
clearComposerInput();
|
|
6047
|
+
const steer = async () => {
|
|
6048
|
+
const commands = serviceRef.current ?? (await service());
|
|
6049
|
+
if (turnControllerRef.current !== controller ||
|
|
6050
|
+
controller.signal.aborted) {
|
|
6051
|
+
restoreSteeringText();
|
|
6052
|
+
append({
|
|
6053
|
+
kind: 'warning',
|
|
6054
|
+
text: 'Active turn changed; input was retained.',
|
|
6055
|
+
});
|
|
6056
|
+
return;
|
|
6057
|
+
}
|
|
6058
|
+
const activeSessionId = sessionIdRef.current;
|
|
6059
|
+
const result = activeSessionId
|
|
6060
|
+
? commands.steer?.(activeSessionId, pendingText)
|
|
6061
|
+
: undefined;
|
|
6062
|
+
if (result?.kind === 'accepted') {
|
|
6063
|
+
addPending({
|
|
6064
|
+
id: result.item.id,
|
|
6065
|
+
kind: 'steering',
|
|
6066
|
+
text: result.item.content,
|
|
6067
|
+
});
|
|
6068
|
+
}
|
|
6069
|
+
else {
|
|
6070
|
+
restoreSteeringText();
|
|
6071
|
+
append({
|
|
6072
|
+
kind: 'warning',
|
|
6073
|
+
text: result?.kind === 'turn-completing'
|
|
6074
|
+
? 'Turn is completing; input was retained.'
|
|
6075
|
+
: result?.kind === 'not-steerable'
|
|
6076
|
+
? 'This turn cannot be steered; input was retained.'
|
|
6077
|
+
: 'Steering is unavailable; input was retained.',
|
|
6078
|
+
});
|
|
6079
|
+
}
|
|
6080
|
+
};
|
|
6081
|
+
void steer().catch((error) => {
|
|
6082
|
+
restoreSteeringText();
|
|
6083
|
+
warn(error);
|
|
6084
|
+
});
|
|
6085
|
+
return;
|
|
6086
|
+
}
|
|
6087
|
+
if (key.tab || (key.return && key.meta)) {
|
|
6088
|
+
if (!pendingText)
|
|
6089
|
+
return;
|
|
6090
|
+
if (!turnControllerRef.current) {
|
|
6091
|
+
append({
|
|
6092
|
+
kind: 'warning',
|
|
6093
|
+
text: 'Turn is completing; follow-up input was retained.',
|
|
6094
|
+
});
|
|
6095
|
+
return;
|
|
6096
|
+
}
|
|
6097
|
+
const item = { id: randomUUID(), text: pendingText };
|
|
6098
|
+
followUpQueueRef.current.push(item);
|
|
6099
|
+
addPending({ id: item.id, kind: 'follow-up', text: item.text });
|
|
6100
|
+
clearComposerInput();
|
|
6101
|
+
return;
|
|
6102
|
+
}
|
|
6103
|
+
editComposer();
|
|
5764
6104
|
return;
|
|
5765
6105
|
}
|
|
5766
6106
|
if (isKeybinding('chat:imagePaste')) {
|
|
@@ -6442,6 +6782,11 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
6442
6782
|
}
|
|
6443
6783
|
return;
|
|
6444
6784
|
}
|
|
6785
|
+
if (isKeybinding('history:previous') &&
|
|
6786
|
+
inputRef.current.length === 0 &&
|
|
6787
|
+
withdrawLatestPending()) {
|
|
6788
|
+
return;
|
|
6789
|
+
}
|
|
6445
6790
|
if (isKeybinding('history:previous')) {
|
|
6446
6791
|
restorePromptHistory('previous');
|
|
6447
6792
|
return;
|
|
@@ -6535,6 +6880,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
6535
6880
|
shellMode,
|
|
6536
6881
|
busy,
|
|
6537
6882
|
status: conciseStatus,
|
|
6883
|
+
pendingItems: pendingInputs,
|
|
6538
6884
|
display: runtimeDisplay,
|
|
6539
6885
|
}), [
|
|
6540
6886
|
screen,
|
|
@@ -6543,9 +6889,28 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
6543
6889
|
inputCursor,
|
|
6544
6890
|
busy,
|
|
6545
6891
|
conciseStatus,
|
|
6892
|
+
pendingInputs,
|
|
6546
6893
|
runtimeDisplay,
|
|
6547
6894
|
]);
|
|
6548
|
-
|
|
6895
|
+
const terminalSelectionContext = useMemo(() => createTerminalSelectionContext(quietFrame, conversationScreen?.transcript.rows.length ?? 0, transcriptScrollOffset, maxTranscriptScrollOffset), [
|
|
6896
|
+
quietFrame,
|
|
6897
|
+
conversationScreen?.transcript.rows.length,
|
|
6898
|
+
transcriptScrollOffset,
|
|
6899
|
+
maxTranscriptScrollOffset,
|
|
6900
|
+
]);
|
|
6901
|
+
terminalSelectionContextRef.current = terminalSelectionContext;
|
|
6902
|
+
terminalSelectionRef.current = terminalSelection;
|
|
6903
|
+
useEffect(() => {
|
|
6904
|
+
if (!ansiActive || terminalSelection.phase !== 'dragging')
|
|
6905
|
+
return;
|
|
6906
|
+
const refreshed = refreshTerminalSelection(terminalSelection, terminalSelectionContext);
|
|
6907
|
+
if (refreshed !== terminalSelection) {
|
|
6908
|
+
terminalSelectionRef.current = refreshed;
|
|
6909
|
+
setTerminalSelection(refreshed);
|
|
6910
|
+
}
|
|
6911
|
+
}, [ansiActive, terminalSelection, terminalSelectionContext]);
|
|
6912
|
+
const selectedQuietFrame = useMemo(() => projectTerminalSelection(quietFrame, terminalSelection, terminalSelectionContext), [quietFrame, terminalSelection, terminalSelectionContext]);
|
|
6913
|
+
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
6914
|
}
|
|
6550
6915
|
/**
|
|
6551
6916
|
* 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) {
|