min-agent 0.2.1 → 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.
Files changed (137) hide show
  1. package/README.md +242 -31
  2. package/dist/agent.js +1233 -485
  3. package/dist/assistant-stream.js +11 -7
  4. package/dist/cli/commands/chat.js +10 -0
  5. package/dist/cli/commands/exec.js +32 -0
  6. package/dist/cli/commands/history.js +58 -0
  7. package/dist/cli/commands/index.js +224 -0
  8. package/dist/cli/commands/init.js +18 -0
  9. package/dist/cli/commands/mcp.js +173 -0
  10. package/dist/cli/commands/memory.js +69 -0
  11. package/dist/cli/commands/models.js +21 -0
  12. package/dist/cli/commands/permission.js +12 -0
  13. package/dist/cli/commands/rules.js +33 -0
  14. package/dist/cli/commands/sandbox.js +13 -0
  15. package/dist/cli/commands/serve.js +9 -0
  16. package/dist/cli/commands/setup.js +4 -0
  17. package/dist/cli/commands/shared.js +16 -0
  18. package/dist/cli/commands/skills.js +119 -0
  19. package/dist/cli/commands/update.js +7 -0
  20. package/dist/cli/commands/write-config.js +30 -0
  21. package/dist/cli/errors.js +36 -0
  22. package/dist/cli/exec-prompt.js +26 -0
  23. package/dist/cli/option-helpers.js +53 -0
  24. package/dist/cli/program.js +180 -0
  25. package/dist/cli.js +7 -632
  26. package/dist/clipboard.js +59 -23
  27. package/dist/code-mode.js +35 -17
  28. package/dist/compaction.js +457 -169
  29. package/dist/config.js +298 -38
  30. package/dist/confirm.js +105 -9
  31. package/dist/context-window.js +156 -75
  32. package/dist/doom-loop.js +268 -26
  33. package/dist/fetch-timeout.js +152 -0
  34. package/dist/http-approvals.js +60 -0
  35. package/dist/http.js +119 -0
  36. package/dist/instructions.js +72 -33
  37. package/dist/logger.js +95 -0
  38. package/dist/markdown.js +35 -50
  39. package/dist/mcp.js +847 -102
  40. package/dist/memory.js +128 -45
  41. package/dist/output.js +42 -31
  42. package/dist/paste-handler.js +3 -3
  43. package/dist/permission-cli.js +43 -0
  44. package/dist/plugins.js +76 -11
  45. package/dist/pricing.js +119 -0
  46. package/dist/provider.js +34 -15
  47. package/dist/question-format.js +60 -0
  48. package/dist/sandbox-cli.js +82 -0
  49. package/dist/sandbox.js +403 -0
  50. package/dist/save-throttle.js +45 -0
  51. package/dist/serve/common.js +404 -0
  52. package/dist/serve/routes-chat.js +347 -0
  53. package/dist/serve/routes-mcp.js +212 -0
  54. package/dist/serve/routes-memory.js +66 -0
  55. package/dist/serve/routes-meta.js +205 -0
  56. package/dist/serve/routes-sessions.js +61 -0
  57. package/dist/serve/routes-skills.js +70 -0
  58. package/dist/serve.js +74 -635
  59. package/dist/sessions.js +197 -15
  60. package/dist/skills.js +531 -77
  61. package/dist/synthetic.js +7 -0
  62. package/dist/title-gen.js +9 -2
  63. package/dist/token-display.js +36 -0
  64. package/dist/tool-display.js +178 -0
  65. package/dist/tool-output.js +53 -46
  66. package/dist/tools/apply_patch.js +265 -0
  67. package/dist/tools/atomic-file.js +35 -0
  68. package/dist/tools/backend.js +61 -0
  69. package/dist/tools/bash.js +186 -71
  70. package/dist/tools/code_search.js +13 -6
  71. package/dist/tools/edit.js +26 -9
  72. package/dist/tools/explore.js +144 -16
  73. package/dist/tools/glob.js +7 -3
  74. package/dist/tools/grep.js +153 -14
  75. package/dist/tools/index.js +9 -24
  76. package/dist/tools/question.js +31 -30
  77. package/dist/tools/read.js +77 -15
  78. package/dist/tools/search-searxng.js +223 -0
  79. package/dist/tools/search-serper.js +189 -0
  80. package/dist/tools/task.js +100 -33
  81. package/dist/tools/todo.js +178 -67
  82. package/dist/tools/web_fetch.js +158 -46
  83. package/dist/tools/web_search.js +217 -29
  84. package/dist/tools/write.js +34 -11
  85. package/dist/tui/App.js +89 -6
  86. package/dist/tui/ConfirmBar.js +57 -4
  87. package/dist/tui/InputBar.js +504 -44
  88. package/dist/tui/MessageList.js +674 -20
  89. package/dist/tui/ModelPicker.js +113 -0
  90. package/dist/tui/QuestionBar.js +136 -0
  91. package/dist/tui/SessionPicker.js +79 -0
  92. package/dist/tui/StatusBar.js +14 -12
  93. package/dist/tui/agent-runner.js +223 -0
  94. package/dist/tui/caret-pos.js +177 -0
  95. package/dist/tui/caret.js +69 -0
  96. package/dist/tui/click-count.js +13 -0
  97. package/dist/tui/diff-view.js +61 -0
  98. package/dist/tui/drag-state.js +49 -0
  99. package/dist/tui/hydrate.js +129 -0
  100. package/dist/tui/index.js +189 -31
  101. package/dist/tui/input-history.js +125 -0
  102. package/dist/tui/layout.js +88 -0
  103. package/dist/tui/mouse.js +46 -0
  104. package/dist/tui/prompt-queue.js +24 -0
  105. package/dist/tui/selection.js +226 -0
  106. package/dist/tui/session-switch.js +28 -0
  107. package/dist/tui/slash-commands.js +106 -0
  108. package/dist/tui/slash-handler.js +545 -0
  109. package/dist/tui/text-width.js +113 -0
  110. package/dist/tui/theme.js +12 -0
  111. package/dist/tui/token-info.js +7 -0
  112. package/dist/tui/tool-children.js +19 -0
  113. package/dist/tui/undo-stack.js +14 -0
  114. package/dist/tui/use-sgr-mouse.js +29 -0
  115. package/dist/tui-chat.js +346 -330
  116. package/dist/updater.js +116 -0
  117. package/dist/xml-search.js +194 -0
  118. package/docs/API.md +410 -32
  119. package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +1510 -0
  120. package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +2105 -0
  121. package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +1595 -0
  122. package/docs/superpowers/plans/2026-08-16-input-caret.md +782 -0
  123. package/docs/superpowers/plans/2026-08-20-tui-completeness.md +873 -0
  124. package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +631 -0
  125. package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +183 -0
  126. package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +220 -0
  127. package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +196 -0
  128. package/docs/superpowers/specs/2026-08-16-input-caret-design.md +63 -0
  129. package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +116 -0
  130. package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +47 -0
  131. package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +37 -0
  132. package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +68 -0
  133. package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +273 -0
  134. package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +165 -0
  135. package/package.json +12 -8
  136. package/skills/self-config/SKILL.md +90 -0
  137. package/skills/self-config/reference.md +149 -0
