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 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, and Ctrl-C restoration,
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,
@@ -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
- return (_jsx(TuiThemeProvider, { settings: themeSettings, screenReader: axScreenReader, children: ansiActive ? (_jsx(TuiAnsiSurface, { frame: quietFrame, onError: () => setAnsiRendererFailed(true) })) : (_jsx(QuietInkFrame, { frame: quietFrame, screenReader: axScreenReader })) }));
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.
@@ -20,6 +20,7 @@ export declare class AnsiFullscreenRenderer {
20
20
  setStyles(styles?: Partial<Record<TuiTextRole, string>>): void;
21
21
  get mounted(): boolean;
22
22
  mount(): void;
23
+ clear(): void;
23
24
  draw(frame: AnsiFrame): void;
24
25
  dispose(): void;
25
26
  }
@@ -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
- this.#writer.write(SYNCHRONIZED_END);
198
- this.#writer.write(SHOW_CURSOR);
199
- this.#writer.write(ALTERNATE_SCREEN_LEAVE);
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.noColor || theme.screenReader)
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',
@@ -12,6 +12,7 @@ const ROLE_MAP = {
12
12
  error: 'error',
13
13
  tool: 'info',
14
14
  selection: 'focusMarker',
15
+ textSelection: 'inputCursor',
15
16
  input: 'inputMarker',
16
17
  diffAdded: 'diffAdded',
17
18
  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: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "praxis-agent",
3
- "version": "0.45.2",
3
+ "version": "0.45.4",
4
4
  "description": "Local-first, single-user general agent for the command line.",
5
5
  "license": "MIT",
6
6
  "author": "wuqisen",