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
|
@@ -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',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { TuiScreenModel } from './tui-screen-model.js';
|
|
2
2
|
import type { TuiRow, TuiTextRole } from './tui-row-ir.js';
|
|
3
3
|
export type QuietFrameDensity = 'full' | 'standard' | 'compact' | 'narrow' | 'minimal';
|
|
4
|
-
export type QuietFrameRegion = 'identity' | 'transcript' | 'active' | 'focus' | 'composer' | 'status';
|
|
4
|
+
export type QuietFrameRegion = 'identity' | 'transcript' | 'active' | 'pending' | 'focus' | 'composer' | 'status';
|
|
5
5
|
export interface QuietFrameRow extends TuiRow {
|
|
6
6
|
readonly region: QuietFrameRegion;
|
|
7
7
|
readonly accessibleText?: string;
|
|
@@ -26,6 +26,11 @@ export interface QuietFrameInput {
|
|
|
26
26
|
readonly shellMode: boolean;
|
|
27
27
|
readonly busy: boolean;
|
|
28
28
|
readonly status: string;
|
|
29
|
+
readonly pendingItems?: readonly {
|
|
30
|
+
readonly id: string;
|
|
31
|
+
readonly kind: 'steering' | 'follow-up';
|
|
32
|
+
readonly text: string;
|
|
33
|
+
}[];
|
|
29
34
|
readonly display?: {
|
|
30
35
|
readonly cwd?: string;
|
|
31
36
|
readonly model?: string;
|
|
@@ -280,6 +280,10 @@ export function projectQuietFrame(input) {
|
|
|
280
280
|
thinking.lines.forEach(({ text: part, start }, index) => lines.push(createQuietFrameRow(`quiet:active:thinking:${start}`, `${index === 0 ? '✻ ' : ' '}${part}`, 'active', 'muted', `${index === 0 ? 'Thinking: ' : ''}${part}`)));
|
|
281
281
|
}
|
|
282
282
|
}
|
|
283
|
+
for (const item of input.pendingItems ?? []) {
|
|
284
|
+
const label = item.kind === 'steering' ? 'steer' : 'follow-up';
|
|
285
|
+
lines.push(createQuietFrameRow(`quiet:pending:${item.id}`, `${label} · ${clean(item.text)}`, 'pending', 'muted', `${label} pending input: ${clean(item.text)}`));
|
|
286
|
+
}
|
|
283
287
|
const focus = input.focusRows.length > 0
|
|
284
288
|
? input.focusRows.map((item) => withRegion(item, 'focus'))
|
|
285
289
|
: [
|
|
@@ -19,6 +19,7 @@ export interface QuietScreenProjectionInput {
|
|
|
19
19
|
readonly shellMode: boolean;
|
|
20
20
|
readonly busy: boolean;
|
|
21
21
|
readonly status: string;
|
|
22
|
+
readonly pendingItems?: QuietFrameInput['pendingItems'];
|
|
22
23
|
readonly display?: QuietFrameInput['display'];
|
|
23
24
|
}
|
|
24
25
|
export declare function projectQuietScreenFrame(input: QuietScreenProjectionInput): QuietFrame;
|
|
@@ -103,6 +103,9 @@ export function projectQuietScreenFrame(input) {
|
|
|
103
103
|
shellMode: input.shellMode,
|
|
104
104
|
busy: input.busy,
|
|
105
105
|
status: input.status,
|
|
106
|
+
...(input.pendingItems === undefined
|
|
107
|
+
? {}
|
|
108
|
+
: { pendingItems: input.pendingItems }),
|
|
106
109
|
...(input.display === undefined ? {} : { display: input.display }),
|
|
107
110
|
focusRows,
|
|
108
111
|
});
|
|
@@ -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
|
|
@@ -71,7 +71,7 @@ export function routeTuiInteraction(snapshot, input) {
|
|
|
71
71
|
}
|
|
72
72
|
if (input.action === 'chat:cancel')
|
|
73
73
|
return handled([...confirmationEffects, { kind: 'interrupt-turn' }]);
|
|
74
|
-
return
|
|
74
|
+
return delegated(confirmationEffects);
|
|
75
75
|
}
|
|
76
76
|
if (input.callerIntent !== 'none')
|
|
77
77
|
return delegated(confirmationEffects);
|
|
@@ -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;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** A user input item accepted by an active, steerable turn. */
|
|
2
|
+
export interface SteeringItem {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly content: string;
|
|
5
|
+
}
|
|
6
|
+
export type SteeringEnqueueResult = {
|
|
7
|
+
readonly kind: 'accepted';
|
|
8
|
+
readonly item: SteeringItem;
|
|
9
|
+
} | {
|
|
10
|
+
readonly kind: 'empty';
|
|
11
|
+
} | {
|
|
12
|
+
readonly kind: 'sealed';
|
|
13
|
+
};
|
|
14
|
+
export type SteeringWithdrawResult = {
|
|
15
|
+
readonly kind: 'withdrawn';
|
|
16
|
+
readonly item: SteeringItem;
|
|
17
|
+
} | {
|
|
18
|
+
readonly kind: 'not-pending';
|
|
19
|
+
};
|
|
20
|
+
/** Results returned by the session-level steering commands. */
|
|
21
|
+
export type ActiveTurnInputCommandResult = {
|
|
22
|
+
readonly kind: 'accepted';
|
|
23
|
+
readonly item: SteeringItem;
|
|
24
|
+
} | {
|
|
25
|
+
readonly kind: 'withdrawn';
|
|
26
|
+
readonly item: SteeringItem;
|
|
27
|
+
} | {
|
|
28
|
+
readonly kind: 'empty';
|
|
29
|
+
} | {
|
|
30
|
+
readonly kind: 'no-active-turn';
|
|
31
|
+
} | {
|
|
32
|
+
readonly kind: 'not-steerable';
|
|
33
|
+
} | {
|
|
34
|
+
readonly kind: 'turn-completing';
|
|
35
|
+
} | {
|
|
36
|
+
readonly kind: 'not-pending';
|
|
37
|
+
};
|
|
38
|
+
/** The synchronous port exposed to the runtime at safe continuation points. */
|
|
39
|
+
export interface ActiveTurnInputPort {
|
|
40
|
+
take(): SteeringItem | undefined;
|
|
41
|
+
takeOrSeal(): SteeringItem | undefined;
|
|
42
|
+
/** Drain and seal the port, returning every item that was not delivered. */
|
|
43
|
+
close(): readonly SteeringItem[];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A small synchronous FIFO. Keeping all state transitions synchronous makes
|
|
47
|
+
* enqueue/take-or-seal atomic with respect to the runtime's await boundaries.
|
|
48
|
+
*/
|
|
49
|
+
export declare class ActiveTurnInputMailbox implements ActiveTurnInputPort {
|
|
50
|
+
private readonly createId;
|
|
51
|
+
private readonly items;
|
|
52
|
+
private sealed;
|
|
53
|
+
constructor(createId: () => string);
|
|
54
|
+
enqueue(content: string): SteeringEnqueueResult;
|
|
55
|
+
take(): SteeringItem | undefined;
|
|
56
|
+
/** Take one item, or seal an empty mailbox before returning. */
|
|
57
|
+
takeOrSeal(): SteeringItem | undefined;
|
|
58
|
+
withdraw(id: string): SteeringWithdrawResult;
|
|
59
|
+
close(): readonly SteeringItem[];
|
|
60
|
+
isSealed(): boolean;
|
|
61
|
+
get pendingCount(): number;
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=active-turn-input.d.ts.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small synchronous FIFO. Keeping all state transitions synchronous makes
|
|
3
|
+
* enqueue/take-or-seal atomic with respect to the runtime's await boundaries.
|
|
4
|
+
*/
|
|
5
|
+
export class ActiveTurnInputMailbox {
|
|
6
|
+
createId;
|
|
7
|
+
items = [];
|
|
8
|
+
sealed = false;
|
|
9
|
+
constructor(createId) {
|
|
10
|
+
this.createId = createId;
|
|
11
|
+
}
|
|
12
|
+
enqueue(content) {
|
|
13
|
+
const trimmed = content.trim();
|
|
14
|
+
if (trimmed.length === 0)
|
|
15
|
+
return { kind: 'empty' };
|
|
16
|
+
if (this.sealed)
|
|
17
|
+
return { kind: 'sealed' };
|
|
18
|
+
const item = { id: this.createId(), content: trimmed };
|
|
19
|
+
this.items.push(item);
|
|
20
|
+
return { kind: 'accepted', item };
|
|
21
|
+
}
|
|
22
|
+
take() {
|
|
23
|
+
return this.items.shift();
|
|
24
|
+
}
|
|
25
|
+
/** Take one item, or seal an empty mailbox before returning. */
|
|
26
|
+
takeOrSeal() {
|
|
27
|
+
const item = this.items.shift();
|
|
28
|
+
if (item !== undefined)
|
|
29
|
+
return item;
|
|
30
|
+
this.sealed = true;
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
withdraw(id) {
|
|
34
|
+
const index = this.items.findIndex((item) => item.id === id);
|
|
35
|
+
if (index < 0)
|
|
36
|
+
return { kind: 'not-pending' };
|
|
37
|
+
const [item] = this.items.splice(index, 1);
|
|
38
|
+
return item === undefined
|
|
39
|
+
? { kind: 'not-pending' }
|
|
40
|
+
: { kind: 'withdrawn', item };
|
|
41
|
+
}
|
|
42
|
+
close() {
|
|
43
|
+
this.sealed = true;
|
|
44
|
+
return this.items.splice(0);
|
|
45
|
+
}
|
|
46
|
+
isSealed() {
|
|
47
|
+
return this.sealed;
|
|
48
|
+
}
|
|
49
|
+
get pendingCount() {
|
|
50
|
+
return this.items.length;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=active-turn-input.js.map
|
package/dist/core/runtime.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ActiveTurnInputPort, SteeringItem } from './active-turn-input.js';
|
|
1
2
|
export type RuntimeState = 'idle' | 'assembling-context' | 'compacting' | 'awaiting-model' | 'streaming' | 'awaiting-permission' | 'executing-tools' | 'persisting-results' | 'completed' | 'cancelled' | 'failed';
|
|
2
3
|
export type ModelMessage = {
|
|
3
4
|
role: 'system';
|
|
@@ -175,6 +176,15 @@ export type RuntimeEvent = {
|
|
|
175
176
|
message: string;
|
|
176
177
|
attachments?: readonly string[];
|
|
177
178
|
status: 'normal' | 'proactive';
|
|
179
|
+
} | {
|
|
180
|
+
type: 'user-input-delivered';
|
|
181
|
+
id: string;
|
|
182
|
+
content: string;
|
|
183
|
+
} | {
|
|
184
|
+
type: 'user-input-rejected';
|
|
185
|
+
id: string;
|
|
186
|
+
content: string;
|
|
187
|
+
reason: 'cancelled' | 'failed' | 'closed';
|
|
178
188
|
} | {
|
|
179
189
|
type: 'usage';
|
|
180
190
|
usage: ModelUsage;
|
|
@@ -434,6 +444,7 @@ export interface AgentRunObserver {
|
|
|
434
444
|
}): Promise<void>;
|
|
435
445
|
toolCompleted(call: ModelToolCall, result: ToolExecutionResult): Promise<void>;
|
|
436
446
|
followUpUserMessagesCompleted?(messages: readonly string[]): Promise<void>;
|
|
447
|
+
userInputDelivered?(item: SteeringItem): Promise<void>;
|
|
437
448
|
}
|
|
438
449
|
export interface ToolUseSummaryOutcome {
|
|
439
450
|
summary: string | null;
|
|
@@ -471,6 +482,7 @@ export interface AgentRunRequest {
|
|
|
471
482
|
cwd?: string;
|
|
472
483
|
toolResultDirectory?: string;
|
|
473
484
|
observer?: AgentRunObserver;
|
|
485
|
+
steering?: ActiveTurnInputPort;
|
|
474
486
|
reloadMessages?: () => Promise<readonly ModelMessage[]>;
|
|
475
487
|
approveTool?: (call: ModelToolCall, originalCall?: ModelToolCall, decision?: PermissionDecision) => PermissionApproval | Promise<PermissionApproval>;
|
|
476
488
|
permissionUpdates?: readonly PermissionUpdate[];
|