min-agent 0.4.1 → 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.
- package/README.md +32 -2
- package/dist/agent.js +36 -22
- package/dist/cli/commands/chat.js +3 -0
- package/dist/cli/commands/exec.js +3 -0
- package/dist/cli/commands/index.js +22 -5
- package/dist/cli/commands/memory.js +33 -15
- 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 +50 -13
- package/dist/code-mode.js +1 -1
- package/dist/config.js +41 -0
- package/dist/context-window.js +8 -28
- package/dist/memory-cli.js +33 -0
- package/dist/memory.js +127 -46
- package/dist/model-catalog.js +285 -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 +34 -6
- package/dist/think-cli.js +36 -0
- package/dist/thinking-wire.js +228 -0
- package/dist/thinking.js +142 -0
- package/dist/token-display.js +10 -7
- package/dist/tools/todo.js +22 -8
- package/dist/tui/App.js +36 -8
- package/dist/tui/InputBar.js +109 -36
- package/dist/tui/MessageList.js +53 -22
- package/dist/tui/StatusBar.js +7 -3
- package/dist/tui/ThinkPicker.js +77 -0
- package/dist/tui/bracketed-paste.js +37 -0
- package/dist/tui/caret-pos.js +10 -8
- package/dist/tui/index.js +7 -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 +18 -1
- package/dist/tui/slash-handler.js +61 -17
- package/dist/tui/text-width.js +6 -6
- package/dist/tui-chat.js +63 -7
- package/docs/API.md +50 -4
- 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-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 +5 -4
- package/skills/self-config/reference.md +10 -5
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { fetchModels, getActiveProvider, getEffectiveConfig, getBudgetSnapshot, setBudgetMaxCost, setSandboxConfig, setPermissionMode, parsePermissionMode, } from "../config.js";
|
|
1
|
+
import { fetchModels, getActiveProvider, getEffectiveConfig, getBudgetSnapshot, setBudgetMaxCost, setSandboxConfig, setPermissionMode, parsePermissionMode, parseThinkingEffort, } from "../config.js";
|
|
2
|
+
import { setThinkingEffort } from "../thinking.js";
|
|
2
3
|
import { parseNetworkPolicy, parseSandboxMode } from "../sandbox.js";
|
|
3
|
-
import {
|
|
4
|
+
import { readScope } from "../scope.js";
|
|
4
5
|
import { getWorkingTreeDiff } from "../tui/diff-view.js";
|
|
5
6
|
import { scanProject } from "../code-mode.js";
|
|
6
7
|
import { getContextWindowInfo } from "../context-window.js";
|
|
7
8
|
import { getModelPrice, estimateCost } from "../pricing.js";
|
|
8
9
|
import { checkForUpdate } from "../updater.js";
|
|
9
|
-
import { sendJson, sendJsonError, readJsonBody, readStringPathList, sandboxPayload, permissionPayload, } from "./common.js";
|
|
10
|
+
import { sendJson, sendJsonError, readJsonBody, readStringPathList, sandboxPayload, permissionPayload, thinkingPayload, memoryPayload, } from "./common.js";
|
|
10
11
|
export const handleMetaRoutes = async (req, res, ctx, url, pathname) => {
|
|
11
12
|
if (req.method === "GET" && pathname === "/health") {
|
|
12
13
|
sendJson(res, 200, { ok: true, service: "min-agent", version: ctx.version });
|
|
@@ -19,6 +20,8 @@ export const handleMetaRoutes = async (req, res, ctx, url, pathname) => {
|
|
|
19
20
|
instructions_chars: ctx.instructions.join("\n").length,
|
|
20
21
|
sandbox: sandboxPayload(),
|
|
21
22
|
permission: permissionPayload(),
|
|
23
|
+
thinking: thinkingPayload(),
|
|
24
|
+
memory: memoryPayload(),
|
|
22
25
|
});
|
|
23
26
|
return true;
|
|
24
27
|
}
|
|
@@ -64,7 +67,7 @@ export const handleMetaRoutes = async (req, res, ctx, url, pathname) => {
|
|
|
64
67
|
sendJson(res, 400, { error: "invalid_max_cost", detail: "maxCostUSD must be a positive number" });
|
|
65
68
|
return true;
|
|
66
69
|
}
|
|
67
|
-
const scopeParsed =
|
|
70
|
+
const scopeParsed = readScope(parsed.value.scope);
|
|
68
71
|
if (!scopeParsed.ok) {
|
|
69
72
|
sendJson(res, 400, { error: "invalid_scope", allowed: ["global", "project"] });
|
|
70
73
|
return true;
|
|
@@ -94,7 +97,7 @@ export const handleMetaRoutes = async (req, res, ctx, url, pathname) => {
|
|
|
94
97
|
sendJson(res, 400, { error: "invalid_permission", detail: "permission must be ask, accept-edits, or allow-all" });
|
|
95
98
|
return true;
|
|
96
99
|
}
|
|
97
|
-
const scopeParsed =
|
|
100
|
+
const scopeParsed = readScope(parsed.value.scope);
|
|
98
101
|
if (!scopeParsed.ok) {
|
|
99
102
|
sendJson(res, 400, { error: "invalid_scope", allowed: ["global", "project"] });
|
|
100
103
|
return true;
|
|
@@ -104,6 +107,31 @@ export const handleMetaRoutes = async (req, res, ctx, url, pathname) => {
|
|
|
104
107
|
sendJson(res, 200, { ok: true, scope, ...permissionPayload() });
|
|
105
108
|
return true;
|
|
106
109
|
}
|
|
110
|
+
if (req.method === "GET" && pathname === "/v1/thinking") {
|
|
111
|
+
sendJson(res, 200, thinkingPayload());
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
if (req.method === "POST" && pathname === "/v1/thinking") {
|
|
115
|
+
const parsed = await readJsonBody(req);
|
|
116
|
+
if (!parsed.ok) {
|
|
117
|
+
sendJsonError(res, parsed);
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
const effort = parseThinkingEffort(parsed.value.thinking);
|
|
121
|
+
if (!effort) {
|
|
122
|
+
sendJson(res, 400, { error: "invalid_thinking", detail: "thinking must be off, low, medium, high, or max" });
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
const scopeParsed = readScope(parsed.value.scope);
|
|
126
|
+
if (!scopeParsed.ok) {
|
|
127
|
+
sendJson(res, 400, { error: "invalid_scope", allowed: ["global", "project"] });
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
const scope = scopeParsed.scope ?? "global";
|
|
131
|
+
setThinkingEffort(effort, scope);
|
|
132
|
+
sendJson(res, 200, { ok: true, scope, ...thinkingPayload() });
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
107
135
|
if (req.method === "POST" && pathname === "/v1/sandbox") {
|
|
108
136
|
const parsed = await readJsonBody(req);
|
|
109
137
|
if (!parsed.ok) {
|
|
@@ -134,7 +162,7 @@ export const handleMetaRoutes = async (req, res, ctx, url, pathname) => {
|
|
|
134
162
|
sendJson(res, 400, { error: "invalid_body", detail: "Provide mode, network, extraWriteRoots, or extraReadRoots" });
|
|
135
163
|
return true;
|
|
136
164
|
}
|
|
137
|
-
const scopeParsed =
|
|
165
|
+
const scopeParsed = readScope(body.scope);
|
|
138
166
|
if (!scopeParsed.ok) {
|
|
139
167
|
sendJson(res, 400, { error: "invalid_scope", allowed: ["global", "project"] });
|
|
140
168
|
return true;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { takeScopeFlags, scopeLabel } from "./scope.js";
|
|
2
|
+
import { parseThinkingEffort, resolveThinkingEffort, setThinkingEffort, setThinkingOverride, thinkingEffortLabel, } from "./thinking.js";
|
|
3
|
+
export const THINK_CLI_USAGE = [
|
|
4
|
+
"Usage: min-agent think [off|low|medium|high|max] [--project|--global]",
|
|
5
|
+
" min-agent --think off|low|medium|high|max [--project|--global]",
|
|
6
|
+
].join("\n");
|
|
7
|
+
function sourceLabel(source) {
|
|
8
|
+
return source === "cli" ? "this run" : source === "project" ? "project" : source === "global" ? "global" : "default";
|
|
9
|
+
}
|
|
10
|
+
export function runThinkCli(input) {
|
|
11
|
+
const { scope: posScope, rest } = takeScopeFlags(input.positionals);
|
|
12
|
+
const scope = posScope ?? input.scope ?? "global";
|
|
13
|
+
if (rest[0] === "--help" || rest[0] === "-h") {
|
|
14
|
+
return { ok: true, lines: [THINK_CLI_USAGE] };
|
|
15
|
+
}
|
|
16
|
+
if (rest.length === 1) {
|
|
17
|
+
const parsed = parseThinkingEffort(rest[0]);
|
|
18
|
+
if (!parsed)
|
|
19
|
+
return { ok: false, lines: [THINK_CLI_USAGE] };
|
|
20
|
+
setThinkingEffort(parsed, scope);
|
|
21
|
+
setThinkingOverride(parsed);
|
|
22
|
+
return { ok: true, lines: [`✓ Thinking set to ${thinkingEffortLabel(parsed)} (${scopeLabel(scope)})`] };
|
|
23
|
+
}
|
|
24
|
+
if (rest.length > 1)
|
|
25
|
+
return { ok: false, lines: [THINK_CLI_USAGE] };
|
|
26
|
+
if (input.flagEffort) {
|
|
27
|
+
setThinkingEffort(input.flagEffort, scope);
|
|
28
|
+
setThinkingOverride(input.flagEffort);
|
|
29
|
+
return { ok: true, lines: [`✓ Thinking set to ${thinkingEffortLabel(input.flagEffort)} (${scopeLabel(scope)})`] };
|
|
30
|
+
}
|
|
31
|
+
const { thinking, source } = resolveThinkingEffort();
|
|
32
|
+
return {
|
|
33
|
+
ok: true,
|
|
34
|
+
lines: [`Current thinking: ${thinkingEffortLabel(thinking)} (${sourceLabel(source)})`, THINK_CLI_USAGE],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
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 { atomicWriteFileSync } from "./tools/atomic-file.js";
|
|
6
|
+
export const thinkingRequestStore = new AsyncLocalStorage();
|
|
7
|
+
export const THINKING_WIRE_VERSION = 1;
|
|
8
|
+
export const THINKING_WIRE_TTL = 7 * 24 * 60 * 60 * 1000;
|
|
9
|
+
export const DEFAULT_THINKING_WIRE = {
|
|
10
|
+
version: THINKING_WIRE_VERSION,
|
|
11
|
+
thinkingType: "enabled-disabled",
|
|
12
|
+
effort: "reasoning_effort",
|
|
13
|
+
};
|
|
14
|
+
const memoryCache = new Map();
|
|
15
|
+
function isRecord(value) {
|
|
16
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
function isThinkingTypeMode(value) {
|
|
19
|
+
return value === "enabled-disabled" || value === "adaptive-disabled" || value === "omit";
|
|
20
|
+
}
|
|
21
|
+
function isEffortMode(value) {
|
|
22
|
+
return value === "reasoning_effort" || value === "omit";
|
|
23
|
+
}
|
|
24
|
+
function isProfile(value) {
|
|
25
|
+
if (!isRecord(value))
|
|
26
|
+
return false;
|
|
27
|
+
return (value.version === THINKING_WIRE_VERSION &&
|
|
28
|
+
isThinkingTypeMode(value.thinkingType) &&
|
|
29
|
+
isEffortMode(value.effort) &&
|
|
30
|
+
typeof value.timestamp === "number" &&
|
|
31
|
+
Number.isFinite(value.timestamp));
|
|
32
|
+
}
|
|
33
|
+
function cacheFile() {
|
|
34
|
+
return path.join(getConfigDir(), "thinking-wire-cache.json");
|
|
35
|
+
}
|
|
36
|
+
function loadDiskCache() {
|
|
37
|
+
const file = cacheFile();
|
|
38
|
+
if (!existsSync(file))
|
|
39
|
+
return {};
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(readFileSync(file, "utf-8"));
|
|
42
|
+
if (!isRecord(parsed))
|
|
43
|
+
return {};
|
|
44
|
+
const out = {};
|
|
45
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
46
|
+
if (isProfile(value))
|
|
47
|
+
out[key] = value;
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function saveDiskCache(cache) {
|
|
56
|
+
const file = cacheFile();
|
|
57
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
58
|
+
atomicWriteFileSync(file, JSON.stringify(cache));
|
|
59
|
+
}
|
|
60
|
+
function liveProfile(entry, now) {
|
|
61
|
+
if (entry.version !== THINKING_WIRE_VERSION)
|
|
62
|
+
return null;
|
|
63
|
+
if (now - entry.timestamp > THINKING_WIRE_TTL)
|
|
64
|
+
return null;
|
|
65
|
+
return entry;
|
|
66
|
+
}
|
|
67
|
+
export function thinkingWireCacheKey(host, modelId) {
|
|
68
|
+
const h = host.trim().toLowerCase();
|
|
69
|
+
const m = modelId.trim().toLowerCase();
|
|
70
|
+
if (!h)
|
|
71
|
+
return m;
|
|
72
|
+
if (!m)
|
|
73
|
+
return h;
|
|
74
|
+
return `${h}::${m}`;
|
|
75
|
+
}
|
|
76
|
+
export function inferThinkingWire(modelId, host, providerType) {
|
|
77
|
+
const haystack = `${modelId} ${host}`.toLowerCase();
|
|
78
|
+
if (haystack.includes("minimax")) {
|
|
79
|
+
return { thinkingType: "adaptive-disabled", effort: "reasoning_effort" };
|
|
80
|
+
}
|
|
81
|
+
if (providerType === "openai") {
|
|
82
|
+
return { thinkingType: "omit", effort: "reasoning_effort" };
|
|
83
|
+
}
|
|
84
|
+
return { thinkingType: DEFAULT_THINKING_WIRE.thinkingType, effort: DEFAULT_THINKING_WIRE.effort };
|
|
85
|
+
}
|
|
86
|
+
function readCachedProfile(key, now) {
|
|
87
|
+
const memory = memoryCache.get(key);
|
|
88
|
+
if (memory) {
|
|
89
|
+
const live = liveProfile(memory, now);
|
|
90
|
+
if (live)
|
|
91
|
+
return live;
|
|
92
|
+
memoryCache.delete(key);
|
|
93
|
+
}
|
|
94
|
+
const disk = loadDiskCache()[key];
|
|
95
|
+
if (!disk)
|
|
96
|
+
return null;
|
|
97
|
+
const live = liveProfile(disk, now);
|
|
98
|
+
if (!live)
|
|
99
|
+
return null;
|
|
100
|
+
memoryCache.set(key, live);
|
|
101
|
+
return live;
|
|
102
|
+
}
|
|
103
|
+
export function resolveThinkingWireProfile(host, modelId, providerType, now = Date.now()) {
|
|
104
|
+
const cached = readCachedProfile(thinkingWireCacheKey(host, modelId), now);
|
|
105
|
+
if (cached)
|
|
106
|
+
return cached;
|
|
107
|
+
return {
|
|
108
|
+
version: THINKING_WIRE_VERSION,
|
|
109
|
+
...inferThinkingWire(modelId, host, providerType),
|
|
110
|
+
timestamp: now,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export function saveLearnedThinkingWire(host, modelId, profile, now = Date.now()) {
|
|
114
|
+
const entry = {
|
|
115
|
+
version: THINKING_WIRE_VERSION,
|
|
116
|
+
thinkingType: profile.thinkingType,
|
|
117
|
+
effort: profile.effort,
|
|
118
|
+
timestamp: profile.timestamp ?? now,
|
|
119
|
+
};
|
|
120
|
+
const key = thinkingWireCacheKey(host, modelId);
|
|
121
|
+
memoryCache.set(key, entry);
|
|
122
|
+
const cache = loadDiskCache();
|
|
123
|
+
cache[key] = entry;
|
|
124
|
+
saveDiskCache(cache);
|
|
125
|
+
return entry;
|
|
126
|
+
}
|
|
127
|
+
export function clearThinkingWireCache() {
|
|
128
|
+
memoryCache.clear();
|
|
129
|
+
}
|
|
130
|
+
export function applyThinkingToChatBody(body, spec, profile = DEFAULT_THINKING_WIRE) {
|
|
131
|
+
const next = { ...body };
|
|
132
|
+
if (profile.effort === "omit")
|
|
133
|
+
delete next.reasoning_effort;
|
|
134
|
+
else
|
|
135
|
+
next.reasoning_effort = spec.reasoningEffort;
|
|
136
|
+
if (profile.thinkingType === "omit")
|
|
137
|
+
delete next.thinking;
|
|
138
|
+
else {
|
|
139
|
+
const on = profile.thinkingType === "adaptive-disabled" ? "adaptive" : "enabled";
|
|
140
|
+
next.thinking = { type: spec.thinkingEnabled ? on : "disabled" };
|
|
141
|
+
}
|
|
142
|
+
return next;
|
|
143
|
+
}
|
|
144
|
+
function splitAllowed(raw) {
|
|
145
|
+
return new Set(raw
|
|
146
|
+
.split(",")
|
|
147
|
+
.map((part) => part
|
|
148
|
+
.trim()
|
|
149
|
+
.replace(/^['"]|['"]$/g, "")
|
|
150
|
+
.toLowerCase())
|
|
151
|
+
.filter(Boolean));
|
|
152
|
+
}
|
|
153
|
+
export function parseThinkingWireHint(text) {
|
|
154
|
+
const allowedMatch = text.match(/thinking\.type[\s\S]*?allowed:\s*([^)]+)/i);
|
|
155
|
+
if (allowedMatch?.[1]) {
|
|
156
|
+
const allowed = splitAllowed(allowedMatch[1]);
|
|
157
|
+
if (allowed.has("adaptive") && allowed.has("disabled"))
|
|
158
|
+
return { thinkingType: "adaptive-disabled" };
|
|
159
|
+
if (allowed.has("enabled") && allowed.has("disabled"))
|
|
160
|
+
return { thinkingType: "enabled-disabled" };
|
|
161
|
+
}
|
|
162
|
+
if (/unknown parameter[:\s]+['"`]?thinking['"`]?(?![\w.])/i.test(text))
|
|
163
|
+
return { thinkingType: "omit" };
|
|
164
|
+
if (/unknown parameter[:\s]+['"`]?reasoning_effort['"`]?(?![\w.])/i.test(text))
|
|
165
|
+
return { effort: "omit" };
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
function hostnameFromInput(input) {
|
|
169
|
+
try {
|
|
170
|
+
const raw = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
171
|
+
return new URL(raw).hostname;
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return "";
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function rebuild400(response, text) {
|
|
178
|
+
return new Response(text, {
|
|
179
|
+
status: 400,
|
|
180
|
+
statusText: response.statusText,
|
|
181
|
+
headers: response.headers,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
function profileUnchanged(current, next) {
|
|
185
|
+
return current.thinkingType === next.thinkingType && current.effort === next.effort;
|
|
186
|
+
}
|
|
187
|
+
export async function fetchWithThinkingWire(inner, input, init, opts) {
|
|
188
|
+
const spec = thinkingRequestStore.getStore();
|
|
189
|
+
if (!spec || !init || typeof init.body !== "string")
|
|
190
|
+
return inner(input, init);
|
|
191
|
+
let parsed;
|
|
192
|
+
try {
|
|
193
|
+
parsed = JSON.parse(init.body);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return inner(input, init);
|
|
197
|
+
}
|
|
198
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
199
|
+
return inner(input, init);
|
|
200
|
+
const body = parsed;
|
|
201
|
+
const host = hostnameFromInput(input);
|
|
202
|
+
const modelId = typeof body.model === "string" ? body.model : "";
|
|
203
|
+
const providerType = opts?.providerType ?? getActiveProvider(getEffectiveConfig())?.type;
|
|
204
|
+
const profile = resolveThinkingWireProfile(host, modelId, providerType);
|
|
205
|
+
const first = await inner(input, {
|
|
206
|
+
...init,
|
|
207
|
+
body: JSON.stringify(applyThinkingToChatBody(body, spec, profile)),
|
|
208
|
+
});
|
|
209
|
+
if (first.status !== 400)
|
|
210
|
+
return first;
|
|
211
|
+
const text = await first.text();
|
|
212
|
+
const hint = parseThinkingWireHint(text);
|
|
213
|
+
if (!hint)
|
|
214
|
+
return rebuild400(first, text);
|
|
215
|
+
const next = {
|
|
216
|
+
version: THINKING_WIRE_VERSION,
|
|
217
|
+
thinkingType: hint.thinkingType ?? profile.thinkingType,
|
|
218
|
+
effort: hint.effort ?? profile.effort,
|
|
219
|
+
timestamp: Date.now(),
|
|
220
|
+
};
|
|
221
|
+
if (profileUnchanged(profile, next))
|
|
222
|
+
return rebuild400(first, text);
|
|
223
|
+
saveLearnedThinkingWire(host, modelId, next);
|
|
224
|
+
return inner(input, {
|
|
225
|
+
...init,
|
|
226
|
+
body: JSON.stringify(applyThinkingToChatBody(body, spec, next)),
|
|
227
|
+
});
|
|
228
|
+
}
|
package/dist/thinking.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { getEffectiveConfig, loadConfig, loadProjectConfig, saveConfig, saveProjectConfig, getActiveProvider, parseThinkingEffort, } from "./config.js";
|
|
2
|
+
import { getCachedModelCatalog } from "./model-catalog.js";
|
|
3
|
+
import { applyThinkingToChatBody, thinkingRequestStore } from "./thinking-wire.js";
|
|
4
|
+
export { parseThinkingEffort, applyThinkingToChatBody, thinkingRequestStore };
|
|
5
|
+
export const THINKING_EFFORTS = ["off", "low", "medium", "high", "max"];
|
|
6
|
+
export const DEFAULT_THINKING_EFFORT = "medium";
|
|
7
|
+
let thinkingOverride;
|
|
8
|
+
export function setThinkingOverride(effort) {
|
|
9
|
+
thinkingOverride = effort;
|
|
10
|
+
}
|
|
11
|
+
export function getThinkingOverride() {
|
|
12
|
+
return thinkingOverride;
|
|
13
|
+
}
|
|
14
|
+
export function thinkingEffortLabel(effort) {
|
|
15
|
+
return effort;
|
|
16
|
+
}
|
|
17
|
+
export function thinkingChoiceIndex(effort, choices = THINKING_EFFORTS) {
|
|
18
|
+
const list = choices.length > 0 ? choices : THINKING_EFFORTS;
|
|
19
|
+
const clamped = clampThinkingEffort(effort, list);
|
|
20
|
+
const i = list.indexOf(clamped);
|
|
21
|
+
return i >= 0 ? i : 0;
|
|
22
|
+
}
|
|
23
|
+
export function thinkingChoicesFromReasoning(info) {
|
|
24
|
+
const set = new Set();
|
|
25
|
+
if (info.toggle || info.efforts.includes("none"))
|
|
26
|
+
set.add("off");
|
|
27
|
+
for (const raw of info.efforts) {
|
|
28
|
+
const mapped = parseThinkingEffort(raw);
|
|
29
|
+
if (mapped)
|
|
30
|
+
set.add(mapped);
|
|
31
|
+
}
|
|
32
|
+
const enabled = THINKING_EFFORTS.filter((effort) => effort !== "off" && set.has(effort));
|
|
33
|
+
// Toggle-only catalog rows must not collapse the picker to off.
|
|
34
|
+
if (enabled.length === 0)
|
|
35
|
+
return [...THINKING_EFFORTS];
|
|
36
|
+
return THINKING_EFFORTS.filter((effort) => set.has(effort));
|
|
37
|
+
}
|
|
38
|
+
export function thinkingChoicesForModel(modelId) {
|
|
39
|
+
if (!modelId)
|
|
40
|
+
return [...THINKING_EFFORTS];
|
|
41
|
+
const entry = getCachedModelCatalog(modelId);
|
|
42
|
+
if (!entry)
|
|
43
|
+
return [...THINKING_EFFORTS];
|
|
44
|
+
return thinkingChoicesFromReasoning(entry.reasoning);
|
|
45
|
+
}
|
|
46
|
+
export function clampThinkingEffort(effort, choices = THINKING_EFFORTS) {
|
|
47
|
+
if (choices.length === 0 || choices.includes(effort))
|
|
48
|
+
return effort;
|
|
49
|
+
if (choices.includes("medium"))
|
|
50
|
+
return "medium";
|
|
51
|
+
if (choices.includes("high"))
|
|
52
|
+
return "high";
|
|
53
|
+
const enabled = choices.filter((item) => item !== "off");
|
|
54
|
+
if (enabled.length > 0)
|
|
55
|
+
return enabled[Math.floor((enabled.length - 1) / 2)];
|
|
56
|
+
return choices.find((item) => item !== "off") ?? DEFAULT_THINKING_EFFORT;
|
|
57
|
+
}
|
|
58
|
+
export function thinkingSourceLabel(source) {
|
|
59
|
+
if (source === "cli")
|
|
60
|
+
return "本次启动参数";
|
|
61
|
+
if (source === "project")
|
|
62
|
+
return "项目";
|
|
63
|
+
if (source === "global")
|
|
64
|
+
return "全局";
|
|
65
|
+
return "默认";
|
|
66
|
+
}
|
|
67
|
+
export function resolveThinkingEffort(runOverride, modelId) {
|
|
68
|
+
let thinking;
|
|
69
|
+
let source;
|
|
70
|
+
if (runOverride) {
|
|
71
|
+
thinking = runOverride;
|
|
72
|
+
source = "cli";
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
const override = getThinkingOverride();
|
|
76
|
+
if (override) {
|
|
77
|
+
thinking = override;
|
|
78
|
+
source = "cli";
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
const snap = getThinkingSnapshot();
|
|
82
|
+
thinking = snap.thinking ?? DEFAULT_THINKING_EFFORT;
|
|
83
|
+
source = snap.source;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const id = modelId ?? getActiveProvider(getEffectiveConfig())?.defaultModel;
|
|
87
|
+
return { thinking: clampThinkingEffort(thinking, thinkingChoicesForModel(id)), source };
|
|
88
|
+
}
|
|
89
|
+
export function getThinkingSnapshot() {
|
|
90
|
+
const project = parseThinkingEffort(loadProjectConfig().thinking);
|
|
91
|
+
if (project !== undefined)
|
|
92
|
+
return { thinking: project, source: "project" };
|
|
93
|
+
const global = parseThinkingEffort(loadConfig().thinking);
|
|
94
|
+
if (global !== undefined)
|
|
95
|
+
return { thinking: global, source: "global" };
|
|
96
|
+
return { thinking: undefined, source: null };
|
|
97
|
+
}
|
|
98
|
+
function clearProjectThinking() {
|
|
99
|
+
const project = loadProjectConfig();
|
|
100
|
+
if (project.thinking === undefined)
|
|
101
|
+
return;
|
|
102
|
+
const { thinking: _removed, ...rest } = project;
|
|
103
|
+
saveProjectConfig(rest);
|
|
104
|
+
}
|
|
105
|
+
export function setThinkingEffort(effort, scope) {
|
|
106
|
+
if (scope === "project") {
|
|
107
|
+
saveProjectConfig({ ...loadProjectConfig(), thinking: effort });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
saveConfig({ ...loadConfig(), thinking: effort });
|
|
111
|
+
// A leftover project value (including off) would keep shadowing the global file.
|
|
112
|
+
clearProjectThinking();
|
|
113
|
+
}
|
|
114
|
+
export function thinkingPayload() {
|
|
115
|
+
const { thinking, source } = resolveThinkingEffort();
|
|
116
|
+
return {
|
|
117
|
+
thinking,
|
|
118
|
+
source,
|
|
119
|
+
label: thinkingEffortLabel(thinking),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
export function toWireEffort(effort, providerType, modelId) {
|
|
123
|
+
if (effort === "off")
|
|
124
|
+
return "none";
|
|
125
|
+
if (effort !== "max")
|
|
126
|
+
return effort;
|
|
127
|
+
const catalog = modelId ? getCachedModelCatalog(modelId) : undefined;
|
|
128
|
+
const supported = catalog?.reasoning.efforts ?? [];
|
|
129
|
+
if (supported.includes("max"))
|
|
130
|
+
return "max";
|
|
131
|
+
if (supported.includes("xhigh") || providerType === "openai")
|
|
132
|
+
return "xhigh";
|
|
133
|
+
return "max";
|
|
134
|
+
}
|
|
135
|
+
export function resolveThinkingRequest(opts) {
|
|
136
|
+
const { thinking } = resolveThinkingEffort(opts?.runOverride, opts?.modelId);
|
|
137
|
+
const providerType = opts?.providerType ?? getActiveProvider(getEffectiveConfig())?.type;
|
|
138
|
+
return {
|
|
139
|
+
reasoningEffort: toWireEffort(thinking, providerType, opts?.modelId),
|
|
140
|
+
thinkingEnabled: thinking !== "off",
|
|
141
|
+
};
|
|
142
|
+
}
|
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,12 @@ 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";
|
|
11
12
|
import { SessionPicker, sessionPickerRows } from "./SessionPicker.js";
|
|
12
13
|
import { setCaretPosition } from "./caret.js";
|
|
13
14
|
import { computeMessageMaxHeight, frameRows, INPUT_BAR_ROWS } from "./layout.js";
|
|
14
15
|
import { isCtrlC } from "./input-history.js";
|
|
15
|
-
export function App({ initialState, onSubmit, onConfirm, onQuestionAnswer, onModelPick, onModelCancel, onSessionPick, onSessionCancel, onToggleSelectionMode, onExitSelectionMode, onExit, onCopyNotice, }) {
|
|
16
|
+
export function App({ initialState, onSubmit, onConfirm, onQuestionAnswer, onModelPick, onModelCancel, onThinkPick, onThinkCancel, onSessionPick, onSessionCancel, onToggleSelectionMode, onExitSelectionMode, onExit, onCopyNotice, onNotice, }) {
|
|
16
17
|
// Ink recalculates its own layout on resize without re-rendering React, so
|
|
17
18
|
// track terminal size ourselves to keep heights/widths in sync.
|
|
18
19
|
const { stdout } = useStdout();
|
|
@@ -38,6 +39,10 @@ export function App({ initialState, onSubmit, onConfirm, onQuestionAnswer, onMod
|
|
|
38
39
|
onModelCancel();
|
|
39
40
|
return;
|
|
40
41
|
}
|
|
42
|
+
if (initialState.thinkPicker) {
|
|
43
|
+
onThinkCancel();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
41
46
|
if (initialState.sessionPicker) {
|
|
42
47
|
onSessionCancel();
|
|
43
48
|
return;
|
|
@@ -55,6 +60,7 @@ export function App({ initialState, onSubmit, onConfirm, onQuestionAnswer, onMod
|
|
|
55
60
|
if (initialState.confirmMessage ||
|
|
56
61
|
initialState.question ||
|
|
57
62
|
initialState.modelPicker ||
|
|
63
|
+
initialState.thinkPicker ||
|
|
58
64
|
initialState.sessionPicker)
|
|
59
65
|
return;
|
|
60
66
|
if (initialState.isRunning)
|
|
@@ -69,30 +75,52 @@ export function App({ initialState, onSubmit, onConfirm, onQuestionAnswer, onMod
|
|
|
69
75
|
!initialState.confirmMessage &&
|
|
70
76
|
!initialState.question &&
|
|
71
77
|
!initialState.modelPicker &&
|
|
78
|
+
!initialState.thinkPicker &&
|
|
72
79
|
!initialState.sessionPicker) {
|
|
73
80
|
onExit();
|
|
74
81
|
}
|
|
75
82
|
});
|
|
76
|
-
if (initialState.confirmMessage ||
|
|
83
|
+
if (initialState.confirmMessage ||
|
|
84
|
+
initialState.question ||
|
|
85
|
+
initialState.modelPicker ||
|
|
86
|
+
initialState.thinkPicker ||
|
|
87
|
+
initialState.sessionPicker)
|
|
77
88
|
setCaretPosition(null);
|
|
78
89
|
const columns = terminal.columns;
|
|
79
90
|
const terminalRows = terminal.rows;
|
|
80
91
|
const layoutRows = frameRows(terminalRows);
|
|
92
|
+
const [inputBarRows, setInputBarRows] = useState(INPUT_BAR_ROWS);
|
|
93
|
+
const onInputBarRowsChange = useCallback((rows) => {
|
|
94
|
+
setInputBarRows((prev) => (prev === rows ? prev : rows));
|
|
95
|
+
}, []);
|
|
81
96
|
const confirmActive = Boolean(initialState.confirmMessage);
|
|
82
97
|
const questionActive = Boolean(initialState.question);
|
|
83
98
|
const modelPickerActive = Boolean(initialState.modelPicker);
|
|
99
|
+
const thinkPickerActive = Boolean(initialState.thinkPicker);
|
|
84
100
|
const sessionPickerActive = Boolean(initialState.sessionPicker);
|
|
101
|
+
const overlayActive = confirmActive || questionActive || modelPickerActive || thinkPickerActive || sessionPickerActive;
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (overlayActive)
|
|
104
|
+
setInputBarRows(INPUT_BAR_ROWS);
|
|
105
|
+
}, [overlayActive]);
|
|
85
106
|
const footerRows = confirmActive
|
|
86
107
|
? confirmBarRows(initialState.confirmMessage ?? "", columns, layoutRows)
|
|
87
108
|
: questionActive
|
|
88
109
|
? questionBarRows(initialState.question.prompt, initialState.question.options, columns, layoutRows)
|
|
89
110
|
: modelPickerActive
|
|
90
111
|
? modelPickerRows(layoutRows)
|
|
91
|
-
:
|
|
92
|
-
?
|
|
93
|
-
:
|
|
94
|
-
|
|
112
|
+
: thinkPickerActive
|
|
113
|
+
? thinkPickerRows()
|
|
114
|
+
: sessionPickerActive
|
|
115
|
+
? sessionPickerRows(layoutRows)
|
|
116
|
+
: inputBarRows;
|
|
117
|
+
const spinnerVisible = initialState.isRunning &&
|
|
118
|
+
!confirmActive &&
|
|
119
|
+
!questionActive &&
|
|
120
|
+
!modelPickerActive &&
|
|
121
|
+
!thinkPickerActive &&
|
|
122
|
+
!sessionPickerActive;
|
|
95
123
|
const spinnerRows = spinnerVisible ? 1 : 0;
|
|
96
124
|
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 }))] }));
|
|
125
|
+
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.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
126
|
}
|