@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.
Files changed (51) hide show
  1. package/README.md +91 -97
  2. package/dist/core/agentSpec.d.ts +31 -1
  3. package/dist/core/hooks/stepGate.js +3 -2
  4. package/dist/core/language.d.ts +39 -0
  5. package/dist/core/language.js +59 -0
  6. package/dist/core/messages/de.d.ts +2 -0
  7. package/dist/core/messages/de.js +103 -0
  8. package/dist/core/messages/en.d.ts +90 -0
  9. package/dist/core/messages/en.js +102 -0
  10. package/dist/core/messages/es.d.ts +2 -0
  11. package/dist/core/messages/es.js +103 -0
  12. package/dist/core/messages/fr.d.ts +2 -0
  13. package/dist/core/messages/fr.js +103 -0
  14. package/dist/core/messages/index.d.ts +23 -0
  15. package/dist/core/messages/index.js +67 -0
  16. package/dist/core/runs.d.ts +51 -0
  17. package/dist/core/runs.js +194 -0
  18. package/dist/core/session.d.ts +8 -0
  19. package/dist/core/session.js +42 -7
  20. package/dist/core/toolLabels.d.ts +2 -1
  21. package/dist/core/toolLabels.js +31 -51
  22. package/dist/core/tools/humanApproval.js +3 -2
  23. package/dist/index.d.ts +5 -0
  24. package/dist/index.js +6 -0
  25. package/dist/tui/chatTui.d.ts +23 -2
  26. package/dist/tui/chatTui.js +71 -7
  27. package/dist/tui/claudeAuth.d.ts +3 -1
  28. package/dist/tui/claudeAuth.js +12 -17
  29. package/dist/tui/consoleRenderer.js +2 -1
  30. package/dist/tui/ink/PromptInput.d.ts +3 -4
  31. package/dist/tui/ink/PromptInput.js +30 -17
  32. package/dist/tui/ink/SessionView.js +16 -15
  33. package/dist/tui/ink/inkInteraction.js +4 -7
  34. package/dist/tui/ink/progressView.d.ts +6 -0
  35. package/dist/tui/ink/progressView.js +2 -0
  36. package/dist/tui/ink/promptEditing.d.ts +11 -5
  37. package/dist/tui/ink/promptEditing.js +28 -3
  38. package/dist/tui/ink/runChatInk.d.ts +25 -2
  39. package/dist/tui/ink/runChatInk.js +156 -48
  40. package/dist/tui/ink/sessionModel.d.ts +10 -0
  41. package/dist/tui/ink/sessionModel.js +27 -2
  42. package/dist/tui/ink/toolGroup.d.ts +2 -1
  43. package/dist/tui/ink/toolGroup.js +5 -18
  44. package/dist/tui/ink/wizard.d.ts +6 -0
  45. package/dist/tui/ink/wizard.js +4 -1
  46. package/dist/tui/language.d.ts +5 -0
  47. package/dist/tui/language.js +11 -0
  48. package/dist/tui/runs.d.ts +27 -0
  49. package/dist/tui/runs.js +30 -0
  50. package/dist/tui/terminalInteraction.js +2 -2
  51. 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 also arrives as `delete` on
65
- * Windows terminals, so both erase backwards.
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} (tab)` : null;
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 = `(reverse-i-search)'${search.query}': `;
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 ? SHORTCUTS.map((line) => _jsx(Text, { children: ui.dim(fitWidth(` ${line}`, width)) }, line)) : null] }));
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 DECISION_OPTIONS = [
13
- { label: "1. Yes", value: "y" },
14
- { label: "2. No", value: "n" },
15
- { label: "3. Stop", value: "q" },
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 MANUAL_OPTIONS = [{ label: "1. Done, continue", value: "continue" }];
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(`… (${all.length - max + 1} more lines)`)];
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: "Do you want to proceed?" }) })) : null, _jsx(Box, { marginTop: decision ? 0 : 1, children: _jsx(Select, { options: decision ? DECISION_OPTIONS : MANUAL_OPTIONS, onChange: (value) => checkpoint.answer(decision ? value : "") }, checkpoint.id) })] }));
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 ? " (shift+tab)" : ""}`] : []),
49
- `${turns} ${turns === 1 ? "turn" : "turns"}`,
50
- ...(usage ? [`${formatTokens(usage.inputTokens)} in / ${formatTokens(usage.outputTokens)} out`] : []),
51
- ...(extra.contextPercent != null ? [`context ${Math.round(extra.contextPercent)}%`] : []),
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 · esc to interrupt)`)}`, width) });
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 ?? "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
+ _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(`copied ${text.length} ${text.length === 1 ? "character" : "characters"}`);
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 : `↓ ${below} more ${below === 1 ? "line" : "lines"} (Ctrl+End)`, copied]
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
- const OUTCOMES = {
3
- y: ui.success("✔ Approved"),
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" ? (OUTCOMES[value] ?? value) : ui.success("✔ Done"));
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("(answered through the response file)"));
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
- const PASTE_TOKEN = /\[Pasted text #(\d+) \+\d+ lines?\]/g;
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
- const extra = text.split("\n").length - 1;
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>;