min-agent 0.5.0 → 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 +14 -0
- package/dist/agent.js +53 -7
- package/dist/cli/commands/ctx.js +7 -0
- package/dist/cli/commands/index.js +10 -2
- package/dist/cli/commands/setup.js +55 -3
- package/dist/cli/commands/shared.js +10 -1
- package/dist/cli/program.js +7 -1
- 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/config.js +52 -159
- package/dist/context-window.js +33 -23
- package/dist/ctx-cli.js +30 -0
- package/dist/ctx.js +80 -0
- package/dist/ollama-model.js +234 -0
- package/dist/ollama-openai-bridge.js +383 -0
- package/dist/serve/routes-meta.js +35 -0
- package/dist/thinking-wire.js +15 -4
- package/dist/thinking.js +26 -2
- package/dist/tui/App.js +18 -6
- package/dist/tui/CtxPicker.js +68 -0
- package/dist/tui/InputBar.js +4 -2
- package/dist/tui/ThinkPicker.js +4 -6
- package/dist/tui/index.js +7 -1
- package/dist/tui/slash-commands.js +6 -0
- package/dist/tui/slash-handler.js +27 -1
- package/dist/tui-chat.js +25 -3
- package/docs/API.md +19 -2
- package/docs/superpowers/plans/2026-08-23-cli-setup.md +501 -0
- package/docs/superpowers/specs/2026-08-23-cli-setup-design.md +282 -0
- package/package.json +1 -1
- package/skills/self-config/SKILL.md +3 -1
- package/skills/self-config/reference.md +4 -3
package/dist/thinking-wire.js
CHANGED
|
@@ -2,6 +2,7 @@ import { AsyncLocalStorage } from "async_hooks";
|
|
|
2
2
|
import { readFileSync, mkdirSync, existsSync } from "fs";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { getActiveProvider, getConfigDir, getEffectiveConfig } from "./config.js";
|
|
5
|
+
import { fetchOllamaNativeChat, readFetchBody } from "./ollama-openai-bridge.js";
|
|
5
6
|
import { atomicWriteFileSync } from "./tools/atomic-file.js";
|
|
6
7
|
export const thinkingRequestStore = new AsyncLocalStorage();
|
|
7
8
|
export const THINKING_WIRE_VERSION = 1;
|
|
@@ -74,6 +75,9 @@ export function thinkingWireCacheKey(host, modelId) {
|
|
|
74
75
|
return `${h}::${m}`;
|
|
75
76
|
}
|
|
76
77
|
export function inferThinkingWire(modelId, host, providerType) {
|
|
78
|
+
if (providerType === "ollama") {
|
|
79
|
+
return { thinkingType: "omit", effort: "reasoning_effort" };
|
|
80
|
+
}
|
|
77
81
|
const haystack = `${modelId} ${host}`.toLowerCase();
|
|
78
82
|
if (haystack.includes("minimax")) {
|
|
79
83
|
return { thinkingType: "adaptive-disabled", effort: "reasoning_effort" };
|
|
@@ -185,22 +189,29 @@ function profileUnchanged(current, next) {
|
|
|
185
189
|
return current.thinkingType === next.thinkingType && current.effort === next.effort;
|
|
186
190
|
}
|
|
187
191
|
export async function fetchWithThinkingWire(inner, input, init, opts) {
|
|
188
|
-
|
|
189
|
-
|
|
192
|
+
if (!init?.body)
|
|
193
|
+
return inner(input, init);
|
|
194
|
+
const rawBody = await readFetchBody(init.body);
|
|
195
|
+
if (rawBody == null)
|
|
190
196
|
return inner(input, init);
|
|
191
197
|
let parsed;
|
|
192
198
|
try {
|
|
193
|
-
parsed = JSON.parse(
|
|
199
|
+
parsed = JSON.parse(rawBody);
|
|
194
200
|
}
|
|
195
201
|
catch {
|
|
196
202
|
return inner(input, init);
|
|
197
203
|
}
|
|
198
204
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
199
205
|
return inner(input, init);
|
|
206
|
+
const spec = thinkingRequestStore.getStore();
|
|
207
|
+
const providerType = opts?.providerType ?? getActiveProvider(getEffectiveConfig())?.type;
|
|
200
208
|
const body = parsed;
|
|
209
|
+
if (providerType === "ollama")
|
|
210
|
+
return fetchOllamaNativeChat(inner, input, init, body, spec);
|
|
211
|
+
if (!spec)
|
|
212
|
+
return inner(input, init);
|
|
201
213
|
const host = hostnameFromInput(input);
|
|
202
214
|
const modelId = typeof body.model === "string" ? body.model : "";
|
|
203
|
-
const providerType = opts?.providerType ?? getActiveProvider(getEffectiveConfig())?.type;
|
|
204
215
|
const profile = resolveThinkingWireProfile(host, modelId, providerType);
|
|
205
216
|
const first = await inner(input, {
|
|
206
217
|
...init,
|
package/dist/thinking.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getEffectiveConfig, loadConfig, loadProjectConfig, saveConfig, saveProjectConfig, getActiveProvider, parseThinkingEffort, } from "./config.js";
|
|
2
|
-
import { getCachedModelCatalog } from "./model-catalog.js";
|
|
2
|
+
import { getCachedModelCatalog, getModelCatalog } from "./model-catalog.js";
|
|
3
|
+
import { getCachedOllamaModel, getOllamaModel, thinkingChoicesFromOllama } from "./ollama-model.js";
|
|
3
4
|
import { applyThinkingToChatBody, thinkingRequestStore } from "./thinking-wire.js";
|
|
4
5
|
export { parseThinkingEffort, applyThinkingToChatBody, thinkingRequestStore };
|
|
5
6
|
export const THINKING_EFFORTS = ["off", "low", "medium", "high", "max"];
|
|
@@ -36,6 +37,15 @@ export function thinkingChoicesFromReasoning(info) {
|
|
|
36
37
|
return THINKING_EFFORTS.filter((effort) => set.has(effort));
|
|
37
38
|
}
|
|
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
|
+
}
|
|
39
49
|
if (!modelId)
|
|
40
50
|
return [...THINKING_EFFORTS];
|
|
41
51
|
const entry = getCachedModelCatalog(modelId);
|
|
@@ -43,6 +53,18 @@ export function thinkingChoicesForModel(modelId) {
|
|
|
43
53
|
return [...THINKING_EFFORTS];
|
|
44
54
|
return thinkingChoicesFromReasoning(entry.reasoning);
|
|
45
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
|
+
}
|
|
46
68
|
export function clampThinkingEffort(effort, choices = THINKING_EFFORTS) {
|
|
47
69
|
if (choices.length === 0 || choices.includes(effort))
|
|
48
70
|
return effort;
|
|
@@ -53,7 +75,9 @@ export function clampThinkingEffort(effort, choices = THINKING_EFFORTS) {
|
|
|
53
75
|
const enabled = choices.filter((item) => item !== "off");
|
|
54
76
|
if (enabled.length > 0)
|
|
55
77
|
return enabled[Math.floor((enabled.length - 1) / 2)];
|
|
56
|
-
|
|
78
|
+
if (choices.includes("off"))
|
|
79
|
+
return "off";
|
|
80
|
+
return DEFAULT_THINKING_EFFORT;
|
|
57
81
|
}
|
|
58
82
|
export function thinkingSourceLabel(source) {
|
|
59
83
|
if (source === "cli")
|
package/dist/tui/App.js
CHANGED
|
@@ -9,11 +9,12 @@ import { QuestionBar, questionBarRows } from "./QuestionBar.js";
|
|
|
9
9
|
import { StatusBar } from "./StatusBar.js";
|
|
10
10
|
import { ModelPicker, modelPickerRows } from "./ModelPicker.js";
|
|
11
11
|
import { ThinkPicker, thinkPickerRows } from "./ThinkPicker.js";
|
|
12
|
+
import { CtxPicker, ctxPickerRows } from "./CtxPicker.js";
|
|
12
13
|
import { SessionPicker, sessionPickerRows } from "./SessionPicker.js";
|
|
13
14
|
import { setCaretPosition } from "./caret.js";
|
|
14
15
|
import { computeMessageMaxHeight, frameRows, INPUT_BAR_ROWS } from "./layout.js";
|
|
15
16
|
import { isCtrlC } from "./input-history.js";
|
|
16
|
-
export function App({ initialState, onSubmit, onConfirm, onQuestionAnswer, onModelPick, onModelCancel, onThinkPick, onThinkCancel, onSessionPick, onSessionCancel, onToggleSelectionMode, onExitSelectionMode, onExit, onCopyNotice, onNotice, }) {
|
|
17
|
+
export function App({ initialState, onSubmit, onConfirm, onQuestionAnswer, onModelPick, onModelCancel, onThinkPick, onThinkCancel, onCtxPick, onCtxCancel, onSessionPick, onSessionCancel, onToggleSelectionMode, onExitSelectionMode, onExit, onCopyNotice, onNotice, }) {
|
|
17
18
|
// Ink recalculates its own layout on resize without re-rendering React, so
|
|
18
19
|
// track terminal size ourselves to keep heights/widths in sync.
|
|
19
20
|
const { stdout } = useStdout();
|
|
@@ -43,6 +44,10 @@ export function App({ initialState, onSubmit, onConfirm, onQuestionAnswer, onMod
|
|
|
43
44
|
onThinkCancel();
|
|
44
45
|
return;
|
|
45
46
|
}
|
|
47
|
+
if (initialState.ctxPicker) {
|
|
48
|
+
onCtxCancel();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
46
51
|
if (initialState.sessionPicker) {
|
|
47
52
|
onSessionCancel();
|
|
48
53
|
return;
|
|
@@ -61,6 +66,7 @@ export function App({ initialState, onSubmit, onConfirm, onQuestionAnswer, onMod
|
|
|
61
66
|
initialState.question ||
|
|
62
67
|
initialState.modelPicker ||
|
|
63
68
|
initialState.thinkPicker ||
|
|
69
|
+
initialState.ctxPicker ||
|
|
64
70
|
initialState.sessionPicker)
|
|
65
71
|
return;
|
|
66
72
|
if (initialState.isRunning)
|
|
@@ -76,6 +82,7 @@ export function App({ initialState, onSubmit, onConfirm, onQuestionAnswer, onMod
|
|
|
76
82
|
!initialState.question &&
|
|
77
83
|
!initialState.modelPicker &&
|
|
78
84
|
!initialState.thinkPicker &&
|
|
85
|
+
!initialState.ctxPicker &&
|
|
79
86
|
!initialState.sessionPicker) {
|
|
80
87
|
onExit();
|
|
81
88
|
}
|
|
@@ -84,6 +91,7 @@ export function App({ initialState, onSubmit, onConfirm, onQuestionAnswer, onMod
|
|
|
84
91
|
initialState.question ||
|
|
85
92
|
initialState.modelPicker ||
|
|
86
93
|
initialState.thinkPicker ||
|
|
94
|
+
initialState.ctxPicker ||
|
|
87
95
|
initialState.sessionPicker)
|
|
88
96
|
setCaretPosition(null);
|
|
89
97
|
const columns = terminal.columns;
|
|
@@ -97,8 +105,9 @@ export function App({ initialState, onSubmit, onConfirm, onQuestionAnswer, onMod
|
|
|
97
105
|
const questionActive = Boolean(initialState.question);
|
|
98
106
|
const modelPickerActive = Boolean(initialState.modelPicker);
|
|
99
107
|
const thinkPickerActive = Boolean(initialState.thinkPicker);
|
|
108
|
+
const ctxPickerActive = Boolean(initialState.ctxPicker);
|
|
100
109
|
const sessionPickerActive = Boolean(initialState.sessionPicker);
|
|
101
|
-
const overlayActive = confirmActive || questionActive || modelPickerActive || thinkPickerActive || sessionPickerActive;
|
|
110
|
+
const overlayActive = confirmActive || questionActive || modelPickerActive || thinkPickerActive || ctxPickerActive || sessionPickerActive;
|
|
102
111
|
useEffect(() => {
|
|
103
112
|
if (overlayActive)
|
|
104
113
|
setInputBarRows(INPUT_BAR_ROWS);
|
|
@@ -111,16 +120,19 @@ export function App({ initialState, onSubmit, onConfirm, onQuestionAnswer, onMod
|
|
|
111
120
|
? modelPickerRows(layoutRows)
|
|
112
121
|
: thinkPickerActive
|
|
113
122
|
? thinkPickerRows()
|
|
114
|
-
:
|
|
115
|
-
?
|
|
116
|
-
:
|
|
123
|
+
: ctxPickerActive
|
|
124
|
+
? ctxPickerRows()
|
|
125
|
+
: sessionPickerActive
|
|
126
|
+
? sessionPickerRows(layoutRows)
|
|
127
|
+
: inputBarRows;
|
|
117
128
|
const spinnerVisible = initialState.isRunning &&
|
|
118
129
|
!confirmActive &&
|
|
119
130
|
!questionActive &&
|
|
120
131
|
!modelPickerActive &&
|
|
121
132
|
!thinkPickerActive &&
|
|
133
|
+
!ctxPickerActive &&
|
|
122
134
|
!sessionPickerActive;
|
|
123
135
|
const spinnerRows = spinnerVisible ? 1 : 0;
|
|
124
136
|
const messageMaxHeight = computeMessageMaxHeight(layoutRows, footerRows, spinnerRows);
|
|
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 }))] }));
|
|
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 }))] }));
|
|
126
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
|
+
}
|
package/dist/tui/InputBar.js
CHANGED
|
@@ -5,7 +5,8 @@ import { setCaretPosition } from "./caret.js";
|
|
|
5
5
|
import { displayWidth } from "./text-width.js";
|
|
6
6
|
import { visualRows, moveCaretHorizontal, moveCaretVertical, insertAt, backspaceAt, deleteAt, caretIndexFromClick, scanDeleteKeys, scanHomeEndKeys, moveToLineStart, moveToLineEnd, deleteToLineStart, deleteToLineEnd, deleteWordBefore, } from "./caret-pos.js";
|
|
7
7
|
import { searchHistory, loadInputHistory, saveInputHistory, createHistory, pushInput, browseHistory, resetHistory, isCtrlC, clearInputOnCtrlC, EXIT_CTRL_C_WINDOW_MS, } from "./input-history.js";
|
|
8
|
-
import { filterSlashCommands, getSlashArgOptions, applySlashArgCompletion, isKnownSlashCommandInput, slashMenuWindow, } from "./slash-commands.js";
|
|
8
|
+
import { filterSlashCommands, sessionSlashCommands, getSlashArgOptions, applySlashArgCompletion, isKnownSlashCommandInput, slashMenuWindow, } from "./slash-commands.js";
|
|
9
|
+
import { getActiveProvider, getEffectiveConfig } from "../config.js";
|
|
9
10
|
import { MOUSE_ENABLE, MOUSE_DISABLE } from "./mouse.js";
|
|
10
11
|
import { scanSgrMouse } from "./mouse.js";
|
|
11
12
|
import { TEXT_START_COLUMN, inputTextWidth, inputBarPaintRows, slashMenuMaxVisible, SLASH_COMMAND_MENU_CHROME, SLASH_ARG_MENU_CHROME, INPUT_BAR_ROWS, } from "./layout.js";
|
|
@@ -150,7 +151,8 @@ export function InputBar({ onSubmit, disabled, placeholder, columns, terminalRow
|
|
|
150
151
|
}, []);
|
|
151
152
|
// Slash command menu — closes once a space follows the command name so the
|
|
152
153
|
// argument menu can take over immediately.
|
|
153
|
-
const
|
|
154
|
+
const providerType = getActiveProvider(getEffectiveConfig())?.type;
|
|
155
|
+
const matches = useMemo(() => filterSlashCommands(value, sessionSlashCommands(providerType)), [value, providerType]);
|
|
154
156
|
const menuOpen = value.startsWith("/") && !/\s/.test(value.slice(1)) && !menuDismissed && matches.length > 0;
|
|
155
157
|
const shownIndex = menuOpen ? Math.min(menuIndex, matches.length - 1) : 0;
|
|
156
158
|
const selected = menuOpen ? matches[shownIndex] : undefined;
|
package/dist/tui/ThinkPicker.js
CHANGED
|
@@ -2,8 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useEffect, useRef, useState } from "react";
|
|
3
3
|
import { Box, Text, useInput } from "ink";
|
|
4
4
|
import { getActiveProvider, getEffectiveConfig } from "../config.js";
|
|
5
|
-
import {
|
|
6
|
-
import { resolveThinkingEffort, thinkingChoiceIndex, thinkingChoicesForModel, thinkingChoicesFromReasoning, thinkingEffortLabel, thinkingSourceLabel, } from "../thinking.js";
|
|
5
|
+
import { resolveThinkingEffort, thinkingChoiceIndex, thinkingChoicesForModel, thinkingEffortLabel, thinkingSourceLabel, refreshThinkingChoices, } from "../thinking.js";
|
|
7
6
|
import { shouldAcceptOverlayConfirm } from "./overlay-input.js";
|
|
8
7
|
import { theme } from "./theme.js";
|
|
9
8
|
export function thinkPickerRows() {
|
|
@@ -31,10 +30,9 @@ export function ThinkPicker({ scope, modelId, onSelect, onCancel }) {
|
|
|
31
30
|
if (!id)
|
|
32
31
|
return;
|
|
33
32
|
let cancelled = false;
|
|
34
|
-
void
|
|
35
|
-
if (cancelled ||
|
|
33
|
+
void refreshThinkingChoices(id, { type: provider?.type, baseURL: provider?.baseURL }).then((next) => {
|
|
34
|
+
if (cancelled || next.length === 0)
|
|
36
35
|
return;
|
|
37
|
-
const next = thinkingChoicesFromReasoning(entry.reasoning);
|
|
38
36
|
setChoices(next);
|
|
39
37
|
if (moved.current) {
|
|
40
38
|
setIndex((i) => Math.min(i, Math.max(0, next.length - 1)));
|
|
@@ -45,7 +43,7 @@ export function ThinkPicker({ scope, modelId, onSelect, onCancel }) {
|
|
|
45
43
|
return () => {
|
|
46
44
|
cancelled = true;
|
|
47
45
|
};
|
|
48
|
-
}, [id, provider?.baseURL]);
|
|
46
|
+
}, [id, provider?.baseURL, provider?.type]);
|
|
49
47
|
useInput((input, key) => {
|
|
50
48
|
if (key.escape) {
|
|
51
49
|
onCancel();
|
package/dist/tui/index.js
CHANGED
|
@@ -28,7 +28,7 @@ export class TuiRenderer {
|
|
|
28
28
|
this.callbacks = callbacks;
|
|
29
29
|
}
|
|
30
30
|
appElement() {
|
|
31
|
-
return (_jsx(App, { initialState: this.state, onSubmit: this.callbacks.onSubmit, onConfirm: this.callbacks.onConfirm, onQuestionAnswer: this.callbacks.onQuestionAnswer, onModelPick: this.callbacks.onModelPick, onModelCancel: this.callbacks.onModelCancel, onThinkPick: this.callbacks.onThinkPick, onThinkCancel: this.callbacks.onThinkCancel, onSessionPick: this.callbacks.onSessionPick, onSessionCancel: this.callbacks.onSessionCancel, onToggleSelectionMode: this.callbacks.onToggleSelectionMode, onExitSelectionMode: this.callbacks.onExitSelectionMode, onExit: this.callbacks.onExit, onCopyNotice: (text) => this.showCopyNotice(text), onNotice: this.callbacks.onNotice }));
|
|
31
|
+
return (_jsx(App, { initialState: this.state, onSubmit: this.callbacks.onSubmit, onConfirm: this.callbacks.onConfirm, onQuestionAnswer: this.callbacks.onQuestionAnswer, onModelPick: this.callbacks.onModelPick, onModelCancel: this.callbacks.onModelCancel, onThinkPick: this.callbacks.onThinkPick, onThinkCancel: this.callbacks.onThinkCancel, onCtxPick: this.callbacks.onCtxPick, onCtxCancel: this.callbacks.onCtxCancel, onSessionPick: this.callbacks.onSessionPick, onSessionCancel: this.callbacks.onSessionCancel, onToggleSelectionMode: this.callbacks.onToggleSelectionMode, onExitSelectionMode: this.callbacks.onExitSelectionMode, onExit: this.callbacks.onExit, onCopyNotice: (text) => this.showCopyNotice(text), onNotice: this.callbacks.onNotice }));
|
|
32
32
|
}
|
|
33
33
|
start() {
|
|
34
34
|
this.inkInstance = render(this.appElement(), { exitOnCtrlC: false, stdout: createCaretAwareStdout(process.stdout) });
|
|
@@ -197,6 +197,12 @@ export class TuiRenderer {
|
|
|
197
197
|
hideThinkPicker() {
|
|
198
198
|
this.update({ thinkPicker: undefined });
|
|
199
199
|
}
|
|
200
|
+
showCtxPicker() {
|
|
201
|
+
this.update({ ctxPicker: true, selectionMode: false });
|
|
202
|
+
}
|
|
203
|
+
hideCtxPicker() {
|
|
204
|
+
this.update({ ctxPicker: undefined });
|
|
205
|
+
}
|
|
200
206
|
showSessionPicker() {
|
|
201
207
|
this.update({ sessionPicker: true, selectionMode: false });
|
|
202
208
|
}
|
|
@@ -6,6 +6,7 @@ export const SLASH_COMMANDS = [
|
|
|
6
6
|
{ name: "provider", description: "查看/切换 Provider", argHint: "[name]" },
|
|
7
7
|
{ name: "memory", description: "查看/开关记忆,或保存一条", argHint: "[on|off|t] [--project]" },
|
|
8
8
|
{ name: "tokens", description: "显示上下文占用、累计用量与成本" },
|
|
9
|
+
{ name: "ctx", description: "选择本地模型上下文窗口" },
|
|
9
10
|
{ name: "budget", description: "查看/设置预算上限", argHint: "[n] [--project]" },
|
|
10
11
|
{ name: "permission", description: "查看/设置确认模式", argHint: "[ask|accept-edits|allow-all] [--project]" },
|
|
11
12
|
{ name: "think", description: "选择思考强度", aliases: ["thinking"] },
|
|
@@ -35,6 +36,11 @@ export function filterSlashCommands(input, commands = SLASH_COMMANDS) {
|
|
|
35
36
|
return commands;
|
|
36
37
|
return commands.filter((c) => c.name.startsWith(q) || (c.aliases ?? []).some((a) => a.startsWith(q)));
|
|
37
38
|
}
|
|
39
|
+
export function sessionSlashCommands(providerType, commands = SLASH_COMMANDS) {
|
|
40
|
+
if (providerType === "ollama")
|
|
41
|
+
return commands;
|
|
42
|
+
return commands.filter((c) => c.name !== "ctx");
|
|
43
|
+
}
|
|
38
44
|
/** Keep `selectedIndex` on screen; the highlight stays on the last row once the list scrolls. */
|
|
39
45
|
export function slashMenuWindow(items, selectedIndex, maxVisible) {
|
|
40
46
|
const max = Number.isFinite(maxVisible) ? Math.max(1, Math.floor(maxVisible)) : Math.max(1, items.length);
|
|
@@ -294,7 +294,7 @@ export async function handleSlashCommand(input, tui, messages, currentModel, tra
|
|
|
294
294
|
const price = await getModelPrice(currentModel ?? "");
|
|
295
295
|
const cost = estimateCost({ inputTokens: tracker.totalInputTokens, outputTokens: tracker.totalOutputTokens }, price);
|
|
296
296
|
const occupancyNote = tracker.lastInputTokens > 0 ? "" : ",按对话内容估算";
|
|
297
|
-
const windowNote = ctx.source === "fallback" ?
|
|
297
|
+
const windowNote = ctx.source === "fallback" ? `,未读到接口窗口,按 ${ctx.tokens} 默认` : "";
|
|
298
298
|
sysMsg(tui, [
|
|
299
299
|
`上下文: ${occupied} / ${ctx.tokens} tokens (${pct}%${occupancyNote}${windowNote})`,
|
|
300
300
|
`累计: ${tracker.totalInputTokens} in / ${tracker.totalOutputTokens} out`,
|
|
@@ -383,6 +383,31 @@ export async function handleSlashCommand(input, tui, messages, currentModel, tra
|
|
|
383
383
|
sysMsg(tui, `✓ 思考强度已设为 ${thinkingEffortLabel(parsed)}(${resolved === "project" ? "项目" : "全局"})`);
|
|
384
384
|
break;
|
|
385
385
|
}
|
|
386
|
+
case "ctx": {
|
|
387
|
+
const { getActiveProvider, getEffectiveConfig } = await import("../config.js");
|
|
388
|
+
const { CTX_CHOICES, parseCtxChoice, setOllamaContextChoice, ctxChoiceLabel } = await import("../ctx.js");
|
|
389
|
+
if (getActiveProvider(getEffectiveConfig())?.type !== "ollama") {
|
|
390
|
+
sysMsg(tui, "窗口档位仅本地模型可用");
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
if (rest.length === 0) {
|
|
394
|
+
tui.showCtxPicker();
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
const parsed = parseCtxChoice(rest[0]);
|
|
398
|
+
if (!parsed || rest.length > 1) {
|
|
399
|
+
sysMsg(tui, `用法: /ctx [${CTX_CHOICES.join("|")}]`);
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
const result = setOllamaContextChoice(parsed);
|
|
403
|
+
if (!result.ok) {
|
|
404
|
+
sysMsg(tui, "窗口档位仅本地模型可用");
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
state.refreshTokenBar();
|
|
408
|
+
sysMsg(tui, `✓ 上下文窗口已设为${ctxChoiceLabel(parsed)}`);
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
386
411
|
case "mcp": {
|
|
387
412
|
const { loadMcpConfig } = await import("../mcp.js");
|
|
388
413
|
const { getMcpStatus } = await import("../mcp.js");
|
|
@@ -544,6 +569,7 @@ export async function handleSlashCommand(input, tui, messages, currentModel, tra
|
|
|
544
569
|
" /provider [n] 查看/切换 Provider",
|
|
545
570
|
" /memory 查看记忆开关与已保存内容(/memory on|off 开关;也可 /memory 文本 保存)",
|
|
546
571
|
" /tokens 显示上下文占用、累计用量与成本",
|
|
572
|
+
" /ctx 选择本地模型上下文窗口(也可 /ctx 2k|4k|8k|12k|16k|32k|64k|128k|256k|auto)",
|
|
547
573
|
" /budget [n] 查看/设置预算上限(--project 写入当前项目)",
|
|
548
574
|
" /permission 查看/设置确认模式(ask|accept-edits|allow-all,--project 写入当前项目)",
|
|
549
575
|
" /think 交互式选择思考强度(档位随当前模型;也可 /think off|low|medium|high|max)",
|
package/dist/tui-chat.js
CHANGED
|
@@ -17,8 +17,8 @@ import { listSessions, loadSession, saveSession } from "./sessions.js";
|
|
|
17
17
|
import { hydrateMessages, lastUserText } from "./tui/hydrate.js";
|
|
18
18
|
import { createPromptQueue } from "./tui/prompt-queue.js";
|
|
19
19
|
import { prepareSwitch, switchFailMessage, prepareNew, newFailMessage } from "./tui/session-switch.js";
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
20
|
+
import { setThinkingEffort, setThinkingOverride, thinkingChoicesForModel, thinkingEffortLabel, refreshThinkingChoices, } from "./thinking.js";
|
|
21
|
+
import { CTX_CHOICES, ctxChoiceLabel, setOllamaContextChoice } from "./ctx.js";
|
|
22
22
|
let confirmResolver = null;
|
|
23
23
|
let questionResolver = null;
|
|
24
24
|
/** Only one confirm/question overlay can be shown at a time; parallel tool calls queue up. */
|
|
@@ -28,6 +28,13 @@ function registerSessionCompletion() {
|
|
|
28
28
|
setSlashArgProvider("sessions", provider);
|
|
29
29
|
setSlashArgProvider("resume", provider);
|
|
30
30
|
}
|
|
31
|
+
function registerCtxCompletion() {
|
|
32
|
+
setSlashArgProvider("ctx", (tokens) => {
|
|
33
|
+
if (tokens.length > 0)
|
|
34
|
+
return [];
|
|
35
|
+
return CTX_CHOICES.map((value) => (value === "auto" ? { value, description: "按模型探测" } : { value }));
|
|
36
|
+
});
|
|
37
|
+
}
|
|
31
38
|
function registerThinkCompletion() {
|
|
32
39
|
const provider = (tokens) => {
|
|
33
40
|
const flags = [
|
|
@@ -144,6 +151,7 @@ export async function runTui(opts) {
|
|
|
144
151
|
registerSkillCompletion();
|
|
145
152
|
registerPermissionCompletion();
|
|
146
153
|
registerThinkCompletion();
|
|
154
|
+
registerCtxCompletion();
|
|
147
155
|
registerMemoryCompletion();
|
|
148
156
|
registerSandboxCompletion();
|
|
149
157
|
registerSessionCompletion();
|
|
@@ -156,7 +164,7 @@ export async function runTui(opts) {
|
|
|
156
164
|
let currentModel = modelId ?? getActiveProvider(config)?.defaultModel;
|
|
157
165
|
const currentProvider = providerName;
|
|
158
166
|
if (currentModel)
|
|
159
|
-
void
|
|
167
|
+
void refreshThinkingChoices(currentModel);
|
|
160
168
|
const tracker = new TokenTracker();
|
|
161
169
|
const taskState = emptyTaskState();
|
|
162
170
|
let undoStack = createUndoStack();
|
|
@@ -205,6 +213,7 @@ export async function runTui(opts) {
|
|
|
205
213
|
startNewSession,
|
|
206
214
|
openSessionPicker,
|
|
207
215
|
taskState,
|
|
216
|
+
refreshTokenBar,
|
|
208
217
|
});
|
|
209
218
|
if (result === "run" && runner) {
|
|
210
219
|
await runner.run();
|
|
@@ -262,6 +271,19 @@ export async function runTui(opts) {
|
|
|
262
271
|
onThinkCancel: () => {
|
|
263
272
|
tui.hideThinkPicker();
|
|
264
273
|
},
|
|
274
|
+
onCtxPick: (choice) => {
|
|
275
|
+
const result = setOllamaContextChoice(choice);
|
|
276
|
+
tui.hideCtxPicker();
|
|
277
|
+
if (!result.ok) {
|
|
278
|
+
sysMsg(tui, "窗口档位仅本地模型可用");
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
void refreshTokenBar();
|
|
282
|
+
sysMsg(tui, `✓ 上下文窗口已设为${ctxChoiceLabel(choice)}`);
|
|
283
|
+
},
|
|
284
|
+
onCtxCancel: () => {
|
|
285
|
+
tui.hideCtxPicker();
|
|
286
|
+
},
|
|
265
287
|
onSessionPick: (id) => {
|
|
266
288
|
tui.hideSessionPicker();
|
|
267
289
|
applySession(id);
|
package/docs/API.md
CHANGED
|
@@ -33,6 +33,7 @@ min-agent serve --host 0.0.0.0 --port 3000
|
|
|
33
33
|
| GET | `/v1/update` | 检查 npm 是否有新版本(不执行安装) |
|
|
34
34
|
| GET | `/v1/models` | 模型列表 |
|
|
35
35
|
| GET | `/v1/context` | 上下文窗口信息 |
|
|
36
|
+
| POST | `/v1/context` | 设置本地模型上下文档位 |
|
|
36
37
|
| GET | `/v1/project` | 项目扫描 |
|
|
37
38
|
| POST | `/v1/chat` | 对话(项目感知) |
|
|
38
39
|
| POST | `/v1/code` | 与 `/v1/chat` 相同(兼容路径) |
|
|
@@ -118,10 +119,26 @@ CLI 升级请使用 `min-agent update`(执行 `npm install -g min-agent`)。
|
|
|
118
119
|
## `GET /v1/context`
|
|
119
120
|
|
|
120
121
|
```json
|
|
121
|
-
{ "context_window": 1000000, "source": "config", "model": "gpt-4o" }
|
|
122
|
+
{ "context_window": 1000000, "source": "config", "model": "gpt-4o", "configurable": false, "level": null }
|
|
122
123
|
```
|
|
123
124
|
|
|
124
|
-
`source` 为 `config`(配置中的 `contextWindow`)、`detected`(从接口读到)或 `fallback
|
|
125
|
+
`source` 为 `config`(配置中的 `contextWindow`)、`detected`(从接口读到)或 `fallback`(未读到:Ollama 按 2048,其余按 512k)。Ollama 的 detected 值是运行时窗口(优先 Modelfile `num_ctx`,否则不超过 32768),不是架构标称上限。对话请求走原生 `/api/chat` 并带上该窗口;只改 OpenAI 兼容接口的 `num_ctx` 不会生效。
|
|
126
|
+
|
|
127
|
+
当前为 Ollama 时额外返回 `configurable: true` 与 `level`(`2k`…`256k` 或 `auto`)。其它 provider 为 `configurable: false`、`level: null`。
|
|
128
|
+
|
|
129
|
+
## `POST /v1/context`
|
|
130
|
+
|
|
131
|
+
仅 Ollama provider。将当前 provider 的 `contextWindow` 写成对应档位;`auto` 删除覆盖,回到探测值。
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{ "level": "8k" }
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{ "ok": true, "context_window": 8192, "source": "config", "model": "qwen3:4b", "configurable": true, "level": "8k" }
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
非法档位返回 `400 invalid_context_level`。当前不是 Ollama 时返回 `400 ctx_not_supported`。
|
|
125
142
|
|
|
126
143
|
## `GET /v1/project`
|
|
127
144
|
|