min-agent 0.4.1 → 0.5.1

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 (69) hide show
  1. package/README.md +46 -2
  2. package/dist/agent.js +89 -29
  3. package/dist/cli/commands/chat.js +3 -0
  4. package/dist/cli/commands/ctx.js +7 -0
  5. package/dist/cli/commands/exec.js +3 -0
  6. package/dist/cli/commands/index.js +32 -7
  7. package/dist/cli/commands/memory.js +33 -15
  8. package/dist/cli/commands/setup.js +55 -3
  9. package/dist/cli/commands/shared.js +10 -1
  10. package/dist/cli/commands/think.js +12 -0
  11. package/dist/cli/commands/write-config.js +22 -0
  12. package/dist/cli/option-helpers.js +13 -1
  13. package/dist/cli/program.js +57 -14
  14. package/dist/cli/setup/detect.js +17 -0
  15. package/dist/cli/setup/flags.js +12 -0
  16. package/dist/cli/setup/flow.js +108 -0
  17. package/dist/cli/setup/provider-form.js +102 -0
  18. package/dist/cli/setup/ui.js +534 -0
  19. package/dist/code-mode.js +1 -1
  20. package/dist/config.js +93 -159
  21. package/dist/context-window.js +39 -49
  22. package/dist/ctx-cli.js +30 -0
  23. package/dist/ctx.js +80 -0
  24. package/dist/memory-cli.js +33 -0
  25. package/dist/memory.js +127 -46
  26. package/dist/model-catalog.js +285 -0
  27. package/dist/ollama-model.js +234 -0
  28. package/dist/ollama-openai-bridge.js +383 -0
  29. package/dist/permission-cli.js +1 -4
  30. package/dist/provider.js +4 -1
  31. package/dist/reasoning-stream.js +158 -0
  32. package/dist/sandbox-cli.js +1 -4
  33. package/dist/scope.js +23 -0
  34. package/dist/serve/common.js +22 -1
  35. package/dist/serve/routes-chat.js +21 -1
  36. package/dist/serve/routes-memory.js +31 -2
  37. package/dist/serve/routes-meta.js +69 -6
  38. package/dist/think-cli.js +36 -0
  39. package/dist/thinking-wire.js +239 -0
  40. package/dist/thinking.js +166 -0
  41. package/dist/token-display.js +10 -7
  42. package/dist/tools/todo.js +22 -8
  43. package/dist/tui/App.js +48 -8
  44. package/dist/tui/CtxPicker.js +68 -0
  45. package/dist/tui/InputBar.js +112 -37
  46. package/dist/tui/MessageList.js +53 -22
  47. package/dist/tui/StatusBar.js +7 -3
  48. package/dist/tui/ThinkPicker.js +75 -0
  49. package/dist/tui/bracketed-paste.js +37 -0
  50. package/dist/tui/caret-pos.js +10 -8
  51. package/dist/tui/index.js +13 -1
  52. package/dist/tui/layout.js +17 -0
  53. package/dist/tui/overlay-input.js +12 -0
  54. package/dist/tui/paste-draft.js +173 -0
  55. package/dist/tui/selection.js +8 -2
  56. package/dist/tui/slash-commands.js +24 -1
  57. package/dist/tui/slash-handler.js +88 -18
  58. package/dist/tui/text-width.js +6 -6
  59. package/dist/tui-chat.js +85 -7
  60. package/docs/API.md +69 -6
  61. package/docs/superpowers/plans/2026-08-23-cli-setup.md +501 -0
  62. package/docs/superpowers/plans/2026-08-23-input-paste-attachments.md +475 -0
  63. package/docs/superpowers/plans/2026-08-23-thinking-wire-profile.md +450 -0
  64. package/docs/superpowers/specs/2026-08-23-cli-setup-design.md +282 -0
  65. package/docs/superpowers/specs/2026-08-23-input-paste-attachments-design.md +174 -0
  66. package/docs/superpowers/specs/2026-08-23-thinking-wire-profile-design.md +140 -0
  67. package/package.json +1 -1
  68. package/skills/self-config/SKILL.md +7 -4
  69. package/skills/self-config/reference.md +12 -6
