min-agent 0.4.0 → 0.5.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 (54) hide show
  1. package/README.md +169 -284
  2. package/dist/agent.js +36 -22
  3. package/dist/cli/commands/chat.js +3 -0
  4. package/dist/cli/commands/exec.js +3 -0
  5. package/dist/cli/commands/index.js +22 -5
  6. package/dist/cli/commands/memory.js +33 -15
  7. package/dist/cli/commands/think.js +12 -0
  8. package/dist/cli/commands/write-config.js +22 -0
  9. package/dist/cli/option-helpers.js +13 -1
  10. package/dist/cli/program.js +50 -13
  11. package/dist/code-mode.js +1 -1
  12. package/dist/config.js +41 -0
  13. package/dist/context-window.js +8 -28
  14. package/dist/memory-cli.js +33 -0
  15. package/dist/memory.js +127 -46
  16. package/dist/model-catalog.js +285 -0
  17. package/dist/permission-cli.js +1 -4
  18. package/dist/provider.js +4 -1
  19. package/dist/reasoning-stream.js +158 -0
  20. package/dist/sandbox-cli.js +1 -4
  21. package/dist/scope.js +23 -0
  22. package/dist/serve/common.js +22 -1
  23. package/dist/serve/routes-chat.js +21 -1
  24. package/dist/serve/routes-memory.js +31 -2
  25. package/dist/serve/routes-meta.js +34 -6
  26. package/dist/think-cli.js +36 -0
  27. package/dist/thinking-wire.js +228 -0
  28. package/dist/thinking.js +142 -0
  29. package/dist/token-display.js +10 -7
  30. package/dist/tools/todo.js +22 -8
  31. package/dist/tui/App.js +36 -8
  32. package/dist/tui/InputBar.js +109 -36
  33. package/dist/tui/MessageList.js +53 -22
  34. package/dist/tui/StatusBar.js +7 -3
  35. package/dist/tui/ThinkPicker.js +77 -0
  36. package/dist/tui/bracketed-paste.js +37 -0
  37. package/dist/tui/caret-pos.js +10 -8
  38. package/dist/tui/index.js +7 -1
  39. package/dist/tui/layout.js +17 -0
  40. package/dist/tui/overlay-input.js +12 -0
  41. package/dist/tui/paste-draft.js +173 -0
  42. package/dist/tui/selection.js +8 -2
  43. package/dist/tui/slash-commands.js +18 -1
  44. package/dist/tui/slash-handler.js +61 -17
  45. package/dist/tui/text-width.js +6 -6
  46. package/dist/tui-chat.js +63 -7
  47. package/docs/API.md +50 -4
  48. package/docs/superpowers/plans/2026-08-23-input-paste-attachments.md +475 -0
  49. package/docs/superpowers/plans/2026-08-23-thinking-wire-profile.md +450 -0
  50. package/docs/superpowers/specs/2026-08-23-input-paste-attachments-design.md +174 -0
  51. package/docs/superpowers/specs/2026-08-23-thinking-wire-profile-design.md +140 -0
  52. package/package.json +1 -1
  53. package/skills/self-config/SKILL.md +5 -4
  54. package/skills/self-config/reference.md +10 -5
