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.
- package/README.md +46 -2
- package/dist/agent.js +89 -29
- package/dist/cli/commands/chat.js +3 -0
- package/dist/cli/commands/ctx.js +7 -0
- package/dist/cli/commands/exec.js +3 -0
- package/dist/cli/commands/index.js +32 -7
- package/dist/cli/commands/memory.js +33 -15
- package/dist/cli/commands/setup.js +55 -3
- package/dist/cli/commands/shared.js +10 -1
- package/dist/cli/commands/think.js +12 -0
- package/dist/cli/commands/write-config.js +22 -0
- package/dist/cli/option-helpers.js +13 -1
- package/dist/cli/program.js +57 -14
- package/dist/cli/setup/detect.js +17 -0
- package/dist/cli/setup/flags.js +12 -0
- package/dist/cli/setup/flow.js +108 -0
- package/dist/cli/setup/provider-form.js +102 -0
- package/dist/cli/setup/ui.js +534 -0
- package/dist/code-mode.js +1 -1
- package/dist/config.js +93 -159
- package/dist/context-window.js +39 -49
- package/dist/ctx-cli.js +30 -0
- package/dist/ctx.js +80 -0
- package/dist/memory-cli.js +33 -0
- package/dist/memory.js +127 -46
- package/dist/model-catalog.js +285 -0
- package/dist/ollama-model.js +234 -0
- package/dist/ollama-openai-bridge.js +383 -0
- package/dist/permission-cli.js +1 -4
- package/dist/provider.js +4 -1
- package/dist/reasoning-stream.js +158 -0
- package/dist/sandbox-cli.js +1 -4
- package/dist/scope.js +23 -0
- package/dist/serve/common.js +22 -1
- package/dist/serve/routes-chat.js +21 -1
- package/dist/serve/routes-memory.js +31 -2
- package/dist/serve/routes-meta.js +69 -6
- package/dist/think-cli.js +36 -0
- package/dist/thinking-wire.js +239 -0
- package/dist/thinking.js +166 -0
- package/dist/token-display.js +10 -7
- package/dist/tools/todo.js +22 -8
- package/dist/tui/App.js +48 -8
- package/dist/tui/CtxPicker.js +68 -0
- package/dist/tui/InputBar.js +112 -37
- package/dist/tui/MessageList.js +53 -22
- package/dist/tui/StatusBar.js +7 -3
- package/dist/tui/ThinkPicker.js +75 -0
- package/dist/tui/bracketed-paste.js +37 -0
- package/dist/tui/caret-pos.js +10 -8
- package/dist/tui/index.js +13 -1
- package/dist/tui/layout.js +17 -0
- package/dist/tui/overlay-input.js +12 -0
- package/dist/tui/paste-draft.js +173 -0
- package/dist/tui/selection.js +8 -2
- package/dist/tui/slash-commands.js +24 -1
- package/dist/tui/slash-handler.js +88 -18
- package/dist/tui/text-width.js +6 -6
- package/dist/tui-chat.js +85 -7
- package/docs/API.md +69 -6
- package/docs/superpowers/plans/2026-08-23-cli-setup.md +501 -0
- package/docs/superpowers/plans/2026-08-23-input-paste-attachments.md +475 -0
- package/docs/superpowers/plans/2026-08-23-thinking-wire-profile.md +450 -0
- package/docs/superpowers/specs/2026-08-23-cli-setup-design.md +282 -0
- package/docs/superpowers/specs/2026-08-23-input-paste-attachments-design.md +174 -0
- package/docs/superpowers/specs/2026-08-23-thinking-wire-profile-design.md +140 -0
- package/package.json +1 -1
- package/skills/self-config/SKILL.md +7 -4
- 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
|
+
}
|
package/dist/thinking.js
ADDED
|
@@ -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
|
+
}
|
package/dist/token-display.js
CHANGED
|
@@ -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
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
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
|
}
|
package/dist/tools/todo.js
CHANGED
|
@@ -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
|
|
74
|
-
if (!
|
|
85
|
+
const goal = durableGoal(messageText(msg));
|
|
86
|
+
if (!goal)
|
|
75
87
|
continue;
|
|
76
|
-
state.goal =
|
|
88
|
+
state.goal = goal;
|
|
77
89
|
return;
|
|
78
90
|
}
|
|
79
91
|
}
|
|
80
92
|
export function formatTaskStatePrompt(state) {
|
|
81
|
-
|
|
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
|
|
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 (
|
|
94
|
-
lines.push("", "### Original 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 ||
|
|
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
|
-
:
|
|
92
|
-
?
|
|
93
|
-
:
|
|
94
|
-
|
|
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
|
+
}
|