@@ -0,0 +1,239 @@
1
+ import { AsyncLocalStorage } from "async_hooks";
2
+ import { readFileSync, mkdirSync, existsSync } from "fs";
3
+ import path from "path";
4
+ import { getActiveProvider, getConfigDir, getEffectiveConfig } from "./config.js";
5
+ import { fetchOllamaNativeChat, readFetchBody } from "./ollama-openai-bridge.js";
6
+ import { atomicWriteFileSync } from "./tools/atomic-file.js";
7
+ export const thinkingRequestStore = new AsyncLocalStorage();
8
+ export const THINKING_WIRE_VERSION = 1;
9
+ export const THINKING_WIRE_TTL = 7 * 24 * 60 * 60 * 1000;
10
+ export const DEFAULT_THINKING_WIRE = {
11
+ version: THINKING_WIRE_VERSION,
12
+ thinkingType: "enabled-disabled",
13
+ effort: "reasoning_effort",
14
+ };
15
+ const memoryCache = new Map();
16
+ function isRecord(value) {
17
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
18
+ }
19
+ function isThinkingTypeMode(value) {
20
+ return value === "enabled-disabled" || value === "adaptive-disabled" || value === "omit";
21
+ }
22
+ function isEffortMode(value) {
23
+ return value === "reasoning_effort" || value === "omit";
24
+ }
25
+ function isProfile(value) {
26
+ if (!isRecord(value))
27
+ return false;
28
+ return (value.version === THINKING_WIRE_VERSION &&
29
+ isThinkingTypeMode(value.thinkingType) &&
30
+ isEffortMode(value.effort) &&
31
+ typeof value.timestamp === "number" &&
32
+ Number.isFinite(value.timestamp));
33
+ }
34
+ function cacheFile() {
35
+ return path.join(getConfigDir(), "thinking-wire-cache.json");
36
+ }
37
+ function loadDiskCache() {
38
+ const file = cacheFile();
39
+ if (!existsSync(file))
40
+ return {};
41
+ try {
42
+ const parsed = JSON.parse(readFileSync(file, "utf-8"));
43
+ if (!isRecord(parsed))
44
+ return {};
45
+ const out = {};
46
+ for (const [key, value] of Object.entries(parsed)) {
47
+ if (isProfile(value))
48
+ out[key] = value;
49
+ }
50
+ return out;
51
+ }
52
+ catch {
53
+ return {};
54
+ }
55
+ }
56
+ function saveDiskCache(cache) {
57
+ const file = cacheFile();
58
+ mkdirSync(path.dirname(file), { recursive: true });
59
+ atomicWriteFileSync(file, JSON.stringify(cache));
60
+ }
61
+ function liveProfile(entry, now) {
62
+ if (entry.version !== THINKING_WIRE_VERSION)
63
+ return null;
64
+ if (now - entry.timestamp > THINKING_WIRE_TTL)
65
+ return null;
66
+ return entry;
67
+ }
68
+ export function thinkingWireCacheKey(host, modelId) {
69
+ const h = host.trim().toLowerCase();
70
+ const m = modelId.trim().toLowerCase();
71
+ if (!h)
72
+ return m;
73
+ if (!m)
74
+ return h;
75
+ return `${h}::${m}`;
76
+ }
77
+ export function inferThinkingWire(modelId, host, providerType) {
78
+ if (providerType === "ollama") {
79
+ return { thinkingType: "omit", effort: "reasoning_effort" };
80
+ }
81
+ const haystack = `${modelId} ${host}`.toLowerCase();
82
+ if (haystack.includes("minimax")) {
83
+ return { thinkingType: "adaptive-disabled", effort: "reasoning_effort" };
84
+ }
85
+ if (providerType === "openai") {
86
+ return { thinkingType: "omit", effort: "reasoning_effort" };
87
+ }
88
+ return { thinkingType: DEFAULT_THINKING_WIRE.thinkingType, effort: DEFAULT_THINKING_WIRE.effort };
89
+ }
90
+ function readCachedProfile(key, now) {
91
+ const memory = memoryCache.get(key);
92
+ if (memory) {
93
+ const live = liveProfile(memory, now);
94
+ if (live)
95
+ return live;
96
+ memoryCache.delete(key);
97
+ }
98
+ const disk = loadDiskCache()[key];
99
+ if (!disk)
100
+ return null;
101
+ const live = liveProfile(disk, now);
102
+ if (!live)
103
+ return null;
104
+ memoryCache.set(key, live);
105
+ return live;
106
+ }
107
+ export function resolveThinkingWireProfile(host, modelId, providerType, now = Date.now()) {
108
+ const cached = readCachedProfile(thinkingWireCacheKey(host, modelId), now);
109
+ if (cached)
110
+ return cached;
111
+ return {
112
+ version: THINKING_WIRE_VERSION,
113
+ ...inferThinkingWire(modelId, host, providerType),
114
+ timestamp: now,
115
+ };
116
+ }
117
+ export function saveLearnedThinkingWire(host, modelId, profile, now = Date.now()) {
118
+ const entry = {
119
+ version: THINKING_WIRE_VERSION,
120
+ thinkingType: profile.thinkingType,
121
+ effort: profile.effort,
122
+ timestamp: profile.timestamp ?? now,
123
+ };
124
+ const key = thinkingWireCacheKey(host, modelId);
125
+ memoryCache.set(key, entry);
126
+ const cache = loadDiskCache();
127
+ cache[key] = entry;
128
+ saveDiskCache(cache);
129
+ return entry;
130
+ }
131
+ export function clearThinkingWireCache() {
132
+ memoryCache.clear();
133
+ }
134
+ export function applyThinkingToChatBody(body, spec, profile = DEFAULT_THINKING_WIRE) {
135
+ const next = { ...body };
136
+ if (profile.effort === "omit")
137
+ delete next.reasoning_effort;
138
+ else
139
+ next.reasoning_effort = spec.reasoningEffort;
140
+ if (profile.thinkingType === "omit")
141
+ delete next.thinking;
142
+ else {
143
+ const on = profile.thinkingType === "adaptive-disabled" ? "adaptive" : "enabled";
144
+ next.thinking = { type: spec.thinkingEnabled ? on : "disabled" };
145
+ }
146
+ return next;
147
+ }
148
+ function splitAllowed(raw) {
149
+ return new Set(raw
150
+ .split(",")
151
+ .map((part) => part
152
+ .trim()
153
+ .replace(/^['"]|['"]$/g, "")
154
+ .toLowerCase())
155
+ .filter(Boolean));
156
+ }
157
+ export function parseThinkingWireHint(text) {
158
+ const allowedMatch = text.match(/thinking\.type[\s\S]*?allowed:\s*([^)]+)/i);
159
+ if (allowedMatch?.[1]) {
160
+ const allowed = splitAllowed(allowedMatch[1]);
161
+ if (allowed.has("adaptive") && allowed.has("disabled"))
162
+ return { thinkingType: "adaptive-disabled" };
163
+ if (allowed.has("enabled") && allowed.has("disabled"))
164
+ return { thinkingType: "enabled-disabled" };
165
+ }
166
+ if (/unknown parameter[:\s]+['"`]?thinking['"`]?(?![\w.])/i.test(text))
167
+ return { thinkingType: "omit" };
168
+ if (/unknown parameter[:\s]+['"`]?reasoning_effort['"`]?(?![\w.])/i.test(text))
169
+ return { effort: "omit" };
170
+ return null;
171
+ }
172
+ function hostnameFromInput(input) {
173
+ try {
174
+ const raw = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
175
+ return new URL(raw).hostname;
176
+ }
177
+ catch {
178
+ return "";
179
+ }
180
+ }
181
+ function rebuild400(response, text) {
182
+ return new Response(text, {
183
+ status: 400,
184
+ statusText: response.statusText,
185
+ headers: response.headers,
186
+ });
187
+ }
188
+ function profileUnchanged(current, next) {
189
+ return current.thinkingType === next.thinkingType && current.effort === next.effort;
190
+ }
191
+ export async function fetchWithThinkingWire(inner, input, init, opts) {
192
+ if (!init?.body)
193
+ return inner(input, init);
194
+ const rawBody = await readFetchBody(init.body);
195
+ if (rawBody == null)
196
+ return inner(input, init);
197
+ let parsed;
198
+ try {
199
+ parsed = JSON.parse(rawBody);
200
+ }
201
+ catch {
202
+ return inner(input, init);
203
+ }
204
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
205
+ return inner(input, init);
206
+ const spec = thinkingRequestStore.getStore();
207
+ const providerType = opts?.providerType ?? getActiveProvider(getEffectiveConfig())?.type;
208
+ const body = parsed;
209
+ if (providerType === "ollama")
210
+ return fetchOllamaNativeChat(inner, input, init, body, spec);
211
+ if (!spec)
212
+ return inner(input, init);
213
+ const host = hostnameFromInput(input);
214
+ const modelId = typeof body.model === "string" ? body.model : "";
215
+ const profile = resolveThinkingWireProfile(host, modelId, providerType);
216
+ const first = await inner(input, {
217
+ ...init,
218
+ body: JSON.stringify(applyThinkingToChatBody(body, spec, profile)),
219
+ });
220
+ if (first.status !== 400)
221
+ return first;
222
+ const text = await first.text();
223
+ const hint = parseThinkingWireHint(text);
224
+ if (!hint)
225
+ return rebuild400(first, text);
226
+ const next = {
227
+ version: THINKING_WIRE_VERSION,
228
+ thinkingType: hint.thinkingType ?? profile.thinkingType,
229
+ effort: hint.effort ?? profile.effort,
230
+ timestamp: Date.now(),
231
+ };
232
+ if (profileUnchanged(profile, next))
233
+ return rebuild400(first, text);
234
+ saveLearnedThinkingWire(host, modelId, next);
235
+ return inner(input, {
236
+ ...init,
237
+ body: JSON.stringify(applyThinkingToChatBody(body, spec, next)),
238
+ });
239
+ }
@@ -0,0 +1,166 @@
1
+ import { getEffectiveConfig, loadConfig, loadProjectConfig, saveConfig, saveProjectConfig, getActiveProvider, parseThinkingEffort, } from "./config.js";
2
+ import { getCachedModelCatalog, getModelCatalog } from "./model-catalog.js";
3
+ import { getCachedOllamaModel, getOllamaModel, thinkingChoicesFromOllama } from "./ollama-model.js";
4
+ import { applyThinkingToChatBody, thinkingRequestStore } from "./thinking-wire.js";
5
+ export { parseThinkingEffort, applyThinkingToChatBody, thinkingRequestStore };
6
+ export const THINKING_EFFORTS = ["off", "low", "medium", "high", "max"];
7
+ export const DEFAULT_THINKING_EFFORT = "medium";
8
+ let thinkingOverride;
9
+ export function setThinkingOverride(effort) {
10
+ thinkingOverride = effort;
11
+ }
12
+ export function getThinkingOverride() {
13
+ return thinkingOverride;
14
+ }
15
+ export function thinkingEffortLabel(effort) {
16
+ return effort;
17
+ }
18
+ export function thinkingChoiceIndex(effort, choices = THINKING_EFFORTS) {
19
+ const list = choices.length > 0 ? choices : THINKING_EFFORTS;
20
+ const clamped = clampThinkingEffort(effort, list);
21
+ const i = list.indexOf(clamped);
22
+ return i >= 0 ? i : 0;
23
+ }
24
+ export function thinkingChoicesFromReasoning(info) {
25
+ const set = new Set();
26
+ if (info.toggle || info.efforts.includes("none"))
27
+ set.add("off");
28
+ for (const raw of info.efforts) {
29
+ const mapped = parseThinkingEffort(raw);
30
+ if (mapped)
31
+ set.add(mapped);
32
+ }
33
+ const enabled = THINKING_EFFORTS.filter((effort) => effort !== "off" && set.has(effort));
34
+ // Toggle-only catalog rows must not collapse the picker to off.
35
+ if (enabled.length === 0)
36
+ return [...THINKING_EFFORTS];
37
+ return THINKING_EFFORTS.filter((effort) => set.has(effort));
38
+ }
39
+ export function thinkingChoicesForModel(modelId) {
40
+ const provider = getActiveProvider(getEffectiveConfig());
41
+ if (provider?.type === "ollama") {
42
+ if (!modelId)
43
+ return [...THINKING_EFFORTS];
44
+ const cached = getCachedOllamaModel(modelId, provider.baseURL);
45
+ if (!cached)
46
+ return [...THINKING_EFFORTS];
47
+ return thinkingChoicesFromOllama(cached);
48
+ }
49
+ if (!modelId)
50
+ return [...THINKING_EFFORTS];
51
+ const entry = getCachedModelCatalog(modelId);
52
+ if (!entry)
53
+ return [...THINKING_EFFORTS];
54
+ return thinkingChoicesFromReasoning(entry.reasoning);
55
+ }
56
+ export async function refreshThinkingChoices(modelId, hint) {
57
+ const provider = getActiveProvider(getEffectiveConfig());
58
+ const type = hint?.type ?? provider?.type;
59
+ const baseURL = hint?.baseURL ?? provider?.baseURL;
60
+ if (modelId) {
61
+ if (type === "ollama")
62
+ await getOllamaModel(modelId, baseURL);
63
+ else
64
+ await getModelCatalog(modelId, baseURL);
65
+ }
66
+ return thinkingChoicesForModel(modelId);
67
+ }
68
+ export function clampThinkingEffort(effort, choices = THINKING_EFFORTS) {
69
+ if (choices.length === 0 || choices.includes(effort))
70
+ return effort;
71
+ if (choices.includes("medium"))
72
+ return "medium";
73
+ if (choices.includes("high"))
74
+ return "high";
75
+ const enabled = choices.filter((item) => item !== "off");
76
+ if (enabled.length > 0)
77
+ return enabled[Math.floor((enabled.length - 1) / 2)];
78
+ if (choices.includes("off"))
79
+ return "off";
80
+ return DEFAULT_THINKING_EFFORT;
81
+ }
82
+ export function thinkingSourceLabel(source) {
83
+ if (source === "cli")
84
+ return "本次启动参数";
85
+ if (source === "project")
86
+ return "项目";
87
+ if (source === "global")
88
+ return "全局";
89
+ return "默认";
90
+ }
91
+ export function resolveThinkingEffort(runOverride, modelId) {
92
+ let thinking;
93
+ let source;
94
+ if (runOverride) {
95
+ thinking = runOverride;
96
+ source = "cli";
97
+ }
98
+ else {
99
+ const override = getThinkingOverride();
100
+ if (override) {
101
+ thinking = override;
102
+ source = "cli";
103
+ }
104
+ else {
105
+ const snap = getThinkingSnapshot();
106
+ thinking = snap.thinking ?? DEFAULT_THINKING_EFFORT;
107
+ source = snap.source;
108
+ }
109
+ }
110
+ const id = modelId ?? getActiveProvider(getEffectiveConfig())?.defaultModel;
111
+ return { thinking: clampThinkingEffort(thinking, thinkingChoicesForModel(id)), source };
112
+ }
113
+ export function getThinkingSnapshot() {
114
+ const project = parseThinkingEffort(loadProjectConfig().thinking);
115
+ if (project !== undefined)
116
+ return { thinking: project, source: "project" };
117
+ const global = parseThinkingEffort(loadConfig().thinking);
118
+ if (global !== undefined)
119
+ return { thinking: global, source: "global" };
120
+ return { thinking: undefined, source: null };
121
+ }
122
+ function clearProjectThinking() {
123
+ const project = loadProjectConfig();
124
+ if (project.thinking === undefined)
125
+ return;
126
+ const { thinking: _removed, ...rest } = project;
127
+ saveProjectConfig(rest);
128
+ }
129
+ export function setThinkingEffort(effort, scope) {
130
+ if (scope === "project") {
131
+ saveProjectConfig({ ...loadProjectConfig(), thinking: effort });
132
+ return;
133
+ }
134
+ saveConfig({ ...loadConfig(), thinking: effort });
135
+ // A leftover project value (including off) would keep shadowing the global file.
136
+ clearProjectThinking();
137
+ }
138
+ export function thinkingPayload() {
139
+ const { thinking, source } = resolveThinkingEffort();
140
+ return {
141
+ thinking,
142
+ source,
143
+ label: thinkingEffortLabel(thinking),
144
+ };
145
+ }
146
+ export function toWireEffort(effort, providerType, modelId) {
147
+ if (effort === "off")
148
+ return "none";
149
+ if (effort !== "max")
150
+ return effort;
151
+ const catalog = modelId ? getCachedModelCatalog(modelId) : undefined;
152
+ const supported = catalog?.reasoning.efforts ?? [];
153
+ if (supported.includes("max"))
154
+ return "max";
155
+ if (supported.includes("xhigh") || providerType === "openai")
156
+ return "xhigh";
157
+ return "max";
158
+ }
159
+ export function resolveThinkingRequest(opts) {
160
+ const { thinking } = resolveThinkingEffort(opts?.runOverride, opts?.modelId);
161
+ const providerType = opts?.providerType ?? getActiveProvider(getEffectiveConfig())?.type;
162
+ return {
163
+ reasoningEffort: toWireEffort(thinking, providerType, opts?.modelId),
164
+ thinkingEnabled: thinking !== "off",
165
+ };
166
+ }
@@ -24,13 +24,16 @@ export function tokenInfoFromTracker(tracker, contextWindow, estimatedContext =
24
24
  };
25
25
  }
26
26
  export function formatTokenStatusLabel(info) {
27
- const parts = [];
28
27
  const pct = contextUsagePercent(info.context, info.contextWindow);
29
- if (pct != null) {
30
- parts.push(`${formatTokenCount(info.context)}/${formatTokenCount(info.contextWindow)} ${pct}%`);
31
- }
32
- if (info.input > 0 || info.output > 0) {
33
- parts.push(`累计 ${formatTokenCount(info.input)} in · ${formatTokenCount(info.output)} out`);
34
- }
28
+ if (pct == null)
29
+ return "";
30
+ return `${formatTokenCount(info.context)}/${formatTokenCount(info.contextWindow)} ${pct}%`;
31
+ }
32
+ export function formatModelStatusMeta(thinkingLabel, contextWindow, memoryOn = false) {
33
+ const parts = [`思考 ${thinkingLabel}`];
34
+ if (memoryOn)
35
+ parts.push("记忆 开");
36
+ if (contextWindow && contextWindow > 0)
37
+ parts.push(`窗口 ${formatTokenCount(contextWindow)}`);
35
38
  return parts.join(" · ");
36
39
  }
@@ -61,24 +61,37 @@ function messageText(msg) {
61
61
  .map((p) => p.text)
62
62
  .join("\n");
63
63
  }
64
+ const CHITCHAT_RE = /^(你好|您好|嗨|哈喽|哈罗|哈囉|在吗|在嗎|早上好|晚上好|早安|午安|hello|hi|hey|yo|sup|thanks|thankyou|thx|ty|谢谢|谢谢你|多谢|感谢|ok|okay|好的|收到)$/i;
65
+ /** Greetings / acknowledgements are not a durable task. "你好,帮我修 bug" is. */
66
+ export function isChitchatUserText(text) {
67
+ const stripped = text.trim().replace(/[\s\p{P}\p{S}\p{Emoji_Presentation}\p{Extended_Pictographic}]/gu, "");
68
+ return stripped.length === 0 || CHITCHAT_RE.test(stripped);
69
+ }
70
+ function durableGoal(text) {
71
+ const trimmed = text.trim();
72
+ if (!trimmed || isChitchatUserText(trimmed))
73
+ return undefined;
74
+ return trimmed.slice(0, GOAL_MAX_CHARS);
75
+ }
64
76
  /** Keep the first real user request so compaction cannot drop the original goal. */
65
77
  export function captureGoal(state, messages) {
66
- if (state.goal.trim())
78
+ if (state.goal.trim() && !isChitchatUserText(state.goal))
67
79
  return;
68
80
  for (const msg of messages) {
69
81
  if (msg.role !== "user")
70
82
  continue;
71
83
  if (isSyntheticMessage(msg))
72
84
  continue;
73
- const text = messageText(msg).trim();
74
- if (!text)
85
+ const goal = durableGoal(messageText(msg));
86
+ if (!goal)
75
87
  continue;
76
- state.goal = text.slice(0, GOAL_MAX_CHARS);
88
+ state.goal = goal;
77
89
  return;
78
90
  }
79
91
  }
80
92
  export function formatTaskStatePrompt(state) {
81
- if (!state.goal && state.todos.length === 0)
93
+ const goal = state.goal.trim() && !isChitchatUserText(state.goal) ? state.goal : "";
94
+ if (!goal && state.todos.length === 0)
82
95
  return "";
83
96
  const icons = {
84
97
  pending: "pending",
@@ -88,10 +101,11 @@ export function formatTaskStatePrompt(state) {
88
101
  };
89
102
  const lines = [
90
103
  "## Current Task",
91
- "This block is the durable task state. It survives context compaction. Keep it accurate with the todo tool.",
104
+ "This block tracks progress for the current user request. It is not a reason to start extra work.",
105
+ "If the task list is missing or empty, there is no pending work to resume. Follow the current user message only. Keep the list accurate with the todo tool.",
92
106
  ];
93
- if (state.goal) {
94
- lines.push("", "### Original goal", state.goal);
107
+ if (goal) {
108
+ lines.push("", "### Original goal", goal);
95
109
  }
96
110
  if (state.todos.length > 0) {
97
111
  lines.push("", "### Task list");
package/dist/tui/App.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useState } from "react";
2
+ import { useCallback, useEffect, useState } from "react";
3
3
  import { Box, useInput, useStdout } from "ink";
4
4
  import { Spinner } from "./Spinner.js";
5
5
  import { InputBar } from "./InputBar.js";
@@ -8,11 +8,13 @@ import { ConfirmBar, confirmBarRows } from "./ConfirmBar.js";
8
8
  import { QuestionBar, questionBarRows } from "./QuestionBar.js";
9
9
  import { StatusBar } from "./StatusBar.js";
10
10
  import { ModelPicker, modelPickerRows } from "./ModelPicker.js";
11
+ import { ThinkPicker, thinkPickerRows } from "./ThinkPicker.js";
12
+ import { CtxPicker, ctxPickerRows } from "./CtxPicker.js";
11
13
  import { SessionPicker, sessionPickerRows } from "./SessionPicker.js";
12
14
  import { setCaretPosition } from "./caret.js";
13
15
  import { computeMessageMaxHeight, frameRows, INPUT_BAR_ROWS } from "./layout.js";
14
16
  import { isCtrlC } from "./input-history.js";
15
- export function App({ initialState, onSubmit, onConfirm, onQuestionAnswer, onModelPick, onModelCancel, onSessionPick, onSessionCancel, onToggleSelectionMode, onExitSelectionMode, onExit, onCopyNotice, }) {
17
+ export function App({ initialState, onSubmit, onConfirm, onQuestionAnswer, onModelPick, onModelCancel, onThinkPick, onThinkCancel, onCtxPick, onCtxCancel, onSessionPick, onSessionCancel, onToggleSelectionMode, onExitSelectionMode, onExit, onCopyNotice, onNotice, }) {
16
18
  // Ink recalculates its own layout on resize without re-rendering React, so
17
19
  // track terminal size ourselves to keep heights/widths in sync.
18
20
  const { stdout } = useStdout();
@@ -38,6 +40,14 @@ export function App({ initialState, onSubmit, onConfirm, onQuestionAnswer, onMod
38
40
  onModelCancel();
39
41
  return;
40
42
  }
43
+ if (initialState.thinkPicker) {
44
+ onThinkCancel();
45
+ return;
46
+ }
47
+ if (initialState.ctxPicker) {
48
+ onCtxCancel();
49
+ return;
50
+ }
41
51
  if (initialState.sessionPicker) {
42
52
  onSessionCancel();
43
53
  return;
@@ -55,6 +65,8 @@ export function App({ initialState, onSubmit, onConfirm, onQuestionAnswer, onMod
55
65
  if (initialState.confirmMessage ||
56
66
  initialState.question ||
57
67
  initialState.modelPicker ||
68
+ initialState.thinkPicker ||
69
+ initialState.ctxPicker ||
58
70
  initialState.sessionPicker)
59
71
  return;
60
72
  if (initialState.isRunning)
@@ -69,30 +81,58 @@ export function App({ initialState, onSubmit, onConfirm, onQuestionAnswer, onMod
69
81
  !initialState.confirmMessage &&
70
82
  !initialState.question &&
71
83
  !initialState.modelPicker &&
84
+ !initialState.thinkPicker &&
85
+ !initialState.ctxPicker &&
72
86
  !initialState.sessionPicker) {
73
87
  onExit();
74
88
  }
75
89
  });
76
- if (initialState.confirmMessage || initialState.question || initialState.modelPicker || initialState.sessionPicker)
90
+ if (initialState.confirmMessage ||
91
+ initialState.question ||
92
+ initialState.modelPicker ||
93
+ initialState.thinkPicker ||
94
+ initialState.ctxPicker ||
95
+ initialState.sessionPicker)
77
96
  setCaretPosition(null);
78
97
  const columns = terminal.columns;
79
98
  const terminalRows = terminal.rows;
80
99
  const layoutRows = frameRows(terminalRows);
100
+ const [inputBarRows, setInputBarRows] = useState(INPUT_BAR_ROWS);
101
+ const onInputBarRowsChange = useCallback((rows) => {
102
+ setInputBarRows((prev) => (prev === rows ? prev : rows));
103
+ }, []);
81
104
  const confirmActive = Boolean(initialState.confirmMessage);
82
105
  const questionActive = Boolean(initialState.question);
83
106
  const modelPickerActive = Boolean(initialState.modelPicker);
107
+ const thinkPickerActive = Boolean(initialState.thinkPicker);
108
+ const ctxPickerActive = Boolean(initialState.ctxPicker);
84
109
  const sessionPickerActive = Boolean(initialState.sessionPicker);
110
+ const overlayActive = confirmActive || questionActive || modelPickerActive || thinkPickerActive || ctxPickerActive || sessionPickerActive;
111
+ useEffect(() => {
112
+ if (overlayActive)
113
+ setInputBarRows(INPUT_BAR_ROWS);
114
+ }, [overlayActive]);
85
115
  const footerRows = confirmActive
86
116
  ? confirmBarRows(initialState.confirmMessage ?? "", columns, layoutRows)
87
117
  : questionActive
88
118
  ? questionBarRows(initialState.question.prompt, initialState.question.options, columns, layoutRows)
89
119
  : modelPickerActive
90
120
  ? modelPickerRows(layoutRows)
91
- : sessionPickerActive
92
- ? sessionPickerRows(layoutRows)
93
- : INPUT_BAR_ROWS;
94
- const spinnerVisible = initialState.isRunning && !confirmActive && !questionActive && !modelPickerActive && !sessionPickerActive;
121
+ : thinkPickerActive
122
+ ? thinkPickerRows()
123
+ : ctxPickerActive
124
+ ? ctxPickerRows()
125
+ : sessionPickerActive
126
+ ? sessionPickerRows(layoutRows)
127
+ : inputBarRows;
128
+ const spinnerVisible = initialState.isRunning &&
129
+ !confirmActive &&
130
+ !questionActive &&
131
+ !modelPickerActive &&
132
+ !thinkPickerActive &&
133
+ !ctxPickerActive &&
134
+ !sessionPickerActive;
95
135
  const spinnerRows = spinnerVisible ? 1 : 0;
96
136
  const messageMaxHeight = computeMessageMaxHeight(layoutRows, footerRows, spinnerRows);
97
- return (_jsxs(Box, { flexDirection: "column", width: "100%", height: layoutRows || undefined, overflow: "hidden", children: [!modelPickerActive && (_jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, minHeight: 0, overflow: "hidden", width: "100%", children: _jsx(MessageList, { messages: initialState.messages, maxHeight: messageMaxHeight, columns: columns, onCopyNotice: onCopyNotice, selectionMode: Boolean(initialState.selectionMode), onExitSelectionMode: onExitSelectionMode, interactive: !confirmActive && !questionActive }) })), spinnerVisible && (_jsx(Box, { paddingLeft: 1, flexShrink: 0, width: "100%", children: _jsx(Spinner, { label: initialState.spinnerText || "思考中..." }) })), _jsx(StatusBar, { state: initialState, columns: columns }), initialState.confirmMessage ? (_jsx(ConfirmBar, { message: initialState.confirmMessage, onConfirm: onConfirm, columns: columns, maxRows: footerRows })) : initialState.question ? (_jsx(QuestionBar, { prompt: initialState.question.prompt, options: initialState.question.options, onAnswer: onQuestionAnswer, columns: columns, maxRows: footerRows })) : initialState.modelPicker ? (_jsx(ModelPicker, { currentModel: initialState.model, onSelect: onModelPick, onCancel: onModelCancel })) : initialState.sessionPicker ? (_jsx(SessionPicker, { currentId: initialState.sessionId, onSelect: onSessionPick, onCancel: onSessionCancel })) : (_jsx(InputBar, { onSubmit: onSubmit, disabled: initialState.isRunning || Boolean(initialState.selectionMode), placeholder: initialState.isRunning ? "按 Esc 取消运行..." : "输入消息... (Ctrl+J 换行)", columns: columns, terminalRows: layoutRows, onSelectionMode: onToggleSelectionMode, onExit: onExit }))] }));
137
+ return (_jsxs(Box, { flexDirection: "column", width: "100%", height: layoutRows || undefined, overflow: "hidden", children: [!modelPickerActive && (_jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, minHeight: 0, overflow: "hidden", width: "100%", children: _jsx(MessageList, { messages: initialState.messages, maxHeight: messageMaxHeight, columns: columns, onCopyNotice: onCopyNotice, selectionMode: Boolean(initialState.selectionMode), onExitSelectionMode: onExitSelectionMode, interactive: !confirmActive && !questionActive }) })), spinnerVisible && (_jsx(Box, { paddingLeft: 1, flexShrink: 0, width: "100%", children: _jsx(Spinner, { label: initialState.spinnerText || "思考中..." }) })), _jsx(StatusBar, { state: initialState, columns: columns }), initialState.confirmMessage ? (_jsx(ConfirmBar, { message: initialState.confirmMessage, onConfirm: onConfirm, columns: columns, maxRows: footerRows })) : initialState.question ? (_jsx(QuestionBar, { prompt: initialState.question.prompt, options: initialState.question.options, onAnswer: onQuestionAnswer, columns: columns, maxRows: footerRows })) : initialState.modelPicker ? (_jsx(ModelPicker, { currentModel: initialState.model, onSelect: onModelPick, onCancel: onModelCancel })) : initialState.thinkPicker ? (_jsx(ThinkPicker, { scope: initialState.thinkPicker.scope, modelId: initialState.model, onSelect: (effort) => onThinkPick(effort, initialState.thinkPicker.scope), onCancel: onThinkCancel })) : initialState.ctxPicker ? (_jsx(CtxPicker, { onSelect: onCtxPick, onCancel: onCtxCancel })) : initialState.sessionPicker ? (_jsx(SessionPicker, { currentId: initialState.sessionId, onSelect: onSessionPick, onCancel: onSessionCancel })) : (_jsx(InputBar, { onSubmit: onSubmit, disabled: initialState.isRunning || Boolean(initialState.selectionMode), placeholder: initialState.isRunning ? "按 Esc 取消运行..." : "输入消息... (Ctrl+J 换行)", columns: columns, terminalRows: layoutRows, onSelectionMode: onToggleSelectionMode, onExit: onExit, onRowsChange: onInputBarRowsChange, onNotice: onNotice }))] }));
98
138
  }
@@ -0,0 +1,68 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import { getActiveProvider, loadConfig } from "../config.js";
5
+ import { CTX_CHOICES, ctxChoiceLabel, matchCtxLevel } from "../ctx.js";
6
+ import { formatTokenCount } from "../token-display.js";
7
+ import { shouldAcceptOverlayConfirm } from "./overlay-input.js";
8
+ import { theme } from "./theme.js";
9
+ export function ctxPickerRows() {
10
+ return 2 + 1 + 1 + CTX_CHOICES.length + 1;
11
+ }
12
+ function configuredTokens() {
13
+ const n = getActiveProvider(loadConfig())?.contextWindow;
14
+ if (typeof n === "number" && Number.isFinite(n) && n > 0)
15
+ return Math.floor(n);
16
+ return undefined;
17
+ }
18
+ function currentLabel() {
19
+ const tokens = configuredTokens();
20
+ if (tokens == null)
21
+ return ctxChoiceLabel("auto");
22
+ return matchCtxLevel(tokens) ?? formatTokenCount(tokens);
23
+ }
24
+ function markedChoice() {
25
+ const tokens = configuredTokens();
26
+ if (tokens == null)
27
+ return "auto";
28
+ return matchCtxLevel(tokens);
29
+ }
30
+ export function CtxPicker({ onSelect, onCancel }) {
31
+ const marked = markedChoice();
32
+ const [index, setIndex] = useState(() => {
33
+ const i = marked ? CTX_CHOICES.indexOf(marked) : -1;
34
+ return i >= 0 ? i : CTX_CHOICES.length - 1;
35
+ });
36
+ const armed = useRef(false);
37
+ const indexRef = useRef(index);
38
+ indexRef.current = index;
39
+ useEffect(() => {
40
+ const timer = setTimeout(() => {
41
+ armed.current = true;
42
+ }, 0);
43
+ return () => clearTimeout(timer);
44
+ }, []);
45
+ useInput((input, key) => {
46
+ if (key.escape) {
47
+ onCancel();
48
+ return;
49
+ }
50
+ if (key.downArrow) {
51
+ setIndex((i) => (i + 1) % CTX_CHOICES.length);
52
+ return;
53
+ }
54
+ if (key.upArrow) {
55
+ setIndex((i) => (i - 1 + CTX_CHOICES.length) % CTX_CHOICES.length);
56
+ return;
57
+ }
58
+ if (shouldAcceptOverlayConfirm(armed.current, key, input)) {
59
+ const picked = CTX_CHOICES[indexRef.current];
60
+ if (picked)
61
+ onSelect(picked);
62
+ }
63
+ });
64
+ return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, width: "100%", borderStyle: "round", borderColor: theme.pickerBorder, paddingX: 1, children: [_jsx(Box, { children: _jsx(Text, { bold: true, color: theme.pickerBorder, children: "\u9009\u62E9\u4E0A\u4E0B\u6587\u7A97\u53E3" }) }), _jsx(Box, { children: _jsxs(Text, { color: theme.muted, children: ["\u5F53\u524D: ", currentLabel()] }) }), CTX_CHOICES.map((choice, i) => {
65
+ const active = i === index;
66
+ return (_jsxs(Box, { children: [_jsx(Text, { color: active ? theme.accent : theme.muted, children: active ? "❯ " : " " }), _jsx(Text, { bold: active, color: active ? theme.accent : undefined, children: ctxChoiceLabel(choice) }), choice === marked && _jsx(Text, { color: theme.success, children: " \u2190 \u5F53\u524D" })] }, choice));
67
+ }), _jsx(Box, { children: _jsx(Text, { color: theme.muted, dimColor: true, wrap: "truncate-end", children: "\u2191\u2193 \u9009\u62E9 \u00B7 Enter \u786E\u8BA4 \u00B7 Esc \u53D6\u6D88" }) })] }));
68
+ }