@@ -0,0 +1,113 @@
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 { loadConfig, getActiveProvider, fetchModels } from "../config.js";
5
+ import { theme } from "./theme.js";
6
+ /**
7
+ * Worst-case terminal rows the picker occupies (header/filter + status rows +
8
+ * list + footer + borders), used by App so the message area never overlaps it.
9
+ */
10
+ export function modelPickerRows(terminalRows) {
11
+ const maxListRows = Math.max(4, terminalRows - 11);
12
+ return Math.min(6 + maxListRows + 3, Math.max(6, terminalRows - 2));
13
+ }
14
+ /**
15
+ * Interactive model picker — replaces the input bar while open.
16
+ * - Fetches the provider's model list, falls back to cache
17
+ * - Type to filter, ↑↓ to navigate, Enter to select, Esc to cancel
18
+ * - With a non-matching filter, Enter applies the typed name directly
19
+ * - List is capped so the picker never overflows the terminal
20
+ */
21
+ export function ModelPicker({ currentModel, onSelect, onCancel }) {
22
+ const [models, setModels] = useState(null);
23
+ const [query, setQuery] = useState("");
24
+ const [index, setIndex] = useState(0);
25
+ const { stdout } = useStdout();
26
+ useEffect(() => {
27
+ let cancelled = false;
28
+ const cfg = loadConfig();
29
+ const provider = getActiveProvider(cfg);
30
+ if (provider?.baseURL && provider?.apiKey) {
31
+ fetchModels(provider.baseURL, provider.apiKey)
32
+ .then((m) => {
33
+ if (!cancelled)
34
+ setModels(m);
35
+ })
36
+ .catch(() => {
37
+ if (!cancelled)
38
+ setModels([]);
39
+ });
40
+ }
41
+ else {
42
+ setModels([]);
43
+ }
44
+ return () => {
45
+ cancelled = true;
46
+ };
47
+ }, []);
48
+ const terminalRows = stdout.rows || 24;
49
+ const maxListRows = Math.max(4, terminalRows - 11); // chrome + status bar + safety
50
+ const q = query.trim().toLowerCase();
51
+ const filtered = (models ?? []).filter((m) => m.toLowerCase().includes(q));
52
+ const shownIndex = Math.min(index, Math.max(0, filtered.length - 1));
53
+ const windowStart = Math.min(shownIndex, Math.max(0, filtered.length - maxListRows));
54
+ const shown = filtered.slice(windowStart, windowStart + maxListRows);
55
+ const extraAbove = windowStart;
56
+ const extraBelow = filtered.length - (windowStart + shown.length);
57
+ useEffect(() => {
58
+ setIndex(0);
59
+ }, []);
60
+ useInput((input, key) => {
61
+ const cleanInput = input.replace(/[\r\n]+$/, "");
62
+ const isEnter = key.return || /[\r\n]+$/.test(input);
63
+ if (key.escape) {
64
+ onCancel();
65
+ return;
66
+ }
67
+ if (key.pageDown) {
68
+ if (filtered.length > 0)
69
+ setIndex((i) => Math.min(filtered.length - 1, i + maxListRows));
70
+ return;
71
+ }
72
+ if (key.pageUp) {
73
+ if (filtered.length > 0)
74
+ setIndex((i) => Math.max(0, i - maxListRows));
75
+ return;
76
+ }
77
+ if (filtered.length > 0) {
78
+ if (key.downArrow) {
79
+ setIndex((i) => (i + 1) % filtered.length);
80
+ return;
81
+ }
82
+ if (key.upArrow) {
83
+ setIndex((i) => (i - 1 + filtered.length) % filtered.length);
84
+ return;
85
+ }
86
+ }
87
+ if (isEnter) {
88
+ if (filtered.length > 0)
89
+ onSelect(filtered[shownIndex]);
90
+ else if (q !== "")
91
+ onSelect(q);
92
+ return;
93
+ }
94
+ if (key.backspace || key.delete) {
95
+ setQuery((v) => Array.from(v).slice(0, -1).join(""));
96
+ return;
97
+ }
98
+ if (key.ctrl && input === "u") {
99
+ setQuery("");
100
+ return;
101
+ }
102
+ if (key.ctrl || key.meta || key.tab)
103
+ return;
104
+ if (cleanInput)
105
+ setQuery((v) => v + cleanInput);
106
+ });
107
+ const loading = models === null;
108
+ const empty = models !== null && filtered.length === 0 && q === "";
109
+ // Debounce filter: avoid re-filtering on every keystroke burst
110
+ const debouncedQ = q;
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" }) })] }));
113
+ }
@@ -0,0 +1,136 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useMemo, useState } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import { wrapByWidth } from "./text-width.js";
5
+ import { footerMaxRows, innerTextWidth, QUESTION_CHROME_ROWS, sliceLineWindow } from "./layout.js";
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
+ }
16
+ /**
17
+ * Terminal rows the question bar occupies, mirroring the JSX layout below.
18
+ * Capped so a huge prompt cannot push the input row off the screen.
19
+ */
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));
23
+ }
24
+ /** Free-form question prompt: type an answer (or a number to pick an option), Enter to submit, Esc to cancel. */
25
+ export function QuestionBar({ prompt, options, onAnswer, columns, maxRows }) {
26
+ const [value, setValue] = useState("");
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);
34
+ useInput((input, key) => {
35
+ if (key.escape) {
36
+ onAnswer(null);
37
+ return;
38
+ }
39
+ if (key.return) {
40
+ if (/^\d+$/.test(value.trim()) && choices.length > 0) {
41
+ const n = parseInt(value.trim(), 10);
42
+ if (n >= 1 && n <= choices.length) {
43
+ onAnswer(choices[n - 1]);
44
+ return;
45
+ }
46
+ }
47
+ onAnswer(value.trim());
48
+ return;
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
+ }
69
+ if (key.leftArrow) {
70
+ setCaret((c) => Math.max(0, c - 1));
71
+ return;
72
+ }
73
+ if (key.rightArrow) {
74
+ setCaret((c) => Math.min(Array.from(value).length, c + 1));
75
+ return;
76
+ }
77
+ if (key.backspace || key.delete) {
78
+ if (key.backspace) {
79
+ if (caret === 0)
80
+ return;
81
+ const chars = Array.from(value);
82
+ chars.splice(caret - 1, 1);
83
+ setValue(chars.join(""));
84
+ setCaret((c) => c - 1);
85
+ }
86
+ else {
87
+ const chars = Array.from(value);
88
+ if (caret >= chars.length)
89
+ return;
90
+ chars.splice(caret, 1);
91
+ setValue(chars.join(""));
92
+ }
93
+ return;
94
+ }
95
+ if (key.ctrl && input === "a") {
96
+ setCaret(0);
97
+ return;
98
+ }
99
+ if (key.ctrl && input === "e") {
100
+ setCaret(Array.from(value).length);
101
+ return;
102
+ }
103
+ if (key.ctrl && input === "u") {
104
+ setValue("");
105
+ setCaret(0);
106
+ return;
107
+ }
108
+ if (key.ctrl && input === "k") {
109
+ const chars = Array.from(value);
110
+ chars.splice(caret);
111
+ setValue(chars.join(""));
112
+ return;
113
+ }
114
+ if (key.ctrl || key.meta)
115
+ return;
116
+ if (key.upArrow || key.downArrow || key.tab)
117
+ return;
118
+ if (input) {
119
+ const clean = input.replace(/[\r\n]+$/, "");
120
+ if (!clean)
121
+ return;
122
+ const chars = Array.from(value);
123
+ const ins = Array.from(clean);
124
+ chars.splice(caret, 0, ...ins);
125
+ setValue(chars.join(""));
126
+ setCaret((c) => c + ins.length);
127
+ }
128
+ });
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 }) })] }));
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
+ }
@@ -1,16 +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
- export function StatusBar({ state }) {
3
+ import { formatTokenStatusLabel } from "../token-display.js";
4
+ import { displayWidth } from "./text-width.js";
5
+ export function StatusBar({ state, columns = 80 }) {
4
6
  const model = state.model || "unknown";
5
- const tokens = state.tokenInfo;
6
- let bar = "";
7
- let barColor = "green";
8
- if (tokens && tokens.contextWindow > 0) {
9
- const pct = Math.round((tokens.input / tokens.contextWindow) * 100);
10
- const filled = Math.round(pct / 10);
11
- const empty = 10 - filled;
12
- bar = `${tokens.input}/${tokens.contextWindow} ${"".repeat(filled)}${"░".repeat(empty)} ${pct}%`;
13
- barColor = pct >= 80 ? "red" : pct >= 50 ? "yellow" : "green";
14
- }
15
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "─".repeat(60) }), _jsxs(Box, { children: [_jsx(Text, { children: " \uD83E\uDD16 min-agent" }), _jsxs(Text, { color: "gray", children: [" (", model, ")"] }), bar && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: barColor, children: bar })] }))] })] }));
7
+ const tokensLabel = state.tokenInfo ? formatTokenStatusLabel(state.tokenInfo) : "";
8
+ // Transient copy feedback rides on the separator row itself (right end) so
9
+ // it never adds a row and the layout does not jump while it is visible.
10
+ const sepWidth = Math.max(8, columns - 2);
11
+ const notice = state.copyNotice
12
+ ? `✓ ${state.copyNotice}`
13
+ : state.selectionMode
14
+ ? "选择模式 ↑↓←→扩选 Enter复制 Esc退出"
15
+ : null;
16
+ const noticeWidth = notice ? displayWidth(notice) : 0;
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 })] }))] })] }));
16
18
  }
