@falkenslab/agent-kit 0.10.1 → 0.12.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 +91 -97
- package/dist/core/agentSpec.d.ts +31 -1
- package/dist/core/hooks/stepGate.js +3 -2
- package/dist/core/language.d.ts +39 -0
- package/dist/core/language.js +59 -0
- package/dist/core/messages/de.d.ts +2 -0
- package/dist/core/messages/de.js +103 -0
- package/dist/core/messages/en.d.ts +90 -0
- package/dist/core/messages/en.js +102 -0
- package/dist/core/messages/es.d.ts +2 -0
- package/dist/core/messages/es.js +103 -0
- package/dist/core/messages/fr.d.ts +2 -0
- package/dist/core/messages/fr.js +103 -0
- package/dist/core/messages/index.d.ts +23 -0
- package/dist/core/messages/index.js +67 -0
- package/dist/core/runs.d.ts +51 -0
- package/dist/core/runs.js +194 -0
- package/dist/core/session.d.ts +8 -0
- package/dist/core/session.js +42 -7
- package/dist/core/toolLabels.d.ts +2 -1
- package/dist/core/toolLabels.js +31 -51
- package/dist/core/tools/humanApproval.js +3 -2
- package/dist/index.d.ts +5 -0
- package/dist/index.js +6 -0
- package/dist/tui/chatTui.d.ts +23 -2
- package/dist/tui/chatTui.js +71 -7
- package/dist/tui/claudeAuth.d.ts +3 -1
- package/dist/tui/claudeAuth.js +12 -17
- package/dist/tui/consoleRenderer.js +2 -1
- package/dist/tui/ink/PromptInput.d.ts +3 -4
- package/dist/tui/ink/PromptInput.js +30 -17
- package/dist/tui/ink/SessionView.js +16 -15
- package/dist/tui/ink/inkInteraction.js +4 -7
- package/dist/tui/ink/progressView.d.ts +6 -0
- package/dist/tui/ink/progressView.js +2 -0
- package/dist/tui/ink/promptEditing.d.ts +11 -5
- package/dist/tui/ink/promptEditing.js +28 -3
- package/dist/tui/ink/runChatInk.d.ts +25 -2
- package/dist/tui/ink/runChatInk.js +156 -48
- package/dist/tui/ink/sessionModel.d.ts +10 -0
- package/dist/tui/ink/sessionModel.js +27 -2
- package/dist/tui/ink/toolGroup.d.ts +2 -1
- package/dist/tui/ink/toolGroup.js +5 -18
- package/dist/tui/ink/wizard.d.ts +6 -0
- package/dist/tui/ink/wizard.js +4 -1
- package/dist/tui/language.d.ts +5 -0
- package/dist/tui/language.js +11 -0
- package/dist/tui/runs.d.ts +27 -0
- package/dist/tui/runs.js +30 -0
- package/dist/tui/terminalInteraction.js +2 -2
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useContext, useLayoutEffect, useReducer, useRef } from "react";
|
|
3
|
-
import { Box, Text, useInput, useStdout } from "ink";
|
|
3
|
+
import { Box, Text, useInput, useStdin, useStdout } from "ink";
|
|
4
4
|
import stringWidth from "string-width";
|
|
5
5
|
import { fitWidth, stripAnsi } from "./lineBuffer.js";
|
|
6
6
|
import { liveWidth } from "./sessionModel.js";
|
|
@@ -8,8 +8,9 @@ import { isMouseReport } from "./fullscreen.js";
|
|
|
8
8
|
import { CursorContext } from "./terminalCursor.js";
|
|
9
9
|
import { isFocusReport } from "./terminalStatus.js";
|
|
10
10
|
import { matchingFiles, mentionAt } from "./fileMentions.js";
|
|
11
|
-
import { continueLine, createPasteRegistry, cursorLine, deleteBack, deleteWordBack, insert, isBlockPaste, killToLineEnd, moveLine, normalizePaste, searchHistory, toLineEnd, toLineStart, wordLeft, wordRight, } from "./promptEditing.js";
|
|
11
|
+
import { continueLine, createPasteRegistry, cursorLine, deleteBack, deleteForward, deleteWordBack, insert, isBlockPaste, isForwardDelete, killToLineEnd, moveLine, normalizePaste, PASTE_TOKEN_AFTER, PASTE_TOKEN_BEFORE, searchHistory, toLineEnd, toLineStart, wordLeft, wordRight, } from "./promptEditing.js";
|
|
12
12
|
import * as ui from "../ui.js";
|
|
13
|
+
import { t } from "../../core/messages/index.js";
|
|
13
14
|
const MAX_SUGGESTIONS = 6;
|
|
14
15
|
/** Commands (names without "/") a partial "/name" line could still become. */
|
|
15
16
|
export function matchingCommands(value, commands) {
|
|
@@ -44,25 +45,16 @@ export function visibleWindow(length, cursor, room) {
|
|
|
44
45
|
const start = Math.min(Math.max(0, cursor + 1 - span), Math.max(0, length + 1 - span));
|
|
45
46
|
return [start, Math.min(length, start + span)];
|
|
46
47
|
}
|
|
47
|
-
/** The shortcuts panel, opened with "?" on an empty prompt. */
|
|
48
|
-
export const SHORTCUTS = [
|
|
49
|
-
"Enter send · \\ + Enter or Ctrl+J new line · Tab complete or take the suggestion",
|
|
50
|
-
"↑/↓ history · Ctrl+R search history · ↑ on an empty prompt edits the last queued message",
|
|
51
|
-
"Ctrl+W delete word · Ctrl+K to line end · Ctrl+U clear · Ctrl+←/→ move by word",
|
|
52
|
-
"@ mention a file · /copy copy the last reply · Shift+Tab switch mode · Ctrl+O unfold tool calls",
|
|
53
|
-
"Esc interrupt · Ctrl+C interrupt or exit · PgUp/PgDn or wheel scroll · Ctrl+End bottom",
|
|
54
|
-
"Drag to select, right-click to copy (full screen)",
|
|
55
|
-
];
|
|
56
48
|
// A paste can reach us in several chunks; pieces this close together extend the same one.
|
|
57
49
|
const PASTE_CHUNK_MS = 100;
|
|
58
|
-
const PASTE_TOKEN_BEFORE = /\[Pasted text #\d+ \+\d+ lines?\]$/;
|
|
59
50
|
/**
|
|
60
51
|
* The chat's prompt: history (↑/↓), "/command" completion and suggestions (Tab), pasted
|
|
61
52
|
* blocks folded into a token, multi-line input (`\` + Enter or Ctrl+J), word and line
|
|
62
53
|
* shortcuts (Ctrl+W/K/U, Ctrl+←/→, Home/End, Ctrl+A/E), reverse history search (Ctrl+R)
|
|
63
54
|
* and taking a queued line back (↑ on an empty prompt). Written here rather than taken from
|
|
64
|
-
* @inkjs/ui because its TextInput is uncontrolled. Backspace
|
|
65
|
-
*
|
|
55
|
+
* @inkjs/ui because its TextInput is uncontrolled. Ink reports Backspace and Delete both as
|
|
56
|
+
* `key.delete`: the raw input chunk (read before Ink parses it) tells Delete apart, which
|
|
57
|
+
* erases forwards; anything else reported as delete erases backwards.
|
|
66
58
|
*/
|
|
67
59
|
export function PromptInput({ label, inset = 0, suggestion, history, commands, onSubmit, onExit, onRecallQueued, files = [] }) {
|
|
68
60
|
// Kept in a ref, not in state: two keystrokes can arrive before React re-renders, and a
|
|
@@ -78,6 +70,19 @@ export function PromptInput({ label, inset = 0, suggestion, history, commands, o
|
|
|
78
70
|
});
|
|
79
71
|
const pastes = useRef(createPasteRegistry()).current;
|
|
80
72
|
const [, rerender] = useReducer((n) => n + 1, 0);
|
|
73
|
+
// Whether the chunk being handled is the Delete key. Prepended, so it runs before the
|
|
74
|
+
// useInput listener below, which reads it (both listen to the same chunk on Ink's emitter).
|
|
75
|
+
const forwardDelete = useRef(false);
|
|
76
|
+
const { internal_eventEmitter: stdinEvents } = useStdin();
|
|
77
|
+
useLayoutEffect(() => {
|
|
78
|
+
const onInput = (data) => {
|
|
79
|
+
forwardDelete.current = isForwardDelete(String(data));
|
|
80
|
+
};
|
|
81
|
+
stdinEvents.prependListener("input", onInput);
|
|
82
|
+
return () => {
|
|
83
|
+
stdinEvents.removeListener("input", onInput);
|
|
84
|
+
};
|
|
85
|
+
}, [stdinEvents]);
|
|
81
86
|
function apply(next) {
|
|
82
87
|
state.current.value = next.value;
|
|
83
88
|
state.current.cursor = next.cursor;
|
|
@@ -106,6 +111,9 @@ export function PromptInput({ label, inset = 0, suggestion, history, commands, o
|
|
|
106
111
|
else if (key.return) {
|
|
107
112
|
finish(true);
|
|
108
113
|
}
|
|
114
|
+
else if (key.delete && forwardDelete.current) {
|
|
115
|
+
// The search query has no cursor inside it: Delete has nothing after it to erase.
|
|
116
|
+
}
|
|
109
117
|
else if (key.backspace || key.delete) {
|
|
110
118
|
search.query = search.query.slice(0, -1);
|
|
111
119
|
search.index = searchHistory(history, search.query);
|
|
@@ -242,6 +250,11 @@ export function PromptInput({ label, inset = 0, suggestion, history, commands, o
|
|
|
242
250
|
else if (key.end || (key.ctrl && input === "e")) {
|
|
243
251
|
apply(toLineEnd(current));
|
|
244
252
|
}
|
|
253
|
+
else if (key.delete && forwardDelete.current) {
|
|
254
|
+
// A pasted block goes away whole, as with Backspace.
|
|
255
|
+
const token = PASTE_TOKEN_AFTER.exec(value.slice(cursor))?.[0];
|
|
256
|
+
apply(token ? { value: value.slice(0, cursor) + value.slice(cursor + token.length), cursor } : deleteForward(current));
|
|
257
|
+
}
|
|
245
258
|
else if (key.backspace || key.delete) {
|
|
246
259
|
// A pasted block goes away whole.
|
|
247
260
|
const token = PASTE_TOKEN_BEFORE.exec(value.slice(0, cursor))?.[0];
|
|
@@ -287,7 +300,7 @@ export function PromptInput({ label, inset = 0, suggestion, history, commands, o
|
|
|
287
300
|
const width = liveWidth(stdout.columns) - inset;
|
|
288
301
|
const labelWidth = stringWidth(stripAnsi(label));
|
|
289
302
|
const room = width - labelWidth;
|
|
290
|
-
const ghost = suggestion ? `${suggestion} (
|
|
303
|
+
const ghost = suggestion ? `${suggestion} (${t().suggestionKey})` : null;
|
|
291
304
|
// Suggestions give way on a short terminal: the live area must stay shorter than it.
|
|
292
305
|
const maxSuggestions = Math.min(MAX_SUGGESTIONS, Math.max(0, (stdout.rows || 24) - 10));
|
|
293
306
|
const mention = mentionAt(value, cursor);
|
|
@@ -303,7 +316,7 @@ export function PromptInput({ label, inset = 0, suggestion, history, commands, o
|
|
|
303
316
|
let rows;
|
|
304
317
|
let cursorColumn;
|
|
305
318
|
if (search) {
|
|
306
|
-
const prefix =
|
|
319
|
+
const prefix = t().reverseSearch(search.query);
|
|
307
320
|
const match = search.index >= 0 ? history[search.index] : "";
|
|
308
321
|
cursorColumn = Math.min(stringWidth(prefix) - 3, width - 1);
|
|
309
322
|
rows = [
|
|
@@ -329,5 +342,5 @@ export function PromptInput({ label, inset = 0, suggestion, history, commands, o
|
|
|
329
342
|
cursorController.setTarget({ node: cursorLineRef.current, column: cursorColumn });
|
|
330
343
|
});
|
|
331
344
|
useLayoutEffect(() => () => cursorController?.setTarget(null), [cursorController]);
|
|
332
|
-
return (_jsxs(Box, { flexDirection: "column", children: [rows, suggestions.map((item) => (_jsx(Text, { children: ui.dim(fitWidth(` ${item}`, width)) }, item))), showHelp ?
|
|
345
|
+
return (_jsxs(Box, { flexDirection: "column", children: [rows, suggestions.map((item) => (_jsx(Text, { children: ui.dim(fitWidth(` ${item}`, width)) }, item))), showHelp ? t().shortcuts.map((line) => _jsx(Text, { children: ui.dim(fitWidth(` ${line}`, width)) }, line)) : null] }));
|
|
333
346
|
}
|
|
@@ -8,13 +8,14 @@ import { framePosition } from "./terminalCursor.js";
|
|
|
8
8
|
import { isFocusReport } from "./terminalStatus.js";
|
|
9
9
|
import { fitWidth, stripAnsi } from "./lineBuffer.js";
|
|
10
10
|
import * as ui from "../ui.js";
|
|
11
|
+
import { t } from "../../core/messages/index.js";
|
|
11
12
|
// Numbered as in the Claude Code CLI's permission prompt; the digits and y/n/q answer too.
|
|
12
|
-
const
|
|
13
|
-
{ label:
|
|
14
|
-
{ label:
|
|
15
|
-
{ label:
|
|
13
|
+
const decisionOptions = () => [
|
|
14
|
+
{ label: `1. ${t().yes}`, value: "y" },
|
|
15
|
+
{ label: `2. ${t().no}`, value: "n" },
|
|
16
|
+
{ label: `3. ${t().stop}`, value: "q" },
|
|
16
17
|
];
|
|
17
|
-
const
|
|
18
|
+
const manualOptions = () => [{ label: `1. ${t().doneContinue}`, value: "continue" }];
|
|
18
19
|
const DECISION_KEYS = { "1": "y", "2": "n", "3": "q", y: "y", n: "n", q: "q" };
|
|
19
20
|
const ACCENT_HEX = "#d77757";
|
|
20
21
|
/**
|
|
@@ -25,7 +26,7 @@ const ACCENT_HEX = "#d77757";
|
|
|
25
26
|
export function previewLines(lines, terminalRows) {
|
|
26
27
|
const all = lines.flatMap((line) => line.split("\n"));
|
|
27
28
|
const max = Math.max(3, (terminalRows || 24) - 12);
|
|
28
|
-
return all.length <= max ? all : [...all.slice(0, max - 1), ui.dim(
|
|
29
|
+
return all.length <= max ? all : [...all.slice(0, max - 1), ui.dim(t().moreLines(all.length - max + 1))];
|
|
29
30
|
}
|
|
30
31
|
function CheckpointPanel({ checkpoint, renderApproval }) {
|
|
31
32
|
const decision = checkpoint.kind === "decision";
|
|
@@ -38,17 +39,17 @@ function CheckpointPanel({ checkpoint, renderApproval }) {
|
|
|
38
39
|
});
|
|
39
40
|
const { prompt } = checkpoint;
|
|
40
41
|
const { stdout } = useStdout();
|
|
41
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: ACCENT_HEX, paddingX: 1, children: [renderApproval ? (renderApproval(prompt)) : (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: prompt.title }), previewLines(prompt.lines, stdout.rows).map((line, index) => (_jsx(Text, { children: line }, index))), !decision && prompt.question ? _jsx(Text, { dimColor: true, children: prompt.question }) : null] })), decision ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { children:
|
|
42
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: ACCENT_HEX, paddingX: 1, children: [renderApproval ? (renderApproval(prompt)) : (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: prompt.title }), previewLines(prompt.lines, stdout.rows).map((line, index) => (_jsx(Text, { children: line }, index))), !decision && prompt.question ? _jsx(Text, { dimColor: true, children: prompt.question }) : null] })), decision ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { children: t().proceed }) })) : null, _jsx(Box, { marginTop: decision ? 0 : 1, children: _jsx(Select, { options: decision ? decisionOptions() : manualOptions(), onChange: (value) => checkpoint.answer(decision ? value : "") }, checkpoint.id) })] }));
|
|
42
43
|
}
|
|
43
44
|
function formatTokens(count) {
|
|
44
45
|
return count >= 1000 ? `${(count / 1000).toFixed(1)}k` : String(count);
|
|
45
46
|
}
|
|
46
47
|
export function statusText(mode, turns, usage, extra = {}) {
|
|
47
48
|
const parts = [
|
|
48
|
-
...(mode ? [`⏵⏵ ${mode}${extra.modeSwitchable ?
|
|
49
|
-
|
|
50
|
-
...(usage ? [
|
|
51
|
-
...(extra.contextPercent != null ? [
|
|
49
|
+
...(mode ? [`⏵⏵ ${t().mode(mode)}${extra.modeSwitchable ? ` (${t().switchModeKey})` : ""}`] : []),
|
|
50
|
+
t().turns(turns),
|
|
51
|
+
...(usage ? [t().tokens(formatTokens(usage.inputTokens), formatTokens(usage.outputTokens))] : []),
|
|
52
|
+
...(extra.contextPercent != null ? [t().context(Math.round(extra.contextPercent))] : []),
|
|
52
53
|
];
|
|
53
54
|
return parts.join(" · ");
|
|
54
55
|
}
|
|
@@ -63,7 +64,7 @@ function Working({ label, startedAt, width }) {
|
|
|
63
64
|
}, []);
|
|
64
65
|
const seconds = startedAt === null ? 0 : Math.floor((Date.now() - startedAt) / 1000);
|
|
65
66
|
const glyph = WORKING_GLYPHS[tick % WORKING_GLYPHS.length];
|
|
66
|
-
return _jsx(Text, { children: fitWidth(`${ui.working(`${glyph} ${label}`)} ${ui.dim(`(${seconds}s ·
|
|
67
|
+
return _jsx(Text, { children: fitWidth(`${ui.working(`${glyph} ${label}`)} ${ui.dim(`(${seconds}s · ${t().escToInterrupt})`)}`, width) });
|
|
67
68
|
}
|
|
68
69
|
/** Line in progress, checkpoint panel or spinner, the input, and the status bar. */
|
|
69
70
|
function LiveArea({ session, checkpoint, renderApproval, mode, width, statusExtra, modeSwitchable, children }) {
|
|
@@ -74,7 +75,7 @@ function LiveArea({ session, checkpoint, renderApproval, mode, width, statusExtr
|
|
|
74
75
|
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, width: width, marginTop: session.liveGap ? 1 : 0, children: [stripAnsi(session.live) ? _jsx(Text, { children: session.live }) : null, checkpoint ? (_jsx(CheckpointPanel, { checkpoint: checkpoint, renderApproval: renderApproval })) : session.busy ? (
|
|
75
76
|
// Always one blank line above the spinner: from this margin, or from the live area's
|
|
76
77
|
// own when a separator is pending and nothing is written above the spinner yet.
|
|
77
|
-
_jsxs(Box, { flexDirection: "column", marginTop: stripAnsi(session.live) || !session.liveGap ? 1 : 0, children: [_jsx(Working, { label: session.activity ??
|
|
78
|
+
_jsxs(Box, { flexDirection: "column", marginTop: stripAnsi(session.live) || !session.liveGap ? 1 : 0, children: [_jsx(Working, { label: session.activity ?? t().thinking, startedAt: session.turnStartedAt, width: width }), session.subagentActivity ? _jsx(Text, { dimColor: true, children: fitWidth(` ↳ ${session.subagentActivity}`, width) }) : null] })) : null, checkpoint ? null : children, _jsx(Text, { children: fitWidth(status, width) })] }));
|
|
78
79
|
}
|
|
79
80
|
/** A history line for the inline view: the human's line as a bar, a tool group unfolded with Ctrl+O. */
|
|
80
81
|
function inlineItem(item, expanded, width) {
|
|
@@ -184,7 +185,7 @@ function FullscreenSession({ session, checkpoint, renderApproval, mode, header,
|
|
|
184
185
|
setSelection(null);
|
|
185
186
|
if (text && onCopy) {
|
|
186
187
|
onCopy(text);
|
|
187
|
-
setCopied(
|
|
188
|
+
setCopied(t().copiedCharacters(text.length));
|
|
188
189
|
}
|
|
189
190
|
}
|
|
190
191
|
}
|
|
@@ -205,7 +206,7 @@ function FullscreenSession({ session, checkpoint, renderApproval, mode, header,
|
|
|
205
206
|
setAnchor(null); // typing
|
|
206
207
|
});
|
|
207
208
|
const below = rowsBelow(anchor, total);
|
|
208
|
-
const statusExtra = [anchor === null ? null :
|
|
209
|
+
const statusExtra = [anchor === null ? null : t().linesBelow(below), copied]
|
|
209
210
|
.filter(Boolean)
|
|
210
211
|
.join(" · ");
|
|
211
212
|
return (_jsxs(Box, { flexDirection: "column", width: columns - 1, height: rows, children: [header && header.length > 0 ? (_jsx(Box, { flexDirection: "column", flexShrink: 0, marginBottom: 1, children: header.map((line, i) => (_jsx(Text, { wrap: "truncate-end", children: line || " " }, i))) })) : null, _jsx(Box, { ref: historyRef, flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", justifyContent: "flex-end", children: wrapped.rows.slice(start, end).map((row, i) => (_jsx(Text, { wrap: "truncate-end", children: highlightRow(row, selection, start + i) || " " }, start + i))) }), _jsx(LiveArea, { session: session, checkpoint: checkpoint, renderApproval: renderApproval, mode: mode, width: width, statusExtra: statusExtra || undefined, modeSwitchable: modeSwitchable, children: children })] }));
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import * as ui from "../ui.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
n: ui.warn("✘ Rejected"),
|
|
5
|
-
q: ui.error("■ Stopped"),
|
|
6
|
-
};
|
|
2
|
+
import { t } from "../../core/messages/index.js";
|
|
3
|
+
const outcome = (answer) => ({ y: ui.success(t().approved), n: ui.warn(t().rejected), q: ui.error(t().stopped) })[answer];
|
|
7
4
|
/**
|
|
8
5
|
* An `InteractionPort` for the Ink views: each checkpoint waits in a queue (the step gate
|
|
9
6
|
* can fire for several tool calls at once) until the panel answers it, or until the
|
|
@@ -37,13 +34,13 @@ export function createInkInteraction(note) {
|
|
|
37
34
|
answer(value) {
|
|
38
35
|
if (!remove(checkpoint))
|
|
39
36
|
return;
|
|
40
|
-
record(prompt, kind === "decision" ? (
|
|
37
|
+
record(prompt, kind === "decision" ? (outcome(value) ?? value) : ui.success(t().done));
|
|
41
38
|
resolve(value);
|
|
42
39
|
},
|
|
43
40
|
};
|
|
44
41
|
signal.addEventListener("abort", () => {
|
|
45
42
|
if (remove(checkpoint))
|
|
46
|
-
record(prompt, ui.dim(
|
|
43
|
+
record(prompt, ui.dim(t().answeredByFile));
|
|
47
44
|
}, { once: true });
|
|
48
45
|
queue = [...queue, checkpoint];
|
|
49
46
|
emit();
|
|
@@ -11,6 +11,12 @@ export interface ProgressViewOptions extends Omit<ConsoleRendererOptions, "outpu
|
|
|
11
11
|
mode?: Mode;
|
|
12
12
|
/** Use the plain console renderer even on a TTY. */
|
|
13
13
|
plain?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* The language of the kit's texts ("en", "es", "fr", "de"). `--language=<code>` on the
|
|
16
|
+
* command line wins; without either, the one `buildSessionOptions()` chose from
|
|
17
|
+
* `config.language`, or else the system's.
|
|
18
|
+
*/
|
|
19
|
+
language?: string;
|
|
14
20
|
}
|
|
15
21
|
export interface ProgressView extends ConsoleRenderer {
|
|
16
22
|
/** Removes the live area and restores the previous interaction port; await it before printing anything else. */
|
|
@@ -7,6 +7,7 @@ import { createConsoleRenderer } from "../consoleRenderer.js";
|
|
|
7
7
|
import { createInkInteraction } from "./inkInteraction.js";
|
|
8
8
|
import { createSessionModel, liveWidth } from "./sessionModel.js";
|
|
9
9
|
import { SessionView } from "./SessionView.js";
|
|
10
|
+
import { applyLanguage } from "../language.js";
|
|
10
11
|
function ProgressApp({ model, interaction, renderApproval, mode, closed }) {
|
|
11
12
|
const checkpoint = useSyncExternalStore(interaction.subscribe, interaction.getSnapshot);
|
|
12
13
|
// Only a panel reads the keyboard (and so puts stdin in raw mode, where Ctrl+C no longer
|
|
@@ -29,6 +30,7 @@ function ProgressApp({ model, interaction, renderApproval, mode, closed }) {
|
|
|
29
30
|
* Without a TTY or with `plain`, it is `createConsoleRenderer()` with a no-op `close()`.
|
|
30
31
|
*/
|
|
31
32
|
export function createProgressView(options = {}) {
|
|
33
|
+
applyLanguage(options.language);
|
|
32
34
|
if (options.plain || !stdin.isTTY || !stdout.isTTY) {
|
|
33
35
|
const renderer = createConsoleRenderer(options);
|
|
34
36
|
return Object.assign(renderer, { close: async () => { } });
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure editing operations on the prompt's text and cursor (a UTF-16 index into `value`),
|
|
3
|
-
* so PromptInput.tsx only maps keys to them. The value may hold line breaks ("\n"): a
|
|
4
|
-
* multi-line prompt.
|
|
5
|
-
*/
|
|
6
1
|
export interface EditState {
|
|
7
2
|
value: string;
|
|
8
3
|
cursor: number;
|
|
9
4
|
}
|
|
10
5
|
export declare function insert({ value, cursor }: EditState, text: string): EditState;
|
|
11
6
|
export declare function deleteBack({ value, cursor }: EditState): EditState;
|
|
7
|
+
/** Delete (Supr): removes the character after the cursor, which stays where it is. */
|
|
8
|
+
export declare function deleteForward({ value, cursor }: EditState): EditState;
|
|
9
|
+
/**
|
|
10
|
+
* Whether a raw input chunk is the Delete key: `ESC [ 3 ~`, with modifiers `ESC [ 3 ; 5 ~`,
|
|
11
|
+
* rxvt's `ESC [ 3 $` / `ESC [ 3 ^`, or the kitty protocol's `ESC [ 3 ; …~`. Ink reports it and
|
|
12
|
+
* Backspace (`DEL`, 0x7f, which Windows terminals send too) both as `key.delete`, so only the
|
|
13
|
+
* raw sequence tells them apart.
|
|
14
|
+
*/
|
|
15
|
+
export declare function isForwardDelete(data: string): boolean;
|
|
12
16
|
/** Ctrl+W: deletes the word before the cursor. */
|
|
13
17
|
export declare function deleteWordBack({ value, cursor }: EditState): EditState;
|
|
14
18
|
/** Ctrl+K: deletes from the cursor to the end of its line. */
|
|
@@ -42,6 +46,8 @@ export interface PasteRegistry {
|
|
|
42
46
|
/** `value` with every token replaced by its text. */
|
|
43
47
|
expand(value: string): string;
|
|
44
48
|
}
|
|
49
|
+
export declare const PASTE_TOKEN_BEFORE: RegExp;
|
|
50
|
+
export declare const PASTE_TOKEN_AFTER: RegExp;
|
|
45
51
|
/** Normalizes a pasted chunk's line breaks (terminals send "\r" or "\r\n"). */
|
|
46
52
|
export declare function normalizePaste(text: string): string;
|
|
47
53
|
/** A paste worth folding into a token: more than one line. */
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure editing operations on the prompt's text and cursor (a UTF-16 index into `value`),
|
|
3
|
+
* so PromptInput.tsx only maps keys to them. The value may hold line breaks ("\n"): a
|
|
4
|
+
* multi-line prompt.
|
|
5
|
+
*/
|
|
6
|
+
import { t } from "../../core/messages/index.js";
|
|
1
7
|
export function insert({ value, cursor }, text) {
|
|
2
8
|
return { value: value.slice(0, cursor) + text + value.slice(cursor), cursor: cursor + text.length };
|
|
3
9
|
}
|
|
@@ -6,6 +12,23 @@ export function deleteBack({ value, cursor }) {
|
|
|
6
12
|
return { value, cursor };
|
|
7
13
|
return { value: value.slice(0, cursor - 1) + value.slice(cursor), cursor: cursor - 1 };
|
|
8
14
|
}
|
|
15
|
+
/** Delete (Supr): removes the character after the cursor, which stays where it is. */
|
|
16
|
+
export function deleteForward({ value, cursor }) {
|
|
17
|
+
if (cursor >= value.length)
|
|
18
|
+
return { value, cursor };
|
|
19
|
+
return { value: value.slice(0, cursor) + value.slice(cursor + 1), cursor };
|
|
20
|
+
}
|
|
21
|
+
// eslint-disable-next-line no-control-regex -- \x1b is the ESC byte the Delete key's sequence starts with
|
|
22
|
+
const FORWARD_DELETE = /^\x1b\[3(;[\d:]*)?[~$^]$/;
|
|
23
|
+
/**
|
|
24
|
+
* Whether a raw input chunk is the Delete key: `ESC [ 3 ~`, with modifiers `ESC [ 3 ; 5 ~`,
|
|
25
|
+
* rxvt's `ESC [ 3 $` / `ESC [ 3 ^`, or the kitty protocol's `ESC [ 3 ; …~`. Ink reports it and
|
|
26
|
+
* Backspace (`DEL`, 0x7f, which Windows terminals send too) both as `key.delete`, so only the
|
|
27
|
+
* raw sequence tells them apart.
|
|
28
|
+
*/
|
|
29
|
+
export function isForwardDelete(data) {
|
|
30
|
+
return FORWARD_DELETE.test(data);
|
|
31
|
+
}
|
|
9
32
|
const isSpace = (char) => char !== undefined && /\s/.test(char);
|
|
10
33
|
/** Where the word before the cursor starts (spaces right before it skipped). */
|
|
11
34
|
function wordStartBefore(value, cursor) {
|
|
@@ -74,10 +97,12 @@ export function continueLine(state) {
|
|
|
74
97
|
return null;
|
|
75
98
|
return insert(deleteBack(state), "\n");
|
|
76
99
|
}
|
|
77
|
-
|
|
100
|
+
// A paste token in any language ("[Pasted text #1 +2 lines]", "[Texto pegado #1 +2 líneas]"...).
|
|
101
|
+
const PASTE_TOKEN = /\[[^\]\n#]+#(\d+) \+\d+ [^\]\n]+\]/g;
|
|
102
|
+
export const PASTE_TOKEN_BEFORE = /\[[^\]\n#]+#\d+ \+\d+ [^\]\n]+\]$/;
|
|
103
|
+
export const PASTE_TOKEN_AFTER = /^\[[^\]\n#]+#\d+ \+\d+ [^\]\n]+\]/;
|
|
78
104
|
function pasteToken(id, text) {
|
|
79
|
-
|
|
80
|
-
return `[Pasted text #${id} +${extra} ${extra === 1 ? "line" : "lines"}]`;
|
|
105
|
+
return t().pastedText(id, text.split("\n").length - 1);
|
|
81
106
|
}
|
|
82
107
|
/** Normalizes a pasted chunk's line breaks (terminals send "\r" or "\r\n"). */
|
|
83
108
|
export function normalizePaste(text) {
|
|
@@ -2,6 +2,7 @@ import type { Options } from "@anthropic-ai/claude-agent-sdk";
|
|
|
2
2
|
import type { Mode } from "../../core/agentSpec.js";
|
|
3
3
|
import { type ModeControl } from "../../core/session.js";
|
|
4
4
|
import { type ChatTuiOptions } from "../chatTui.js";
|
|
5
|
+
import { type SessionOpener } from "../runs.js";
|
|
5
6
|
import { type RenderApproval } from "./SessionView.js";
|
|
6
7
|
import type { ToolPhrase } from "./toolGroup.js";
|
|
7
8
|
import { type HeaderInfo } from "./header.js";
|
|
@@ -46,7 +47,8 @@ export interface InkChatOptions extends ChatTuiOptions {
|
|
|
46
47
|
/**
|
|
47
48
|
* The session's mode control (`buildSessionOptions()` returns it): the status bar shows the
|
|
48
49
|
* current mode, and Shift+Tab switches between the modes it allows ("guided" and
|
|
49
|
-
* "interactive"; an autonomous session can't switch).
|
|
50
|
+
* "interactive"; an autonomous session can't switch). With a session opener, the one it
|
|
51
|
+
* returns is used instead.
|
|
50
52
|
*/
|
|
51
53
|
modeControl?: ModeControl;
|
|
52
54
|
/**
|
|
@@ -55,6 +57,16 @@ export interface InkChatOptions extends ChatTuiOptions {
|
|
|
55
57
|
*/
|
|
56
58
|
firstPromptSuggestion?: string;
|
|
57
59
|
}
|
|
60
|
+
/** A choice asked in place of the prompt (the /resume list). */
|
|
61
|
+
export interface ChatPicker {
|
|
62
|
+
title: string;
|
|
63
|
+
options: {
|
|
64
|
+
label: string;
|
|
65
|
+
value: string;
|
|
66
|
+
}[];
|
|
67
|
+
/** Settles it with the chosen value, or null if cancelled. */
|
|
68
|
+
resolve(value: string | null): void;
|
|
69
|
+
}
|
|
58
70
|
export interface ChatInputSnapshot {
|
|
59
71
|
/** The chat loop is waiting for a line (no turn in flight). */
|
|
60
72
|
prompting: boolean;
|
|
@@ -68,6 +80,8 @@ export interface ChatInputSnapshot {
|
|
|
68
80
|
files: string[];
|
|
69
81
|
/** The current mode, when a mode control is given. */
|
|
70
82
|
mode: Mode | null;
|
|
83
|
+
/** A choice shown in place of the prompt, if any. */
|
|
84
|
+
picker: ChatPicker | null;
|
|
71
85
|
closed: boolean;
|
|
72
86
|
}
|
|
73
87
|
/**
|
|
@@ -85,6 +99,11 @@ export declare function createChatInput(): {
|
|
|
85
99
|
setSuggestion: (suggestion: string | null) => void;
|
|
86
100
|
setFiles: (files: string[]) => void;
|
|
87
101
|
setMode: (mode: Mode | null) => void;
|
|
102
|
+
/** Shows a choice in place of the prompt; resolves with the value chosen, or null if cancelled. */
|
|
103
|
+
pick(title: string, options: {
|
|
104
|
+
label: string;
|
|
105
|
+
value: string;
|
|
106
|
+
}[]): Promise<string | null>;
|
|
88
107
|
/** Takes the last queued line back out of the queue (to edit it in the prompt), or null. */
|
|
89
108
|
unqueueLast(): string | null;
|
|
90
109
|
close: () => void;
|
|
@@ -98,6 +117,10 @@ export declare function createChatInput(): {
|
|
|
98
117
|
*
|
|
99
118
|
* With `fullscreen` it takes the whole terminal instead (see `InkChatOptions.fullscreen`).
|
|
100
119
|
*
|
|
120
|
+
* Given a session opener instead of options, and `runsDir`, each run keeps its conversation
|
|
121
|
+
* in its own folder and can be resumed: `--continue` on the command line starts with the
|
|
122
|
+
* latest, and `/resume` picks one (see `ChatTuiOptions.runsDir`).
|
|
123
|
+
*
|
|
101
124
|
* Without a TTY (piped, background) or with `plain`, it is `runChatTui()` unchanged.
|
|
102
125
|
*/
|
|
103
|
-
export declare function runChatInk(options: Options, tuiOptions?: InkChatOptions): Promise<void>;
|
|
126
|
+
export declare function runChatInk(options: Options | SessionOpener, tuiOptions?: InkChatOptions): Promise<void>;
|