@megacode/cli 0.2.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/LICENSE +21 -0
- package/README.md +268 -0
- package/dist/adapters/accounts.js +34 -0
- package/dist/adapters/auth/anthropic.js +36 -0
- package/dist/adapters/auth/chatgpt.js +100 -0
- package/dist/adapters/auth/oauth.js +68 -0
- package/dist/adapters/auth/openrouter.js +27 -0
- package/dist/adapters/auth/store.js +25 -0
- package/dist/adapters/git/worktree.js +113 -0
- package/dist/adapters/mcp/config.js +59 -0
- package/dist/adapters/mcp/manager.js +165 -0
- package/dist/adapters/mcp/results.js +21 -0
- package/dist/adapters/mcp/transport.js +19 -0
- package/dist/adapters/project.js +15 -0
- package/dist/adapters/providers/anthropic.js +103 -0
- package/dist/adapters/providers/catalog.js +80 -0
- package/dist/adapters/providers/chatgpt.js +117 -0
- package/dist/adapters/providers/credentials.js +55 -0
- package/dist/adapters/providers/gemini.js +123 -0
- package/dist/adapters/providers/openai.js +128 -0
- package/dist/adapters/providers/registry.js +68 -0
- package/dist/adapters/providers/shared.js +12 -0
- package/dist/adapters/providers/usage.js +95 -0
- package/dist/adapters/settings.js +15 -0
- package/dist/adapters/skills.js +163 -0
- package/dist/adapters/storage.js +30 -0
- package/dist/adapters/tools/files.js +113 -0
- package/dist/adapters/tools/image.js +63 -0
- package/dist/adapters/tools/index.js +26 -0
- package/dist/adapters/tools/output.js +56 -0
- package/dist/adapters/tools/paths.js +5 -0
- package/dist/adapters/tools/questions.js +52 -0
- package/dist/adapters/tools/shell.js +73 -0
- package/dist/adapters/tools/validation.js +34 -0
- package/dist/args.js +30 -0
- package/dist/cli.js +111 -0
- package/dist/composition.js +18 -0
- package/dist/core/agent.js +128 -0
- package/dist/core/conversation.js +37 -0
- package/dist/core/prompts.js +12 -0
- package/dist/core/provider.js +8 -0
- package/dist/core/settings.js +15 -0
- package/dist/core/suggestion.js +29 -0
- package/dist/core/tools.js +10 -0
- package/dist/lib/async.js +6 -0
- package/dist/ui/App.js +218 -0
- package/dist/ui/ApprovalDialog.js +39 -0
- package/dist/ui/ConfigMenu.js +106 -0
- package/dist/ui/Dialog.js +10 -0
- package/dist/ui/DiffLines.js +12 -0
- package/dist/ui/EffortPicker.js +8 -0
- package/dist/ui/Help.js +21 -0
- package/dist/ui/KeyList.js +10 -0
- package/dist/ui/LoginDialog.js +154 -0
- package/dist/ui/McpMenu.js +138 -0
- package/dist/ui/ModelPicker.js +117 -0
- package/dist/ui/PromptInput.js +161 -0
- package/dist/ui/Questionnaire.js +35 -0
- package/dist/ui/Select.js +28 -0
- package/dist/ui/Spinner.js +18 -0
- package/dist/ui/StatusLine.js +19 -0
- package/dist/ui/TextField.js +22 -0
- package/dist/ui/Transcript.js +40 -0
- package/dist/ui/WorktreeMenu.js +83 -0
- package/dist/ui/autocomplete.js +6 -0
- package/dist/ui/commands.js +117 -0
- package/dist/ui/diff.js +130 -0
- package/dist/ui/format.js +138 -0
- package/dist/ui/history.js +9 -0
- package/dist/ui/hooks/useAgentSession.js +34 -0
- package/dist/ui/hooks/usePromptSuggestion.js +23 -0
- package/dist/ui/hooks/useStreamedText.js +38 -0
- package/dist/ui/hooks/useWorktree.js +30 -0
- package/dist/ui/loginFlow.js +37 -0
- package/dist/ui/mcpWizard.js +18 -0
- package/dist/ui/plain.js +60 -0
- package/dist/ui/session.js +168 -0
- package/package.json +57 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { loadSettings } from "../adapters/settings.js";
|
|
5
|
+
import { configDir } from "../adapters/storage.js";
|
|
6
|
+
import { DEFAULT_SETTINGS } from "../core/settings.js";
|
|
7
|
+
import { Dialog } from "./Dialog.js";
|
|
8
|
+
import { tildify } from "./format.js";
|
|
9
|
+
const seconds = (ms) => (ms < 60_000 ? `${ms / 1000}s` : `${ms / 60_000}m`);
|
|
10
|
+
const onOff = [
|
|
11
|
+
{ value: true, label: "on" },
|
|
12
|
+
{ value: false, label: "off" },
|
|
13
|
+
];
|
|
14
|
+
const ENTRIES = [
|
|
15
|
+
{
|
|
16
|
+
key: "permissionMode",
|
|
17
|
+
label: "Permission mode",
|
|
18
|
+
description: "Ask before writes, edits and shell commands, auto-accept edits only, or bypass all prompts. Applies now and to new sessions (--ask overrides; shift+tab cycles)",
|
|
19
|
+
choices: [
|
|
20
|
+
{ value: "ask", label: "ask" },
|
|
21
|
+
{ value: "accept-edits", label: "accept edits" },
|
|
22
|
+
{ value: "yolo", label: "bypass" },
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
key: "maxSteps",
|
|
27
|
+
label: "Max steps per turn",
|
|
28
|
+
description: "Model calls the agent may make for one message before it stops",
|
|
29
|
+
choices: [25, 50, 100, 200].map((n) => ({ value: n, label: String(n) })),
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
key: "bashTimeoutMs",
|
|
33
|
+
label: "Shell command timeout",
|
|
34
|
+
description: "How long a bash command may run, unless the model asks for a different timeout",
|
|
35
|
+
choices: [30_000, 120_000, 300_000, 600_000].map((n) => ({ value: n, label: seconds(n) })),
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
key: "maxToolOutput",
|
|
39
|
+
label: "Max tool output",
|
|
40
|
+
description: "Characters of command and MCP output sent to the model; longer output keeps both ends and is saved to a temp file the model can read",
|
|
41
|
+
choices: [6_000, 12_000, 30_000, 100_000].map((n) => ({ value: n, label: `${n / 1000}k chars` })),
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
key: "projectInstructions",
|
|
45
|
+
label: "Load AGENTS.md / CLAUDE.md",
|
|
46
|
+
description: "Add project instruction files from the working directory to the system prompt",
|
|
47
|
+
choices: onOff,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
key: "worktreeBase",
|
|
51
|
+
label: "Worktree base",
|
|
52
|
+
description: "What /worktree and -w branch from: the remote's default branch (origin/HEAD), or the commit you're on",
|
|
53
|
+
choices: [
|
|
54
|
+
{ value: "fresh", label: "default branch" },
|
|
55
|
+
{ value: "head", label: "current commit" },
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
key: "promptAutocomplete",
|
|
60
|
+
label: "Prompt autocomplete",
|
|
61
|
+
description: "Suggest a next prompt from the current conversation; tab accepts without sending. Uses an extra model request after each turn",
|
|
62
|
+
choices: onOff,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
key: "saveHistory",
|
|
66
|
+
label: "Save prompt history",
|
|
67
|
+
description: "Keep ↑/↓ history across sessions in history.json",
|
|
68
|
+
choices: onOff,
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
const label = (e, value) => e.choices.find((c) => c.value === value)?.label ?? String(value);
|
|
72
|
+
/** Every setting in one list: ↑/↓ to move, enter/space/←/→ to change, esc to close. Changes save immediately. */
|
|
73
|
+
export function ConfigMenu({ model, mode, onChange, onModel, onClose, }) {
|
|
74
|
+
const [settings, setSettings] = useState(() => ({ ...loadSettings(), permissionMode: mode }));
|
|
75
|
+
const [index, setIndex] = useState(0);
|
|
76
|
+
const rows = ENTRIES.length + 1; // row 0 is the model
|
|
77
|
+
const cycle = (e, step) => {
|
|
78
|
+
const i = e.choices.findIndex((c) => c.value === settings[e.key]);
|
|
79
|
+
// A value hand-edited into settings.json may not be a choice; start from the first one.
|
|
80
|
+
const next = e.choices[i === -1 ? 0 : (i + step + e.choices.length) % e.choices.length].value;
|
|
81
|
+
const patch = { [e.key]: next };
|
|
82
|
+
setSettings((s) => ({ ...s, ...patch }));
|
|
83
|
+
onChange(patch);
|
|
84
|
+
};
|
|
85
|
+
useInput((input, key) => {
|
|
86
|
+
if (key.escape)
|
|
87
|
+
return onClose();
|
|
88
|
+
if (key.upArrow || (key.ctrl && input === "p"))
|
|
89
|
+
return setIndex((i) => (i - 1 + rows) % rows);
|
|
90
|
+
if (key.downArrow || (key.ctrl && input === "n"))
|
|
91
|
+
return setIndex((i) => (i + 1) % rows);
|
|
92
|
+
const activate = key.return || input === " ";
|
|
93
|
+
if (index === 0)
|
|
94
|
+
return activate ? onModel() : undefined;
|
|
95
|
+
if (activate || key.rightArrow)
|
|
96
|
+
cycle(ENTRIES[index - 1], 1);
|
|
97
|
+
else if (key.leftArrow)
|
|
98
|
+
cycle(ENTRIES[index - 1], -1);
|
|
99
|
+
});
|
|
100
|
+
const selected = index === 0 ? null : ENTRIES[index - 1];
|
|
101
|
+
const description = selected ? selected.description : "Model for new messages (opens the model picker)";
|
|
102
|
+
return (_jsxs(Dialog, { title: "Settings", subtitle: `saved to ${tildify(configDir())}/settings.json`, footer: "\u2191\u2193 navigate \u00B7 enter/space/\u2190\u2192 change \u00B7 esc close", children: [_jsxs(Box, { flexDirection: "column", children: [_jsx(Row, { selected: index === 0, label: "Model", value: model }), ENTRIES.map((e, i) => (_jsx(Row, { selected: index === i + 1, label: e.label, value: label(e, settings[e.key]), isDefault: settings[e.key] === DEFAULT_SETTINGS[e.key] }, e.key)))] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: description }) })] }));
|
|
103
|
+
}
|
|
104
|
+
function Row({ selected, label, value, isDefault = true }) {
|
|
105
|
+
return (_jsxs(Text, { color: selected ? "cyan" : undefined, children: [selected ? "❯ " : " ", label.padEnd(30), _jsx(Text, { bold: !isDefault, children: value })] }));
|
|
106
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
/**
|
|
4
|
+
* The frame every dialog uses, so spacing is the same everywhere: title line, a blank line,
|
|
5
|
+
* the body, then dim key hints. `warn` marks dialogs that ask before something happens.
|
|
6
|
+
*/
|
|
7
|
+
export function Dialog({ title, subtitle, footer, tone = "default", children, }) {
|
|
8
|
+
const color = tone === "warn" ? "yellow" : "cyan";
|
|
9
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: color, paddingX: 1, marginTop: 1, children: [_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, color: tone === "warn" ? color : undefined, children: title }), subtitle ? _jsxs(Text, { dimColor: true, children: [" \u00B7 ", subtitle] }) : null] }), children ? _jsx(Box, { flexDirection: "column", marginTop: 1, children: children }) : null, footer ? _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: footer }) }) : null] }));
|
|
10
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { DIFF_COLORS, gutter, pieces, rowStyle } from "./diff.js";
|
|
4
|
+
/** Diff rows as Claude Code draws them. Changed rows fill their width; long lines wrap under the code. */
|
|
5
|
+
export function DiffLines({ model }) {
|
|
6
|
+
return (_jsxs(Box, { flexDirection: "column", children: [model.rows.map((row, i) => {
|
|
7
|
+
if (row.type === "gap")
|
|
8
|
+
return (_jsxs(Text, { color: DIFF_COLORS.muted, children: [gutter(row, model), "\u2026"] }, i));
|
|
9
|
+
const style = rowStyle(row);
|
|
10
|
+
return (_jsxs(Box, { backgroundColor: style.background, children: [_jsx(Box, { flexShrink: 0, children: _jsx(Text, { color: style.color, backgroundColor: style.background, children: gutter(row, model) }) }), _jsx(Box, { flexGrow: 1, flexShrink: 1, minWidth: 0, children: _jsx(Text, { wrap: "wrap", color: style.color, backgroundColor: style.background, children: pieces(row).map((p, j) => (_jsx(Text, { backgroundColor: p.changed ? style.word : style.background, children: p.text }, j))) }) })] }, i));
|
|
11
|
+
}), model.hidden > 0 && _jsxs(Text, { color: DIFF_COLORS.muted, children: ["\u2026 +", model.hidden, " lines"] })] }));
|
|
12
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { EFFORTS } from "../core/provider.js";
|
|
3
|
+
import { Dialog } from "./Dialog.js";
|
|
4
|
+
import { Select } from "./Select.js";
|
|
5
|
+
/** /effort without an argument: pick the reasoning effort for future turns. */
|
|
6
|
+
export function EffortPicker({ current = "default", onSelect, onCancel }) {
|
|
7
|
+
return (_jsx(Dialog, { title: "Model effort", subtitle: current, footer: "Saved for future turns \u00B7 model support varies \u00B7 esc cancel", children: _jsx(Select, { options: EFFORTS.map((effort) => ({ label: effort, value: effort })), initialIndex: Math.max(0, EFFORTS.indexOf(current)), onSelect: onSelect, onCancel: onCancel }) }));
|
|
8
|
+
}
|
package/dist/ui/Help.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box } from "ink";
|
|
3
|
+
import { COMMANDS } from "./commands.js";
|
|
4
|
+
import { KeyList } from "./KeyList.js";
|
|
5
|
+
const SHORTCUTS = [
|
|
6
|
+
["enter", "send message (queued while running)"],
|
|
7
|
+
["ctrl+s", "send queued messages now (interrupts the running turn)"],
|
|
8
|
+
["\\ + enter, option+enter", "newline"],
|
|
9
|
+
["↑ / ↓", "prompt history"],
|
|
10
|
+
["tab", "accept prompt suggestion / complete command"],
|
|
11
|
+
["alt+← / alt+→", "move cursor by word"],
|
|
12
|
+
["/", "commands"],
|
|
13
|
+
["esc", "interrupt · clear input"],
|
|
14
|
+
["shift+tab", "cycle permission mode"],
|
|
15
|
+
["ctrl+a / ctrl+e", "start / end of line"],
|
|
16
|
+
["ctrl+u / ctrl+k / ctrl+w", "delete to start / end / word"],
|
|
17
|
+
["ctrl+c", "interrupt · clear · exit (twice)"],
|
|
18
|
+
];
|
|
19
|
+
export function Help() {
|
|
20
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, marginTop: 1, gap: 1, children: [_jsx(KeyList, { rows: SHORTCUTS }), _jsx(KeyList, { rows: COMMANDS.map((c) => [c.name, c.description]) })] }));
|
|
21
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
/**
|
|
4
|
+
* Keys or commands in one column, what they do in the next. Long descriptions wrap inside
|
|
5
|
+
* their own column, so the left column stays readable at any width.
|
|
6
|
+
*/
|
|
7
|
+
export function KeyList({ rows, selected }) {
|
|
8
|
+
const width = Math.max(...rows.map(([key]) => key.length)) + 2;
|
|
9
|
+
return (_jsx(Box, { flexDirection: "column", children: rows.map(([key, text], i) => (_jsxs(Box, { children: [_jsx(Box, { width: width, flexShrink: 0, children: _jsx(Text, { color: "cyan", children: key }) }), _jsx(Box, { flexShrink: 1, minWidth: 0, children: _jsx(Text, { dimColor: i !== selected, color: i === selected ? "cyan" : undefined, children: text }) })] }, key))) }));
|
|
10
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import { useRef, useState } from "react";
|
|
4
|
+
import { countModels, saveChatGPTLogin, verifyAndSaveKey } from "../adapters/accounts.js";
|
|
5
|
+
import { loginAnthropicCLI } from "../adapters/auth/anthropic.js";
|
|
6
|
+
import { loginChatGPT } from "../adapters/auth/chatgpt.js";
|
|
7
|
+
import { loginOpenRouter } from "../adapters/auth/openrouter.js";
|
|
8
|
+
import { savedCredentials, savedProviders } from "../adapters/auth/store.js";
|
|
9
|
+
import { configDir } from "../adapters/storage.js";
|
|
10
|
+
import { envKeyName, PROVIDER_INFO, providerInfo } from "../adapters/providers/catalog.js";
|
|
11
|
+
import { authStatus, isConfigured } from "../adapters/providers/credentials.js";
|
|
12
|
+
import { Dialog } from "./Dialog.js";
|
|
13
|
+
import { tildify } from "./format.js";
|
|
14
|
+
import { firstStep, friendlyError, stepAfterFailure, stepBack } from "./loginFlow.js";
|
|
15
|
+
import { Select } from "./Select.js";
|
|
16
|
+
import { Waiting } from "./Spinner.js";
|
|
17
|
+
import { TextField } from "./TextField.js";
|
|
18
|
+
const METHOD_LABELS = {
|
|
19
|
+
chatgpt: ["Sign in with ChatGPT", "use your Plus / Pro / Business plan"],
|
|
20
|
+
"openrouter-oauth": ["Sign in with OpenRouter", "in your browser; creates a key for megacode"],
|
|
21
|
+
"ant-cli": ["Sign in with the Anthropic CLI", "runs `ant auth login` in your browser"],
|
|
22
|
+
key: ["Paste an API key", ""],
|
|
23
|
+
url: ["Connect to a server", ""],
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* /login: provider → sign-in method → browser OAuth, API key or endpoint → verify by listing models → save.
|
|
27
|
+
*/
|
|
28
|
+
export function LoginDialog({ initialProvider, welcome, onDone, onCancel, }) {
|
|
29
|
+
const [provider, setProvider] = useState(initialProvider);
|
|
30
|
+
const [step, setStep] = useState(initialProvider ? firstStep(providerInfo(initialProvider)) : "pick");
|
|
31
|
+
const [method, setMethod] = useState();
|
|
32
|
+
const [baseURL, setBaseURL] = useState(initialProvider ? defaultURL(initialProvider) : "");
|
|
33
|
+
const [apiKey, setApiKey] = useState("");
|
|
34
|
+
const [error, setError] = useState("");
|
|
35
|
+
const [browserUrl, setBrowserUrl] = useState("");
|
|
36
|
+
const [output, setOutput] = useState([]);
|
|
37
|
+
const abort = useRef(null);
|
|
38
|
+
const info = provider ? providerInfo(provider) : undefined;
|
|
39
|
+
function pick(name) {
|
|
40
|
+
setProvider(name);
|
|
41
|
+
setBaseURL(defaultURL(name));
|
|
42
|
+
setApiKey("");
|
|
43
|
+
setError("");
|
|
44
|
+
setStep(firstStep(providerInfo(name)));
|
|
45
|
+
}
|
|
46
|
+
function back() {
|
|
47
|
+
setError("");
|
|
48
|
+
if (step === "browser")
|
|
49
|
+
abort.current?.abort();
|
|
50
|
+
const previous = stepBack(step, info, !!initialProvider);
|
|
51
|
+
if (previous === "close")
|
|
52
|
+
onCancel();
|
|
53
|
+
else
|
|
54
|
+
setStep(previous);
|
|
55
|
+
}
|
|
56
|
+
function chooseMethod(m) {
|
|
57
|
+
setMethod(m);
|
|
58
|
+
setError("");
|
|
59
|
+
if (m === "key")
|
|
60
|
+
return setStep("key");
|
|
61
|
+
if (m === "url")
|
|
62
|
+
return setStep("url");
|
|
63
|
+
browserLogin(m);
|
|
64
|
+
}
|
|
65
|
+
async function browserLogin(m) {
|
|
66
|
+
const name = provider;
|
|
67
|
+
const ctrl = (abort.current = new AbortController());
|
|
68
|
+
setBrowserUrl("");
|
|
69
|
+
setOutput([]);
|
|
70
|
+
setStep("browser");
|
|
71
|
+
try {
|
|
72
|
+
if (m === "chatgpt") {
|
|
73
|
+
saveChatGPTLogin(await loginChatGPT(setBrowserUrl, ctrl.signal));
|
|
74
|
+
return await finish(name, "", m);
|
|
75
|
+
}
|
|
76
|
+
if (m === "openrouter-oauth")
|
|
77
|
+
return await verify(await loginOpenRouter(setBrowserUrl, ctrl.signal), m);
|
|
78
|
+
if (m === "ant-cli") {
|
|
79
|
+
await loginAnthropicCLI((line) => setOutput((o) => [...o.slice(-4), line]), ctrl.signal);
|
|
80
|
+
return await finish(name, "", m);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
if (ctrl.signal.aborted)
|
|
85
|
+
return;
|
|
86
|
+
setError(e.message);
|
|
87
|
+
setStep("method");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// `via` and `url` are passed along rather than read from state: they're often set in the same
|
|
91
|
+
// handler that calls these, and the state wouldn't show them yet.
|
|
92
|
+
/** Save an API key / endpoint after checking it works. */
|
|
93
|
+
async function verify(key, via = method, url = baseURL) {
|
|
94
|
+
const name = provider;
|
|
95
|
+
setStep("verifying");
|
|
96
|
+
setError("");
|
|
97
|
+
try {
|
|
98
|
+
await verifyAndSaveKey(name, { apiKey: key, baseURL: url });
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
return fail(e, key, via, url);
|
|
102
|
+
}
|
|
103
|
+
await finish(name, key, via);
|
|
104
|
+
}
|
|
105
|
+
/** Credentials are in place: count the models and hand back to the app. */
|
|
106
|
+
async function finish(name, key, via) {
|
|
107
|
+
setStep("verifying");
|
|
108
|
+
try {
|
|
109
|
+
onDone(name, await countModels(name));
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
fail(e, key, via);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function fail(e, key, via, url = baseURL) {
|
|
116
|
+
const p = providerInfo(provider);
|
|
117
|
+
setError(friendlyError(e, p, url));
|
|
118
|
+
setStep(stepAfterFailure(p, via, key));
|
|
119
|
+
}
|
|
120
|
+
useInput((_, key) => key.escape && back(), { isActive: step === "browser" });
|
|
121
|
+
const envKey = info && envKeyName(info);
|
|
122
|
+
const envNote = envKey && _jsxs(Text, { color: "yellow", children: ["$", envKey, " is set and takes precedence over anything saved here."] });
|
|
123
|
+
const errorLine = error && _jsx(Text, { color: "red", children: error });
|
|
124
|
+
const keys = "enter continue · esc back";
|
|
125
|
+
if (step === "pick" || !info)
|
|
126
|
+
return (_jsxs(Dialog, { title: welcome ? "Welcome to megacode" : "Log in to a provider", footer: `Saved to ${tildify(configDir())}/auth.json, readable only by you · esc close`, children: [welcome && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: "Connect a model provider to get started. You can add more later with /login." }) })), _jsx(Select, { options: PROVIDER_INFO.map((p) => ({
|
|
127
|
+
label: p.label,
|
|
128
|
+
value: p.name,
|
|
129
|
+
hint: p.local ? defaultURL(p.name) : isConfigured(p.name) ? `✔ ${authStatus(p.name)}` : undefined,
|
|
130
|
+
})), onSelect: pick, onCancel: onCancel })] }));
|
|
131
|
+
if (step === "method")
|
|
132
|
+
return (_jsxs(Dialog, { title: `Log in to ${info.label}`, subtitle: "how do you want to sign in?", footer: "enter select \u00B7 esc back", children: [errorLine, envNote, _jsx(Box, { marginTop: error || envNote ? 1 : 0, children: _jsx(Select, { options: info.methods.map((m) => ({ label: METHOD_LABELS[m][0], value: m, hint: METHOD_LABELS[m][1] || undefined })), onSelect: chooseMethod, onCancel: back }) })] }));
|
|
133
|
+
if (step === "browser")
|
|
134
|
+
return (_jsxs(Dialog, { title: method ? METHOD_LABELS[method][0] : "Sign in", footer: "esc cancel", children: [_jsx(Text, { dimColor: true, children: method === "ant-cli" ? "Running `ant auth login`. Finish signing in in your browser." : "Finish signing in in your browser. If it didn't open, visit:" }), browserUrl && _jsx(Text, { color: "cyan", children: browserUrl }), output.map((line, i) => (_jsx(Text, { dimColor: true, children: line }, i))), _jsx(Box, { marginTop: 1, children: _jsx(Waiting, { text: "Waiting for sign-in\u2026" }) })] }));
|
|
135
|
+
if (step === "url")
|
|
136
|
+
return (_jsxs(Dialog, { title: `Log in to ${info.label}`, subtitle: "endpoint URL", footer: keys, children: [_jsx(Text, { dimColor: true, children: info.local ? `Make sure the server is running. Default: ${info.baseURL}` : "Base URL of the OpenAI-compatible API, e.g. https://host/v1" }), errorLine, _jsx(Box, { marginTop: 1, children: _jsx(TextField, { value: baseURL, onChange: setBaseURL, placeholder: info.baseURL ?? "https://…/v1", onSubmit: (v) => {
|
|
137
|
+
const url = v || info.baseURL || "";
|
|
138
|
+
if (!url)
|
|
139
|
+
return setError("An endpoint URL is required.");
|
|
140
|
+
setBaseURL(url);
|
|
141
|
+
if (info.local)
|
|
142
|
+
verify("", method, url);
|
|
143
|
+
else
|
|
144
|
+
setStep("key");
|
|
145
|
+
}, onCancel: back }) })] }));
|
|
146
|
+
if (step === "key")
|
|
147
|
+
return (_jsxs(Dialog, { title: `Log in to ${info.label}`, subtitle: "API key", footer: keys, children: [info.keyUrl && _jsxs(Text, { dimColor: true, children: ["Create one at ", info.keyUrl] }), info.keyOptional && _jsx(Text, { dimColor: true, children: "Leave empty if the endpoint doesn't need a key." }), envNote, errorLine, _jsx(Box, { marginTop: info.keyUrl || info.keyOptional || envNote || error ? 1 : 0, children: _jsx(TextField, { value: apiKey, onChange: setApiKey, mask: true, placeholder: "paste your key and press enter", onSubmit: (v) => (v || info.keyOptional ? verify(v) : setError("Paste a key, or esc to go back.")), onCancel: back }) })] }));
|
|
148
|
+
return (_jsx(Dialog, { title: `Log in to ${info.label}`, children: _jsx(Waiting, { text: `Connecting to ${info.label}…` }) }));
|
|
149
|
+
}
|
|
150
|
+
/** /logout: pick which saved credentials to remove. */
|
|
151
|
+
export function LogoutDialog({ onSelect, onCancel }) {
|
|
152
|
+
return (_jsx(Dialog, { title: "Remove saved credentials", footer: "enter remove \u00B7 esc cancel", children: _jsx(Select, { options: savedProviders().map((n) => ({ label: providerInfo(n).label, value: n, hint: authStatus(n) })), onSelect: onSelect, onCancel: onCancel }) }));
|
|
153
|
+
}
|
|
154
|
+
const defaultURL = (name) => savedCredentials(name)?.baseURL ?? providerInfo(name).baseURL ?? "";
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { useEffect, useReducer, useState } from "react";
|
|
4
|
+
import { describeServer, draftToConfig, MCP_FILE, parseExtra, transportOf, } from "../adapters/mcp/config.js";
|
|
5
|
+
import { mcp } from "../adapters/mcp/manager.js";
|
|
6
|
+
import { configDir } from "../adapters/storage.js";
|
|
7
|
+
import { previousAddStep, serverNameError, targetError } from "./mcpWizard.js";
|
|
8
|
+
import { tildify } from "./format.js";
|
|
9
|
+
import { Dialog } from "./Dialog.js";
|
|
10
|
+
import { Select } from "./Select.js";
|
|
11
|
+
import { TextField } from "./TextField.js";
|
|
12
|
+
const MAX_TOOLS_SHOWN = 15;
|
|
13
|
+
function statusText(s) {
|
|
14
|
+
switch (s.state) {
|
|
15
|
+
case "connected":
|
|
16
|
+
return `✔ connected · ${s.tools.length} tool${s.tools.length === 1 ? "" : "s"}`;
|
|
17
|
+
case "connecting":
|
|
18
|
+
return "… connecting";
|
|
19
|
+
case "failed":
|
|
20
|
+
return `✗ ${s.error}`;
|
|
21
|
+
case "disabled":
|
|
22
|
+
return "○ disabled";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const statusColor = (s) => ({ connected: "green", connecting: "yellow", failed: "red", disabled: "gray" })[s.state];
|
|
26
|
+
/** List, add, inspect, reconnect, enable/disable and remove MCP servers. Changes save to ~/.megacode/mcp.json. */
|
|
27
|
+
export function McpMenu({ onClose }) {
|
|
28
|
+
const [, rerender] = useReducer((n) => n + 1, 0);
|
|
29
|
+
useEffect(() => mcp.subscribe(rerender), []);
|
|
30
|
+
const [screen, setScreen] = useState({ type: "list" });
|
|
31
|
+
const [input, setInput] = useState("");
|
|
32
|
+
const [error, setError] = useState("");
|
|
33
|
+
const servers = mcp.servers();
|
|
34
|
+
const file = `${tildify(configDir())}/${MCP_FILE}`;
|
|
35
|
+
const go = (s) => {
|
|
36
|
+
setScreen(s);
|
|
37
|
+
setInput("");
|
|
38
|
+
setError("");
|
|
39
|
+
};
|
|
40
|
+
let body;
|
|
41
|
+
let help = "↑↓ navigate · enter select · esc back";
|
|
42
|
+
if (screen.type === "list") {
|
|
43
|
+
help = "↑↓ navigate · enter select · esc close";
|
|
44
|
+
body = (_jsxs(_Fragment, { children: [servers.length === 0 && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "No MCP servers yet. Add one to give megacode more tools." }) })), _jsx(Select, { options: [
|
|
45
|
+
...servers.map((s) => ({ label: s.name, value: s.name, hint: statusText(s.status) })),
|
|
46
|
+
{ label: "Add server…", value: "" },
|
|
47
|
+
], onSelect: (name) => (name ? go({ type: "server", name }) : go({ type: "add", step: "name", draft: { name: "", type: "stdio", target: "", extras: {} } })), onCancel: onClose })] }));
|
|
48
|
+
}
|
|
49
|
+
else if (screen.type === "server") {
|
|
50
|
+
const server = servers.find((s) => s.name === screen.name);
|
|
51
|
+
if (!server) {
|
|
52
|
+
body = _jsx(Text, { dimColor: true, children: "Server removed." });
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
const { config, status } = server;
|
|
56
|
+
const extras = "url" in config ? Object.keys(config.headers ?? {}) : Object.keys(config.env ?? {});
|
|
57
|
+
const tools = status.state === "connected" ? status.tools : [];
|
|
58
|
+
body = (_jsxs(_Fragment, { children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, children: server.name }), " ", _jsxs(Text, { dimColor: true, children: ["(", transportOf(config), ")"] }), " ", _jsx(Text, { color: statusColor(status), children: statusText(status) })] }), _jsx(Text, { dimColor: true, children: describeServer(config) }), extras.length > 0 && _jsxs(Text, { dimColor: true, children: ["url" in config ? "headers" : "env", ": ", extras.join(", ")] }), tools.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [tools.slice(0, MAX_TOOLS_SHOWN).map((t) => (_jsxs(Text, { children: [" ", t.tool, _jsxs(Text, { dimColor: true, children: [" ", t.description.split("\n")[0].slice(0, 70)] })] }, t.name))), tools.length > MAX_TOOLS_SHOWN && _jsx(Text, { dimColor: true, children: ` … and ${tools.length - MAX_TOOLS_SHOWN} more` })] })), _jsx(Box, { marginTop: 1, children: _jsx(Select, { options: [
|
|
59
|
+
...(config.disabled
|
|
60
|
+
? [{ label: "Enable", value: "enable" }]
|
|
61
|
+
: [
|
|
62
|
+
{ label: "Reconnect", value: "reconnect" },
|
|
63
|
+
{ label: "Disable", value: "disable" },
|
|
64
|
+
]),
|
|
65
|
+
{ label: "Remove", value: "remove" },
|
|
66
|
+
{ label: "Back", value: "back" },
|
|
67
|
+
], onSelect: (action) => {
|
|
68
|
+
if (action === "reconnect")
|
|
69
|
+
mcp.connect(server.name);
|
|
70
|
+
else if (action === "enable")
|
|
71
|
+
mcp.setEnabled(server.name, true);
|
|
72
|
+
else if (action === "disable")
|
|
73
|
+
mcp.setEnabled(server.name, false);
|
|
74
|
+
else if (action === "remove")
|
|
75
|
+
go({ type: "remove", name: server.name });
|
|
76
|
+
else
|
|
77
|
+
go({ type: "list" });
|
|
78
|
+
}, onCancel: () => go({ type: "list" }) }, `${server.name}-${status.state}`) })] }));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else if (screen.type === "remove") {
|
|
82
|
+
body = (_jsxs(_Fragment, { children: [_jsxs(Text, { children: ["Remove ", screen.name, " from ", file, "?"] }), _jsx(Select, { options: [
|
|
83
|
+
{ label: "No, keep it", value: false },
|
|
84
|
+
{ label: "Yes, remove", value: true },
|
|
85
|
+
], onSelect: async (yes) => {
|
|
86
|
+
if (!yes)
|
|
87
|
+
return go({ type: "server", name: screen.name });
|
|
88
|
+
await mcp.remove(screen.name);
|
|
89
|
+
go({ type: "list" });
|
|
90
|
+
}, onCancel: () => go({ type: "server", name: screen.name }) })] }));
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
const { step, draft } = screen;
|
|
94
|
+
const isUrl = draft.type !== "stdio";
|
|
95
|
+
const back = () => {
|
|
96
|
+
const previous = previousAddStep(step);
|
|
97
|
+
go(previous ? { type: "add", step: previous, draft } : { type: "list" });
|
|
98
|
+
};
|
|
99
|
+
help = "enter continue · esc back";
|
|
100
|
+
const field = (prompt, placeholder, submit) => (_jsxs(_Fragment, { children: [_jsx(Text, { children: prompt }), _jsx(TextField, { value: input, onChange: (v) => {
|
|
101
|
+
setInput(v);
|
|
102
|
+
setError("");
|
|
103
|
+
}, onSubmit: submit, onCancel: back, placeholder: placeholder })] }));
|
|
104
|
+
if (step === "name")
|
|
105
|
+
body = field("Server name (used in tool names, e.g. mcp__github__create_issue):", "e.g. github", (v) => {
|
|
106
|
+
const problem = serverNameError(v, servers.map((s) => s.name));
|
|
107
|
+
if (problem)
|
|
108
|
+
return setError(problem);
|
|
109
|
+
go({ type: "add", step: "type", draft: { ...draft, name: v } });
|
|
110
|
+
});
|
|
111
|
+
else if (step === "type")
|
|
112
|
+
body = (_jsxs(_Fragment, { children: [_jsxs(Text, { children: ["How does megacode reach ", draft.name, "?"] }), _jsx(Select, { options: [
|
|
113
|
+
{ label: "stdio", value: "stdio", hint: "run a local command (npx, uvx, docker, a binary…)" },
|
|
114
|
+
{ label: "http", value: "http", hint: "remote server, streamable HTTP" },
|
|
115
|
+
{ label: "sse", value: "sse", hint: "remote server, legacy SSE" },
|
|
116
|
+
], onSelect: (type) => go({ type: "add", step: "target", draft: { ...draft, type, extras: {} } }), onCancel: back })] }));
|
|
117
|
+
else if (step === "target")
|
|
118
|
+
body = field(isUrl ? "Server URL:" : "Command to start the server:", isUrl ? "https://example.com/mcp" : "npx -y @modelcontextprotocol/server-filesystem ~/projects", (v) => {
|
|
119
|
+
const problem = targetError(draft.type, v);
|
|
120
|
+
if (problem)
|
|
121
|
+
return setError(problem);
|
|
122
|
+
go({ type: "add", step: "extras", draft: { ...draft, target: v } });
|
|
123
|
+
});
|
|
124
|
+
else
|
|
125
|
+
body = (_jsxs(_Fragment, { children: [Object.entries(draft.extras).map(([k]) => (_jsxs(Text, { dimColor: true, children: [" ", isUrl ? `${k}: ••••` : `${k}=••••`] }, k))), field(isUrl ? "Add a header (Name: value), or press enter to finish:" : "Add an environment variable (KEY=value), or press enter to finish:", isUrl ? "Authorization: Bearer …" : "GITHUB_TOKEN=…", (v) => {
|
|
126
|
+
if (!v) {
|
|
127
|
+
mcp.add(draft.name, draftToConfig(draft));
|
|
128
|
+
return go({ type: "server", name: draft.name });
|
|
129
|
+
}
|
|
130
|
+
const extra = parseExtra(draft.type, v);
|
|
131
|
+
if (!extra)
|
|
132
|
+
return setError(isUrl ? "Use the form Name: value" : "Use the form KEY=value");
|
|
133
|
+
const [key, value] = extra;
|
|
134
|
+
go({ type: "add", step: "extras", draft: { ...draft, extras: { ...draft.extras, [key]: value } } });
|
|
135
|
+
})] }));
|
|
136
|
+
}
|
|
137
|
+
return (_jsxs(Dialog, { title: "MCP servers", subtitle: file, footer: help, children: [body, error && _jsx(Text, { color: "red", children: error })] }));
|
|
138
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput, usePaste } from "ink";
|
|
3
|
+
import { useEffect, useMemo, useState } from "react";
|
|
4
|
+
import { PROVIDER_INFO } from "../adapters/providers/catalog.js";
|
|
5
|
+
import { isConfigured } from "../adapters/providers/credentials.js";
|
|
6
|
+
import { listModels } from "../adapters/providers/registry.js";
|
|
7
|
+
import { Dialog } from "./Dialog.js";
|
|
8
|
+
const WINDOW = 12;
|
|
9
|
+
/** Searchable list of every model from every configured provider, fetched live. */
|
|
10
|
+
export function ModelPicker({ current, initialQuery = "", onSelect, onLogin, onCancel, }) {
|
|
11
|
+
const [query, setQuery] = useState(initialQuery);
|
|
12
|
+
const [lists, setLists] = useState({});
|
|
13
|
+
const [index, setIndex] = useState(0);
|
|
14
|
+
const [touched, setTouched] = useState(false); // user moved the cursor; stop auto-selecting current
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
for (const p of PROVIDER_INFO) {
|
|
17
|
+
if (!isConfigured(p.name))
|
|
18
|
+
continue;
|
|
19
|
+
setLists((l) => ({ ...l, [p.name]: { status: "loading" } }));
|
|
20
|
+
listModels(p.name).then((models) => setLists((l) => ({ ...l, [p.name]: { status: "ok", models } })), (e) => setLists((l) => ({ ...l, [p.name]: { status: "error", error: e.message.split("\n")[0] } })));
|
|
21
|
+
}
|
|
22
|
+
}, []);
|
|
23
|
+
const rows = useMemo(() => {
|
|
24
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
25
|
+
const matches = (s) => terms.every((t) => s.toLowerCase().includes(t));
|
|
26
|
+
const out = [];
|
|
27
|
+
const seen = new Set();
|
|
28
|
+
for (const p of PROVIDER_INFO) {
|
|
29
|
+
const state = lists[p.name];
|
|
30
|
+
if (!isConfigured(p.name)) {
|
|
31
|
+
if (!p.local && !p.keyOptional && matches(`${p.name} ${p.label} login`))
|
|
32
|
+
out.push({ provider: p.name, label: "Log in to see models…", action: { type: "login", provider: p.name }, dim: true });
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (!state || state.status === "loading") {
|
|
36
|
+
if (matches(p.name))
|
|
37
|
+
out.push({ provider: p.name, label: "loading…", action: { type: "none" }, dim: true });
|
|
38
|
+
}
|
|
39
|
+
else if (state.status === "error") {
|
|
40
|
+
// Local servers that aren't running just don't show up.
|
|
41
|
+
if (!p.local && matches(p.name))
|
|
42
|
+
out.push({
|
|
43
|
+
provider: p.name,
|
|
44
|
+
label: `couldn't list models: ${state.error}`,
|
|
45
|
+
action: { type: "login", provider: p.name },
|
|
46
|
+
dim: true,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
for (const m of state.models) {
|
|
51
|
+
const spec = `${p.name}:${m}`;
|
|
52
|
+
seen.add(spec);
|
|
53
|
+
if (matches(spec))
|
|
54
|
+
out.push({ provider: p.name, label: m, action: { type: "model", spec } });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Anything typed as provider:model can be used even if the provider doesn't list it.
|
|
59
|
+
const typed = query.trim();
|
|
60
|
+
if (/^[a-z]+:\S+$/.test(typed) && !seen.has(typed))
|
|
61
|
+
out.unshift({ provider: typed.split(":")[0], label: `use "${typed.slice(typed.indexOf(":") + 1)}"`, action: { type: "model", spec: typed } });
|
|
62
|
+
return out;
|
|
63
|
+
}, [lists, query]);
|
|
64
|
+
// Start on the current model until the user moves; clamp when the list shrinks.
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (!touched) {
|
|
67
|
+
const i = rows.findIndex((r) => r.action.type === "model" && r.action.spec === current);
|
|
68
|
+
setIndex(Math.max(0, i));
|
|
69
|
+
}
|
|
70
|
+
else
|
|
71
|
+
setIndex((i) => Math.min(i, Math.max(0, rows.length - 1)));
|
|
72
|
+
}, [rows]);
|
|
73
|
+
const move = (d) => {
|
|
74
|
+
setTouched(true);
|
|
75
|
+
setIndex((i) => Math.max(0, Math.min(rows.length - 1, i + d)));
|
|
76
|
+
};
|
|
77
|
+
usePaste((text) => setQuery((q) => q + text.trim()));
|
|
78
|
+
useInput((input, key) => {
|
|
79
|
+
if (key.escape)
|
|
80
|
+
return onCancel();
|
|
81
|
+
if (key.upArrow)
|
|
82
|
+
return move(-1);
|
|
83
|
+
if (key.downArrow)
|
|
84
|
+
return move(1);
|
|
85
|
+
if (key.pageUp)
|
|
86
|
+
return move(-WINDOW);
|
|
87
|
+
if (key.pageDown)
|
|
88
|
+
return move(WINDOW);
|
|
89
|
+
if (key.return) {
|
|
90
|
+
const row = rows[index];
|
|
91
|
+
if (row?.action.type === "model")
|
|
92
|
+
onSelect(row.action.spec);
|
|
93
|
+
else if (row?.action.type === "login")
|
|
94
|
+
onLogin(row.action.provider);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (key.backspace || key.delete)
|
|
98
|
+
return setQuery((q) => q.slice(0, -1));
|
|
99
|
+
if (key.ctrl && input === "u")
|
|
100
|
+
return setQuery("");
|
|
101
|
+
if (key.ctrl || key.meta || key.tab || key.leftArrow || key.rightArrow)
|
|
102
|
+
return;
|
|
103
|
+
if (input) {
|
|
104
|
+
setTouched(true);
|
|
105
|
+
setIndex(0);
|
|
106
|
+
setQuery((q) => q + input);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
const start = Math.max(0, Math.min(index - Math.floor(WINDOW / 2), rows.length - WINDOW));
|
|
110
|
+
const visible = rows.slice(start, start + WINDOW);
|
|
111
|
+
const loading = Object.values(lists).some((l) => l.status === "loading");
|
|
112
|
+
return (_jsxs(Dialog, { title: "Select model", subtitle: `${rows.filter((r) => r.action.type === "model").length} models${loading ? " · loading…" : ""}`, footer: "\u2191\u2193 navigate \u00B7 enter select \u00B7 esc cancel \u00B7 /login to add a provider", children: [_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "\u2315 " }), query || _jsx(Text, { dimColor: true, children: "type to filter, or provider:model for anything unlisted" }), query ? _jsx(Text, { inverse: true, children: " " }) : null] }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [start > 0 && _jsx(Text, { dimColor: true, children: ` ↑ ${start} more` }), visible.map((r, i) => {
|
|
113
|
+
const selected = start + i === index;
|
|
114
|
+
const isCurrent = r.action.type === "model" && r.action.spec === current;
|
|
115
|
+
return (_jsxs(Text, { color: selected ? "cyan" : undefined, dimColor: !selected && r.dim, children: [selected ? "❯ " : " ", _jsx(Text, { dimColor: !selected, children: r.provider.padEnd(11) }), r.label, isCurrent ? _jsx(Text, { color: "green", children: " \u2714" }) : null] }, `${r.provider}:${r.label}`));
|
|
116
|
+
}), rows.length === 0 && _jsx(Text, { dimColor: true, children: " No matches." }), start + WINDOW < rows.length && _jsx(Text, { dimColor: true, children: ` ↓ ${rows.length - start - WINDOW} more` })] })] }));
|
|
117
|
+
}
|