@@ -0,0 +1,223 @@
1
+ export function createAgentRunner(deps) {
2
+ let abortController = null;
3
+ const getController = () => abortController;
4
+ const abort = (tui) => {
5
+ if (!abortController)
6
+ return false;
7
+ import("../tools/bash.js").then((m) => m.killActiveProcesses()).catch(() => { });
8
+ abortController.abort();
9
+ abortController = null;
10
+ tui.setRunning(false);
11
+ tui.addMessage({ id: `sys-${Date.now()}`, role: "system", content: "(已取消)", timestamp: Date.now() });
12
+ return true;
13
+ };
14
+ const run = async () => {
15
+ const turnStart = deps.messages.length;
16
+ const uiStart = deps.tui.messageCount();
17
+ let compactedDuringRun = false;
18
+ abortController = new AbortController();
19
+ deps.tui.setRunning(true, "思考中...");
20
+ const { summarizeToolCall, summarizeToolResult, toolResultText } = await import("../tool-display.js");
21
+ const { getContextWindowInfo } = await import("../context-window.js");
22
+ const { getModelPrice, estimateCost, formatCost } = await import("../pricing.js");
23
+ const { runOnceWithSystem, emptyResponseMessage, WRAP_UP_MESSAGE } = await import("../agent.js");
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();
59
+ const callbacks = {
60
+ onAssistantDisplayDelta: (delta) => deps.tui.appendToLast(delta),
61
+ onThinkingDelta: (delta) => {
62
+ deps.tui.setRunning(true, "推理中...");
63
+ deps.tui.appendToThinking(delta);
64
+ },
65
+ onToolCall: (toolName, input, toolCallId) => {
66
+ deps.tui.setRunning(true, `执行 ${toolName}...`);
67
+ deps.tui.addMessage({
68
+ id: `tool-${toolCallId || `${Date.now()}-${Math.random().toString(36).slice(2, 6)}`}`,
69
+ role: "tool",
70
+ content: summarizeToolCall(toolName, input),
71
+ toolName,
72
+ toolCallId,
73
+ timestamp: Date.now(),
74
+ });
75
+ },
76
+ onToolResult: (toolName, output, meta) => {
77
+ deps.tui.setRunning(true, "思考中...");
78
+ deps.tui.setToolResult(meta.toolCallId, toolName, summarizeToolResult(toolName, output, meta.isError), toolResultText(output), meta.isError);
79
+ },
80
+ onStreamError: (msg) => {
81
+ deps.tui.addMessage({ id: `err-${Date.now()}`, role: "system", content: `错误: ${msg}`, timestamp: Date.now() });
82
+ },
83
+ onCompaction: (line) => {
84
+ compactedDuringRun = true;
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
+ });
112
+ },
113
+ onRunFinish: async (info) => {
114
+ abortController = null;
115
+ deps.tui.setRunning(false);
116
+ await flushTokenInfo();
117
+ if (info.aborted)
118
+ return;
119
+ if (info.budgetExceeded)
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;
132
+ const price = await getModelPrice(deps.getModel() ?? "");
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
+ });
146
+ }
147
+ if (info.maxStepsReached)
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
+ });
182
+ },
183
+ };
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
+ });
193
+ const runOptions = {
194
+ providerName: deps.getProvider(),
195
+ taskState: deps.getTaskState(),
196
+ onStepPersist: () => saveThrottle.request(),
197
+ ...(deps.getPlanMode() ? { planMode: true } : {}),
198
+ };
199
+ await runOnceWithSystem(deps.messages, deps.getSystemPrompt(), deps.getModel(), abortController?.signal, callbacks, deps.tracker, runOptions);
200
+ }
201
+ catch (err) {
202
+ if (!abortController)
203
+ return;
204
+ const msg = err instanceof Error ? err.message : String(err);
205
+ callbacks.onStreamError(msg);
206
+ }
207
+ finally {
208
+ unsubscribeTokens();
209
+ if (tokenTimer != null) {
210
+ clearTimeout(tokenTimer);
211
+ tokenTimer = null;
212
+ }
213
+ setSubAgentListener(null);
214
+ saveThrottle.cancel();
215
+ if (!compactedDuringRun)
216
+ deps.setUndoStack(pushTurn(deps.getUndoStack(), turnStart, deps.messages.length, uiStart));
217
+ abortController = null;
218
+ deps.tui.setRunning(false);
219
+ await flushTokenInfo();
220
+ }
221
+ };
222
+ return { run, abort, getController };
223
+ }