@@ -1,11 +1,11 @@
1
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
- export function visualRows(value, maxWidth) {
3
+ export function visualRows(value, maxWidth, lookup) {
4
4
  const rows = [];
5
5
  let start = 0;
6
6
  const lines = value.split("\n");
7
7
  for (let i = 0; i < lines.length; i++) {
8
- for (const row of wrapByWidth(lines[i], maxWidth)) {
8
+ for (const row of wrapByWidth(lines[i], maxWidth, lookup)) {
9
9
  rows.push({ text: row, startIndex: start });
10
10
  start += Array.from(row).length;
11
11
  }
@@ -23,7 +23,7 @@ export function moveCaretHorizontal(value, caretIndex, direction) {
23
23
  * Map a click column (1-based terminal column) on a visual row to a character
24
24
  * index in the value. Fullwidth characters snap to the nearer boundary.
25
25
  */
26
- export function caretIndexFromClick(rows, rowIndex, clickColumn, textStartColumn) {
26
+ export function caretIndexFromClick(rows, rowIndex, clickColumn, textStartColumn, lookup) {
27
27
  const row = rows[rowIndex];
28
28
  if (!row)
29
29
  return 0;
@@ -32,8 +32,10 @@ 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 { text: g, width: w } of clustersOf(row.text)) {
35
+ for (const { text: g, width: w } of clustersOf(row.text, lookup)) {
36
36
  if (width + w > target) {
37
+ if (lookup?.(g) !== undefined)
38
+ return row.startIndex + idx;
37
39
  return row.startIndex + idx + (target - width < w / 2 ? 0 : Array.from(g).length);
38
40
  }
39
41
  width += w;
@@ -64,8 +66,8 @@ export function deleteAt(value, caretIndex) {
64
66
  return { value: chars.join(""), caretIndex };
65
67
  }
66
68
  /** 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
+ export function moveCaretVertical(value, caretIndex, direction, maxWidth, lookup) {
70
+ const rows = visualRows(value, maxWidth, lookup);
69
71
  let rowIndex = 0;
70
72
  let offset = 0;
71
73
  for (let i = 0; i < rows.length; i++) {
@@ -84,8 +86,8 @@ export function moveCaretVertical(value, caretIndex, direction, maxWidth) {
84
86
  const next = rowIndex + direction;
85
87
  if (next < 0 || next >= rows.length)
86
88
  return caretIndex;
87
- const col = displayWidth(Array.from(rows[rowIndex].text).slice(0, offset).join(""));
88
- return caretIndexFromClick(rows, next, col, 0);
89
+ const col = displayWidth(Array.from(rows[rowIndex].text).slice(0, offset).join(""), lookup);
90
+ return caretIndexFromClick(rows, next, col, 0, lookup);
89
91
  }
90
92
  export function moveToLineStart(value, caretIndex) {
91
93
  const chars = Array.from(value);
package/dist/tui/index.js CHANGED
@@ -28,7 +28,7 @@ export class TuiRenderer {
28
28
  this.callbacks = callbacks;
29
29
  }
30
30
  appElement() {
31
- return (_jsx(App, { initialState: this.state, onSubmit: this.callbacks.onSubmit, onConfirm: this.callbacks.onConfirm, onQuestionAnswer: this.callbacks.onQuestionAnswer, onModelPick: this.callbacks.onModelPick, onModelCancel: this.callbacks.onModelCancel, onSessionPick: this.callbacks.onSessionPick, onSessionCancel: this.callbacks.onSessionCancel, onToggleSelectionMode: this.callbacks.onToggleSelectionMode, onExitSelectionMode: this.callbacks.onExitSelectionMode, onExit: this.callbacks.onExit, onCopyNotice: (text) => this.showCopyNotice(text) }));
31
+ return (_jsx(App, { initialState: this.state, onSubmit: this.callbacks.onSubmit, onConfirm: this.callbacks.onConfirm, onQuestionAnswer: this.callbacks.onQuestionAnswer, onModelPick: this.callbacks.onModelPick, onModelCancel: this.callbacks.onModelCancel, onThinkPick: this.callbacks.onThinkPick, onThinkCancel: this.callbacks.onThinkCancel, onSessionPick: this.callbacks.onSessionPick, onSessionCancel: this.callbacks.onSessionCancel, onToggleSelectionMode: this.callbacks.onToggleSelectionMode, onExitSelectionMode: this.callbacks.onExitSelectionMode, onExit: this.callbacks.onExit, onCopyNotice: (text) => this.showCopyNotice(text), onNotice: this.callbacks.onNotice }));
32
32
  }
33
33
  start() {
34
34
  this.inkInstance = render(this.appElement(), { exitOnCtrlC: false, stdout: createCaretAwareStdout(process.stdout) });
@@ -191,6 +191,12 @@ export class TuiRenderer {
191
191
  hideModelPicker() {
192
192
  this.update({ modelPicker: false });
193
193
  }
194
+ showThinkPicker(scope = "global") {
195
+ this.update({ thinkPicker: { scope }, selectionMode: false });
196
+ }
197
+ hideThinkPicker() {
198
+ this.update({ thinkPicker: undefined });
199
+ }
194
200
  showSessionPicker() {
195
201
  this.update({ sessionPicker: true, selectionMode: false });
196
202
  }
@@ -4,6 +4,9 @@ export const TEXT_START_COLUMN = 5;
4
4
  export const STATUS_BAR_ROWS = 2;
5
5
  export const INPUT_BAR_ROWS = 3;
6
6
  export const MIN_MESSAGE_ROWS = 2;
7
+ export const SLASH_MENU_MIN_ITEMS = 4;
8
+ export const SLASH_COMMAND_MENU_CHROME = 5;
9
+ export const SLASH_ARG_MENU_CHROME = 4;
7
10
  /** Ink paddingLeft on every message row. */
8
11
  export const MESSAGE_PAD_LEFT = 1;
9
12
  /**
@@ -51,6 +54,20 @@ export function footerMaxRows(frameRowCount, minChrome) {
51
54
  return Number.POSITIVE_INFINITY;
52
55
  return Math.max(minChrome, frameRowCount - STATUS_BAR_ROWS - MIN_MESSAGE_ROWS);
53
56
  }
57
+ export function inputBarBoxRows(contentRows, hintRows) {
58
+ return Math.max(1, contentRows) + Math.max(0, hintRows) + 2;
59
+ }
60
+ export function inputBarPaintRows(contentRows, hintRows, menuShown, argShown) {
61
+ const menu = menuShown > 0 ? menuShown + SLASH_COMMAND_MENU_CHROME : argShown > 0 ? argShown + SLASH_ARG_MENU_CHROME : 0;
62
+ return inputBarBoxRows(contentRows, hintRows) + menu;
63
+ }
64
+ /** How many slash-menu rows fit without covering the status bar and a two-row message peek. */
65
+ export function slashMenuMaxVisible(frameRowCount, contentRows, hintRows, menuChromeRows) {
66
+ const cap = footerMaxRows(frameRowCount, INPUT_BAR_ROWS);
67
+ if (!Number.isFinite(cap))
68
+ return Number.POSITIVE_INFINITY;
69
+ return Math.max(SLASH_MENU_MIN_ITEMS, cap - inputBarBoxRows(contentRows, hintRows) - menuChromeRows);
70
+ }
54
71
  /** Rows a line window actually paints: content plus one indicator per hidden side. */
55
72
  export function lineWindowPaintRows(window) {
56
73
  return window.lines.length + (window.above > 0 ? 1 : 0) + (window.below > 0 ? 1 : 0);
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Pickers replace the input bar on the same Enter that opened them. Ink may
3
+ * still deliver that key (or a leftover LF after CR) to the newly mounted
4
+ * handler, which would auto-confirm the first row.
5
+ */
6
+ export function shouldAcceptOverlayConfirm(armed, key, input) {
7
+ if (!armed)
8
+ return false;
9
+ if (input === "\n" && !key.return)
10
+ return false;
11
+ return Boolean(key.return);
12
+ }
@@ -0,0 +1,173 @@
1
+ import { displayWidth, clustersOf } from "./text-width.js";
2
+ export const PASTE_CHIP_BASE = 0xe000;
3
+ export const SNIPPET_MAX_CHARS = 1_000_000;
4
+ export const IMAGE_MAX_BYTES = 5 * 1024 * 1024;
5
+ export const NOTICE_IMAGE_TOO_LARGE = "图片超过 5MB,未附加";
6
+ export const NOTICE_SNIPPET_TOO_LARGE = "粘贴内容过大,未附加";
7
+ const PUA_START = 0xe000;
8
+ const PUA_END = 0xf8ff;
9
+ const UNMAPPED = "�";
10
+ export function createPasteDraft() {
11
+ return {
12
+ nextCodePoint: PASTE_CHIP_BASE,
13
+ nextImageId: 1,
14
+ nextSnippetId: 1,
15
+ attachments: new Map(),
16
+ };
17
+ }
18
+ export function resetPasteDraft() {
19
+ return createPasteDraft();
20
+ }
21
+ export function imageLabel(id) {
22
+ return `[图片 #${id}]`;
23
+ }
24
+ export function snippetLabel(id, lineCount) {
25
+ return `[粘贴 #${id} · ${lineCount} 行]`;
26
+ }
27
+ export function normalizePastedText(raw) {
28
+ const normalized = raw.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
29
+ return normalized.endsWith("\n") ? normalized.slice(0, -1) : normalized;
30
+ }
31
+ export function applyPastedText(draft, raw) {
32
+ const text = normalizePastedText(raw);
33
+ if (text.length > SNIPPET_MAX_CHARS) {
34
+ return { kind: "reject", notice: NOTICE_SNIPPET_TOO_LARGE, draft };
35
+ }
36
+ if (!text.includes("\n")) {
37
+ return { kind: "text", text, draft };
38
+ }
39
+ const { char, draft: next } = allocChip(draft, {
40
+ kind: "snippet",
41
+ id: draft.nextSnippetId,
42
+ text,
43
+ lineCount: text.split("\n").length,
44
+ });
45
+ return { kind: "snippet", char, draft: next };
46
+ }
47
+ export function applyClipboardImage(draft, image) {
48
+ if (image == null)
49
+ return { kind: "none", draft };
50
+ if (image.data.length > IMAGE_MAX_BYTES) {
51
+ return { kind: "reject", notice: NOTICE_IMAGE_TOO_LARGE, draft };
52
+ }
53
+ const { char, draft: next } = allocChip(draft, {
54
+ kind: "image",
55
+ id: draft.nextImageId,
56
+ data: image.data,
57
+ mimeType: image.mimeType,
58
+ });
59
+ return { kind: "image", char, draft: next };
60
+ }
61
+ export function pruneAttachments(draft, value) {
62
+ const present = new Set(Array.from(value));
63
+ const attachments = new Map([...draft.attachments].filter(([char]) => present.has(char)));
64
+ if (attachments.size === draft.attachments.size)
65
+ return draft;
66
+ return { ...draft, attachments };
67
+ }
68
+ export function expandDisplay(value, draft) {
69
+ return Array.from(value)
70
+ .map((char) => {
71
+ const att = draft.attachments.get(char);
72
+ if (att)
73
+ return attachmentLabel(att);
74
+ return isPrivateUse(char) ? UNMAPPED : char;
75
+ })
76
+ .join("");
77
+ }
78
+ export function expandContent(value, draft) {
79
+ const parts = [];
80
+ let textBuf = "";
81
+ const flushText = () => {
82
+ if (!textBuf)
83
+ return;
84
+ parts.push({ type: "text", text: textBuf });
85
+ textBuf = "";
86
+ };
87
+ for (const char of Array.from(value)) {
88
+ const att = draft.attachments.get(char);
89
+ if (att?.kind === "image") {
90
+ flushText();
91
+ parts.push({ type: "image", image: att.data, mimeType: att.mimeType });
92
+ continue;
93
+ }
94
+ if (att?.kind === "snippet") {
95
+ flushText();
96
+ parts.push({ type: "text", text: snippetBlock(att.id, att.text, parts) });
97
+ continue;
98
+ }
99
+ textBuf += isPrivateUse(char) ? UNMAPPED : char;
100
+ }
101
+ flushText();
102
+ if (!parts.some((part) => part.type === "image")) {
103
+ return parts.map((part) => (part.type === "text" ? part.text : "")).join("");
104
+ }
105
+ return parts;
106
+ }
107
+ export function buildSubmittedPrompt(value, draft) {
108
+ return { display: expandDisplay(value, draft), content: expandContent(value, draft) };
109
+ }
110
+ export function chipClusterWidth(cluster, draft, maxWidth) {
111
+ const att = draft.attachments.get(cluster);
112
+ if (!att)
113
+ return undefined;
114
+ return Math.min(displayWidth(attachmentLabel(att)), maxWidth);
115
+ }
116
+ export function paintedChip(cluster, draft, maxWidth) {
117
+ const att = draft.attachments.get(cluster);
118
+ if (!att)
119
+ return undefined;
120
+ return truncateToWidth(attachmentLabel(att), maxWidth);
121
+ }
122
+ export function paintChar(char, draft, maxWidth) {
123
+ return paintedChip(char, draft, maxWidth) ?? (isPrivateUse(char) ? UNMAPPED : char);
124
+ }
125
+ function attachmentLabel(att) {
126
+ return att.kind === "image" ? imageLabel(att.id) : snippetLabel(att.id, att.lineCount);
127
+ }
128
+ function snippetBlock(id, text, previous) {
129
+ const body = `--- 粘贴 #${id} ---\n${text}\n--- 粘贴 #${id} ---`;
130
+ if (previous.length === 0)
131
+ return body;
132
+ const last = previous[previous.length - 1];
133
+ if (last?.type === "text" && last.text.endsWith("\n"))
134
+ return body;
135
+ return `\n${body}`;
136
+ }
137
+ function allocChip(draft, attachment) {
138
+ const char = String.fromCodePoint(draft.nextCodePoint);
139
+ const attachments = new Map(draft.attachments);
140
+ attachments.set(char, attachment);
141
+ return {
142
+ char,
143
+ draft: {
144
+ nextCodePoint: draft.nextCodePoint + 1,
145
+ nextImageId: attachment.kind === "image" ? draft.nextImageId + 1 : draft.nextImageId,
146
+ nextSnippetId: attachment.kind === "snippet" ? draft.nextSnippetId + 1 : draft.nextSnippetId,
147
+ attachments,
148
+ },
149
+ };
150
+ }
151
+ function isPrivateUse(char) {
152
+ const cp = char.codePointAt(0);
153
+ return cp !== undefined && cp >= PUA_START && cp <= PUA_END;
154
+ }
155
+ function truncateToWidth(text, maxWidth) {
156
+ if (maxWidth < 1)
157
+ return "";
158
+ if (displayWidth(text) <= maxWidth)
159
+ return text;
160
+ const ellipsis = "…";
161
+ const ellipsisW = displayWidth(ellipsis);
162
+ let out = "";
163
+ let width = 0;
164
+ for (const { text: g, width: w } of clustersOf(text)) {
165
+ if (width + w + ellipsisW > maxWidth)
166
+ break;
167
+ out += g;
168
+ width += w;
169
+ }
170
+ out += ellipsis;
171
+ width += ellipsisW;
172
+ return width < maxWidth ? `${out}${" ".repeat(maxWidth - width)}` : out;
173
+ }
@@ -97,11 +97,13 @@ export function stripCopyDecorations(line, kind) {
97
97
  return line.slice(2);
98
98
  if (kind === "tool-head")
99
99
  return line.replace(/^⚡ .*?[▸▾] /, "");
100
- if (kind === "tool-body" && line.startsWith(" "))
100
+ if (kind === "thinking-head")
101
+ return line.replace(/^💭 .*?[▸▾] /, "");
102
+ if ((kind === "tool-body" || kind === "thinking-body") && line.startsWith(" "))
101
103
  return line.slice(2);
102
104
  return line;
103
105
  }
104
- /** Visible prefix rendered in its own Ink Text (user `> `, tool `⚡ name ▸ `). */
106
+ /** Visible prefix rendered in its own Ink Text (user `> `, tool `⚡ name ▸ `, thinking `💭 思考 ▸ `). */
105
107
  export function visualPrefix(line, role) {
106
108
  if (role === "user" && line.startsWith("> "))
107
109
  return "> ";
@@ -109,6 +111,10 @@ export function visualPrefix(line, role) {
109
111
  const m = line.match(/^⚡ .*?[▸▾] /);
110
112
  return m ? m[0] : "";
111
113
  }
114
+ if (role === "thinking") {
115
+ const m = line.match(/^💭 .*?[▸▾] /);
116
+ return m ? m[0] : "";
117
+ }
112
118
  return "";
113
119
  }
114
120
  /** Split a display-column range across a prefix of `at` cells. */
@@ -4,10 +4,11 @@ export const SLASH_COMMANDS = [
4
4
  { name: "model", description: "选择/切换模型" },
5
5
  { name: "models", description: "列出可用模型" },
6
6
  { name: "provider", description: "查看/切换 Provider", argHint: "[name]" },
7
- { name: "memory", description: "查看/保存记忆", argHint: "[t] [--project]" },
7
+ { name: "memory", description: "查看/开关记忆,或保存一条", argHint: "[on|off|t] [--project]" },
8
8
  { name: "tokens", description: "显示上下文占用、累计用量与成本" },
9
9
  { name: "budget", description: "查看/设置预算上限", argHint: "[n] [--project]" },
10
10
  { name: "permission", description: "查看/设置确认模式", argHint: "[ask|accept-edits|allow-all] [--project]" },
11
+ { name: "think", description: "选择思考强度", aliases: ["thinking"] },
11
12
  { name: "sandbox", description: "查看/设置隔离", argHint: "[off|workspace|strict] [--project]" },
12
13
  { name: "undo", description: "撤销上一轮" },
13
14
  { name: "redo", description: "重发最后一条消息" },
@@ -34,6 +35,22 @@ export function filterSlashCommands(input, commands = SLASH_COMMANDS) {
34
35
  return commands;
35
36
  return commands.filter((c) => c.name.startsWith(q) || (c.aliases ?? []).some((a) => a.startsWith(q)));
36
37
  }
38
+ /** Keep `selectedIndex` on screen; the highlight stays on the last row once the list scrolls. */
39
+ export function slashMenuWindow(items, selectedIndex, maxVisible) {
40
+ const max = Number.isFinite(maxVisible) ? Math.max(1, Math.floor(maxVisible)) : Math.max(1, items.length);
41
+ const index = items.length === 0 ? 0 : Math.min(Math.max(0, selectedIndex), items.length - 1);
42
+ if (items.length <= max) {
43
+ return { start: 0, shown: [...items], extraAbove: 0, extraBelow: 0 };
44
+ }
45
+ const start = Math.max(0, Math.min(index - max + 1, items.length - max));
46
+ const shown = items.slice(start, start + max);
47
+ return {
48
+ start,
49
+ shown,
50
+ extraAbove: start,
51
+ extraBelow: items.length - (start + shown.length),
52
+ };
53
+ }
37
54
  /** True for submitted input that is a known slash command (including aliases). */
38
55
  export function isKnownSlashCommandInput(text, commands = SLASH_COMMANDS) {
39
56
  if (!text.startsWith("/"))
@@ -215,34 +215,50 @@ export async function handleSlashCommand(input, tui, messages, currentModel, tra
215
215
  break;
216
216
  }
217
217
  case "memory": {
218
- const { takeScopeFlags, defaultMemoryScope } = await import("../memory.js");
218
+ const { takeScopeFlags } = await import("../scope.js");
219
+ const { addMemory, loadMemories, defaultMemoryScope, formatMemoryLine, parseMemoryMode, resolveMemoryMode, setMemoryMode, setMemoryOverride, isMemoryEnabled, } = await import("../memory.js");
219
220
  const { scope, rest: memRest } = takeScopeFlags(rest);
221
+ const parsedMode = memRest.length === 1 ? parseMemoryMode(memRest[0]) : undefined;
222
+ if (parsedMode) {
223
+ const resolved = scope === "project" ? "project" : "global";
224
+ setMemoryMode(parsedMode, resolved);
225
+ setMemoryOverride(parsedMode);
226
+ sysMsg(tui, `✓ 记忆已${parsedMode === "on" ? "开启" : "关闭"}(${resolved === "project" ? "项目" : "全局"})`);
227
+ break;
228
+ }
220
229
  const text = memRest.join(" ").trim();
221
230
  if (text) {
222
- const { addMemory } = await import("../memory.js");
223
231
  const resolved = scope ?? defaultMemoryScope();
224
232
  addMemory(text, [], resolved);
225
- sysMsg(tui, `✓ 已保存${resolved === "project" ? "项目" : "全局"}记忆: "${text}"`);
233
+ const hint = isMemoryEnabled() ? "" : "(当前未开启,下一轮对话不会用到;可用 /memory on 开启)";
234
+ sysMsg(tui, `✓ 已保存${resolved === "project" ? "项目" : "全局"}记忆: "${text}"${hint}`);
226
235
  }
227
236
  else {
228
- const { loadMemories } = await import("../memory.js");
237
+ const snap = resolveMemoryMode();
238
+ const sourceLabel = snap.source === "cli"
239
+ ? "本次启动参数"
240
+ : snap.source === "project"
241
+ ? "项目"
242
+ : snap.source === "global"
243
+ ? "全局"
244
+ : "默认";
245
+ const status = `记忆:${snap.memory === "on" ? "开" : "关"}(${sourceLabel})`;
229
246
  const scopes = scope ? [scope] : ["project", "global"];
230
247
  const sections = scopes.map((s) => {
231
248
  const memories = loadMemories(s);
232
249
  const label = s === "project" ? "项目记忆" : "全局记忆";
233
250
  if (memories.length === 0)
234
251
  return `${label}: 无`;
235
- const lines = memories.map((m, i) => {
236
- const tags = m.tags.length > 0 ? ` [${m.tags.join(", ")}]` : "";
237
- return ` #${i + 1}: ${m.content}${tags}`;
238
- });
252
+ const lines = memories.map((m, i) => formatMemoryLine(m, i));
239
253
  return `${label} (${memories.length}):\n${lines.join("\n")}`;
240
254
  });
241
- if (!scope && loadMemories("project").length === 0 && loadMemories("global").length === 0) {
242
- sysMsg(tui, "无已保存的记忆");
243
- }
244
- else
245
- sysMsg(tui, sections.join("\n"));
255
+ const empty = !scope && loadMemories("project").length === 0 && loadMemories("global").length === 0;
256
+ sysMsg(tui, [
257
+ status,
258
+ empty ? "无已保存的记忆" : sections.join("\n"),
259
+ "设置: /memory on|off [--project]",
260
+ "保存: /memory <文本> [--project]",
261
+ ].join("\n"));
246
262
  }
247
263
  break;
248
264
  }
@@ -288,7 +304,7 @@ export async function handleSlashCommand(input, tui, messages, currentModel, tra
288
304
  break;
289
305
  }
290
306
  case "budget": {
291
- const { takeScopeFlags } = await import("../memory.js");
307
+ const { takeScopeFlags } = await import("../scope.js");
292
308
  const { getBudgetSnapshot, setBudgetMaxCost } = await import("../config.js");
293
309
  const { getModelPrice, estimateCost, formatCost } = await import("../pricing.js");
294
310
  const { scope, rest: budgetRest } = takeScopeFlags(rest);
@@ -315,7 +331,7 @@ export async function handleSlashCommand(input, tui, messages, currentModel, tra
315
331
  break;
316
332
  }
317
333
  case "permission": {
318
- const { takeScopeFlags } = await import("../memory.js");
334
+ const { takeScopeFlags } = await import("../scope.js");
319
335
  const { getPermissionSnapshot, parsePermissionMode, permissionModeLabel, setPermissionMode } = await import("../config.js");
320
336
  const { getPermissionOverride, setPermissionOverride } = await import("../confirm.js");
321
337
  const { scope, rest: permRest } = takeScopeFlags(rest);
@@ -342,6 +358,31 @@ export async function handleSlashCommand(input, tui, messages, currentModel, tra
342
358
  sysMsg(tui, `✓ 确认已设为${permissionModeLabel(parsed)}(${resolved === "project" ? "项目" : "全局"})`);
343
359
  break;
344
360
  }
361
+ case "think":
362
+ case "thinking": {
363
+ const { takeScopeFlags } = await import("../scope.js");
364
+ const { parseThinkingEffort, thinkingEffortLabel, thinkingChoicesForModel, setThinkingEffort, setThinkingOverride, } = await import("../thinking.js");
365
+ const { scope, rest: thinkRest } = takeScopeFlags(rest);
366
+ if (thinkRest.length === 0) {
367
+ tui.showThinkPicker(scope === "project" ? "project" : "global");
368
+ break;
369
+ }
370
+ const parsed = parseThinkingEffort(thinkRest[0]);
371
+ const choices = thinkingChoicesForModel(currentModel);
372
+ if (!parsed || thinkRest.length > 1) {
373
+ sysMsg(tui, `用法: /think [${choices.join("|")}] [--project]`);
374
+ break;
375
+ }
376
+ if (!choices.includes(parsed)) {
377
+ sysMsg(tui, `该模型支持: ${choices.join(", ")}`);
378
+ break;
379
+ }
380
+ const resolved = scope === "project" ? "project" : "global";
381
+ setThinkingEffort(parsed, resolved);
382
+ setThinkingOverride(parsed);
383
+ sysMsg(tui, `✓ 思考强度已设为 ${thinkingEffortLabel(parsed)}(${resolved === "project" ? "项目" : "全局"})`);
384
+ break;
385
+ }
345
386
  case "mcp": {
346
387
  const { loadMcpConfig } = await import("../mcp.js");
347
388
  const { getMcpStatus } = await import("../mcp.js");
@@ -439,7 +480,7 @@ export async function handleSlashCommand(input, tui, messages, currentModel, tra
439
480
  break;
440
481
  }
441
482
  case "sandbox": {
442
- const { takeScopeFlags } = await import("../memory.js");
483
+ const { takeScopeFlags } = await import("../scope.js");
443
484
  const { getSandboxSnapshot, setSandboxConfig } = await import("../config.js");
444
485
  const { parseSandboxMode, parseNetworkPolicy, getEffectiveSandboxPolicy, sandboxEnforcementCaveat, sandboxModeLabel, setSandboxOverride, } = await import("../sandbox.js");
445
486
  const { scope, rest: sbRest } = takeScopeFlags(rest);
@@ -501,10 +542,11 @@ export async function handleSlashCommand(input, tui, messages, currentModel, tra
501
542
  " /model [n] 交互式选择/切换模型",
502
543
  " /models 列出可用模型",
503
544
  " /provider [n] 查看/切换 Provider",
504
- " /memory [t] 查看/保存记忆(--project 写入当前项目)",
545
+ " /memory 查看记忆开关与已保存内容(/memory on|off 开关;也可 /memory 文本 保存)",
505
546
  " /tokens 显示上下文占用、累计用量与成本",
506
547
  " /budget [n] 查看/设置预算上限(--project 写入当前项目)",
507
548
  " /permission 查看/设置确认模式(ask|accept-edits|allow-all,--project 写入当前项目)",
549
+ " /think 交互式选择思考强度(档位随当前模型;也可 /think off|low|medium|high|max)",
508
550
  " /sandbox 查看/设置隔离(off|workspace|strict,--project 写入当前项目)",
509
551
  " /undo 撤销上一轮",
510
552
  " /redo 重发最后一条消息",
@@ -525,6 +567,8 @@ export async function handleSlashCommand(input, tui, messages, currentModel, tra
525
567
  "快捷键:",
526
568
  " Esc 取消当前任务",
527
569
  " Ctrl+J 输入框换行",
570
+ " Ctrl+V 粘贴剪贴板图片到输入框",
571
+ " 多行粘贴 收成片段,发送时带上全文",
528
572
  " Ctrl+C 清空输入(↑↓ 可找回);生成中则中断,空输入连按两次退出",
529
573
  " Ctrl+B 消息区选择模式",
530
574
  " Home/End 行首/行尾",
@@ -75,15 +75,15 @@ export function clusterWidth(cluster) {
75
75
  export function charWidth(char) {
76
76
  return clusterWidth(char);
77
77
  }
78
- export function* clustersOf(text) {
78
+ export function* clustersOf(text, lookup) {
79
79
  for (const { segment } of segmenter.segment(text)) {
80
- yield { text: segment, width: clusterWidth(segment) };
80
+ yield { text: segment, width: lookup?.(segment) ?? clusterWidth(segment) };
81
81
  }
82
82
  }
83
83
  /** Display width of a string (single logical line, no ANSI escapes). */
84
- export function displayWidth(text) {
84
+ export function displayWidth(text, lookup) {
85
85
  let width = 0;
86
- for (const { width: w } of clustersOf(text))
86
+ for (const { width: w } of clustersOf(text, lookup))
87
87
  width += w;
88
88
  return width;
89
89
  }
@@ -92,14 +92,14 @@ export function displayWidth(text) {
92
92
  * Breaks on cluster boundaries (not word boundaries) so the caret column can
93
93
  * be derived exactly — a fullwidth character or emoji is never split across rows.
94
94
  */
95
- export function wrapByWidth(line, maxWidth) {
95
+ export function wrapByWidth(line, maxWidth, lookup) {
96
96
  const text = typeof line === "string" ? line : line == null ? "" : String(line);
97
97
  if (maxWidth < 1)
98
98
  return [text];
99
99
  const rows = [];
100
100
  let current = "";
101
101
  let width = 0;
102
- for (const { text: g, width: w } of clustersOf(text)) {
102
+ for (const { text: g, width: w } of clustersOf(text, lookup)) {
103
103
  if (width + w > maxWidth && current.length > 0) {
104
104
  rows.push(current);
105
105
  current = "";