min-agent 0.3.0 → 0.4.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 +111 -28
- package/dist/agent.js +1119 -256
- package/dist/cli/commands/chat.js +10 -0
- package/dist/cli/commands/exec.js +32 -0
- package/dist/cli/commands/history.js +58 -0
- package/dist/cli/commands/index.js +224 -0
- package/dist/cli/commands/init.js +18 -0
- package/dist/cli/commands/mcp.js +173 -0
- package/dist/cli/commands/memory.js +69 -0
- package/dist/cli/commands/models.js +21 -0
- package/dist/cli/commands/permission.js +12 -0
- package/dist/cli/commands/rules.js +33 -0
- package/dist/cli/commands/sandbox.js +13 -0
- package/dist/cli/commands/serve.js +9 -0
- package/dist/cli/commands/setup.js +4 -0
- package/dist/cli/commands/shared.js +16 -0
- package/dist/cli/commands/skills.js +119 -0
- package/dist/cli/commands/update.js +7 -0
- package/dist/cli/commands/write-config.js +30 -0
- package/dist/cli/errors.js +36 -0
- package/dist/cli/exec-prompt.js +26 -0
- package/dist/cli/option-helpers.js +53 -0
- package/dist/cli/program.js +180 -0
- package/dist/cli.js +5 -888
- package/dist/code-mode.js +32 -14
- package/dist/compaction.js +347 -160
- package/dist/config.js +119 -10
- package/dist/confirm.js +56 -9
- package/dist/context-window.js +107 -39
- package/dist/doom-loop.js +264 -29
- package/dist/fetch-timeout.js +152 -0
- package/dist/http-approvals.js +60 -0
- package/dist/instructions.js +21 -0
- package/dist/logger.js +33 -4
- package/dist/markdown.js +37 -11
- package/dist/mcp.js +328 -30
- package/dist/memory.js +97 -56
- package/dist/output.js +7 -5
- package/dist/permission-cli.js +43 -0
- package/dist/plugins.js +46 -8
- package/dist/pricing.js +4 -4
- package/dist/provider.js +23 -6
- package/dist/question-format.js +60 -0
- package/dist/sandbox-cli.js +82 -0
- package/dist/sandbox.js +403 -0
- package/dist/save-throttle.js +45 -0
- package/dist/serve/common.js +404 -0
- package/dist/serve/routes-chat.js +347 -0
- package/dist/serve/routes-mcp.js +212 -0
- package/dist/serve/routes-memory.js +66 -0
- package/dist/serve/routes-meta.js +205 -0
- package/dist/serve/routes-sessions.js +61 -0
- package/dist/serve/routes-skills.js +70 -0
- package/dist/serve.js +33 -883
- package/dist/sessions.js +53 -9
- package/dist/skills.js +82 -18
- package/dist/title-gen.js +8 -2
- package/dist/token-display.js +36 -0
- package/dist/tool-display.js +5 -0
- package/dist/tool-output.js +1 -3
- package/dist/tools/apply_patch.js +85 -11
- package/dist/tools/atomic-file.js +35 -0
- package/dist/tools/backend.js +2 -2
- package/dist/tools/bash.js +57 -19
- package/dist/tools/code_search.js +7 -1
- package/dist/tools/edit.js +11 -10
- package/dist/tools/explore.js +74 -14
- package/dist/tools/glob.js +4 -0
- package/dist/tools/grep.js +17 -10
- package/dist/tools/index.js +6 -21
- package/dist/tools/question.js +28 -9
- package/dist/tools/read.js +6 -4
- package/dist/tools/search-searxng.js +223 -0
- package/dist/tools/search-serper.js +189 -0
- package/dist/tools/task.js +84 -30
- package/dist/tools/todo.js +120 -19
- package/dist/tools/web_fetch.js +11 -3
- package/dist/tools/web_search.js +66 -556
- package/dist/tools/write.js +23 -6
- package/dist/tui/App.js +63 -14
- package/dist/tui/ConfirmBar.js +45 -13
- package/dist/tui/InputBar.js +150 -35
- package/dist/tui/MessageList.js +266 -125
- package/dist/tui/ModelPicker.js +8 -3
- package/dist/tui/QuestionBar.js +51 -19
- package/dist/tui/SessionPicker.js +79 -0
- package/dist/tui/StatusBar.js +8 -14
- package/dist/tui/agent-runner.js +142 -22
- package/dist/tui/caret-pos.js +48 -5
- package/dist/tui/caret.js +1 -1
- package/dist/tui/click-count.js +13 -0
- package/dist/tui/drag-state.js +8 -3
- package/dist/tui/hydrate.js +129 -0
- package/dist/tui/index.js +42 -13
- package/dist/tui/input-history.js +92 -11
- package/dist/tui/layout.js +75 -4
- package/dist/tui/prompt-queue.js +24 -0
- package/dist/tui/selection.js +113 -21
- package/dist/tui/session-switch.js +28 -0
- package/dist/tui/slash-commands.js +22 -6
- package/dist/tui/slash-handler.js +233 -58
- package/dist/tui/text-width.js +38 -16
- package/dist/tui/token-info.js +7 -0
- package/dist/tui/tool-children.js +19 -0
- package/dist/tui/undo-stack.js +1 -1
- package/dist/tui/use-sgr-mouse.js +3 -1
- package/dist/tui-chat.js +276 -40
- package/dist/updater.js +88 -29
- package/dist/xml-search.js +194 -0
- package/docs/API.md +257 -25
- package/docs/superpowers/plans/2026-08-20-tui-completeness.md +873 -0
- package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +631 -0
- package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +47 -0
- package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +37 -0
- package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +68 -0
- package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +273 -0
- package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +165 -0
- package/package.json +6 -1
- package/skills/self-config/SKILL.md +90 -0
- package/skills/self-config/reference.md +149 -0
package/dist/tui/ModelPicker.js
CHANGED
|
@@ -41,7 +41,9 @@ export function ModelPicker({ currentModel, onSelect, onCancel }) {
|
|
|
41
41
|
else {
|
|
42
42
|
setModels([]);
|
|
43
43
|
}
|
|
44
|
-
return () => {
|
|
44
|
+
return () => {
|
|
45
|
+
cancelled = true;
|
|
46
|
+
};
|
|
45
47
|
}, []);
|
|
46
48
|
const terminalRows = stdout.rows || 24;
|
|
47
49
|
const maxListRows = Math.max(4, terminalRows - 11); // chrome + status bar + safety
|
|
@@ -52,7 +54,9 @@ export function ModelPicker({ currentModel, onSelect, onCancel }) {
|
|
|
52
54
|
const shown = filtered.slice(windowStart, windowStart + maxListRows);
|
|
53
55
|
const extraAbove = windowStart;
|
|
54
56
|
const extraBelow = filtered.length - (windowStart + shown.length);
|
|
55
|
-
useEffect(() => {
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
setIndex(0);
|
|
59
|
+
}, []);
|
|
56
60
|
useInput((input, key) => {
|
|
57
61
|
const cleanInput = input.replace(/[\r\n]+$/, "");
|
|
58
62
|
const isEnter = key.return || /[\r\n]+$/.test(input);
|
|
@@ -104,5 +108,6 @@ export function ModelPicker({ currentModel, onSelect, onCancel }) {
|
|
|
104
108
|
const empty = models !== null && filtered.length === 0 && q === "";
|
|
105
109
|
// Debounce filter: avoid re-filtering on every keystroke burst
|
|
106
110
|
const debouncedQ = q;
|
|
107
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.pickerBorder, paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.pickerBorder, children: "\u9009\u62E9\u6A21\u578B" }), _jsx(Text, { color: theme.muted, children: currentModel ? ` (当前: ${currentModel})` : "" })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.muted, children: "\u7B5B\u9009: " }), _jsx(Text, { color: query ? theme.accent : theme.muted, children: query || "输入字符过滤,↑↓/PgUpDn 导航" })] }), loading && (_jsx(Box, { children: _jsx(Text, { color: theme.muted, dimColor: true, children: "\u6B63\u5728\u83B7\u53D6\u6A21\u578B\u5217\u8868..." }) })), empty && (_jsx(Box, { children: _jsx(Text, { color: theme.muted, dimColor: true, children: "\u65E0\u6CD5\u83B7\u53D6\u6A21\u578B\u5217\u8868\uFF08\u53EF\u8F93\u5165\u6A21\u578B\u540D\u540E\u6309 Enter\uFF09" }) })), models !== null && extraAbove > 0 && (_jsx(Box, { children: _jsxs(Text, { color: theme.muted, dimColor: true, children: ["\u2026 \u4E0A\u65B9\u8FD8\u6709 ", extraAbove, " \u4E2A\u6A21\u578B"] }) })), models !== null &&
|
|
111
|
+
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, width: "100%", borderStyle: "round", borderColor: theme.pickerBorder, paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.pickerBorder, children: "\u9009\u62E9\u6A21\u578B" }), _jsx(Text, { color: theme.muted, children: currentModel ? ` (当前: ${currentModel})` : "" })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.muted, children: "\u7B5B\u9009: " }), _jsx(Text, { color: query ? theme.accent : theme.muted, children: query || "输入字符过滤,↑↓/PgUpDn 导航" })] }), loading && (_jsx(Box, { children: _jsx(Text, { color: theme.muted, dimColor: true, children: "\u6B63\u5728\u83B7\u53D6\u6A21\u578B\u5217\u8868..." }) })), empty && (_jsx(Box, { children: _jsx(Text, { color: theme.muted, dimColor: true, children: "\u65E0\u6CD5\u83B7\u53D6\u6A21\u578B\u5217\u8868\uFF08\u53EF\u8F93\u5165\u6A21\u578B\u540D\u540E\u6309 Enter\uFF09" }) })), models !== null && extraAbove > 0 && (_jsx(Box, { children: _jsxs(Text, { color: theme.muted, dimColor: true, children: ["\u2026 \u4E0A\u65B9\u8FD8\u6709 ", extraAbove, " \u4E2A\u6A21\u578B"] }) })), models !== null &&
|
|
112
|
+
shown.map((m, i) => (_jsxs(Box, { children: [_jsx(Text, { color: i === shownIndex - windowStart ? theme.accent : theme.muted, children: i === shownIndex - windowStart ? "❯ " : " " }), _jsx(Text, { bold: i === shownIndex - windowStart, color: i === shownIndex - windowStart ? theme.accent : undefined, children: m }), m === currentModel && _jsx(Text, { color: theme.success, children: " \u2190 \u5F53\u524D" })] }, m))), extraBelow > 0 && (_jsx(Box, { children: _jsxs(Text, { color: theme.muted, dimColor: true, children: ["\u2026 \u4E0B\u65B9\u8FD8\u6709 ", extraBelow, " \u4E2A\u6A21\u578B\uFF08\u8F93\u5165\u5B57\u7B26\u8FC7\u6EE4\uFF09"] }) })), models !== null && filtered.length === 0 && debouncedQ !== "" && (_jsxs(Box, { children: [_jsxs(Text, { color: theme.accent, bold: true, children: ["\u276F", " "] }), _jsxs(Text, { color: theme.accent, bold: true, children: ["\u4F7F\u7528\u81EA\u5B9A\u4E49\u6A21\u578B: ", debouncedQ] })] })), _jsx(Box, { children: _jsx(Text, { color: theme.muted, dimColor: true, wrap: "truncate-end", children: "\u2191\u2193/PgUpDn \u9009\u62E9 \u00B7 Enter \u786E\u8BA4 \u00B7 Esc \u53D6\u6D88" }) })] }));
|
|
108
113
|
}
|
package/dist/tui/QuestionBar.js
CHANGED
|
@@ -1,45 +1,71 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useState } from "react";
|
|
2
|
+
import { useMemo, useState } from "react";
|
|
3
3
|
import { Box, Text, useInput } from "ink";
|
|
4
4
|
import { wrapByWidth } from "./text-width.js";
|
|
5
|
-
import { innerTextWidth } from "./layout.js";
|
|
5
|
+
import { footerMaxRows, innerTextWidth, QUESTION_CHROME_ROWS, sliceLineWindow } from "./layout.js";
|
|
6
6
|
import { theme } from "./theme.js";
|
|
7
|
+
import { normalizeQuestionOptions, normalizeQuestionPrompt } from "../question-format.js";
|
|
8
|
+
export function questionBodyLines(prompt, options, columns) {
|
|
9
|
+
const textWidth = innerTextWidth(columns);
|
|
10
|
+
const promptLines = `❓ ${normalizeQuestionPrompt(prompt)}`
|
|
11
|
+
.split("\n")
|
|
12
|
+
.flatMap((line) => wrapByWidth(line, textWidth));
|
|
13
|
+
const optionLines = normalizeQuestionOptions(options).flatMap((o, i) => ` ${i + 1}. ${o}`.split("\n").flatMap((l) => wrapByWidth(l, textWidth)));
|
|
14
|
+
return { lines: [...promptLines, ...optionLines], promptLineCount: promptLines.length };
|
|
15
|
+
}
|
|
7
16
|
/**
|
|
8
17
|
* Terminal rows the question bar occupies, mirroring the JSX layout below.
|
|
9
|
-
*
|
|
10
|
-
* terminal height while the prompt is up.
|
|
18
|
+
* Capped so a huge prompt cannot push the input row off the screen.
|
|
11
19
|
*/
|
|
12
|
-
export function questionBarRows(prompt, options, columns) {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
for (const line of prompt.split("\n"))
|
|
16
|
-
rows += wrapByWidth(line, textWidth).length;
|
|
17
|
-
if (options)
|
|
18
|
-
for (const o of options)
|
|
19
|
-
for (const l of o.split("\n"))
|
|
20
|
-
rows += wrapByWidth(l, textWidth).length;
|
|
21
|
-
return rows + 7; /* borders2 + title1 + marginTop1 + hint1 + inputRow1 + margin1 */
|
|
20
|
+
export function questionBarRows(prompt, options, columns, frameRowCount = 0) {
|
|
21
|
+
const natural = questionBodyLines(prompt, options, columns).lines.length + QUESTION_CHROME_ROWS;
|
|
22
|
+
return Math.min(natural, footerMaxRows(frameRowCount, QUESTION_CHROME_ROWS));
|
|
22
23
|
}
|
|
23
24
|
/** Free-form question prompt: type an answer (or a number to pick an option), Enter to submit, Esc to cancel. */
|
|
24
|
-
export function QuestionBar({ prompt, options, onAnswer }) {
|
|
25
|
+
export function QuestionBar({ prompt, options, onAnswer, columns, maxRows }) {
|
|
25
26
|
const [value, setValue] = useState("");
|
|
26
27
|
const [caret, setCaret] = useState(0);
|
|
28
|
+
const [top, setTop] = useState(0);
|
|
29
|
+
const { lines, promptLineCount } = useMemo(() => questionBodyLines(prompt, options, columns), [prompt, options, columns]);
|
|
30
|
+
const bodyMax = Math.max(0, maxRows - QUESTION_CHROME_ROWS);
|
|
31
|
+
const window = useMemo(() => sliceLineWindow(lines, bodyMax, top), [lines, bodyMax, top]);
|
|
32
|
+
const scrollable = window.above > 0 || window.below > 0;
|
|
33
|
+
const choices = normalizeQuestionOptions(options);
|
|
27
34
|
useInput((input, key) => {
|
|
28
35
|
if (key.escape) {
|
|
29
36
|
onAnswer(null);
|
|
30
37
|
return;
|
|
31
38
|
}
|
|
32
39
|
if (key.return) {
|
|
33
|
-
if (/^\d+$/.test(value.trim()) &&
|
|
40
|
+
if (/^\d+$/.test(value.trim()) && choices.length > 0) {
|
|
34
41
|
const n = parseInt(value.trim(), 10);
|
|
35
|
-
if (n >= 1 && n <=
|
|
36
|
-
onAnswer(
|
|
42
|
+
if (n >= 1 && n <= choices.length) {
|
|
43
|
+
onAnswer(choices[n - 1]);
|
|
37
44
|
return;
|
|
38
45
|
}
|
|
39
46
|
}
|
|
40
47
|
onAnswer(value.trim());
|
|
41
48
|
return;
|
|
42
49
|
}
|
|
50
|
+
if (scrollable) {
|
|
51
|
+
const view = Math.max(1, window.lines.length);
|
|
52
|
+
if (key.upArrow) {
|
|
53
|
+
setTop((t) => sliceLineWindow(lines, bodyMax, t - 1).start);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (key.downArrow) {
|
|
57
|
+
setTop((t) => sliceLineWindow(lines, bodyMax, t + 1).start);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (key.pageUp) {
|
|
61
|
+
setTop((t) => sliceLineWindow(lines, bodyMax, t - view).start);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (key.pageDown) {
|
|
65
|
+
setTop((t) => sliceLineWindow(lines, bodyMax, t + view).start);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
43
69
|
if (key.leftArrow) {
|
|
44
70
|
setCaret((c) => Math.max(0, c - 1));
|
|
45
71
|
return;
|
|
@@ -100,5 +126,11 @@ export function QuestionBar({ prompt, options, onAnswer }) {
|
|
|
100
126
|
setCaret((c) => c + ins.length);
|
|
101
127
|
}
|
|
102
128
|
});
|
|
103
|
-
|
|
129
|
+
const hint = scrollable
|
|
130
|
+
? "Enter 提交 · Esc 取消 · ↑↓ 滚动 · 数字选选项"
|
|
131
|
+
: "Enter 提交 · Esc 取消 · ←→ 光标 · 数字选选项";
|
|
132
|
+
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, width: "100%", borderStyle: "round", borderColor: theme.questionBorder, paddingX: 1, children: [bodyMax > 0 && (_jsxs(Box, { flexDirection: "column", height: bodyMax, overflow: "hidden", flexShrink: 0, width: "100%", children: [window.above > 0 && (_jsx(Box, { flexShrink: 0, children: _jsxs(Text, { color: theme.muted, dimColor: true, children: ["\u2026 \u4EE5\u4E0A ", window.above, " \u884C"] }) })), window.lines.map((line, i) => {
|
|
133
|
+
const isPrompt = window.start + i < promptLineCount;
|
|
134
|
+
return (_jsx(Box, { flexShrink: 0, children: _jsx(Text, { bold: isPrompt, color: isPrompt ? theme.questionBorder : theme.muted, wrap: "truncate-end", children: line }) }, i));
|
|
135
|
+
}), window.below > 0 && (_jsx(Box, { flexShrink: 0, children: _jsxs(Text, { color: theme.muted, dimColor: true, children: ["\u2193 \u8FD8\u6709 ", window.below, " \u884C"] }) }))] })), _jsxs(Box, { marginTop: 1, flexShrink: 0, height: 1, overflow: "hidden", width: "100%", children: [_jsxs(Text, { bold: true, color: theme.questionBorder, children: ["\u2192", " "] }), _jsx(Text, { children: Array.from(value).slice(0, caret).join("") }), _jsx(Text, { color: theme.muted, inverse: true, children: " " }), _jsx(Text, { children: Array.from(value).slice(caret).join("") })] }), _jsx(Box, { flexShrink: 0, children: _jsx(Text, { color: theme.muted, dimColor: true, wrap: "truncate-end", children: hint }) })] }));
|
|
104
136
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Box, Text, useInput, useStdout } from "ink";
|
|
4
|
+
import { listSessions } from "../sessions.js";
|
|
5
|
+
import { theme } from "./theme.js";
|
|
6
|
+
export function sessionPickerRows(terminalRows) {
|
|
7
|
+
const maxListRows = Math.max(4, terminalRows - 11);
|
|
8
|
+
return Math.min(6 + maxListRows + 3, Math.max(6, terminalRows - 2));
|
|
9
|
+
}
|
|
10
|
+
function label(s) {
|
|
11
|
+
const date = s.updated.split("T")[0] ?? "";
|
|
12
|
+
return `${s.id} ${date} ${s.title} (${s.messageCount})`;
|
|
13
|
+
}
|
|
14
|
+
export function SessionPicker({ currentId, onSelect, onCancel }) {
|
|
15
|
+
const sessions = listSessions();
|
|
16
|
+
const [query, setQuery] = useState("");
|
|
17
|
+
const [index, setIndex] = useState(0);
|
|
18
|
+
const { stdout } = useStdout();
|
|
19
|
+
const terminalRows = stdout.rows || 24;
|
|
20
|
+
const maxListRows = Math.max(4, terminalRows - 11);
|
|
21
|
+
const q = query.trim().toLowerCase();
|
|
22
|
+
const filtered = sessions.filter((s) => s.id.toLowerCase().includes(q) || s.title.toLowerCase().includes(q));
|
|
23
|
+
const shownIndex = Math.min(index, Math.max(0, filtered.length - 1));
|
|
24
|
+
const windowStart = Math.min(shownIndex, Math.max(0, filtered.length - maxListRows));
|
|
25
|
+
const shown = filtered.slice(windowStart, windowStart + maxListRows);
|
|
26
|
+
const extraAbove = windowStart;
|
|
27
|
+
const extraBelow = filtered.length - (windowStart + shown.length);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
setIndex(0);
|
|
30
|
+
}, []);
|
|
31
|
+
useInput((input, key) => {
|
|
32
|
+
const cleanInput = input.replace(/[\r\n]+$/, "");
|
|
33
|
+
const isEnter = key.return || /[\r\n]+$/.test(input);
|
|
34
|
+
if (key.escape) {
|
|
35
|
+
onCancel();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (key.pageDown) {
|
|
39
|
+
if (filtered.length > 0)
|
|
40
|
+
setIndex((i) => Math.min(filtered.length - 1, i + maxListRows));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (key.pageUp) {
|
|
44
|
+
if (filtered.length > 0)
|
|
45
|
+
setIndex((i) => Math.max(0, i - maxListRows));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (filtered.length > 0) {
|
|
49
|
+
if (key.downArrow) {
|
|
50
|
+
setIndex((i) => (i + 1) % filtered.length);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (key.upArrow) {
|
|
54
|
+
setIndex((i) => (i - 1 + filtered.length) % filtered.length);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (isEnter) {
|
|
59
|
+
if (filtered.length > 0)
|
|
60
|
+
onSelect(filtered[shownIndex].id);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (key.backspace || key.delete) {
|
|
64
|
+
setQuery((v) => Array.from(v).slice(0, -1).join(""));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (key.ctrl && input === "u") {
|
|
68
|
+
setQuery("");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (key.ctrl || key.meta || key.tab)
|
|
72
|
+
return;
|
|
73
|
+
if (cleanInput)
|
|
74
|
+
setQuery((v) => v + cleanInput);
|
|
75
|
+
});
|
|
76
|
+
const empty = filtered.length === 0 && q === "";
|
|
77
|
+
const noMatch = filtered.length === 0 && q !== "";
|
|
78
|
+
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, width: "100%", borderStyle: "round", borderColor: theme.pickerBorder, paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.pickerBorder, children: "\u9009\u62E9\u4F1A\u8BDD" }), _jsx(Text, { color: theme.muted, children: currentId ? ` (当前: ${currentId})` : "" })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.muted, children: "\u7B5B\u9009: " }), _jsx(Text, { color: query ? theme.accent : theme.muted, children: query || "输入字符过滤标题或 id" })] }), empty && (_jsx(Box, { children: _jsx(Text, { color: theme.muted, dimColor: true, children: "\u65E0\u5DF2\u4FDD\u5B58\u7684\u4F1A\u8BDD" }) })), noMatch && (_jsx(Box, { children: _jsx(Text, { color: theme.muted, dimColor: true, children: "\u6CA1\u6709\u5339\u914D\u7684\u4F1A\u8BDD" }) })), extraAbove > 0 && (_jsx(Box, { children: _jsxs(Text, { color: theme.muted, dimColor: true, children: ["\u2026 \u4E0A\u65B9\u8FD8\u6709 ", extraAbove, " \u4E2A\u4F1A\u8BDD"] }) })), shown.map((s, i) => (_jsxs(Box, { children: [_jsx(Text, { color: i === shownIndex - windowStart ? theme.accent : theme.muted, children: i === shownIndex - windowStart ? "❯ " : " " }), _jsx(Text, { bold: i === shownIndex - windowStart, color: i === shownIndex - windowStart ? theme.accent : undefined, wrap: "truncate-end", children: label(s) }), s.id === currentId && _jsx(Text, { color: theme.success, children: " \u2190" })] }, s.id))), extraBelow > 0 && (_jsx(Box, { children: _jsxs(Text, { color: theme.muted, dimColor: true, children: ["\u2026 \u4E0B\u65B9\u8FD8\u6709 ", extraBelow, " \u4E2A\u4F1A\u8BDD"] }) })), _jsx(Box, { children: _jsx(Text, { color: theme.muted, dimColor: true, wrap: "truncate-end", children: "\u2191\u2193/PgUpDn \u9009\u62E9 \u00B7 Enter \u786E\u8BA4 \u00B7 Esc \u53D6\u6D88" }) })] }));
|
|
79
|
+
}
|
package/dist/tui/StatusBar.js
CHANGED
|
@@ -1,24 +1,18 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
+
import { formatTokenStatusLabel } from "../token-display.js";
|
|
3
4
|
import { displayWidth } from "./text-width.js";
|
|
4
5
|
export function StatusBar({ state, columns = 80 }) {
|
|
5
6
|
const model = state.model || "unknown";
|
|
6
|
-
const
|
|
7
|
-
const fmtK = (n) => `${(n / 1000).toFixed(1)}k`;
|
|
8
|
-
const ctxPct = tokens && tokens.contextWindow > 0 ? Math.round((tokens.input / tokens.contextWindow) * 100) : null;
|
|
9
|
-
const tokensLabel = tokens && (tokens.input > 0 || tokens.output > 0)
|
|
10
|
-
? [
|
|
11
|
-
tokens.input > 0 ? `${fmtK(tokens.input)} in` : null,
|
|
12
|
-
tokens.output > 0 ? `${fmtK(tokens.output)} out` : null,
|
|
13
|
-
ctxPct != null ? `${ctxPct}%` : null,
|
|
14
|
-
].filter(Boolean).join(" · ")
|
|
15
|
-
: "";
|
|
7
|
+
const tokensLabel = state.tokenInfo ? formatTokenStatusLabel(state.tokenInfo) : "";
|
|
16
8
|
// Transient copy feedback rides on the separator row itself (right end) so
|
|
17
9
|
// it never adds a row and the layout does not jump while it is visible.
|
|
18
10
|
const sepWidth = Math.max(8, columns - 2);
|
|
19
|
-
const notice = state.copyNotice
|
|
11
|
+
const notice = state.copyNotice
|
|
12
|
+
? `✓ ${state.copyNotice}`
|
|
13
|
+
: state.selectionMode
|
|
14
|
+
? "选择模式 ↑↓←→扩选 Enter复制 Esc退出"
|
|
15
|
+
: null;
|
|
20
16
|
const noticeWidth = notice ? displayWidth(notice) : 0;
|
|
21
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, wrap: "truncate-end", children: notice
|
|
22
|
-
? `${"─".repeat(Math.max(0, sepWidth - noticeWidth - 2))} ${notice}`
|
|
23
|
-
: "─".repeat(sepWidth) }), _jsxs(Box, { children: [_jsx(Text, { children: " \uD83E\uDD16 min-agent" }), _jsxs(Text, { color: "gray", wrap: "truncate-end", children: [" (", model, ")"] }), tokensLabel && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: "gray", wrap: "truncate-end", children: tokensLabel })] }))] })] }));
|
|
17
|
+
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, width: "100%", children: [_jsx(Text, { dimColor: true, wrap: "truncate-end", children: notice ? `${"─".repeat(Math.max(0, sepWidth - noticeWidth - 2))} ${notice}` : "─".repeat(sepWidth) }), _jsxs(Box, { children: [_jsx(Text, { children: " \uD83E\uDD16 min-agent" }), _jsxs(Text, { color: "gray", wrap: "truncate-end", children: [" ", "(", model, ")"] }), tokensLabel && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: "gray", wrap: "truncate-end", children: tokensLabel })] }))] })] }));
|
|
24
18
|
}
|
package/dist/tui/agent-runner.js
CHANGED
|
@@ -18,10 +18,44 @@ export function createAgentRunner(deps) {
|
|
|
18
18
|
abortController = new AbortController();
|
|
19
19
|
deps.tui.setRunning(true, "思考中...");
|
|
20
20
|
const { summarizeToolCall, summarizeToolResult, toolResultText } = await import("../tool-display.js");
|
|
21
|
-
const {
|
|
21
|
+
const { getContextWindowInfo } = await import("../context-window.js");
|
|
22
22
|
const { getModelPrice, estimateCost, formatCost } = await import("../pricing.js");
|
|
23
|
-
const {
|
|
23
|
+
const { runOnceWithSystem, emptyResponseMessage, WRAP_UP_MESSAGE } = await import("../agent.js");
|
|
24
24
|
const { clearStack, pushTurn } = await import("./undo-stack.js");
|
|
25
|
+
const { setSubAgentListener } = await import("../tools/task.js");
|
|
26
|
+
const { LOOP_HALT_MESSAGE } = await import("../doom-loop.js");
|
|
27
|
+
const { createSaveThrottle } = await import("../save-throttle.js");
|
|
28
|
+
const { snapshotTokenInfo } = await import("./token-info.js");
|
|
29
|
+
// Persist while the run is in flight so a crash/kill does not lose the turn.
|
|
30
|
+
const saveThrottle = createSaveThrottle(() => deps.persist?.());
|
|
31
|
+
const billedInAtStart = deps.tracker.totalInputTokens;
|
|
32
|
+
const billedOutAtStart = deps.tracker.totalOutputTokens;
|
|
33
|
+
let tokenTimer = null;
|
|
34
|
+
let tokenPaintSeq = 0;
|
|
35
|
+
const applyTokenInfo = async () => {
|
|
36
|
+
const seq = ++tokenPaintSeq;
|
|
37
|
+
const info = await snapshotTokenInfo(deps.tracker, deps.getModel(), deps.messages);
|
|
38
|
+
if (seq !== tokenPaintSeq)
|
|
39
|
+
return;
|
|
40
|
+
deps.tui.setTokenInfo(info);
|
|
41
|
+
};
|
|
42
|
+
const scheduleTokenInfo = () => {
|
|
43
|
+
if (tokenTimer != null)
|
|
44
|
+
return;
|
|
45
|
+
tokenTimer = setTimeout(() => {
|
|
46
|
+
tokenTimer = null;
|
|
47
|
+
void applyTokenInfo();
|
|
48
|
+
}, 50);
|
|
49
|
+
};
|
|
50
|
+
const flushTokenInfo = async () => {
|
|
51
|
+
if (tokenTimer != null) {
|
|
52
|
+
clearTimeout(tokenTimer);
|
|
53
|
+
tokenTimer = null;
|
|
54
|
+
}
|
|
55
|
+
await applyTokenInfo();
|
|
56
|
+
};
|
|
57
|
+
const unsubscribeTokens = deps.tracker.subscribe(scheduleTokenInfo);
|
|
58
|
+
void applyTokenInfo();
|
|
25
59
|
const callbacks = {
|
|
26
60
|
onAssistantDisplayDelta: (delta) => deps.tui.appendToLast(delta),
|
|
27
61
|
onThinkingDelta: (delta) => {
|
|
@@ -46,45 +80,123 @@ export function createAgentRunner(deps) {
|
|
|
46
80
|
onStreamError: (msg) => {
|
|
47
81
|
deps.tui.addMessage({ id: `err-${Date.now()}`, role: "system", content: `错误: ${msg}`, timestamp: Date.now() });
|
|
48
82
|
},
|
|
49
|
-
onCompaction: () => {
|
|
83
|
+
onCompaction: (line) => {
|
|
50
84
|
compactedDuringRun = true;
|
|
51
85
|
deps.setUndoStack(clearStack(deps.getUndoStack()));
|
|
86
|
+
if (line === "compacting_start") {
|
|
87
|
+
deps.tui.addMessage({
|
|
88
|
+
id: `compact-${Date.now()}`,
|
|
89
|
+
role: "system",
|
|
90
|
+
content: "正在整理过长的对话记录…",
|
|
91
|
+
timestamp: Date.now(),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
else if (line.startsWith("compacted_ok")) {
|
|
95
|
+
deps.tui.addMessage({
|
|
96
|
+
id: `compact-ok-${Date.now()}`,
|
|
97
|
+
role: "system",
|
|
98
|
+
content: "✓ 已整理对话记录,继续执行",
|
|
99
|
+
timestamp: Date.now(),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
onRetryNotice: (info) => {
|
|
104
|
+
deps.tui.addMessage({
|
|
105
|
+
id: `retry-${Date.now()}`,
|
|
106
|
+
role: "system",
|
|
107
|
+
content: info.kind === "wrap_up"
|
|
108
|
+
? "⟳ 本轮预算用尽,正在收尾…"
|
|
109
|
+
: `⟳ 模型返回空响应,重试 ${info.attempt}/${info.maxAttempts}…`,
|
|
110
|
+
timestamp: Date.now(),
|
|
111
|
+
});
|
|
52
112
|
},
|
|
53
113
|
onRunFinish: async (info) => {
|
|
54
114
|
abortController = null;
|
|
55
115
|
deps.tui.setRunning(false);
|
|
116
|
+
await flushTokenInfo();
|
|
56
117
|
if (info.aborted)
|
|
57
118
|
return;
|
|
58
119
|
if (info.budgetExceeded)
|
|
59
|
-
deps.tui.addMessage({
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
120
|
+
deps.tui.addMessage({
|
|
121
|
+
id: `budget-${Date.now()}`,
|
|
122
|
+
role: "system",
|
|
123
|
+
content: "⚠ 已达到预算上限。/budget 查看或调高",
|
|
124
|
+
timestamp: Date.now(),
|
|
125
|
+
});
|
|
126
|
+
const runInput = Math.max(0, deps.tracker.totalInputTokens - billedInAtStart);
|
|
127
|
+
const runOutput = Math.max(0, deps.tracker.totalOutputTokens - billedOutAtStart);
|
|
128
|
+
const occupied = deps.tracker.lastInputTokens;
|
|
129
|
+
if (runInput > 0 || runOutput > 0 || occupied > 0) {
|
|
130
|
+
const ctx = await getContextWindowInfo(deps.getModel());
|
|
131
|
+
const pct = ctx.tokens > 0 && occupied > 0 ? Math.round((occupied / ctx.tokens) * 100) : null;
|
|
67
132
|
const price = await getModelPrice(deps.getModel() ?? "");
|
|
68
|
-
const cost = estimateCost({ inputTokens:
|
|
69
|
-
|
|
133
|
+
const cost = estimateCost({ inputTokens: runInput, outputTokens: runOutput }, price);
|
|
134
|
+
const parts = [
|
|
135
|
+
`Done in ${info.stepCount} step(s)`,
|
|
136
|
+
`Tokens: ${runInput} in / ${runOutput} out / ${runInput + runOutput} total`,
|
|
137
|
+
...(pct != null ? [`Context: ${pct}%`] : []),
|
|
138
|
+
...(cost != null ? [`约 ${formatCost(cost)}`] : []),
|
|
139
|
+
];
|
|
140
|
+
deps.tui.addMessage({
|
|
141
|
+
id: `done-${Date.now()}`,
|
|
142
|
+
role: "system",
|
|
143
|
+
content: parts.join(" | "),
|
|
144
|
+
timestamp: Date.now(),
|
|
145
|
+
});
|
|
70
146
|
}
|
|
71
147
|
if (info.maxStepsReached)
|
|
72
|
-
deps.tui.addMessage({
|
|
148
|
+
deps.tui.addMessage({
|
|
149
|
+
id: `maxsteps-${Date.now()}`,
|
|
150
|
+
role: "system",
|
|
151
|
+
content: "⚠ 已达到自动续跑上限,发送消息继续",
|
|
152
|
+
timestamp: Date.now(),
|
|
153
|
+
});
|
|
154
|
+
if (info.stopped)
|
|
155
|
+
deps.tui.addMessage({
|
|
156
|
+
id: `stopped-${Date.now()}`,
|
|
157
|
+
role: "system",
|
|
158
|
+
content: `⚠ ${LOOP_HALT_MESSAGE}`,
|
|
159
|
+
timestamp: Date.now(),
|
|
160
|
+
});
|
|
161
|
+
else if (info.wrapUp)
|
|
162
|
+
deps.tui.addMessage({
|
|
163
|
+
id: `wrapup-${Date.now()}`,
|
|
164
|
+
role: "system",
|
|
165
|
+
content: `⚠ ${WRAP_UP_MESSAGE}`,
|
|
166
|
+
timestamp: Date.now(),
|
|
167
|
+
});
|
|
168
|
+
else if (info.emptyResponse)
|
|
169
|
+
deps.tui.addMessage({
|
|
170
|
+
id: `empty-${Date.now()}`,
|
|
171
|
+
role: "system",
|
|
172
|
+
content: `⚠ ${emptyResponseMessage(info.emptyAttempts ?? 0)}`,
|
|
173
|
+
timestamp: Date.now(),
|
|
174
|
+
});
|
|
175
|
+
else if (info.incomplete)
|
|
176
|
+
deps.tui.addMessage({
|
|
177
|
+
id: `incomplete-${Date.now()}`,
|
|
178
|
+
role: "system",
|
|
179
|
+
content: "还没有完整回复,发送消息可继续",
|
|
180
|
+
timestamp: Date.now(),
|
|
181
|
+
});
|
|
73
182
|
},
|
|
74
183
|
};
|
|
75
184
|
try {
|
|
185
|
+
setSubAgentListener((ev) => {
|
|
186
|
+
if (ev.phase !== "call")
|
|
187
|
+
return;
|
|
188
|
+
deps.tui.appendToolChild(ev.parentToolCallId, {
|
|
189
|
+
name: ev.name,
|
|
190
|
+
summary: summarizeToolCall(ev.name, ev.input),
|
|
191
|
+
});
|
|
192
|
+
});
|
|
76
193
|
const runOptions = {
|
|
77
194
|
providerName: deps.getProvider(),
|
|
195
|
+
taskState: deps.getTaskState(),
|
|
196
|
+
onStepPersist: () => saveThrottle.request(),
|
|
78
197
|
...(deps.getPlanMode() ? { planMode: true } : {}),
|
|
79
198
|
};
|
|
80
|
-
|
|
81
|
-
await runOnceWithSystem(deps.messages, deps.codeSystemPrompt, deps.getModel(), abortController?.signal, callbacks, deps.tracker, runOptions);
|
|
82
|
-
}
|
|
83
|
-
else {
|
|
84
|
-
const { loadInstructions } = await import("../instructions.js");
|
|
85
|
-
const instructions = await loadInstructions();
|
|
86
|
-
await runOnce(deps.messages, instructions, deps.getModel(), abortController?.signal, callbacks, deps.tracker, runOptions);
|
|
87
|
-
}
|
|
199
|
+
await runOnceWithSystem(deps.messages, deps.getSystemPrompt(), deps.getModel(), abortController?.signal, callbacks, deps.tracker, runOptions);
|
|
88
200
|
}
|
|
89
201
|
catch (err) {
|
|
90
202
|
if (!abortController)
|
|
@@ -93,10 +205,18 @@ export function createAgentRunner(deps) {
|
|
|
93
205
|
callbacks.onStreamError(msg);
|
|
94
206
|
}
|
|
95
207
|
finally {
|
|
208
|
+
unsubscribeTokens();
|
|
209
|
+
if (tokenTimer != null) {
|
|
210
|
+
clearTimeout(tokenTimer);
|
|
211
|
+
tokenTimer = null;
|
|
212
|
+
}
|
|
213
|
+
setSubAgentListener(null);
|
|
214
|
+
saveThrottle.cancel();
|
|
96
215
|
if (!compactedDuringRun)
|
|
97
216
|
deps.setUndoStack(pushTurn(deps.getUndoStack(), turnStart, deps.messages.length, uiStart));
|
|
98
217
|
abortController = null;
|
|
99
218
|
deps.tui.setRunning(false);
|
|
219
|
+
await flushTokenInfo();
|
|
100
220
|
}
|
|
101
221
|
};
|
|
102
222
|
return { run, abort, getController };
|
package/dist/tui/caret-pos.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { displayWidth, wrapByWidth, clustersOf } from "./text-width.js";
|
|
2
2
|
/** Split the value into visual rows, tracking each row's character start index (newlines count as one). */
|
|
3
3
|
export function visualRows(value, maxWidth) {
|
|
4
4
|
const rows = [];
|
|
@@ -32,13 +32,12 @@ export function caretIndexFromClick(rows, rowIndex, clickColumn, textStartColumn
|
|
|
32
32
|
return row.startIndex;
|
|
33
33
|
let width = 0;
|
|
34
34
|
let idx = 0;
|
|
35
|
-
for (const
|
|
36
|
-
const w = charWidth(char);
|
|
35
|
+
for (const { text: g, width: w } of clustersOf(row.text)) {
|
|
37
36
|
if (width + w > target) {
|
|
38
|
-
return row.startIndex + idx + (target - width < w / 2 ? 0 :
|
|
37
|
+
return row.startIndex + idx + (target - width < w / 2 ? 0 : Array.from(g).length);
|
|
39
38
|
}
|
|
40
39
|
width += w;
|
|
41
|
-
idx
|
|
40
|
+
idx += Array.from(g).length;
|
|
42
41
|
}
|
|
43
42
|
return row.startIndex + idx;
|
|
44
43
|
}
|
|
@@ -64,6 +63,30 @@ export function deleteAt(value, caretIndex) {
|
|
|
64
63
|
chars.splice(caretIndex, 1);
|
|
65
64
|
return { value: chars.join(""), caretIndex };
|
|
66
65
|
}
|
|
66
|
+
/** Move the caret one visual row up (`-1`) or down (`1`), keeping the display column. */
|
|
67
|
+
export function moveCaretVertical(value, caretIndex, direction, maxWidth) {
|
|
68
|
+
const rows = visualRows(value, maxWidth);
|
|
69
|
+
let rowIndex = 0;
|
|
70
|
+
let offset = 0;
|
|
71
|
+
for (let i = 0; i < rows.length; i++) {
|
|
72
|
+
const row = rows[i];
|
|
73
|
+
const end = row.startIndex + Array.from(row.text).length;
|
|
74
|
+
if (caretIndex >= row.startIndex && caretIndex <= end) {
|
|
75
|
+
rowIndex = i;
|
|
76
|
+
offset = caretIndex - row.startIndex;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
if (i === rows.length - 1) {
|
|
80
|
+
rowIndex = i;
|
|
81
|
+
offset = Array.from(row.text).length;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const next = rowIndex + direction;
|
|
85
|
+
if (next < 0 || next >= rows.length)
|
|
86
|
+
return caretIndex;
|
|
87
|
+
const col = displayWidth(Array.from(rows[rowIndex].text).slice(0, offset).join(""));
|
|
88
|
+
return caretIndexFromClick(rows, next, col, 0);
|
|
89
|
+
}
|
|
67
90
|
export function moveToLineStart(value, caretIndex) {
|
|
68
91
|
const chars = Array.from(value);
|
|
69
92
|
let i = caretIndex - 1;
|
|
@@ -132,3 +155,23 @@ export function scanDeleteKeys(buffer, chunk) {
|
|
|
132
155
|
const prefix = tail.slice(esc);
|
|
133
156
|
return { buffer: prefix.length > MAX_DELETE_PREFIX ? "\x1b" : prefix, actions };
|
|
134
157
|
}
|
|
158
|
+
const HOME_END_RE = /\x1b\[(?:[HF]|[1478]~)/g;
|
|
159
|
+
const MAX_HOME_END_PREFIX = 8;
|
|
160
|
+
export function scanHomeEndKeys(buffer, chunk) {
|
|
161
|
+
const buf = buffer + chunk;
|
|
162
|
+
const actions = [];
|
|
163
|
+
let lastConsumedEnd = 0;
|
|
164
|
+
HOME_END_RE.lastIndex = 0;
|
|
165
|
+
let m;
|
|
166
|
+
while ((m = HOME_END_RE.exec(buf))) {
|
|
167
|
+
lastConsumedEnd = m.index + m[0].length;
|
|
168
|
+
const seq = m[0];
|
|
169
|
+
actions.push(seq.endsWith("H") || seq.endsWith("1~") || seq.endsWith("7~") ? { kind: "home" } : { kind: "end" });
|
|
170
|
+
}
|
|
171
|
+
const tail = buf.slice(lastConsumedEnd);
|
|
172
|
+
const esc = tail.lastIndexOf("\x1b");
|
|
173
|
+
if (esc === -1)
|
|
174
|
+
return { buffer: "", actions };
|
|
175
|
+
const prefix = tail.slice(esc);
|
|
176
|
+
return { buffer: prefix.length > MAX_HOME_END_PREFIX ? "\x1b" : prefix, actions };
|
|
177
|
+
}
|
package/dist/tui/caret.js
CHANGED
|
@@ -26,7 +26,7 @@ export function setCaretPosition(position) {
|
|
|
26
26
|
export function createCaretAwareStdout(base) {
|
|
27
27
|
let pendingRowsUp = 0;
|
|
28
28
|
const write = (chunk, encoding, callback) => {
|
|
29
|
-
const isBuf =
|
|
29
|
+
const isBuf = Buffer?.isBuffer(chunk);
|
|
30
30
|
let chunkStr;
|
|
31
31
|
if (typeof chunk === "string")
|
|
32
32
|
chunkStr = chunk;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const DOUBLE_CLICK_MS = 400;
|
|
2
|
+
export const DOUBLE_CLICK_COL_SLOP = 2;
|
|
3
|
+
export function createClickCounter() {
|
|
4
|
+
return { row: -1, col: -1, at: Number.NEGATIVE_INFINITY, count: 0 };
|
|
5
|
+
}
|
|
6
|
+
export function noteClick(c, ev) {
|
|
7
|
+
const same = ev.row === c.row && Math.abs(ev.col - c.col) <= DOUBLE_CLICK_COL_SLOP && ev.at - c.at <= DOUBLE_CLICK_MS;
|
|
8
|
+
c.count = same ? c.count + 1 : 1;
|
|
9
|
+
c.row = ev.row;
|
|
10
|
+
c.col = ev.col;
|
|
11
|
+
c.at = ev.at;
|
|
12
|
+
return c.count;
|
|
13
|
+
}
|
package/dist/tui/drag-state.js
CHANGED
|
@@ -23,7 +23,10 @@ export function stepDrag(machine, event, maxHeight) {
|
|
|
23
23
|
const plain = (button & MOD_MASK) === 0;
|
|
24
24
|
const dragging = machine.pending != null;
|
|
25
25
|
if (plain && dragging && ((button & MOTION) !== 0 || (button === 0 && press))) {
|
|
26
|
-
|
|
26
|
+
const origin = machine.pending;
|
|
27
|
+
if (row === origin.row && col === origin.col && machine.selection == null)
|
|
28
|
+
return null;
|
|
29
|
+
machine.selection = { anchor: origin, cur: { row, col } };
|
|
27
30
|
return { type: "selection", selection: machine.selection };
|
|
28
31
|
}
|
|
29
32
|
if (button === 0 && press) {
|
|
@@ -35,10 +38,12 @@ export function stepDrag(machine, event, maxHeight) {
|
|
|
35
38
|
const active = machine.selection;
|
|
36
39
|
const pending = machine.pending;
|
|
37
40
|
machine.pending = null;
|
|
38
|
-
|
|
41
|
+
machine.selection = null;
|
|
42
|
+
if (active && (active.anchor.row !== active.cur.row || active.anchor.col !== active.cur.col)) {
|
|
39
43
|
return { type: "copy", selection: active };
|
|
44
|
+
}
|
|
40
45
|
if (pending)
|
|
41
|
-
return { type: "click", row };
|
|
46
|
+
return { type: "click", row: pending.row, col: pending.col };
|
|
42
47
|
}
|
|
43
48
|
return null;
|
|
44
49
|
}
|