@falkenslab/agent-kit 0.10.2 → 0.13.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 +105 -97
- package/dist/core/agentSpec.d.ts +31 -1
- package/dist/core/claudeAuth.d.ts +1 -0
- package/dist/core/hooks/stepGate.js +3 -2
- package/dist/core/hooks/transcriptLogger.d.ts +1 -0
- package/dist/core/interaction.d.ts +3 -0
- package/dist/core/interaction.js +2 -0
- package/dist/core/language.d.ts +41 -0
- package/dist/core/language.js +60 -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/runner.d.ts +2 -0
- package/dist/core/runs.d.ts +53 -0
- package/dist/core/runs.js +194 -0
- package/dist/core/session.d.ts +9 -0
- package/dist/core/session.js +43 -7
- package/dist/core/toolLabels.d.ts +8 -1
- package/dist/core/toolLabels.js +36 -54
- package/dist/core/tools/humanApproval.d.ts +1 -0
- package/dist/core/tools/humanApproval.js +3 -2
- package/dist/core/tools/manualLogin.d.ts +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +7 -0
- package/dist/tui/chatTui.d.ts +31 -2
- package/dist/tui/chatTui.js +73 -7
- package/dist/tui/claudeAuth.d.ts +3 -1
- package/dist/tui/claudeAuth.js +12 -17
- package/dist/tui/consoleRenderer.d.ts +2 -0
- package/dist/tui/consoleRenderer.js +2 -1
- package/dist/tui/ink/PromptInput.d.ts +0 -2
- package/dist/tui/ink/PromptInput.js +5 -15
- package/dist/tui/ink/SessionView.js +17 -16
- package/dist/tui/ink/header.d.ts +1 -0
- package/dist/tui/ink/inkInteraction.js +4 -7
- package/dist/tui/ink/inkTheme.d.ts +9 -0
- package/dist/tui/ink/inkTheme.js +25 -0
- package/dist/tui/ink/markdown.js +49 -8
- package/dist/tui/ink/progressView.d.ts +15 -0
- package/dist/tui/ink/progressView.js +6 -1
- package/dist/tui/ink/promptEditing.d.ts +2 -5
- package/dist/tui/ink/promptEditing.js +11 -3
- package/dist/tui/ink/runChatInk.d.ts +26 -2
- package/dist/tui/ink/runChatInk.js +159 -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 +10 -21
- package/dist/tui/ink/wizard.d.ts +20 -0
- package/dist/tui/ink/wizard.js +8 -2
- 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.d.ts +2 -0
- package/dist/tui/terminalInteraction.js +4 -2
- package/dist/tui/theme.d.ts +60 -0
- package/dist/tui/theme.js +65 -0
- package/dist/tui/ui.d.ts +23 -3
- package/dist/tui/ui.js +42 -18
- package/package.json +79 -75
package/dist/tui/chatTui.js
CHANGED
|
@@ -5,9 +5,14 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { createInputQueue } from "../core/session.js";
|
|
7
7
|
import { runQuery } from "../core/runner.js";
|
|
8
|
+
import { listRuns, readConversation } from "../core/runs.js";
|
|
8
9
|
import { isSharedQuestionActive, setSharedReadline } from "./terminalInteraction.js";
|
|
9
10
|
import * as ui from "./ui.js";
|
|
11
|
+
import { t } from "../core/messages/index.js";
|
|
12
|
+
import { applyLanguage } from "./language.js";
|
|
13
|
+
import { applyTheme } from "./theme.js";
|
|
10
14
|
import { createConsoleRenderer } from "./consoleRenderer.js";
|
|
15
|
+
import { firstRun, openSession, runDate, runFolderOf, runLabel } from "./runs.js";
|
|
11
16
|
// Only strips picocolors' own SGR sequences (`\x1b[<codes>m`) — the only kind this file
|
|
12
17
|
// ever writes to the console — not a general-purpose ANSI stripper for arbitrary escape
|
|
13
18
|
// sequences (cursor movement, OSC, ...) this kit never emits.
|
|
@@ -92,16 +97,28 @@ export async function drainTurn(events, onEvent) {
|
|
|
92
97
|
* const { options } = await buildSessionOptions(config, runDir, spec);
|
|
93
98
|
* await runChatTui(options, { welcomeMessage: "Ready. Type /exit to quit." });
|
|
94
99
|
*
|
|
100
|
+
* or, to keep each run's conversation in its folder and resume it (see `runsDir`):
|
|
101
|
+
*
|
|
102
|
+
* await runChatTui((run) => buildSessionOptions(config, run.dir, spec, { run }), { runsDir });
|
|
103
|
+
*
|
|
95
104
|
* Resolves once the chat ends (an exit command, Ctrl+C/Ctrl+D at an idle prompt) — this
|
|
96
105
|
* function owns the whole session lifecycle, closing the underlying query and readline
|
|
97
106
|
* interface itself before returning.
|
|
98
107
|
*/
|
|
99
108
|
export async function runChatTui(options, tuiOptions = {}) {
|
|
109
|
+
applyLanguage(tuiOptions.language);
|
|
110
|
+
applyTheme(tuiOptions.theme);
|
|
100
111
|
const exitCommands = new Set((tuiOptions.exitCommands ?? DEFAULT_EXIT_COMMANDS).map((c) => c.toLowerCase()));
|
|
101
112
|
const promptLabel = tuiOptions.promptLabel ?? DEFAULT_PROMPT_LABEL;
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
|
|
113
|
+
const opener = typeof options === "function" ? options : null;
|
|
114
|
+
const runsDir = opener ? tuiOptions.runsDir : undefined;
|
|
115
|
+
if (opener && !runsDir)
|
|
116
|
+
throw new Error("runChatTui(): a session opener needs `runsDir`.");
|
|
117
|
+
// The session in use: with runs, the run's folder, rebuilt on /resume.
|
|
118
|
+
let current = opener && runsDir ? await openSession(opener, await firstRun(runsDir)) : { options: options, run: null };
|
|
119
|
+
let queue = createInputQueue();
|
|
120
|
+
let run = runQuery(queue.iterable, current.options);
|
|
121
|
+
let events = run.events[Symbol.asyncIterator]();
|
|
105
122
|
const historyLimit = tuiOptions.historyLimit ?? DEFAULT_HISTORY_LIMIT;
|
|
106
123
|
let historyEntries = tuiOptions.historyPath ? capHistory(await loadHistory(tuiOptions.historyPath), historyLimit) : [];
|
|
107
124
|
const rl = readline.createInterface({
|
|
@@ -113,7 +130,14 @@ export async function runChatTui(options, tuiOptions = {}) {
|
|
|
113
130
|
historySize: historyLimit,
|
|
114
131
|
});
|
|
115
132
|
setSharedReadline(rl);
|
|
116
|
-
|
|
133
|
+
// With runs, the session log is the run's own; it moves with /resume.
|
|
134
|
+
let sessionLog = null;
|
|
135
|
+
function openLog() {
|
|
136
|
+
sessionLog?.end();
|
|
137
|
+
const file = current.run ? path.join(current.run.dir, "session.log") : tuiOptions.sessionLogPath;
|
|
138
|
+
sessionLog = file ? createWriteStream(file, { flags: "a" }) : null;
|
|
139
|
+
}
|
|
140
|
+
openLog();
|
|
117
141
|
// Mirrors to the log file only — for text already visible on the terminal some other
|
|
118
142
|
// way (readline's own prompt + the human's local echo), so it isn't printed twice.
|
|
119
143
|
function mirror(text) {
|
|
@@ -182,7 +206,7 @@ export async function runChatTui(options, tuiOptions = {}) {
|
|
|
182
206
|
return;
|
|
183
207
|
turnInterrupted = true;
|
|
184
208
|
void run.interrupt();
|
|
185
|
-
writeLine(ui.warn(
|
|
209
|
+
writeLine(ui.warn(t().interrupted));
|
|
186
210
|
}
|
|
187
211
|
rl.on("SIGINT", () => {
|
|
188
212
|
if (turnInFlight) {
|
|
@@ -203,10 +227,48 @@ export async function runChatTui(options, tuiOptions = {}) {
|
|
|
203
227
|
interruptTurn();
|
|
204
228
|
};
|
|
205
229
|
stdin.on("keypress", onKeypress);
|
|
230
|
+
/** Prints the run's earlier conversation (when it has one) and says it was resumed. */
|
|
231
|
+
async function showConversation(folder) {
|
|
232
|
+
if (!folder.sessionId)
|
|
233
|
+
return;
|
|
234
|
+
for (const message of await readConversation(folder.dir)) {
|
|
235
|
+
// Printed, not logged: the run's session log already has it.
|
|
236
|
+
stdout.write(message.role === "user" ? `${promptLabel}${message.text}\n` : `\n${tuiOptions.agentLabel ? `${tuiOptions.agentLabel} ` : ""}${message.text}\n`);
|
|
237
|
+
}
|
|
238
|
+
const [summary] = (await listRuns(path.dirname(folder.dir))).filter((r) => path.resolve(r.dir) === path.resolve(folder.dir));
|
|
239
|
+
writeLine(ui.dim(`\n${t().resumed(runDate(summary?.updatedAt ?? new Date()))}`));
|
|
240
|
+
}
|
|
241
|
+
/** /resume: lists the runs, asks for one and switches the session to it. */
|
|
242
|
+
async function resume() {
|
|
243
|
+
if (!opener || !runsDir)
|
|
244
|
+
return;
|
|
245
|
+
const runs = await listRuns(runsDir);
|
|
246
|
+
if (runs.length === 0) {
|
|
247
|
+
writeLine(ui.dim(t().noEarlierRuns));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
writeLine(ui.heading(t().resumeTitle));
|
|
251
|
+
runs.forEach((summary, i) => writeLine(` ${i + 1}. ${runLabel(summary, current.run ?? undefined)}`));
|
|
252
|
+
const answer = (await rl.question(t().resumeQuestion)).trim();
|
|
253
|
+
const summary = runs[Number(answer) - 1];
|
|
254
|
+
if (!answer || !summary || (current.run && path.resolve(summary.dir) === path.resolve(current.run.dir)))
|
|
255
|
+
return;
|
|
256
|
+
queue.end();
|
|
257
|
+
run.close();
|
|
258
|
+
current = await openSession(opener, await runFolderOf(summary));
|
|
259
|
+
openLog();
|
|
260
|
+
queue = createInputQueue();
|
|
261
|
+
run = runQuery(queue.iterable, current.options);
|
|
262
|
+
events = run.events[Symbol.asyncIterator]();
|
|
263
|
+
knownCommandTokens = null;
|
|
264
|
+
await showConversation(current.run);
|
|
265
|
+
}
|
|
206
266
|
if (tuiOptions.welcomeMessage)
|
|
207
267
|
writeLine(tuiOptions.welcomeMessage);
|
|
208
268
|
try {
|
|
209
|
-
if (
|
|
269
|
+
if (current.run)
|
|
270
|
+
await showConversation(current.run);
|
|
271
|
+
if (tuiOptions.initialPrompt && !current.run?.sessionId) {
|
|
210
272
|
mirror(`${promptLabel}${tuiOptions.initialPrompt}\n`);
|
|
211
273
|
queue.push(tuiOptions.initialPrompt);
|
|
212
274
|
turnInFlight = true;
|
|
@@ -233,6 +295,10 @@ export async function runChatTui(options, tuiOptions = {}) {
|
|
|
233
295
|
}
|
|
234
296
|
if (exitCommands.has(line.toLowerCase()))
|
|
235
297
|
break;
|
|
298
|
+
if (runsDir && line.toLowerCase() === "/resume") {
|
|
299
|
+
await resume();
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
236
302
|
// A line that *looks* like a slash command but matches nothing registered (a typo,
|
|
237
303
|
// an unnamespaced plugin command, ...) would otherwise reach the model as literal
|
|
238
304
|
// text with no special handling — confirmed empirically (captain-whiskers'
|
|
@@ -242,7 +308,7 @@ export async function runChatTui(options, tuiOptions = {}) {
|
|
|
242
308
|
// silence on unrecognized input might mean.
|
|
243
309
|
const commandToken = slashCommandToken(line);
|
|
244
310
|
if (commandToken && !(await isKnownSlashCommand(commandToken))) {
|
|
245
|
-
writeLine(ui.warn(
|
|
311
|
+
writeLine(ui.warn(t().unknownCommand(commandToken)));
|
|
246
312
|
continue;
|
|
247
313
|
}
|
|
248
314
|
queue.push(line);
|
package/dist/tui/claudeAuth.d.ts
CHANGED
|
@@ -16,4 +16,6 @@ import { type ClaudeAuthConfig } from "../core/claudeAuth.js";
|
|
|
16
16
|
* Only makes sense with a real terminal in front of a human (a `runWizard()` confirm, colored console
|
|
17
17
|
* output), which is why it lives here rather than in the non-interactive core.
|
|
18
18
|
*/
|
|
19
|
-
export declare function ensureClaudeAuth(config?: ClaudeAuthConfig
|
|
19
|
+
export declare function ensureClaudeAuth(config?: ClaudeAuthConfig, options?: {
|
|
20
|
+
language?: string;
|
|
21
|
+
}): Promise<string | undefined>;
|
package/dist/tui/claudeAuth.js
CHANGED
|
@@ -3,6 +3,8 @@ import { resolveClaudeAuth } from "../core/claudeAuth.js";
|
|
|
3
3
|
import { isExitPromptError } from "./promptErrors.js";
|
|
4
4
|
import * as ui from "./ui.js";
|
|
5
5
|
import { runWizard } from "./ink/wizard.js";
|
|
6
|
+
import { applyLanguage } from "./language.js";
|
|
7
|
+
import { t } from "../core/messages/index.js";
|
|
6
8
|
// The real format of "claude setup-token" tokens ("sk-ant-oat01-...", seen in the
|
|
7
9
|
// installed @anthropic-ai/claude-code binary) — used to extract the token from its output.
|
|
8
10
|
const TOKEN_PATTERN = /sk-ant-oat\d{2}-[A-Za-z0-9_-]+/;
|
|
@@ -61,23 +63,17 @@ function runSetupToken() {
|
|
|
61
63
|
* Only makes sense with a real terminal in front of a human (a `runWizard()` confirm, colored console
|
|
62
64
|
* output), which is why it lives here rather than in the non-interactive core.
|
|
63
65
|
*/
|
|
64
|
-
export async function ensureClaudeAuth(config = {}) {
|
|
66
|
+
export async function ensureClaudeAuth(config = {}, options = {}) {
|
|
65
67
|
if (resolveClaudeAuth(config))
|
|
66
68
|
return undefined;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
applyLanguage(options.language);
|
|
70
|
+
const texts = t().auth;
|
|
71
|
+
console.log(ui.warn(`\n${texts.noToken}`));
|
|
72
|
+
console.log(ui.dim(texts.noTokenDetail));
|
|
69
73
|
try {
|
|
70
|
-
const { generate } = await runWizard([
|
|
71
|
-
{
|
|
72
|
-
type: "confirm",
|
|
73
|
-
name: "generate",
|
|
74
|
-
message: 'Generate one now with "claude setup-token" (requires a Claude Pro/Max subscription)?',
|
|
75
|
-
default: true,
|
|
76
|
-
},
|
|
77
|
-
]);
|
|
74
|
+
const { generate } = await runWizard([{ type: "confirm", name: "generate", message: texts.generateQuestion, default: true }]);
|
|
78
75
|
if (!generate) {
|
|
79
|
-
console.log(ui.error(
|
|
80
|
-
"(or run this again and accept generating it) and try again."));
|
|
76
|
+
console.log(ui.error(`\n${texts.cantStart}`));
|
|
81
77
|
process.exit(0);
|
|
82
78
|
}
|
|
83
79
|
}
|
|
@@ -86,11 +82,10 @@ export async function ensureClaudeAuth(config = {}) {
|
|
|
86
82
|
process.exit(0);
|
|
87
83
|
throw error;
|
|
88
84
|
}
|
|
89
|
-
console.log(ui.heading(
|
|
90
|
-
console.log(ui.dim(
|
|
91
|
-
"below.\n"));
|
|
85
|
+
console.log(ui.heading(`\n${texts.generatingHeading}`));
|
|
86
|
+
console.log(ui.dim(`${texts.browserWillOpen}\n`));
|
|
92
87
|
const token = await runSetupToken();
|
|
93
88
|
process.env.CLAUDE_CODE_OAUTH_TOKEN = token;
|
|
94
|
-
console.log(ui.success(
|
|
89
|
+
console.log(ui.success(`\n${texts.generated}\n`));
|
|
95
90
|
return token;
|
|
96
91
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentEvent } from "../core/runner.js";
|
|
2
|
+
/** Options of `createConsoleRenderer()`. */
|
|
2
3
|
export interface ConsoleRendererOptions {
|
|
3
4
|
/** Turns a tool call into its console label; defaults to `createFriendlyToolLabel()`. */
|
|
4
5
|
formatAction?: (toolName: string, toolInput: unknown) => string;
|
|
@@ -9,6 +10,7 @@ export interface ConsoleRendererOptions {
|
|
|
9
10
|
/** Called after every write with the same text, e.g. to mirror it into a log file. */
|
|
10
11
|
onWrite?: (text: string) => void;
|
|
11
12
|
}
|
|
13
|
+
/** Prints `runQuery()`'s events as plain console lines (see `createConsoleRenderer()`). */
|
|
12
14
|
export interface ConsoleRenderer {
|
|
13
15
|
/** Prints one normalized event from `runQuery()`. */
|
|
14
16
|
render(event: AgentEvent): void;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { stdout } from "node:process";
|
|
2
2
|
import { createFriendlyToolLabel } from "../core/toolLabels.js";
|
|
3
3
|
import * as ui from "./ui.js";
|
|
4
|
+
import { t } from "../core/messages/index.js";
|
|
4
5
|
/**
|
|
5
6
|
* The console rendering of `runQuery()`'s event stream, shared by `runChatTui()` and by any
|
|
6
7
|
* one-shot run that prints the same events. Streamed text rarely ends in a newline, so
|
|
@@ -46,7 +47,7 @@ export function createConsoleRenderer(options = {}) {
|
|
|
46
47
|
case "prompt-suggestion":
|
|
47
48
|
return;
|
|
48
49
|
case "mcp-error":
|
|
49
|
-
writeLine(ui.warn(
|
|
50
|
+
writeLine(ui.warn(t().mcpFailed(event.failedServers.join(", "))));
|
|
50
51
|
return;
|
|
51
52
|
case "info":
|
|
52
53
|
writeLine((event.level === "warning" ? ui.warn : ui.dim)(`(${event.text})`));
|
|
@@ -27,8 +27,6 @@ export interface PromptInputProps {
|
|
|
27
27
|
/** Project files for `@` mentions (paths relative to the session's folder). */
|
|
28
28
|
files?: readonly string[];
|
|
29
29
|
}
|
|
30
|
-
/** The shortcuts panel, opened with "?" on an empty prompt. */
|
|
31
|
-
export declare const SHORTCUTS: string[];
|
|
32
30
|
/**
|
|
33
31
|
* The chat's prompt: history (↑/↓), "/command" completion and suggestions (Tab), pasted
|
|
34
32
|
* blocks folded into a token, multi-line input (`\` + Enter or Ctrl+J), word and line
|
|
@@ -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, deleteForward, deleteWordBack, insert, isBlockPaste, isForwardDelete, 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,19 +45,8 @@ 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
|
-
const PASTE_TOKEN_AFTER = /^\[Pasted text #\d+ \+\d+ lines?\]/;
|
|
60
50
|
/**
|
|
61
51
|
* The chat's prompt: history (↑/↓), "/command" completion and suggestions (Tab), pasted
|
|
62
52
|
* blocks folded into a token, multi-line input (`\` + Enter or Ctrl+J), word and line
|
|
@@ -310,7 +300,7 @@ export function PromptInput({ label, inset = 0, suggestion, history, commands, o
|
|
|
310
300
|
const width = liveWidth(stdout.columns) - inset;
|
|
311
301
|
const labelWidth = stringWidth(stripAnsi(label));
|
|
312
302
|
const room = width - labelWidth;
|
|
313
|
-
const ghost = suggestion ? `${suggestion} (
|
|
303
|
+
const ghost = suggestion ? `${suggestion} (${t().suggestionKey})` : null;
|
|
314
304
|
// Suggestions give way on a short terminal: the live area must stay shorter than it.
|
|
315
305
|
const maxSuggestions = Math.min(MAX_SUGGESTIONS, Math.max(0, (stdout.rows || 24) - 10));
|
|
316
306
|
const mention = mentionAt(value, cursor);
|
|
@@ -326,7 +316,7 @@ export function PromptInput({ label, inset = 0, suggestion, history, commands, o
|
|
|
326
316
|
let rows;
|
|
327
317
|
let cursorColumn;
|
|
328
318
|
if (search) {
|
|
329
|
-
const prefix =
|
|
319
|
+
const prefix = t().reverseSearch(search.query);
|
|
330
320
|
const match = search.index >= 0 ? history[search.index] : "";
|
|
331
321
|
cursorColumn = Math.min(stringWidth(prefix) - 3, width - 1);
|
|
332
322
|
rows = [
|
|
@@ -352,5 +342,5 @@ export function PromptInput({ label, inset = 0, suggestion, history, commands, o
|
|
|
352
342
|
cursorController.setTarget({ node: cursorLineRef.current, column: cursorColumn });
|
|
353
343
|
});
|
|
354
344
|
useLayoutEffect(() => () => cursorController?.setTarget(null), [cursorController]);
|
|
355
|
-
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] }));
|
|
356
346
|
}
|
|
@@ -8,15 +8,16 @@ 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";
|
|
12
|
+
import { inkColor } from "../theme.js";
|
|
11
13
|
// 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:
|
|
14
|
+
const decisionOptions = () => [
|
|
15
|
+
{ label: `1. ${t().yes}`, value: "y" },
|
|
16
|
+
{ label: `2. ${t().no}`, value: "n" },
|
|
17
|
+
{ label: `3. ${t().stop}`, value: "q" },
|
|
16
18
|
];
|
|
17
|
-
const
|
|
19
|
+
const manualOptions = () => [{ label: `1. ${t().doneContinue}`, value: "continue" }];
|
|
18
20
|
const DECISION_KEYS = { "1": "y", "2": "n", "3": "q", y: "y", n: "n", q: "q" };
|
|
19
|
-
const ACCENT_HEX = "#d77757";
|
|
20
21
|
/**
|
|
21
22
|
* The preview's lines, capped so the whole live area stays shorter than the terminal: Ink
|
|
22
23
|
* clears the entire screen (scrollback included) to redraw anything taller, and a step-gate
|
|
@@ -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:
|
|
42
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: inkColor("accent"), 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 })] }));
|
package/dist/tui/ink/header.d.ts
CHANGED
|
@@ -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();
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* `@inkjs/ui`'s components in the kit's theme: the focused option of a `Select` (and its
|
|
4
|
+
* `❯`) in the theme's `selection` color and bold, instead of the library's blue, which on
|
|
5
|
+
* most dark terminals is a navy that barely shows.
|
|
6
|
+
*/
|
|
7
|
+
export declare function KitTheme({ children }: {
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { defaultTheme, extendTheme, ThemeProvider } from "@inkjs/ui";
|
|
3
|
+
import { inkColor } from "../theme.js";
|
|
4
|
+
/**
|
|
5
|
+
* `@inkjs/ui`'s components in the kit's theme: the focused option of a `Select` (and its
|
|
6
|
+
* `❯`) in the theme's `selection` color and bold, instead of the library's blue, which on
|
|
7
|
+
* most dark terminals is a navy that barely shows.
|
|
8
|
+
*/
|
|
9
|
+
export function KitTheme({ children }) {
|
|
10
|
+
const selection = inkColor("selection");
|
|
11
|
+
const theme = extendTheme(defaultTheme, {
|
|
12
|
+
components: {
|
|
13
|
+
Select: {
|
|
14
|
+
styles: {
|
|
15
|
+
focusIndicator: () => ({ color: selection }),
|
|
16
|
+
label: ({ isFocused, isSelected }) => ({
|
|
17
|
+
color: isFocused ? selection : isSelected ? "green" : undefined,
|
|
18
|
+
bold: isFocused,
|
|
19
|
+
}),
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
return _jsx(ThemeProvider, { theme: theme, children: children });
|
|
25
|
+
}
|
package/dist/tui/ink/markdown.js
CHANGED
|
@@ -79,16 +79,57 @@ function renderList(list, width) {
|
|
|
79
79
|
});
|
|
80
80
|
return lines;
|
|
81
81
|
}
|
|
82
|
+
const visibleWidth = (text) => stringWidth(stripAnsi(text));
|
|
83
|
+
/**
|
|
84
|
+
* The widest columns shrunk, one column at a time, until the grid (`widths` plus one border
|
|
85
|
+
* and two spaces of padding per column, plus the last border) fits `room`; a column never
|
|
86
|
+
* goes below 3.
|
|
87
|
+
*/
|
|
88
|
+
function fitColumns(widths, room) {
|
|
89
|
+
const fitted = [...widths];
|
|
90
|
+
const total = () => fitted.reduce((sum, w) => sum + w, 0) + fitted.length * 3 + 1;
|
|
91
|
+
while (total() > room) {
|
|
92
|
+
const widest = fitted.indexOf(Math.max(...fitted));
|
|
93
|
+
if (fitted[widest] <= 3)
|
|
94
|
+
break;
|
|
95
|
+
fitted[widest]--;
|
|
96
|
+
}
|
|
97
|
+
return fitted;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* A table as a grid: dimmed box-drawing borders, one space of padding, the header in bold,
|
|
101
|
+
* a rule between rows and each column aligned as its markdown says. A table wider than the
|
|
102
|
+
* room shrinks its widest columns and wraps their cells onto more lines.
|
|
103
|
+
*/
|
|
82
104
|
function renderTable(table, width) {
|
|
83
105
|
const cell = (c) => inline(c.tokens, c.text);
|
|
84
|
-
const header = table.header.map(cell);
|
|
85
|
-
const rows = table.rows.map((row) =>
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
106
|
+
const header = table.header.map((c) => ui.bold(cell(c)));
|
|
107
|
+
const rows = table.rows.map((row) => table.header.map((_, i) => (row[i] ? cell(row[i]) : "")));
|
|
108
|
+
const natural = header.map((h, i) => Math.max(1, visibleWidth(h), ...rows.map((row) => visibleWidth(row[i]))));
|
|
109
|
+
const widths = fitColumns(natural, width);
|
|
110
|
+
const place = (text, i) => {
|
|
111
|
+
const room = widths[i] - visibleWidth(text);
|
|
112
|
+
if (room <= 0)
|
|
113
|
+
return text;
|
|
114
|
+
if (table.align[i] === "right")
|
|
115
|
+
return " ".repeat(room) + text;
|
|
116
|
+
if (table.align[i] === "center")
|
|
117
|
+
return " ".repeat(Math.floor(room / 2)) + text + " ".repeat(Math.ceil(room / 2));
|
|
118
|
+
return text + " ".repeat(room);
|
|
119
|
+
};
|
|
120
|
+
const border = (left, middle, right) => ui.dim(left + widths.map((w) => "─".repeat(w + 2)).join(middle) + right);
|
|
121
|
+
const bar = ui.dim("│");
|
|
122
|
+
// A row's cells wrapped to their columns: as many lines as its tallest cell.
|
|
123
|
+
const rowLines = (cells) => {
|
|
124
|
+
const wrapped = cells.map((text, i) => wrapAnsi(text, widths[i], { hard: true, trim: true }).split("\n"));
|
|
125
|
+
const height = Math.max(...wrapped.map((lines) => lines.length));
|
|
126
|
+
return Array.from({ length: height }, (_, line) => `${bar}${wrapped.map((lines, i) => ` ${place(lines[line] ?? "", i)} `).join(bar)}${bar}`);
|
|
127
|
+
};
|
|
128
|
+
const lines = [border("┌", "┬", "┐"), ...rowLines(header)];
|
|
129
|
+
for (const row of rows)
|
|
130
|
+
lines.push(border("├", "┼", "┤"), ...rowLines(row));
|
|
131
|
+
lines.push(border("└", "┴", "┘"));
|
|
132
|
+
return lines;
|
|
92
133
|
}
|
|
93
134
|
/** Inline tokens (bold, italic, code, links...) as styled text. */
|
|
94
135
|
function inline(tokens, fallback) {
|
|
@@ -2,6 +2,8 @@ import type { Mode } from "../../core/agentSpec.js";
|
|
|
2
2
|
import { type ConsoleRenderer, type ConsoleRendererOptions } from "../consoleRenderer.js";
|
|
3
3
|
import { type RenderApproval } from "./SessionView.js";
|
|
4
4
|
import type { ToolPhrase } from "./toolGroup.js";
|
|
5
|
+
import { type Theme } from "../theme.js";
|
|
6
|
+
/** Options of `createProgressView()`. */
|
|
5
7
|
export interface ProgressViewOptions extends Omit<ConsoleRendererOptions, "output"> {
|
|
6
8
|
/** How one of the agent's own tools counts in a folded group's summary (see `InkChatOptions.toolPhrase`). */
|
|
7
9
|
toolPhrase?: (toolName: string) => ToolPhrase | undefined;
|
|
@@ -11,7 +13,20 @@ export interface ProgressViewOptions extends Omit<ConsoleRendererOptions, "outpu
|
|
|
11
13
|
mode?: Mode;
|
|
12
14
|
/** Use the plain console renderer even on a TTY. */
|
|
13
15
|
plain?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Colors for the kit's roles (see `Theme`), on top of the kit's defaults: only the roles
|
|
18
|
+
* given change, e.g. `{ toolResult: "yellow", selection: "#00ff00" }`. One theme per
|
|
19
|
+
* process: without this option, the one already set stays.
|
|
20
|
+
*/
|
|
21
|
+
theme?: Partial<Theme>;
|
|
22
|
+
/**
|
|
23
|
+
* The language of the kit's texts ("en", "es", "fr", "de"). `--language=<code>` on the
|
|
24
|
+
* command line wins; without either, the one `buildSessionOptions()` chose from
|
|
25
|
+
* `config.language`, or else the system's.
|
|
26
|
+
*/
|
|
27
|
+
language?: string;
|
|
14
28
|
}
|
|
29
|
+
/** A one-shot run's live view: a console renderer drawn with Ink, plus `close()`. */
|
|
15
30
|
export interface ProgressView extends ConsoleRenderer {
|
|
16
31
|
/** Removes the live area and restores the previous interaction port; await it before printing anything else. */
|
|
17
32
|
close(): Promise<void>;
|
|
@@ -7,6 +7,9 @@ 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";
|
|
11
|
+
import { applyTheme } from "../theme.js";
|
|
12
|
+
import { KitTheme } from "./inkTheme.js";
|
|
10
13
|
function ProgressApp({ model, interaction, renderApproval, mode, closed }) {
|
|
11
14
|
const checkpoint = useSyncExternalStore(interaction.subscribe, interaction.getSnapshot);
|
|
12
15
|
// Only a panel reads the keyboard (and so puts stdin in raw mode, where Ctrl+C no longer
|
|
@@ -29,6 +32,8 @@ function ProgressApp({ model, interaction, renderApproval, mode, closed }) {
|
|
|
29
32
|
* Without a TTY or with `plain`, it is `createConsoleRenderer()` with a no-op `close()`.
|
|
30
33
|
*/
|
|
31
34
|
export function createProgressView(options = {}) {
|
|
35
|
+
applyLanguage(options.language);
|
|
36
|
+
applyTheme(options.theme);
|
|
32
37
|
if (options.plain || !stdin.isTTY || !stdout.isTTY) {
|
|
33
38
|
const renderer = createConsoleRenderer(options);
|
|
34
39
|
return Object.assign(renderer, { close: async () => { } });
|
|
@@ -37,7 +42,7 @@ export function createProgressView(options = {}) {
|
|
|
37
42
|
const interaction = createInkInteraction((text) => model.note(text));
|
|
38
43
|
const previousPort = getInteractionPort();
|
|
39
44
|
setInteractionPort(interaction.port);
|
|
40
|
-
const app = (closed) => (_jsx(ProgressApp, { model: model, interaction: interaction, renderApproval: options.renderApproval, mode: options.mode, closed: closed }));
|
|
45
|
+
const app = (closed) => (_jsx(KitTheme, { children: _jsx(ProgressApp, { model: model, interaction: interaction, renderApproval: options.renderApproval, mode: options.mode, closed: closed }) }));
|
|
41
46
|
const instance = render(app(false), { exitOnCtrlC: false });
|
|
42
47
|
model.startTurn();
|
|
43
48
|
let closing = null;
|
|
@@ -1,8 +1,3 @@
|
|
|
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;
|
|
@@ -51,6 +46,8 @@ export interface PasteRegistry {
|
|
|
51
46
|
/** `value` with every token replaced by its text. */
|
|
52
47
|
expand(value: string): string;
|
|
53
48
|
}
|
|
49
|
+
export declare const PASTE_TOKEN_BEFORE: RegExp;
|
|
50
|
+
export declare const PASTE_TOKEN_AFTER: RegExp;
|
|
54
51
|
/** Normalizes a pasted chunk's line breaks (terminals send "\r" or "\r\n"). */
|
|
55
52
|
export declare function normalizePaste(text: string): string;
|
|
56
53
|
/** A paste worth folding into a token: more than one line. */
|