flint-agent 1.14.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/.env.example +108 -0
- package/CHANGELOG.md +55 -0
- package/FEATURES.md +298 -0
- package/LICENSE +21 -0
- package/README.md +435 -0
- package/bin/flint.js +47 -0
- package/config/classifier-prompt.md +218 -0
- package/config/models-curated.json +4 -0
- package/config/providers.json +74 -0
- package/package.json +92 -0
- package/patches/ink+6.8.0.patch +78 -0
- package/profiles/desktop.md +65 -0
- package/profiles/generic.md +20 -0
- package/profiles/marketer.md +20 -0
- package/profiles/profiles.json +34 -0
- package/profiles/ux-reviewer.md +25 -0
- package/src/agent/agent.js +1743 -0
- package/src/agent/auto.js +346 -0
- package/src/agent/backoff.js +143 -0
- package/src/agent/compression.js +310 -0
- package/src/agent/content-resolver.js +180 -0
- package/src/agent/flow-controller.js +309 -0
- package/src/agent/intent-manifest.js +231 -0
- package/src/agent/intent-timeout.js +46 -0
- package/src/agent/intent.js +633 -0
- package/src/agent/knowledge.js +114 -0
- package/src/agent/learning.js +180 -0
- package/src/agent/modes.js +187 -0
- package/src/agent/outcome-ask.js +91 -0
- package/src/agent/project-context.js +76 -0
- package/src/agent/prompt-budget.js +117 -0
- package/src/agent/reflection-extractor.js +140 -0
- package/src/agent/steering.js +86 -0
- package/src/agent/supervisor.js +430 -0
- package/src/agent/swap.js +443 -0
- package/src/agent/system-prompt.js +446 -0
- package/src/agent/time-stamp.js +48 -0
- package/src/agent/tool-guard.js +201 -0
- package/src/agent/toolcall-text.js +162 -0
- package/src/agent/usage.js +297 -0
- package/src/agent/vision.js +94 -0
- package/src/agent/watchdog.js +139 -0
- package/src/agent/workspace-changes.js +177 -0
- package/src/api/address.js +14 -0
- package/src/api/client.js +280 -0
- package/src/api/server.js +535 -0
- package/src/api/stream-pipe.js +113 -0
- package/src/app-state.js +39 -0
- package/src/bootstrap.js +501 -0
- package/src/bus/drain-loop.js +497 -0
- package/src/bus/index.js +270 -0
- package/src/bus/plugins.js +65 -0
- package/src/child-idle.js +14 -0
- package/src/cli.js +118 -0
- package/src/commands/commands.js +1297 -0
- package/src/commands/registry.js +132 -0
- package/src/components/App.js +491 -0
- package/src/components/CarefulMenu.js +145 -0
- package/src/components/HistoryWriter.js +86 -0
- package/src/components/LineInput.js +69 -0
- package/src/components/LiveZone.js +294 -0
- package/src/components/OverlayMenu.js +179 -0
- package/src/components/SystemPanel.js +156 -0
- package/src/components/Table.js +54 -0
- package/src/config.js +249 -0
- package/src/free-models.js +230 -0
- package/src/index.js +1111 -0
- package/src/input-handler.js +13 -0
- package/src/input-text.js +123 -0
- package/src/launcher.js +129 -0
- package/src/logging/api-log.js +95 -0
- package/src/logging/chat-log-follower.js +113 -0
- package/src/logging/chat-log.js +15 -0
- package/src/logging/log-collector.js +182 -0
- package/src/logging/logger.js +112 -0
- package/src/logging/tool-log.js +20 -0
- package/src/mcp-client.js +314 -0
- package/src/memory/conversation-digest.js +113 -0
- package/src/memory/extract-facts.js +98 -0
- package/src/memory/facts.js +181 -0
- package/src/memory/inbox.js +63 -0
- package/src/memory/markdown.js +38 -0
- package/src/memory/patterns.js +185 -0
- package/src/memory/project.js +66 -0
- package/src/memory/reflections.js +74 -0
- package/src/memory/retrieval.js +84 -0
- package/src/memory/rules.js +105 -0
- package/src/memory/session-facts.js +125 -0
- package/src/memory/skills.js +191 -0
- package/src/memory/sqlite-store.js +653 -0
- package/src/memory/store.js +208 -0
- package/src/memory/tools.js +196 -0
- package/src/memory/user-model.js +86 -0
- package/src/message-handler.js +775 -0
- package/src/model-check.js +218 -0
- package/src/plugins/loader.js +120 -0
- package/src/plugins/manager.js +88 -0
- package/src/production-env.js +22 -0
- package/src/profiles.js +42 -0
- package/src/providers/adapters/anthropic.js +270 -0
- package/src/providers/adapters/openai.js +120 -0
- package/src/providers/keys-dpapi.js +41 -0
- package/src/providers/keys-fallback.js +31 -0
- package/src/providers/keys.js +132 -0
- package/src/providers/models.js +154 -0
- package/src/providers/registry.js +56 -0
- package/src/providers/state.js +56 -0
- package/src/registry.js +96 -0
- package/src/restart.js +29 -0
- package/src/sandbox/backend.js +130 -0
- package/src/security/api-auth.js +132 -0
- package/src/security/audit.js +98 -0
- package/src/security/child-policy.js +41 -0
- package/src/security/command-guard.js +173 -0
- package/src/security/content-fence.js +250 -0
- package/src/security/content-validator.js +132 -0
- package/src/security/index.js +143 -0
- package/src/security/network-guard.js +126 -0
- package/src/security/pairing.js +180 -0
- package/src/security/path-guard.js +140 -0
- package/src/security/persona-guard.js +67 -0
- package/src/security/policies.js +452 -0
- package/src/security/safety-constants.js +34 -0
- package/src/security/watchdog.js +107 -0
- package/src/sessions.js +130 -0
- package/src/spend.js +97 -0
- package/src/startup-watchdog.js +59 -0
- package/src/stdio/args.js +71 -0
- package/src/stdio/guard.js +59 -0
- package/src/stdio/protocol.js +167 -0
- package/src/stdio/run.js +106 -0
- package/src/stdio/session.js +180 -0
- package/src/store/agent-slice.js +306 -0
- package/src/store/dataset-slice.js +73 -0
- package/src/store/index.js +22 -0
- package/src/store/process-slice.js +135 -0
- package/src/store/session-slice.js +191 -0
- package/src/store/ui-slice.js +119 -0
- package/src/tasks/db.js +184 -0
- package/src/tasks/queries.js +589 -0
- package/src/tools/agent-tools.js +473 -0
- package/src/tools/checkpoint.js +152 -0
- package/src/tools/command-approvals.js +180 -0
- package/src/tools/dataset.js +50 -0
- package/src/tools/filesystem.js +682 -0
- package/src/tools/inbox-tools.js +48 -0
- package/src/tools/mesh.js +135 -0
- package/src/tools/own-env.js +136 -0
- package/src/tools/permissions.js +681 -0
- package/src/tools/plugin-tools.js +123 -0
- package/src/tools/process-tools.js +595 -0
- package/src/tools/registry.js +307 -0
- package/src/tools/swap-tools.js +72 -0
- package/src/tools/system.js +662 -0
- package/src/tools/tasks.js +532 -0
- package/src/tools/tool-search.js +171 -0
- package/src/ui/header.js +140 -0
- package/src/ui/input-cursor.js +23 -0
- package/src/ui/last-line.js +25 -0
- package/src/ui/line-edit.js +135 -0
- package/src/ui/output.js +399 -0
- package/src/ui/paste-tokens.js +131 -0
- package/src/ui/prompt-attention.js +134 -0
- package/src/ui/render-options.js +13 -0
- package/src/ui/replay.js +94 -0
- package/src/ui/splash.js +49 -0
- package/src/ui/status-level.js +36 -0
- package/src/ui/tool-ledger.js +203 -0
- package/src/ui/window-title.js +150 -0
- package/src/update.js +205 -0
- package/system.md +63 -0
package/src/ui/header.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { store } from "../store/index.js";
|
|
4
|
+
import { config } from "../config.js";
|
|
5
|
+
import { app } from "../app-state.js";
|
|
6
|
+
import { printSplash } from "./splash.js";
|
|
7
|
+
import { apiUrl } from "../api/address.js";
|
|
8
|
+
|
|
9
|
+
const _version = (() => { try { return JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf-8")).version; } catch { return "?"; } })();
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
// -- Formatting constants --
|
|
13
|
+
export const INDENT = " ";
|
|
14
|
+
export const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
15
|
+
|
|
16
|
+
export function formatToolArgs(args) {
|
|
17
|
+
if (!args || typeof args !== "object") return String(args || "");
|
|
18
|
+
return Object.entries(args)
|
|
19
|
+
.map(([k, v]) => {
|
|
20
|
+
const val = typeof v === "string" ? v : JSON.stringify(v);
|
|
21
|
+
return val.length > 60 ? `${k}="${val.slice(0, 60)}..."` : `${k}=${val}`;
|
|
22
|
+
})
|
|
23
|
+
.join(" ");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Arguments for an approval prompt: nothing cut. formatToolArgs above cuts
|
|
28
|
+
* every value at 60 characters, which is fine for a log line and wrong for a
|
|
29
|
+
* question: the operator approved a command they could only partly see
|
|
30
|
+
* (owner, 2026-10-01). A command comes first and bare; the rest as key=value.
|
|
31
|
+
*/
|
|
32
|
+
export function formatToolArgsFull(args) {
|
|
33
|
+
if (!args || typeof args !== "object") return String(args || "");
|
|
34
|
+
const parts = [];
|
|
35
|
+
if (typeof args.command === "string") parts.push(args.command);
|
|
36
|
+
for (const [k, v] of Object.entries(args)) {
|
|
37
|
+
if (k === "command" && typeof v === "string") continue;
|
|
38
|
+
parts.push(`${k}=${typeof v === "string" ? v : JSON.stringify(v)}`);
|
|
39
|
+
}
|
|
40
|
+
return parts.join(" ");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function addIndented(indent, text) {
|
|
44
|
+
store.getState().addLine(indent + text);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function userMsgLine(text, sender, { trailingBlank = true } = {}) {
|
|
48
|
+
const cols = process.stdout.columns || 80;
|
|
49
|
+
const bg = chalk.bgHex("#333333").whiteBright;
|
|
50
|
+
const maxWidth = cols - 2;
|
|
51
|
+
const prefix = sender ? `${sender} > ` : "> ";
|
|
52
|
+
const wrapPad = " ".repeat(prefix.length);
|
|
53
|
+
|
|
54
|
+
function emitLine(content, indent) {
|
|
55
|
+
const plain = content.replace(ANSI_RE, "");
|
|
56
|
+
if (indent.length + plain.length <= maxWidth) {
|
|
57
|
+
const pad = Math.max(0, maxWidth - indent.length - plain.length);
|
|
58
|
+
store.getState().addLine(bg(` ${indent}${content}${" ".repeat(pad)} `));
|
|
59
|
+
} else {
|
|
60
|
+
// Word-wrap
|
|
61
|
+
const wrapIndent = " ".repeat(indent.length);
|
|
62
|
+
const words = content.split(" ");
|
|
63
|
+
let line = "";
|
|
64
|
+
let lineLen = 0;
|
|
65
|
+
let first = true;
|
|
66
|
+
for (const word of words) {
|
|
67
|
+
const wordLen = word.replace(ANSI_RE, "").length;
|
|
68
|
+
const limit = maxWidth - indent.length;
|
|
69
|
+
if (lineLen > 0 && lineLen + 1 + wordLen > limit) {
|
|
70
|
+
const pad = Math.max(0, maxWidth - indent.length - lineLen);
|
|
71
|
+
store.getState().addLine(bg(` ${first ? indent : wrapIndent}${line}${" ".repeat(pad)} `));
|
|
72
|
+
first = false;
|
|
73
|
+
line = word;
|
|
74
|
+
lineLen = wordLen;
|
|
75
|
+
} else {
|
|
76
|
+
line = lineLen > 0 ? line + " " + word : word;
|
|
77
|
+
lineLen = lineLen > 0 ? lineLen + 1 + wordLen : wordLen;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (line) {
|
|
81
|
+
const pad = Math.max(0, maxWidth - indent.length - lineLen);
|
|
82
|
+
store.getState().addLine(bg(` ${first ? indent : wrapIndent}${line}${" ".repeat(pad)} `));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
store.getState().addLine("");
|
|
88
|
+
const lines = text.split("\n");
|
|
89
|
+
for (let i = 0; i < lines.length; i++) {
|
|
90
|
+
emitLine(lines[i], i === 0 ? prefix : wrapPad);
|
|
91
|
+
}
|
|
92
|
+
// Without it, a note can sit right under the message ("✓ read").
|
|
93
|
+
if (trailingBlank) store.getState().addLine("");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function printHeader() {
|
|
97
|
+
const s = store.getState();
|
|
98
|
+
if (s.headerPrinted) return;
|
|
99
|
+
s.clearScreen();
|
|
100
|
+
// Mark header as printed AFTER clearScreen (which resets headerPrinted to false)
|
|
101
|
+
store.setState({ headerPrinted: true });
|
|
102
|
+
// Splash: the two-row FLiNT mark with vX.Y.Z (src/ui/splash.js)
|
|
103
|
+
s.addLine("");
|
|
104
|
+
s.addLine(printSplash(_version, true));
|
|
105
|
+
s.addLine("");
|
|
106
|
+
const modeLabel = app.profileConfig.contextMode === "window"
|
|
107
|
+
? `window(${app.profileConfig.windowSize})`
|
|
108
|
+
: app.profileConfig.contextMode;
|
|
109
|
+
s.addLine(chalk.gray(` model: ${config.model} | provider: ${config.provider} | profile: ${app.activeProfile} | context: ${modeLabel}`));
|
|
110
|
+
if (s.pricing) {
|
|
111
|
+
let priceLine = ` price: $${(s.pricing.prompt * 1e6).toFixed(2)} / $${(s.pricing.completion * 1e6).toFixed(2)} per 1M tokens (in/out)`;
|
|
112
|
+
if (s.contextLimit) {
|
|
113
|
+
priceLine += ` | context: ${(s.contextLimit / 1000).toFixed(0)}k tokens`;
|
|
114
|
+
}
|
|
115
|
+
s.addLine(chalk.gray(priceLine));
|
|
116
|
+
}
|
|
117
|
+
s.addLine(chalk.gray(` session: ${s.sessionId}${s.messages.length > 1 ? ` (${s.messages.length - 1} msgs)` : " (new)"}`));
|
|
118
|
+
s.addLine(chalk.gray(` api: ${apiUrl(app.actualPort)}`));
|
|
119
|
+
if (app.securityApi && app.securityApi.token) {
|
|
120
|
+
s.addLine(chalk.gray(` token: ${app.securityApi.token.slice(0, 8)}... (${app.securityApi.policy?.name || "?"} policy)`));
|
|
121
|
+
} else if (app.securityApi && !app.securityApi.disabled) {
|
|
122
|
+
s.addLine(chalk.gray(` access: programs pair with a PIN shown here; /paired lists them (${app.securityApi.policy?.name || "?"} policy)`));
|
|
123
|
+
}
|
|
124
|
+
if (app.mcpStatusList.length) {
|
|
125
|
+
for (const mc of app.mcpStatusList) {
|
|
126
|
+
if (mc.connecting) {
|
|
127
|
+
s.addLine(chalk.yellow(` mcp: ${mc.name} -- connecting...`));
|
|
128
|
+
} else if (mc.ok) {
|
|
129
|
+
s.addLine(chalk.gray(` mcp: ${mc.name} -- ${mc.tools} tools`));
|
|
130
|
+
} else {
|
|
131
|
+
s.addLine(chalk.yellow(` mcp: ${mc.name} -- offline`));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Ctrl+V is the terminal's own paste and sends nothing for a picture, so
|
|
136
|
+
// the key that does is named where it will be seen (owner, 2026-10-02:
|
|
137
|
+
// "I can't paste a picture").
|
|
138
|
+
s.addLine(chalk.gray(' type /help for commands, "exit" to quit · Alt+V pastes a picture'));
|
|
139
|
+
s.addLine("");
|
|
140
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// The real terminal cursor: a thin bar, placed by LineInput (cursorCell in
|
|
2
|
+
// ui/line-edit.js) at the edit position.
|
|
3
|
+
//
|
|
4
|
+
// Owner, 2026-10-01: the drawn inverse block showed up in random places, so it
|
|
5
|
+
// was turned off; with it gone there was no caret at all. A real terminal
|
|
6
|
+
// cursor is what every other console shows, and it is also where IME and
|
|
7
|
+
// dictation tools put their text.
|
|
8
|
+
|
|
9
|
+
// DECSCUSR: 5 = blinking bar, 0 = the terminal's own default.
|
|
10
|
+
export const CURSOR_BAR = "\x1b[5 q";
|
|
11
|
+
export const CURSOR_DEFAULT = "\x1b[0 q";
|
|
12
|
+
|
|
13
|
+
/** Row of an Ink DOM node within the live output, the way Ink's layout.js sums it. */
|
|
14
|
+
export function absoluteTop(node) {
|
|
15
|
+
let y = 0;
|
|
16
|
+
let current = node;
|
|
17
|
+
while (current?.parentNode) {
|
|
18
|
+
if (!current.yogaNode) return null;
|
|
19
|
+
y += current.yogaNode.getComputedTop();
|
|
20
|
+
current = current.parentNode;
|
|
21
|
+
}
|
|
22
|
+
return current ? y : null;
|
|
23
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// The last thing a command printed, as one readable line.
|
|
2
|
+
//
|
|
3
|
+
// Owner, 2026-10-01: a long foreground run_command (a scan, a build) showed
|
|
4
|
+
// only its command and a clock until it finished. Its latest output line is
|
|
5
|
+
// shown on the activity row instead (see run_command in tools/process-tools.js).
|
|
6
|
+
|
|
7
|
+
// eslint-disable-next-line no-control-regex
|
|
8
|
+
const ANSI = /\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b\][^\x07]*(\x07|\x1b\\)/g;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Last non-empty line of a chunk of output, or null.
|
|
12
|
+
* Progress bars redraw with \r, so only the text after the last \r counts.
|
|
13
|
+
*/
|
|
14
|
+
export function lastOutputLine(chunk) {
|
|
15
|
+
const text = String(chunk ?? "").replace(ANSI, "");
|
|
16
|
+
const lines = text.split("\n");
|
|
17
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
18
|
+
const parts = lines[i].split("\r");
|
|
19
|
+
const shown = parts[parts.length - 1] || parts.filter((p) => p.trim()).pop() || "";
|
|
20
|
+
// eslint-disable-next-line no-control-regex
|
|
21
|
+
const clean = shown.replace(/[\x00-\x1f\x7f]/g, "").replace(/\s+/g, " ").trim();
|
|
22
|
+
if (clean) return clean;
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Editing one input line with a cursor (owner, 2026-10-01: the cursor was
|
|
2
|
+
// stuck at the end; arrows, Ctrl+arrows, Home/End did nothing).
|
|
3
|
+
//
|
|
4
|
+
// Pure functions over { value, cursor }. The cursor is a UTF-16 offset that
|
|
5
|
+
// always sits on a code point boundary and never inside a paste/image token:
|
|
6
|
+
// a token moves and deletes as one character.
|
|
7
|
+
|
|
8
|
+
import stringWidth from "string-width";
|
|
9
|
+
|
|
10
|
+
const TOKEN_RE = /\[(?:Pasted text #\d+[^\]]*|Image #\d+)\]/g;
|
|
11
|
+
|
|
12
|
+
/** [start, end) of every token in the value. */
|
|
13
|
+
export function tokenRanges(value) {
|
|
14
|
+
const out = [];
|
|
15
|
+
for (const m of String(value || "").matchAll(TOKEN_RE)) out.push([m.index, m.index + m[0].length]);
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const isLow = (c) => c >= 0xdc00 && c <= 0xdfff;
|
|
20
|
+
|
|
21
|
+
function prevPoint(value, i) {
|
|
22
|
+
if (i <= 0) return 0;
|
|
23
|
+
let j = i - 1;
|
|
24
|
+
if (j > 0 && isLow(value.charCodeAt(j))) j--;
|
|
25
|
+
return j;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function nextPoint(value, i) {
|
|
29
|
+
if (i >= value.length) return value.length;
|
|
30
|
+
let j = i + 1;
|
|
31
|
+
if (j < value.length && isLow(value.charCodeAt(j))) j++;
|
|
32
|
+
return j;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** One step left: over a whole token if one ends here. */
|
|
36
|
+
export function moveLeft({ value, cursor }) {
|
|
37
|
+
const t = tokenRanges(value).find(([s, e]) => e === cursor);
|
|
38
|
+
return { value, cursor: t ? t[0] : prevPoint(value, cursor) };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** One step right: over a whole token if one starts here. */
|
|
42
|
+
export function moveRight({ value, cursor }) {
|
|
43
|
+
const t = tokenRanges(value).find(([s]) => s === cursor);
|
|
44
|
+
return { value, cursor: t ? t[1] : nextPoint(value, cursor) };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const isWordChar = (ch) => /[\p{L}\p{N}_]/u.test(ch);
|
|
48
|
+
|
|
49
|
+
/** Ctrl+Left: to the start of the previous word. */
|
|
50
|
+
export function wordLeft({ value, cursor }) {
|
|
51
|
+
let i = cursor;
|
|
52
|
+
while (i > 0 && !isWordChar(value[i - 1])) i = moveLeft({ value, cursor: i }).cursor;
|
|
53
|
+
while (i > 0 && isWordChar(value[i - 1])) i = prevPoint(value, i);
|
|
54
|
+
return { value, cursor: i };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Ctrl+Right: to the end of the next word. */
|
|
58
|
+
export function wordRight({ value, cursor }) {
|
|
59
|
+
let i = cursor;
|
|
60
|
+
while (i < value.length && !isWordChar(value[i])) i = moveRight({ value, cursor: i }).cursor;
|
|
61
|
+
while (i < value.length && isWordChar(value[i])) i = nextPoint(value, i);
|
|
62
|
+
return { value, cursor: i };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Text typed or pasted at the cursor. */
|
|
66
|
+
export function insertAt({ value, cursor }, text) {
|
|
67
|
+
return { value: value.slice(0, cursor) + text + value.slice(cursor), cursor: cursor + text.length };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Backspace: the character, or the whole token, before the cursor. */
|
|
71
|
+
export function backspace({ value, cursor }) {
|
|
72
|
+
if (cursor <= 0) return { value, cursor };
|
|
73
|
+
const t = tokenRanges(value).find(([, e]) => e === cursor);
|
|
74
|
+
const from = t ? t[0] : prevPoint(value, cursor);
|
|
75
|
+
return { value: value.slice(0, from) + value.slice(cursor), cursor: from };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Delete: the character, or the whole token, after the cursor. */
|
|
79
|
+
export function deleteForward({ value, cursor }) {
|
|
80
|
+
if (cursor >= value.length) return { value, cursor };
|
|
81
|
+
const t = tokenRanges(value).find(([s]) => s === cursor);
|
|
82
|
+
const to = t ? t[1] : nextPoint(value, cursor);
|
|
83
|
+
return { value: value.slice(0, cursor) + value.slice(to), cursor };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Ctrl+W: the word before the cursor and the space after it. */
|
|
87
|
+
export function deleteWordBack(state) {
|
|
88
|
+
const { cursor: to } = state;
|
|
89
|
+
const { cursor: from } = wordLeft(state);
|
|
90
|
+
return { value: state.value.slice(0, from) + state.value.slice(to), cursor: from };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Snap a cursor out of a token and onto a code point boundary. */
|
|
94
|
+
export function clampCursor(value, cursor) {
|
|
95
|
+
let c = Math.max(0, Math.min(cursor, value.length));
|
|
96
|
+
const t = tokenRanges(value).find(([s, e]) => c > s && c < e);
|
|
97
|
+
if (t) c = t[1];
|
|
98
|
+
if (c > 0 && c < value.length && isLow(value.charCodeAt(c))) c--;
|
|
99
|
+
return c;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The value broken into rows of `width` columns, character by character (no
|
|
104
|
+
* word wrap), with explicit line breaks kept. The input draws exactly these
|
|
105
|
+
* rows, so the cursor position computed from them is exact.
|
|
106
|
+
*/
|
|
107
|
+
export function wrapRows(value, width) {
|
|
108
|
+
const w = Math.max(1, width);
|
|
109
|
+
const rows = [];
|
|
110
|
+
for (const line of String(value).split("\n")) {
|
|
111
|
+
let row = "";
|
|
112
|
+
let cols = 0;
|
|
113
|
+
for (const ch of line) {
|
|
114
|
+
const cw = stringWidth(ch);
|
|
115
|
+
if (cols + cw > w) { rows.push(row); row = ""; cols = 0; }
|
|
116
|
+
row += ch;
|
|
117
|
+
cols += cw;
|
|
118
|
+
}
|
|
119
|
+
rows.push(row);
|
|
120
|
+
}
|
|
121
|
+
return rows;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Where the cursor is drawn, relative to the input's first row: the column
|
|
126
|
+
* after `promptWidth`, and the row.
|
|
127
|
+
*/
|
|
128
|
+
export function cursorCell(value, cursor, width) {
|
|
129
|
+
const before = wrapRows(String(value).slice(0, cursor), width);
|
|
130
|
+
let row = before.length - 1;
|
|
131
|
+
let col = stringWidth(before[row]);
|
|
132
|
+
// At the very edge the next character starts a new row.
|
|
133
|
+
if (col >= width) { row++; col = 0; }
|
|
134
|
+
return { row, col };
|
|
135
|
+
}
|