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,177 @@
1
+ import { displayWidth, wrapByWidth, clustersOf } from "./text-width.js";
2
+ /** Split the value into visual rows, tracking each row's character start index (newlines count as one). */
3
+ export function visualRows(value, maxWidth) {
4
+ const rows = [];
5
+ let start = 0;
6
+ const lines = value.split("\n");
7
+ for (let i = 0; i < lines.length; i++) {
8
+ for (const row of wrapByWidth(lines[i], maxWidth)) {
9
+ rows.push({ text: row, startIndex: start });
10
+ start += Array.from(row).length;
11
+ }
12
+ if (i < lines.length - 1)
13
+ start += 1;
14
+ }
15
+ return rows;
16
+ }
17
+ /** Move the caret by one code point; clamps at the start/end of the value. */
18
+ export function moveCaretHorizontal(value, caretIndex, direction) {
19
+ const len = Array.from(value).length;
20
+ return Math.min(Math.max(caretIndex + direction, 0), len);
21
+ }
22
+ /**
23
+ * Map a click column (1-based terminal column) on a visual row to a character
24
+ * index in the value. Fullwidth characters snap to the nearer boundary.
25
+ */
26
+ export function caretIndexFromClick(rows, rowIndex, clickColumn, textStartColumn) {
27
+ const row = rows[rowIndex];
28
+ if (!row)
29
+ return 0;
30
+ const target = clickColumn - textStartColumn;
31
+ if (target <= 0)
32
+ return row.startIndex;
33
+ let width = 0;
34
+ let idx = 0;
35
+ for (const { text: g, width: w } of clustersOf(row.text)) {
36
+ if (width + w > target) {
37
+ return row.startIndex + idx + (target - width < w / 2 ? 0 : Array.from(g).length);
38
+ }
39
+ width += w;
40
+ idx += Array.from(g).length;
41
+ }
42
+ return row.startIndex + idx;
43
+ }
44
+ /** Insert text at the caret; returns the new value and caret index. */
45
+ export function insertAt(value, caretIndex, text) {
46
+ const chars = Array.from(value);
47
+ chars.splice(caretIndex, 0, ...Array.from(text));
48
+ return { value: chars.join(""), caretIndex: caretIndex + Array.from(text).length };
49
+ }
50
+ /** Remove the code point before the caret. */
51
+ export function backspaceAt(value, caretIndex) {
52
+ if (caretIndex <= 0)
53
+ return { value, caretIndex };
54
+ const chars = Array.from(value);
55
+ chars.splice(caretIndex - 1, 1);
56
+ return { value: chars.join(""), caretIndex: caretIndex - 1 };
57
+ }
58
+ /** Remove the code point at the caret. */
59
+ export function deleteAt(value, caretIndex) {
60
+ const chars = Array.from(value);
61
+ if (caretIndex >= chars.length)
62
+ return { value, caretIndex };
63
+ chars.splice(caretIndex, 1);
64
+ return { value: chars.join(""), caretIndex };
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
+ }
90
+ export function moveToLineStart(value, caretIndex) {
91
+ const chars = Array.from(value);
92
+ let i = caretIndex - 1;
93
+ while (i >= 0 && chars[i] !== "\n")
94
+ i--;
95
+ return i + 1;
96
+ }
97
+ export function moveToLineEnd(value, caretIndex) {
98
+ const chars = Array.from(value);
99
+ let i = caretIndex;
100
+ while (i < chars.length && chars[i] !== "\n")
101
+ i++;
102
+ return i;
103
+ }
104
+ export function deleteToLineStart(value, caretIndex) {
105
+ const start = moveToLineStart(value, caretIndex);
106
+ if (start === caretIndex)
107
+ return { value, caretIndex };
108
+ const chars = Array.from(value);
109
+ chars.splice(start, caretIndex - start);
110
+ return { value: chars.join(""), caretIndex: start };
111
+ }
112
+ export function deleteToLineEnd(value, caretIndex) {
113
+ const end = moveToLineEnd(value, caretIndex);
114
+ if (end === caretIndex)
115
+ return { value, caretIndex };
116
+ const chars = Array.from(value);
117
+ chars.splice(caretIndex, end - caretIndex);
118
+ return { value: chars.join(""), caretIndex };
119
+ }
120
+ export function deleteWordBefore(value, caretIndex) {
121
+ if (caretIndex <= 0)
122
+ return { value, caretIndex };
123
+ const chars = Array.from(value);
124
+ let i = caretIndex - 1;
125
+ while (i >= 0 && /\s/.test(chars[i]))
126
+ i--;
127
+ while (i >= 0 && !/\s/.test(chars[i]))
128
+ i--;
129
+ const start = i + 1;
130
+ chars.splice(start, caretIndex - start);
131
+ return { value: chars.join(""), caretIndex: start };
132
+ }
133
+ /** The longest escape sequence this scanner recognises, kept for the cap. */
134
+ const MAX_DELETE_PREFIX = 32;
135
+ /**
136
+ * Scan raw stdin bytes for Backspace (DEL, 0x7F — what most terminals send
137
+ * for the Backspace key) and Delete sequences (CSI 3 ~ and its modifier
138
+ * variants like CSI 3;5 ~ for Ctrl+Delete), which may span chunks. Ink
139
+ * reports all of them as `key.delete`, so they are distinguished here.
140
+ */
141
+ export function scanDeleteKeys(buffer, chunk) {
142
+ const buf = buffer + chunk;
143
+ const actions = [];
144
+ const re = /\x1b\[3(?:;\d+)?[~$^]|\x7f/g;
145
+ let lastConsumedEnd = 0;
146
+ let m;
147
+ while ((m = re.exec(buf))) {
148
+ lastConsumedEnd = m.index + m[0].length;
149
+ actions.push(m[0] === "\x7f" ? { kind: "backspace" } : { kind: "delete" });
150
+ }
151
+ const tail = buf.slice(lastConsumedEnd);
152
+ const esc = tail.lastIndexOf("\x1b");
153
+ if (esc === -1)
154
+ return { buffer: "", actions };
155
+ const prefix = tail.slice(esc);
156
+ return { buffer: prefix.length > MAX_DELETE_PREFIX ? "\x1b" : prefix, actions };
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
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Real terminal caret tracking for the TUI.
3
+ *
4
+ * Ink draws frames with `log-update`, which hides the hardware cursor and
5
+ * always leaves it at the end of the frame. That is fine for ASCII typing
6
+ * (the input bar paints its own `█` block), but an IME draws its preedit text
7
+ * and candidate window at the *hardware* cursor. With the cursor parked below
8
+ * the input box, Chinese input appears in the wrong place.
9
+ *
10
+ * The fix: after every frame Ink writes, move the hardware cursor onto the cell
11
+ * where the input bar painted its caret, and undo that move right before the
12
+ * next frame so Ink's own relative erase bookkeeping stays intact.
13
+ */
14
+ let caret = null;
15
+ /** Called by the input bar on every render; `null` disables caret tracking. */
16
+ export function setCaretPosition(position) {
17
+ caret = position;
18
+ }
19
+ /**
20
+ * Wrap a TTY stream so Ink's writes are followed by a caret move.
21
+ *
22
+ * Only the row offset needs undoing: Ink's frames always end at column 1
23
+ * (either after a trailing newline, or after `clearTerminal` repositions the
24
+ * cursor absolutely), so `\r` is enough to restore the column.
25
+ */
26
+ export function createCaretAwareStdout(base) {
27
+ let pendingRowsUp = 0;
28
+ const write = (chunk, encoding, callback) => {
29
+ const isBuf = Buffer?.isBuffer(chunk);
30
+ let chunkStr;
31
+ if (typeof chunk === "string")
32
+ chunkStr = chunk;
33
+ else if (isBuf)
34
+ chunkStr = chunk.toString(encoding || "utf-8");
35
+ else if (chunk != null)
36
+ chunkStr = String(chunk);
37
+ else
38
+ return base.write(chunk, encoding, callback);
39
+ let out = "";
40
+ if (pendingRowsUp > 0) {
41
+ out += `\x1b[${pendingRowsUp}B\r`;
42
+ pendingRowsUp = 0;
43
+ }
44
+ out += chunkStr;
45
+ const position = caret;
46
+ if (position) {
47
+ const rowsUp = position.rowsAbove + (chunkStr.endsWith("\n") ? 1 : 0);
48
+ if (rowsUp > 0) {
49
+ out += `\x1b[${rowsUp}A\x1b[${position.column}G`;
50
+ pendingRowsUp = rowsUp;
51
+ }
52
+ }
53
+ const finalChunk = typeof chunk === "string" ? out : Buffer.from(out, encoding || "utf-8");
54
+ return base.write(finalChunk, encoding, callback);
55
+ };
56
+ return new Proxy(base, {
57
+ get(target, prop) {
58
+ if (prop === "write")
59
+ return write;
60
+ // Read through to the real stream: getters like `columns`/`rows` and
61
+ // methods like `on`/`off` must run with the stream as `this`.
62
+ const value = Reflect.get(target, prop, target);
63
+ return typeof value === "function" ? value.bind(target) : value;
64
+ },
65
+ set(target, prop, value) {
66
+ return Reflect.set(target, prop, value, target);
67
+ },
68
+ });
69
+ }
@@ -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
+ }
@@ -0,0 +1,61 @@
1
+ import { execFile } from "child_process";
2
+ import { promisify } from "util";
3
+ const MAX_DIFF_CHARS = 8000;
4
+ const execFileAsync = promisify(execFile);
5
+ export async function getWorkingTreeDiff(cwd = process.cwd(), run = defaultRun) {
6
+ try {
7
+ await run("git rev-parse --is-inside-work-tree", cwd);
8
+ }
9
+ catch {
10
+ return { ok: false, reason: "not_git", message: "当前目录不是 git 仓库" };
11
+ }
12
+ let stat = "";
13
+ let full = "";
14
+ try {
15
+ stat = await run("git diff --stat", cwd);
16
+ full = await run("git diff", cwd);
17
+ }
18
+ catch (err) {
19
+ return { ok: false, reason: "error", message: err instanceof Error ? err.message : String(err) };
20
+ }
21
+ if (!full.trim())
22
+ return { ok: false, reason: "no_changes", message: "工作区无未提交变更" };
23
+ const body = full.length > MAX_DIFF_CHARS ? full.slice(0, MAX_DIFF_CHARS) + "\n… [diff 过长,已截断]" : full;
24
+ return { ok: true, diff: stat ? `${stat}\n\n${body}` : body };
25
+ }
26
+ async function defaultRun(cmd, cwd) {
27
+ const parts = parseCmd(cmd);
28
+ const { stdout } = await execFileAsync(parts[0], parts.slice(1), {
29
+ cwd,
30
+ encoding: "utf-8",
31
+ timeout: 10000,
32
+ maxBuffer: 10 * 1024 * 1024,
33
+ });
34
+ return stdout;
35
+ }
36
+ function parseCmd(cmd) {
37
+ const out = [];
38
+ let cur = "";
39
+ let q = null;
40
+ for (const ch of cmd) {
41
+ if (q) {
42
+ if (ch === q)
43
+ q = null;
44
+ else
45
+ cur += ch;
46
+ }
47
+ else if (ch === '"' || ch === "'")
48
+ q = ch;
49
+ else if (ch === " ") {
50
+ if (cur) {
51
+ out.push(cur);
52
+ cur = "";
53
+ }
54
+ }
55
+ else
56
+ cur += ch;
57
+ }
58
+ if (cur)
59
+ out.push(cur);
60
+ return out;
61
+ }
@@ -0,0 +1,49 @@
1
+ import { MOTION, MOD_MASK } from "./mouse.js";
2
+ export function createDragMachine() {
3
+ return { pending: null, selection: null };
4
+ }
5
+ export function clearDrag(machine) {
6
+ machine.pending = null;
7
+ machine.selection = null;
8
+ }
9
+ /**
10
+ * Advance the drag state machine on one SGR mouse event (wheel events are
11
+ * filtered by the caller before this runs). `maxHeight` is the full region
12
+ * allocated to the message list, not just its rendered rows — a press on the
13
+ * blank gap below a short conversation still starts a drag there, and the
14
+ * selection math clamps out-of-range rows.
15
+ *
16
+ * Terminals disagree on drag motion: xterm sends button + 32 (MOTION), but
17
+ * Apple Terminal reports it as a plain button-0 press. While a drag is in
18
+ * progress either form extends the selection instead of resetting it —
19
+ * otherwise every move would re-anchor and clear the highlight.
20
+ */
21
+ export function stepDrag(machine, event, maxHeight) {
22
+ const { button, row, col, press } = event;
23
+ const plain = (button & MOD_MASK) === 0;
24
+ const dragging = machine.pending != null;
25
+ if (plain && dragging && ((button & MOTION) !== 0 || (button === 0 && press))) {
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 } };
30
+ return { type: "selection", selection: machine.selection };
31
+ }
32
+ if (button === 0 && press) {
33
+ machine.selection = null;
34
+ machine.pending = row >= 1 && row <= maxHeight ? { row, col } : null;
35
+ return { type: "selection", selection: null };
36
+ }
37
+ if (button === 0 && !press) {
38
+ const active = machine.selection;
39
+ const pending = machine.pending;
40
+ machine.pending = null;
41
+ machine.selection = null;
42
+ if (active && (active.anchor.row !== active.cur.row || active.anchor.col !== active.cur.col)) {
43
+ return { type: "copy", selection: active };
44
+ }
45
+ if (pending)
46
+ return { type: "click", row: pending.row, col: pending.col };
47
+ }
48
+ return null;
49
+ }
@@ -0,0 +1,129 @@
1
+ import { summarizeToolCall, summarizeToolResult, toolResultText } from "../tool-display.js";
2
+ import { extractXmlSearch } from "../xml-search.js";
3
+ function isRecord(v) {
4
+ return typeof v === "object" && v !== null;
5
+ }
6
+ function textFromContent(content) {
7
+ if (typeof content === "string")
8
+ return content;
9
+ if (!Array.isArray(content))
10
+ return "";
11
+ const parts = [];
12
+ for (const part of content) {
13
+ if (isRecord(part) && part.type === "text" && typeof part.text === "string")
14
+ parts.push(part.text);
15
+ }
16
+ return parts.join("");
17
+ }
18
+ function hasMediaPart(content) {
19
+ if (!Array.isArray(content))
20
+ return false;
21
+ return content.some((part) => isRecord(part) && (part.type === "image" || part.type === "file"));
22
+ }
23
+ function unwrapToolOutput(output) {
24
+ if (!isRecord(output))
25
+ return output;
26
+ if ((output.type === "text" || output.type === "json") && "value" in output)
27
+ return output.value;
28
+ return output;
29
+ }
30
+ function userText(content) {
31
+ const text = textFromContent(content).trim();
32
+ if (text)
33
+ return textFromContent(content);
34
+ if (hasMediaPart(content))
35
+ return "[📎 图片]";
36
+ return null;
37
+ }
38
+ export function lastUserText(messages) {
39
+ for (let i = messages.length - 1; i >= 0; i--) {
40
+ const msg = messages[i];
41
+ if (msg?.role !== "user")
42
+ continue;
43
+ const text = textFromContent(msg.content).trim();
44
+ if (text)
45
+ return textFromContent(msg.content);
46
+ }
47
+ return null;
48
+ }
49
+ export function hydrateMessages(messages) {
50
+ const out = [];
51
+ for (let i = 0; i < messages.length; i++) {
52
+ const msg = messages[i];
53
+ if (msg.role === "assistant") {
54
+ out.push(...hydrateAssistant(msg, i));
55
+ continue;
56
+ }
57
+ const mapped = mapMessage(msg, i);
58
+ if (mapped)
59
+ out.push(mapped);
60
+ }
61
+ return out;
62
+ }
63
+ function hydrateAssistant(msg, index) {
64
+ const content = textFromContent(msg.content);
65
+ if (!content)
66
+ return [];
67
+ const { slices } = extractXmlSearch(content);
68
+ if (slices.every((sl) => sl.type === "display")) {
69
+ return [{ id: `hyd-${index}-assistant`, role: "assistant", content, timestamp: 0 }];
70
+ }
71
+ const out = [];
72
+ let part = 0;
73
+ for (const sl of slices) {
74
+ if (sl.type === "display") {
75
+ if (!sl.text.trim())
76
+ continue;
77
+ out.push({ id: `hyd-${index}-assistant-${part++}`, role: "assistant", content: sl.text, timestamp: 0 });
78
+ continue;
79
+ }
80
+ const query = sl.block.query;
81
+ const result = sl.block.kind === "results" ? sl.block.body : sl.block.query;
82
+ const input = { query };
83
+ out.push({
84
+ id: `hyd-${index}-search-${part++}`,
85
+ role: "tool",
86
+ content: summarizeToolCall("search_web", input),
87
+ toolName: "search_web",
88
+ toolCallId: `xml-search-hyd-${index}-${part}`,
89
+ toolResult: result,
90
+ toolResultSummary: summarizeToolResult("search_web", result),
91
+ timestamp: 0,
92
+ });
93
+ }
94
+ return out;
95
+ }
96
+ function mapMessage(msg, index) {
97
+ if (msg.role === "user") {
98
+ const content = userText(msg.content);
99
+ if (content == null)
100
+ return null;
101
+ return { id: `hyd-${index}-user`, role: "user", content, timestamp: 0 };
102
+ }
103
+ if (msg.role === "tool") {
104
+ if (!Array.isArray(msg.content))
105
+ return null;
106
+ for (const part of msg.content) {
107
+ if (!isRecord(part) || part.type !== "tool-result")
108
+ continue;
109
+ const toolName = typeof part.toolName === "string" ? part.toolName : "";
110
+ if (!toolName)
111
+ continue;
112
+ const output = unwrapToolOutput(part.output);
113
+ const result = toolResultText(output);
114
+ const toolCallId = typeof part.toolCallId === "string" ? part.toolCallId : undefined;
115
+ return {
116
+ id: `hyd-${index}-tool`,
117
+ role: "tool",
118
+ content: toolName,
119
+ toolName,
120
+ toolCallId,
121
+ toolResult: result,
122
+ toolResultSummary: summarizeToolResult(toolName, output),
123
+ timestamp: 0,
124
+ };
125
+ }
126
+ return null;
127
+ }
128
+ return null;
129
+ }