@akon-labs/nexus-cli 0.1.10 → 0.1.11
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/dist/cli/require-login.d.ts.map +1 -1
- package/dist/cli/require-login.js +2 -2
- package/dist/cli/require-login.js.map +1 -1
- package/dist/cli.js +2492 -1088
- package/dist/core/agent-session.d.ts +3 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +4 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +6 -0
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +7 -1
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +14 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +18 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/graph-rail-store.d.ts +10 -0
- package/dist/core/graph-rail-store.d.ts.map +1 -1
- package/dist/core/graph-rail-store.js +9 -0
- package/dist/core/graph-rail-store.js.map +1 -1
- package/dist/core/keybindings.d.ts +2 -1
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +4 -1
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +1 -1
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +1 -1
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/extensions/gitnexus/client.d.ts.map +1 -1
- package/dist/extensions/gitnexus/client.js +45 -14
- package/dist/extensions/gitnexus/client.js.map +1 -1
- package/dist/extensions/gitnexus/config.d.ts +15 -1
- package/dist/extensions/gitnexus/config.d.ts.map +1 -1
- package/dist/extensions/gitnexus/config.js +86 -2
- package/dist/extensions/gitnexus/config.js.map +1 -1
- package/dist/extensions/gitnexus/hub-api.d.ts +56 -0
- package/dist/extensions/gitnexus/hub-api.d.ts.map +1 -0
- package/dist/extensions/gitnexus/hub-api.js +105 -0
- package/dist/extensions/gitnexus/hub-api.js.map +1 -0
- package/dist/extensions/gitnexus/index-repo.d.ts +50 -0
- package/dist/extensions/gitnexus/index-repo.d.ts.map +1 -0
- package/dist/extensions/gitnexus/index-repo.js +162 -0
- package/dist/extensions/gitnexus/index-repo.js.map +1 -0
- package/dist/extensions/gitnexus/index.d.ts.map +1 -1
- package/dist/extensions/gitnexus/index.js +137 -17
- package/dist/extensions/gitnexus/index.js.map +1 -1
- package/dist/extensions/gitnexus/observatory.d.ts.map +1 -1
- package/dist/extensions/gitnexus/observatory.js +2 -2
- package/dist/extensions/gitnexus/observatory.js.map +1 -1
- package/dist/extensions/gitnexus/resolve.d.ts +9 -0
- package/dist/extensions/gitnexus/resolve.d.ts.map +1 -1
- package/dist/extensions/gitnexus/resolve.js +52 -2
- package/dist/extensions/gitnexus/resolve.js.map +1 -1
- package/dist/extensions/index.d.ts.map +1 -1
- package/dist/extensions/index.js +3 -0
- package/dist/extensions/index.js.map +1 -1
- package/dist/extensions/mcp/commands.d.ts +22 -2
- package/dist/extensions/mcp/commands.d.ts.map +1 -1
- package/dist/extensions/mcp/commands.js +201 -9
- package/dist/extensions/mcp/commands.js.map +1 -1
- package/dist/extensions/mcp/config.d.ts +2 -0
- package/dist/extensions/mcp/config.d.ts.map +1 -1
- package/dist/extensions/mcp/config.js +6 -1
- package/dist/extensions/mcp/config.js.map +1 -1
- package/dist/extensions/mcp/index.d.ts.map +1 -1
- package/dist/extensions/mcp/index.js +54 -5
- package/dist/extensions/mcp/index.js.map +1 -1
- package/dist/extensions/mcp/mcp-setup-panel.d.ts +63 -2
- package/dist/extensions/mcp/mcp-setup-panel.d.ts.map +1 -1
- package/dist/extensions/mcp/mcp-setup-panel.js +563 -77
- package/dist/extensions/mcp/mcp-setup-panel.js.map +1 -1
- package/dist/extensions/subagent/index.d.ts +2 -1
- package/dist/extensions/subagent/index.d.ts.map +1 -1
- package/dist/extensions/subagent/index.js +19 -6
- package/dist/extensions/subagent/index.js.map +1 -1
- package/dist/extensions/subagent-render.d.ts +3 -0
- package/dist/extensions/subagent-render.d.ts.map +1 -0
- package/dist/extensions/subagent-render.js +39 -0
- package/dist/extensions/subagent-render.js.map +1 -0
- package/dist/index.js +2524 -1120
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +2 -0
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/command-palette.d.ts +2 -1
- package/dist/modes/interactive/components/command-palette.d.ts.map +1 -1
- package/dist/modes/interactive/components/command-palette.js +11 -4
- package/dist/modes/interactive/components/command-palette.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +9 -3
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/graph-rail.d.ts.map +1 -1
- package/dist/modes/interactive/components/graph-rail.js +18 -3
- package/dist/modes/interactive/components/graph-rail.js.map +1 -1
- package/dist/modes/interactive/components/keybinding-hints.d.ts +1 -0
- package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
- package/dist/modes/interactive/components/keybinding-hints.js +7 -1
- package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
- package/dist/modes/interactive/components/landing-panel.d.ts.map +1 -1
- package/dist/modes/interactive/components/landing-panel.js +15 -2
- package/dist/modes/interactive/components/landing-panel.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +3 -2
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +12 -2
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +3 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +42 -8
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/rpc-entry.js +2492 -1088
- package/dist/utils/hidden-spawn.d.ts +29 -0
- package/dist/utils/hidden-spawn.d.ts.map +1 -0
- package/dist/utils/hidden-spawn.js +101 -0
- package/dist/utils/hidden-spawn.js.map +1 -0
- package/dist/vendor/ai.js +233 -233
- package/dist/vendor/tui.js +1 -1
- package/native/win32/build.mjs +95 -0
- package/native/win32/prebuilds/win32-x64/hidden-spawn.exe +0 -0
- package/native/win32/src/hidden-spawn.c +157 -0
- package/npm-shrinkwrap.json +2 -37
- package/package.json +7 -3
|
@@ -38,14 +38,84 @@ function wrapText(text, width) {
|
|
|
38
38
|
lines.push(current);
|
|
39
39
|
return lines.length > 0 ? lines : [""];
|
|
40
40
|
}
|
|
41
|
+
const SETUP_TABS = [
|
|
42
|
+
{ id: "presets", label: "Presets" },
|
|
43
|
+
{ id: "custom", label: "Custom" },
|
|
44
|
+
{ id: "installed", label: "Installed" },
|
|
45
|
+
];
|
|
46
|
+
/** Server-type tag shown next to a preset row; color must survive any terminal, so it's always plain colored text. */
|
|
47
|
+
/** Color a JSON-ish line: keys cyan, string values green, braces/brackets dim. Safe only for short, unwrapped lines. */
|
|
48
|
+
function colorizeJsonLine(line) {
|
|
49
|
+
let result = "";
|
|
50
|
+
let i = 0;
|
|
51
|
+
while (i < line.length) {
|
|
52
|
+
const ch = line[i];
|
|
53
|
+
if (ch === '"') {
|
|
54
|
+
let end = i + 1;
|
|
55
|
+
while (end < line.length && line[end] !== '"')
|
|
56
|
+
end++;
|
|
57
|
+
const str = line.slice(i, Math.min(end + 1, line.length));
|
|
58
|
+
const isKey = line
|
|
59
|
+
.slice(i + str.length)
|
|
60
|
+
.trimStart()
|
|
61
|
+
.startsWith(":");
|
|
62
|
+
result += fg(isKey ? "36" : "32", str);
|
|
63
|
+
i += str.length;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (ch === "{" || ch === "}" || ch === "[" || ch === "]") {
|
|
67
|
+
result += fg("2", ch);
|
|
68
|
+
i++;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
result += ch;
|
|
72
|
+
i++;
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
/** Diff-line color by prefix, or undefined for a context line that needs no color. */
|
|
77
|
+
function diffLineColor(line) {
|
|
78
|
+
if (line.startsWith("+++") || line.startsWith("---"))
|
|
79
|
+
return "2";
|
|
80
|
+
if (line.startsWith("+"))
|
|
81
|
+
return "32";
|
|
82
|
+
if (line.startsWith("-"))
|
|
83
|
+
return "31";
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
/** Color `/mcp add` command syntax: the command itself cyan, --flags yellow, <placeholders> dim italic. Leaves spacing untouched. */
|
|
87
|
+
function colorizeCommandLine(line) {
|
|
88
|
+
return line.replace(/(\/mcp add\b|--[a-zA-Z-]+|<[^>]+>|\[[^\]]+\])/g, (match) => {
|
|
89
|
+
if (match.startsWith("<") || match.startsWith("["))
|
|
90
|
+
return fg("2;3", match);
|
|
91
|
+
if (match.startsWith("--"))
|
|
92
|
+
return fg("33", match);
|
|
93
|
+
return fg("36", match);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/** Per-preset accent color for the name, so the list scans by identity, not just by type. 256-color codes, distinct from the type-tag palette above. */
|
|
97
|
+
const PRESET_ACCENT_COLORS = {
|
|
98
|
+
deepwiki: "38;5;111", // soft blue
|
|
99
|
+
context7: "38;5;80", // teal
|
|
100
|
+
notion: "38;5;141", // purple
|
|
101
|
+
github: "38;5;215", // orange
|
|
102
|
+
"chrome-devtools": "38;5;211", // pink
|
|
103
|
+
};
|
|
104
|
+
function presetAccent(preset) {
|
|
105
|
+
return PRESET_ACCENT_COLORS[preset.id] ?? "37";
|
|
106
|
+
}
|
|
41
107
|
export class McpSetupPanel {
|
|
42
108
|
screen;
|
|
109
|
+
activeTab;
|
|
43
110
|
actionCursor = 0;
|
|
44
111
|
importCursor = 0;
|
|
45
112
|
pathCursor = 0;
|
|
113
|
+
advancedCursor = 0;
|
|
46
114
|
selectedImports = new Set();
|
|
47
115
|
busy = false;
|
|
48
116
|
notice = null;
|
|
117
|
+
pendingAction;
|
|
118
|
+
maxHeight;
|
|
49
119
|
tui;
|
|
50
120
|
t = DEFAULT_THEME;
|
|
51
121
|
keys;
|
|
@@ -53,16 +123,20 @@ export class McpSetupPanel {
|
|
|
53
123
|
discovery;
|
|
54
124
|
callbacks;
|
|
55
125
|
options;
|
|
126
|
+
/** Mutable copy of options.installedServers, updated in-place after a successful add so the Installed tab and Presets "active" tag reflect it immediately, without needing to close and reopen the panel. */
|
|
127
|
+
installedServers;
|
|
56
128
|
done;
|
|
57
129
|
static INACTIVITY_MS = 60_000;
|
|
58
130
|
constructor(discovery, callbacks, options, tui, done) {
|
|
59
131
|
this.discovery = discovery;
|
|
60
132
|
this.callbacks = callbacks;
|
|
61
133
|
this.options = options;
|
|
134
|
+
this.installedServers = [...options.installedServers];
|
|
62
135
|
this.done = done;
|
|
63
136
|
this.tui = tui;
|
|
64
137
|
this.keys = createPanelKeys(options.keybindings);
|
|
65
138
|
this.screen = options.mode;
|
|
139
|
+
this.activeTab = "presets";
|
|
66
140
|
for (const entry of discovery.imports) {
|
|
67
141
|
this.selectedImports.add(entry.kind);
|
|
68
142
|
}
|
|
@@ -82,15 +156,9 @@ export class McpSetupPanel {
|
|
|
82
156
|
this.inactivityTimeout = null;
|
|
83
157
|
}
|
|
84
158
|
}
|
|
85
|
-
|
|
159
|
+
/** The old "Setup" tab's contents — now a sub-screen opened from Custom's "Advanced options...", not a permanent top-level tab. */
|
|
160
|
+
getAdvancedActions() {
|
|
86
161
|
const actions = [];
|
|
87
|
-
if (this.screen === "empty") {
|
|
88
|
-
actions.push({
|
|
89
|
-
id: "run-setup",
|
|
90
|
-
label: "Run setup",
|
|
91
|
-
description: "Inspect detected configs, adopt imports, and scaffold a minimal `.nexus/mcp.json`.",
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
162
|
if (this.discovery.imports.length > 0) {
|
|
95
163
|
actions.push({
|
|
96
164
|
id: "adopt-imports",
|
|
@@ -122,6 +190,10 @@ export class McpSetupPanel {
|
|
|
122
190
|
description: "Browse the actual config files that Nexus discovered on this machine.",
|
|
123
191
|
});
|
|
124
192
|
}
|
|
193
|
+
return actions;
|
|
194
|
+
}
|
|
195
|
+
getPresetsTabActions() {
|
|
196
|
+
const actions = [];
|
|
125
197
|
for (const preset of KNOWN_SERVER_PRESETS) {
|
|
126
198
|
actions.push({ id: "add-known-server", label: preset.name, description: preset.summary, preset });
|
|
127
199
|
}
|
|
@@ -136,9 +208,86 @@ export class McpSetupPanel {
|
|
|
136
208
|
description: "Write an MCP entry for RepoPrompt to the recommended Nexus target, then reload MCP in-session.",
|
|
137
209
|
});
|
|
138
210
|
}
|
|
139
|
-
actions.push({ id: "close", label: "Close", description: "Exit the onboarding flow." });
|
|
140
211
|
return actions;
|
|
141
212
|
}
|
|
213
|
+
getCustomTabActions() {
|
|
214
|
+
return [
|
|
215
|
+
{
|
|
216
|
+
id: "add-custom-stdio",
|
|
217
|
+
label: "Add a local server (stdio)...",
|
|
218
|
+
description: "Runs a command on your machine, e.g. an npx-installed MCP server.",
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
id: "add-custom-url",
|
|
222
|
+
label: "Add a remote server (URL)...",
|
|
223
|
+
description: "Connects to an MCP server over HTTP.",
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
id: "open-advanced",
|
|
227
|
+
label: "Advanced options...",
|
|
228
|
+
description: "Import from other tools, scaffold an empty config, or check how config precedence works.",
|
|
229
|
+
},
|
|
230
|
+
];
|
|
231
|
+
}
|
|
232
|
+
getInstalledTabActions() {
|
|
233
|
+
const servers = this.installedServers;
|
|
234
|
+
if (servers.length === 0) {
|
|
235
|
+
return [
|
|
236
|
+
{
|
|
237
|
+
id: "view-installed-server",
|
|
238
|
+
label: "No servers configured yet.",
|
|
239
|
+
description: "Add one from the Presets or Custom tab.",
|
|
240
|
+
},
|
|
241
|
+
];
|
|
242
|
+
}
|
|
243
|
+
return servers.map((server) => ({
|
|
244
|
+
id: "view-installed-server",
|
|
245
|
+
label: server.name,
|
|
246
|
+
description: server.disabled
|
|
247
|
+
? "Disabled in the effective config."
|
|
248
|
+
: server.authCapable && !server.signedIn
|
|
249
|
+
? "Active, but not signed in yet."
|
|
250
|
+
: "Active in the effective config.",
|
|
251
|
+
installed: server,
|
|
252
|
+
}));
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Single-glyph prefix instead of a tag chip: ✓ for a preset you already have
|
|
256
|
+
* (dim if you've disabled it), ✓/⊘ for an Installed-tab row's active state.
|
|
257
|
+
* No mark at all for a preset you don't have yet, to keep the list scannable.
|
|
258
|
+
*/
|
|
259
|
+
getRowMark(action) {
|
|
260
|
+
if (action.preset) {
|
|
261
|
+
const installed = this.installedServers.find((server) => server.name === action.preset.id);
|
|
262
|
+
return installed ? fg(installed.disabled ? this.t.muted : this.t.success, "✓ ") : " ";
|
|
263
|
+
}
|
|
264
|
+
if (action.installed) {
|
|
265
|
+
if (action.installed.disabled)
|
|
266
|
+
return fg(this.t.muted, "⊘ ");
|
|
267
|
+
if (action.installed.authCapable && !action.installed.signedIn)
|
|
268
|
+
return fg(this.t.warning, "! ");
|
|
269
|
+
return fg(this.t.success, "✓ ");
|
|
270
|
+
}
|
|
271
|
+
return "";
|
|
272
|
+
}
|
|
273
|
+
getActions() {
|
|
274
|
+
switch (this.activeTab) {
|
|
275
|
+
case "presets":
|
|
276
|
+
return this.getPresetsTabActions();
|
|
277
|
+
case "custom":
|
|
278
|
+
return this.getCustomTabActions();
|
|
279
|
+
case "installed":
|
|
280
|
+
return this.getInstalledTabActions();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
cycleTab(direction) {
|
|
284
|
+
const order = SETUP_TABS.map((tab) => tab.id);
|
|
285
|
+
const index = order.indexOf(this.activeTab);
|
|
286
|
+
this.activeTab = order[(index + direction + order.length) % order.length];
|
|
287
|
+
this.actionCursor = 0;
|
|
288
|
+
this.notice = null;
|
|
289
|
+
this.tui.requestRender();
|
|
290
|
+
}
|
|
142
291
|
getDetectedPaths() {
|
|
143
292
|
const paths = [
|
|
144
293
|
...this.discovery.sources.filter((source) => source.exists).map((source) => source.path),
|
|
@@ -146,10 +295,6 @@ export class McpSetupPanel {
|
|
|
146
295
|
];
|
|
147
296
|
return [...new Set(paths)];
|
|
148
297
|
}
|
|
149
|
-
getSelectedAction() {
|
|
150
|
-
const actions = this.getActions();
|
|
151
|
-
return actions[this.actionCursor];
|
|
152
|
-
}
|
|
153
298
|
handleInput(data) {
|
|
154
299
|
this.resetInactivityTimeout();
|
|
155
300
|
if (!this.busy)
|
|
@@ -161,6 +306,13 @@ export class McpSetupPanel {
|
|
|
161
306
|
}
|
|
162
307
|
if (matchesKey(data, "escape")) {
|
|
163
308
|
if (this.screen === "imports" || this.screen === "paths") {
|
|
309
|
+
// Both are only reachable from the Advanced sub-screen now — back out one level, not all the way to the tabs.
|
|
310
|
+
this.screen = "advanced";
|
|
311
|
+
this.tui.requestRender();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (this.screen === "confirm-preset" || this.screen === "advanced") {
|
|
315
|
+
this.pendingAction = undefined;
|
|
164
316
|
this.screen = this.discovery.hasAnyConfig ? "setup" : "empty";
|
|
165
317
|
this.tui.requestRender();
|
|
166
318
|
return;
|
|
@@ -179,6 +331,22 @@ export class McpSetupPanel {
|
|
|
179
331
|
this.handlePathsInput(data);
|
|
180
332
|
return;
|
|
181
333
|
}
|
|
334
|
+
if (this.screen === "confirm-preset") {
|
|
335
|
+
this.handleConfirmPresetInput(data);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (this.screen === "advanced") {
|
|
339
|
+
this.handleAdvancedInput(data);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (matchesKey(data, "left")) {
|
|
343
|
+
this.cycleTab(-1);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (matchesKey(data, "right")) {
|
|
347
|
+
this.cycleTab(1);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
182
350
|
const actions = this.getActions();
|
|
183
351
|
if (this.keys.selectUp(data)) {
|
|
184
352
|
this.actionCursor = Math.max(0, this.actionCursor - 1);
|
|
@@ -190,6 +358,14 @@ export class McpSetupPanel {
|
|
|
190
358
|
this.tui.requestRender();
|
|
191
359
|
return;
|
|
192
360
|
}
|
|
361
|
+
if (data === "o" || data === "O") {
|
|
362
|
+
this.openSelectedDocs();
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if ((data === "a" || data === "A") && this.activeTab === "installed") {
|
|
366
|
+
this.authenticateSelected();
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
193
369
|
if (this.keys.selectConfirm(data)) {
|
|
194
370
|
const selected = this.getSelectedAction();
|
|
195
371
|
if (selected)
|
|
@@ -247,13 +423,118 @@ export class McpSetupPanel {
|
|
|
247
423
|
});
|
|
248
424
|
}
|
|
249
425
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
this.
|
|
426
|
+
handleAdvancedInput(data) {
|
|
427
|
+
const actions = this.getAdvancedActions();
|
|
428
|
+
if (this.keys.selectUp(data)) {
|
|
429
|
+
this.advancedCursor = Math.max(0, this.advancedCursor - 1);
|
|
430
|
+
this.tui.requestRender();
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (this.keys.selectDown(data)) {
|
|
434
|
+
this.advancedCursor = Math.min(actions.length - 1, this.advancedCursor + 1);
|
|
254
435
|
this.tui.requestRender();
|
|
255
436
|
return;
|
|
256
437
|
}
|
|
438
|
+
if (this.keys.selectConfirm(data)) {
|
|
439
|
+
const selected = actions[this.advancedCursor];
|
|
440
|
+
if (selected)
|
|
441
|
+
void this.runAction(selected);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
handleConfirmPresetInput(data) {
|
|
445
|
+
if (data === "o" || data === "O") {
|
|
446
|
+
this.openSelectedDocs();
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (this.keys.selectConfirm(data)) {
|
|
450
|
+
void this.confirmPendingAction();
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
/** Getter used by openSelectedDocs while the confirm screen is up (no action-list cursor there). */
|
|
454
|
+
getSelectedAction() {
|
|
455
|
+
if (this.screen === "confirm-preset")
|
|
456
|
+
return this.pendingAction;
|
|
457
|
+
const actions = this.getActions();
|
|
458
|
+
return actions[this.actionCursor];
|
|
459
|
+
}
|
|
460
|
+
/** Add or replace an entry so the Installed tab and Presets "active" tag update immediately, without closing the panel. */
|
|
461
|
+
upsertInstalledServer(entry) {
|
|
462
|
+
const index = this.installedServers.findIndex((server) => server.name === entry.name);
|
|
463
|
+
if (index === -1)
|
|
464
|
+
this.installedServers.push(entry);
|
|
465
|
+
else
|
|
466
|
+
this.installedServers[index] = entry;
|
|
467
|
+
}
|
|
468
|
+
async confirmPendingAction() {
|
|
469
|
+
const action = this.pendingAction;
|
|
470
|
+
if (!action)
|
|
471
|
+
return;
|
|
472
|
+
if (action.id === "add-repoprompt") {
|
|
473
|
+
await this.runBusy(async () => {
|
|
474
|
+
const result = await this.callbacks.addRepoPrompt();
|
|
475
|
+
this.callbacks.markSetupCompleted();
|
|
476
|
+
this.upsertInstalledServer({
|
|
477
|
+
name: result.serverName,
|
|
478
|
+
disabled: false,
|
|
479
|
+
kind: "stdio",
|
|
480
|
+
authCapable: false,
|
|
481
|
+
signedIn: false,
|
|
482
|
+
});
|
|
483
|
+
this.notice = {
|
|
484
|
+
text: `Added ${result.serverName} to ${result.path}. Nexus will reload after this panel closes.`,
|
|
485
|
+
tone: "success",
|
|
486
|
+
};
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
else if (action.id === "add-known-server" && action.preset) {
|
|
490
|
+
const preset = action.preset;
|
|
491
|
+
const authCapable = preset.entry.auth === "oauth";
|
|
492
|
+
await this.runBusy(async () => {
|
|
493
|
+
const result = await this.callbacks.addKnownServer(preset);
|
|
494
|
+
this.callbacks.markSetupCompleted();
|
|
495
|
+
const kind = preset.entry.command ? "stdio" : "http";
|
|
496
|
+
this.upsertInstalledServer({
|
|
497
|
+
name: result.serverName,
|
|
498
|
+
disabled: false,
|
|
499
|
+
kind,
|
|
500
|
+
authCapable,
|
|
501
|
+
signedIn: false,
|
|
502
|
+
});
|
|
503
|
+
if (!authCapable) {
|
|
504
|
+
this.notice = {
|
|
505
|
+
text: `Added ${result.serverName} to ${result.path}. Nexus will reload after this panel closes.`,
|
|
506
|
+
tone: "success",
|
|
507
|
+
};
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
// Sign in right now, in the same flow — this is the whole point of a preset:
|
|
511
|
+
// don't make the user hunt down a separate command after the fact.
|
|
512
|
+
this.notice = { text: `Added ${result.serverName}. Opening your browser to sign in…`, tone: "muted" };
|
|
513
|
+
this.tui.requestRender();
|
|
514
|
+
const authResult = await this.callbacks.authenticateServer(result.serverName, preset.entry);
|
|
515
|
+
this.upsertInstalledServer({
|
|
516
|
+
name: result.serverName,
|
|
517
|
+
disabled: false,
|
|
518
|
+
kind,
|
|
519
|
+
authCapable,
|
|
520
|
+
signedIn: authResult.ok,
|
|
521
|
+
});
|
|
522
|
+
this.notice = authResult.ok
|
|
523
|
+
? {
|
|
524
|
+
text: `Added and signed in to ${result.serverName} (${result.path}). Nexus will reload after this panel closes.`,
|
|
525
|
+
tone: "success",
|
|
526
|
+
}
|
|
527
|
+
: {
|
|
528
|
+
text: `Added ${result.serverName} to ${result.path}, but sign-in didn't finish: ${authResult.message} Retry anytime from the Installed tab (press "a").`,
|
|
529
|
+
tone: "warning",
|
|
530
|
+
};
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
this.pendingAction = undefined;
|
|
534
|
+
this.screen = this.discovery.hasAnyConfig ? "setup" : "empty";
|
|
535
|
+
this.tui.requestRender();
|
|
536
|
+
}
|
|
537
|
+
async runAction(action) {
|
|
257
538
|
if (action.id === "adopt-imports") {
|
|
258
539
|
this.screen = "imports";
|
|
259
540
|
this.importCursor = 0;
|
|
@@ -277,34 +558,38 @@ export class McpSetupPanel {
|
|
|
277
558
|
});
|
|
278
559
|
return;
|
|
279
560
|
}
|
|
280
|
-
if (action.id === "add-repoprompt") {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
this.notice = {
|
|
285
|
-
text: `Added ${result.serverName} to ${result.path}. Nexus will reload after this panel closes.`,
|
|
286
|
-
tone: "success",
|
|
287
|
-
};
|
|
288
|
-
});
|
|
561
|
+
if (action.id === "add-repoprompt" || (action.id === "add-known-server" && action.preset)) {
|
|
562
|
+
this.pendingAction = action;
|
|
563
|
+
this.screen = "confirm-preset";
|
|
564
|
+
this.tui.requestRender();
|
|
289
565
|
return;
|
|
290
566
|
}
|
|
291
|
-
if (action.id === "add-
|
|
292
|
-
const
|
|
567
|
+
if (action.id === "add-custom-stdio" || action.id === "add-custom-url") {
|
|
568
|
+
const template = action.id === "add-custom-stdio" ? "/mcp add my-server -- " : "/mcp add my-server --url ";
|
|
569
|
+
this.callbacks.seedEditor(template);
|
|
570
|
+
this.cleanup();
|
|
571
|
+
this.done();
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
if (action.id === "open-advanced") {
|
|
575
|
+
this.screen = "advanced";
|
|
576
|
+
this.advancedCursor = 0;
|
|
577
|
+
this.tui.requestRender();
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (action.id === "view-installed-server" && action.installed) {
|
|
581
|
+
const server = action.installed;
|
|
582
|
+
const nextDisabled = !server.disabled;
|
|
293
583
|
await this.runBusy(async () => {
|
|
294
|
-
const result = await this.callbacks.
|
|
295
|
-
this.
|
|
584
|
+
const result = await this.callbacks.toggleServerDisabled(server.name, nextDisabled);
|
|
585
|
+
this.upsertInstalledServer({ ...server, disabled: nextDisabled });
|
|
296
586
|
this.notice = {
|
|
297
|
-
text:
|
|
587
|
+
text: `${nextDisabled ? "Disabled" : "Enabled"} "${server.name}" in ${result.path} — run /reload to apply.`,
|
|
298
588
|
tone: "success",
|
|
299
589
|
};
|
|
300
590
|
});
|
|
301
591
|
return;
|
|
302
592
|
}
|
|
303
|
-
if (action.id === "close") {
|
|
304
|
-
this.cleanup();
|
|
305
|
-
this.done();
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
593
|
this.notice = {
|
|
309
594
|
text: "Review the details below. Press Enter on an action with a side effect to apply it.",
|
|
310
595
|
tone: "muted",
|
|
@@ -330,8 +615,39 @@ export class McpSetupPanel {
|
|
|
330
615
|
tone: "success",
|
|
331
616
|
}
|
|
332
617
|
: { text: `No changes needed in ${result.path}.`, tone: "muted" };
|
|
333
|
-
this.screen =
|
|
334
|
-
this.
|
|
618
|
+
this.screen = "advanced";
|
|
619
|
+
this.advancedCursor = 0;
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
openSelectedDocs() {
|
|
623
|
+
const selected = this.getSelectedAction();
|
|
624
|
+
const docsUrl = selected?.preset?.docsUrl;
|
|
625
|
+
if (!docsUrl) {
|
|
626
|
+
this.notice = { text: "No docs link for this item.", tone: "muted" };
|
|
627
|
+
this.tui.requestRender();
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
void this.runBusy(async () => {
|
|
631
|
+
await this.callbacks.openPath(docsUrl);
|
|
632
|
+
this.notice = { text: `Opened ${docsUrl}`, tone: "success" };
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
authenticateSelected() {
|
|
636
|
+
const selected = this.getSelectedAction();
|
|
637
|
+
const server = selected?.installed;
|
|
638
|
+
if (!server)
|
|
639
|
+
return;
|
|
640
|
+
if (!server.authCapable) {
|
|
641
|
+
this.notice = { text: `"${server.name}" isn't configured for OAuth sign-in.`, tone: "muted" };
|
|
642
|
+
this.tui.requestRender();
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
void this.runBusy(async () => {
|
|
646
|
+
this.notice = { text: `Opening your browser to sign in to "${server.name}"…`, tone: "muted" };
|
|
647
|
+
this.tui.requestRender();
|
|
648
|
+
const result = await this.callbacks.authenticateServer(server.name);
|
|
649
|
+
this.upsertInstalledServer({ ...server, signedIn: result.ok });
|
|
650
|
+
this.notice = { text: result.message, tone: result.ok ? "success" : "warning" };
|
|
335
651
|
});
|
|
336
652
|
}
|
|
337
653
|
async runBusy(fn) {
|
|
@@ -385,17 +701,56 @@ export class McpSetupPanel {
|
|
|
385
701
|
else if (this.screen === "paths") {
|
|
386
702
|
lines.push(...this.renderPaths(innerW));
|
|
387
703
|
}
|
|
704
|
+
else if (this.screen === "confirm-preset") {
|
|
705
|
+
lines.push(...this.renderConfirmPreset(innerW));
|
|
706
|
+
}
|
|
707
|
+
else if (this.screen === "advanced") {
|
|
708
|
+
lines.push(...this.renderAdvanced(innerW));
|
|
709
|
+
}
|
|
388
710
|
else {
|
|
389
711
|
lines.push(...this.renderActions(innerW));
|
|
390
712
|
}
|
|
391
713
|
lines.push(`└${border}┘`);
|
|
392
|
-
return lines;
|
|
714
|
+
return this.clampHeight(lines, innerW, border);
|
|
715
|
+
}
|
|
716
|
+
setMaxHeight(lines) {
|
|
717
|
+
this.maxHeight = lines;
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* The overlay host may cap total rows and, absent this hook, would slice
|
|
721
|
+
* blindly from the bottom — cutting off the closing border and leaving the
|
|
722
|
+
* box looking broken. Truncate the content instead, keeping the header and
|
|
723
|
+
* action list (the interactive part) and dropping preview/diff lines first.
|
|
724
|
+
*/
|
|
725
|
+
clampHeight(lines, innerW, _border) {
|
|
726
|
+
if (this.maxHeight === undefined || lines.length <= this.maxHeight)
|
|
727
|
+
return lines;
|
|
728
|
+
const budget = Math.max(5, this.maxHeight);
|
|
729
|
+
// The last line is the closing border, the one before it is the hint/controls
|
|
730
|
+
// line — both must always stay visible, or the panel loses its own instructions.
|
|
731
|
+
const closing = lines[lines.length - 1];
|
|
732
|
+
const footer = lines[lines.length - 2];
|
|
733
|
+
const head = lines.slice(0, Math.max(1, budget - 3));
|
|
734
|
+
const hidden = lines.length - head.length - 2;
|
|
735
|
+
if (hidden <= 0)
|
|
736
|
+
return lines;
|
|
737
|
+
return [
|
|
738
|
+
...head,
|
|
739
|
+
this.padLine(fg(this.t.muted, `… ${hidden} more line${hidden === 1 ? "" : "s"} hidden to fit the window`), innerW),
|
|
740
|
+
footer,
|
|
741
|
+
closing,
|
|
742
|
+
];
|
|
743
|
+
}
|
|
744
|
+
renderTabBar() {
|
|
745
|
+
return SETUP_TABS.map((tab) => tab.id === this.activeTab ? fg(this.t.selected, `[ ${tab.label} ]`) : fg(this.t.muted, ` ${tab.label} `)).join(" ");
|
|
393
746
|
}
|
|
394
747
|
renderActions(innerW) {
|
|
395
748
|
const lines = [];
|
|
396
749
|
const actions = this.getActions();
|
|
397
750
|
const compact = innerW < COMPACT_WIDTH;
|
|
398
751
|
const { start, end } = compact ? this.visibleActionRange(actions.length) : { start: 0, end: actions.length };
|
|
752
|
+
lines.push(this.padLine(this.renderTabBar(), innerW));
|
|
753
|
+
lines.push(this.padLine("", innerW));
|
|
399
754
|
if (start > 0) {
|
|
400
755
|
lines.push(this.padLine(fg(this.t.muted, `… ${start} more above`), innerW));
|
|
401
756
|
}
|
|
@@ -403,23 +758,46 @@ export class McpSetupPanel {
|
|
|
403
758
|
const action = actions[index];
|
|
404
759
|
if (!action)
|
|
405
760
|
continue;
|
|
406
|
-
if (action.id === "add-known-server" && (index === start || actions[index - 1]?.id !== "add-known-server")) {
|
|
407
|
-
lines.push(this.padLine(fg(this.t.title, "Add a known server"), innerW));
|
|
408
|
-
}
|
|
409
761
|
const selected = index === this.actionCursor;
|
|
410
762
|
const cursor = selected ? fg(this.t.selected, "›") : " ";
|
|
411
|
-
|
|
763
|
+
const mark = this.getRowMark(action);
|
|
764
|
+
const label = truncateToWidth(action.label, Math.max(1, this.contentWidth(innerW) - 2 - visibleWidth(mark)));
|
|
765
|
+
const installedColor = action.installed
|
|
766
|
+
? action.installed.disabled
|
|
767
|
+
? this.t.muted
|
|
768
|
+
: action.installed.authCapable && !action.installed.signedIn
|
|
769
|
+
? this.t.warning
|
|
770
|
+
: this.t.success
|
|
771
|
+
: undefined;
|
|
772
|
+
const labelRendered = action.preset
|
|
773
|
+
? fg(presetAccent(action.preset), label)
|
|
774
|
+
: installedColor
|
|
775
|
+
? fg(installedColor, label)
|
|
776
|
+
: label;
|
|
777
|
+
lines.push(this.padLine(`${cursor} ${mark}${labelRendered}`, innerW));
|
|
412
778
|
}
|
|
413
779
|
if (end < actions.length) {
|
|
414
780
|
lines.push(this.padLine(fg(this.t.muted, `… ${actions.length - end} more below`), innerW));
|
|
415
781
|
}
|
|
416
782
|
lines.push(this.padLine("", innerW));
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
783
|
+
// Presets stay a clean, scannable list — no inline summary/diff while browsing.
|
|
784
|
+
// The write preview only shows on the confirm screen, after pressing Enter.
|
|
785
|
+
if (this.activeTab !== "presets") {
|
|
786
|
+
const preview = this.getActionPreview(this.getSelectedAction(), this.previewWidth(innerW));
|
|
787
|
+
for (const line of preview) {
|
|
788
|
+
lines.push(this.padLine(line, innerW));
|
|
789
|
+
}
|
|
790
|
+
lines.push(this.padLine("", innerW));
|
|
420
791
|
}
|
|
421
|
-
|
|
422
|
-
const
|
|
792
|
+
const docsHint = this.activeTab === "presets" ? " · o docs" : "";
|
|
793
|
+
const confirmHint = this.activeTab === "presets" ? " · enter to review & confirm" : "";
|
|
794
|
+
const checkHint = this.activeTab === "presets" && this.installedServers.length > 0 ? " · ✓ already added" : "";
|
|
795
|
+
const authHint = this.activeTab === "installed" && this.installedServers.some((server) => server.authCapable)
|
|
796
|
+
? " · a sign in"
|
|
797
|
+
: "";
|
|
798
|
+
const hint = compact
|
|
799
|
+
? `Enter select · ←/→ tabs${docsHint} · Esc back`
|
|
800
|
+
: `Enter selects${confirmHint}, ←/→ switch tabs${docsHint}${checkHint}${authHint}, Esc goes back, Ctrl+C closes.`;
|
|
423
801
|
lines.push(this.padLine(fg(this.t.muted, hint), innerW));
|
|
424
802
|
return lines;
|
|
425
803
|
}
|
|
@@ -458,6 +836,48 @@ export class McpSetupPanel {
|
|
|
458
836
|
}
|
|
459
837
|
return lines;
|
|
460
838
|
}
|
|
839
|
+
renderAdvanced(innerW) {
|
|
840
|
+
const lines = [];
|
|
841
|
+
const actions = this.getAdvancedActions();
|
|
842
|
+
lines.push(this.padLine(`${fg(this.t.muted, "Custom")} > ${fg(this.t.title, "Advanced options")}`, innerW));
|
|
843
|
+
lines.push(this.padLine("", innerW));
|
|
844
|
+
if (actions.length === 0) {
|
|
845
|
+
lines.push(this.padLine(fg(this.t.muted, "Nothing here right now — nice, config is already tidy."), innerW));
|
|
846
|
+
}
|
|
847
|
+
for (let index = 0; index < actions.length; index++) {
|
|
848
|
+
const action = actions[index];
|
|
849
|
+
if (!action)
|
|
850
|
+
continue;
|
|
851
|
+
const cursor = index === this.advancedCursor ? fg(this.t.selected, "›") : " ";
|
|
852
|
+
const label = truncateToWidth(action.label, Math.max(1, this.contentWidth(innerW) - 2));
|
|
853
|
+
lines.push(this.padLine(`${cursor} ${label}`, innerW));
|
|
854
|
+
}
|
|
855
|
+
lines.push(this.padLine("", innerW));
|
|
856
|
+
// This is the one screen where showing the full explanation on highlight is the point —
|
|
857
|
+
// you only ever get here by deliberately asking for "Advanced options".
|
|
858
|
+
const preview = this.getActionPreview(actions[this.advancedCursor], this.previewWidth(innerW));
|
|
859
|
+
for (const line of preview) {
|
|
860
|
+
lines.push(this.padLine(line, innerW));
|
|
861
|
+
}
|
|
862
|
+
lines.push(this.padLine("", innerW));
|
|
863
|
+
lines.push(this.padLine(fg(this.t.muted, "Enter selects, Esc back to Custom, Ctrl+C closes."), innerW));
|
|
864
|
+
return lines;
|
|
865
|
+
}
|
|
866
|
+
renderConfirmPreset(innerW) {
|
|
867
|
+
const lines = [];
|
|
868
|
+
const action = this.pendingAction;
|
|
869
|
+
const name = action?.preset?.name ?? (action?.id === "add-repoprompt" ? "RepoPrompt" : "this server");
|
|
870
|
+
lines.push(this.padLine(fg(this.t.title, `Confirm: add ${name}`), innerW));
|
|
871
|
+
lines.push(this.padLine("", innerW));
|
|
872
|
+
const preview = this.getActionPreview(action, this.previewWidth(innerW));
|
|
873
|
+
for (const line of preview) {
|
|
874
|
+
lines.push(this.padLine(line, innerW));
|
|
875
|
+
}
|
|
876
|
+
lines.push(this.padLine("", innerW));
|
|
877
|
+
const docsHint = action?.preset?.docsUrl ? " · o docs" : "";
|
|
878
|
+
lines.push(this.padLine(fg(this.t.muted, `Enter writes this file${docsHint} · Esc goes back`), innerW));
|
|
879
|
+
return lines;
|
|
880
|
+
}
|
|
461
881
|
discoverySummaryLine() {
|
|
462
882
|
if (!this.discovery.hasAnyConfig) {
|
|
463
883
|
return fg(this.t.warning, this.options.onboardingState.setupCompleted
|
|
@@ -501,10 +921,6 @@ export class McpSetupPanel {
|
|
|
501
921
|
}
|
|
502
922
|
getActionPreview(action, previewW = DESKTOP_PREVIEW_WIDTH) {
|
|
503
923
|
switch (action?.id) {
|
|
504
|
-
case "run-setup":
|
|
505
|
-
return this.formatPreview([
|
|
506
|
-
"Run setup to adopt host-specific imports, inspect detected paths, and scaffold a minimal `.nexus/mcp.json` if needed.",
|
|
507
|
-
], previewW);
|
|
508
924
|
case "adopt-imports":
|
|
509
925
|
return this.formatWritePreview("Compatibility import write preview", this.callbacks.previewImports(this.discovery.imports
|
|
510
926
|
.filter((entry) => this.selectedImports.has(entry.kind))
|
|
@@ -513,31 +929,50 @@ export class McpSetupPanel {
|
|
|
513
929
|
"Selected imports are written to the Nexus global MCP configuration shown above.",
|
|
514
930
|
], previewW);
|
|
515
931
|
case "view-example":
|
|
932
|
+
return [
|
|
933
|
+
...this.formatPreview(["Example Nexus `.nexus/mcp.json`:"], previewW),
|
|
934
|
+
// Truncate, don't word-wrap: wrapText would collapse JSON's indentation.
|
|
935
|
+
...[
|
|
936
|
+
"{",
|
|
937
|
+
' "mcpServers": {',
|
|
938
|
+
' "chrome-devtools": {',
|
|
939
|
+
' "command": "npx",',
|
|
940
|
+
' "args": ["-y", "chrome-devtools-mcp@1.6.0"]',
|
|
941
|
+
" }",
|
|
942
|
+
" }",
|
|
943
|
+
"}",
|
|
944
|
+
].map((line) => colorizeJsonLine(truncateToWidth(line, previewW, "…"))),
|
|
945
|
+
...this.formatPreview([
|
|
946
|
+
"",
|
|
947
|
+
"Use Scaffold project `.nexus/mcp.json` when you want a safe empty shell instead of a live example server.",
|
|
948
|
+
], previewW),
|
|
949
|
+
];
|
|
950
|
+
case "show-precedence": {
|
|
951
|
+
const hostNote = this.discovery.hostConfigDiscovery === "off"
|
|
952
|
+
? "Configs from other tools (Cursor, Claude Code, VS Code, etc.) are NOT being read right now — Nexus only looks at its own two files above. You can turn that on if you'd rather not redefine servers you already set up elsewhere."
|
|
953
|
+
: this.discovery.hostConfigDiscovery === "prompt"
|
|
954
|
+
? "Nexus will ask before reading configs from other tools (Cursor, Claude Code, VS Code, etc.). If you say yes, they're used as a fallback — only for a server name that isn't already defined in either file above."
|
|
955
|
+
: "Nexus is also reading configs from other tools (Cursor, Claude Code, VS Code, etc.) as a fallback — they only apply to a server name that isn't already defined in either file above.";
|
|
956
|
+
const conflictNote = this.discovery.conflicts.length === 0
|
|
957
|
+
? "Right now no server name is defined in more than one place, so there's nothing actually being overridden."
|
|
958
|
+
: `Right now ${this.discovery.conflicts.length} server name${this.discovery.conflicts.length === 1 ? " is" : "s are"} defined in more than one place — whichever file is read last wins, and the loser is silently ignored:`;
|
|
516
959
|
return this.formatPreview([
|
|
517
|
-
"
|
|
518
|
-
"{",
|
|
519
|
-
' "mcpServers": {',
|
|
520
|
-
' "chrome-devtools": {',
|
|
521
|
-
' "command": "npx",',
|
|
522
|
-
' "args": ["-y", "chrome-devtools-mcp@1.6.0"]',
|
|
523
|
-
" }",
|
|
524
|
-
" }",
|
|
525
|
-
"}",
|
|
960
|
+
"Nexus can load MCP servers from a few different places, so when the same server name shows up twice, something has to decide which definition actually gets used.",
|
|
526
961
|
"",
|
|
527
|
-
"
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
"
|
|
533
|
-
"
|
|
534
|
-
|
|
535
|
-
|
|
962
|
+
"It reads, in this order — each one can override a server name defined before it:",
|
|
963
|
+
" 1. Global — ~/.config/nexus/mcp.json (applies to every project on this machine)",
|
|
964
|
+
` 2. Project — ${getConfigDirName()}/mcp.json (just this project; wins over global for the same name)`,
|
|
965
|
+
"",
|
|
966
|
+
hostNote,
|
|
967
|
+
"",
|
|
968
|
+
"Everything you add from this panel (a preset, a custom server) gets written to the project file, so it's scoped to this project and takes priority over anything global.",
|
|
969
|
+
"",
|
|
970
|
+
conflictNote,
|
|
536
971
|
...this.discovery.conflicts
|
|
537
972
|
.slice(0, 8)
|
|
538
|
-
.map((conflict) =>
|
|
539
|
-
"Nexus writes global MCP state to ~/.config/nexus/mcp.json and project state to .nexus/mcp.json.",
|
|
973
|
+
.map((conflict) => ` ${conflict.serverName}: ${conflict.sources.map((source) => source.path).join(" then ")} — "${conflict.winner.path}" wins`),
|
|
540
974
|
], previewW);
|
|
975
|
+
}
|
|
541
976
|
case "open-paths":
|
|
542
977
|
return this.formatPreview(this.getDetectedPaths().length > 0
|
|
543
978
|
? ["Detected paths:", ...this.getDetectedPaths()]
|
|
@@ -558,13 +993,60 @@ export class McpSetupPanel {
|
|
|
558
993
|
const preset = action.preset;
|
|
559
994
|
if (!preset)
|
|
560
995
|
return this.formatPreview(["Known server preset is unavailable."], previewW);
|
|
561
|
-
return this.formatWritePreview(`${preset.name} write preview`, this.callbacks.previewKnownServer(preset), [preset.summary], previewW);
|
|
996
|
+
return this.formatWritePreview(`${preset.name} write preview`, this.callbacks.previewKnownServer(preset), [preset.summary, ...(preset.docsUrl ? [`Docs: ${preset.docsUrl}`, 'Press "o" to open docs.'] : [])], previewW);
|
|
562
997
|
}
|
|
563
998
|
case "scaffold-project":
|
|
564
999
|
return this.formatWritePreview("Starter project `.nexus/mcp.json` write preview", this.callbacks.previewStarterProject(), [
|
|
565
|
-
|
|
566
|
-
"
|
|
1000
|
+
`This creates ${getConfigDirName()}/mcp.json right here in this project — the file everything you add from the Presets or Custom tab actually gets written into. If you already have one, this action doesn't show up (check the Installed tab instead).`,
|
|
1001
|
+
"",
|
|
1002
|
+
`It starts out empty on purpose (just an empty "mcpServers" list). A fake placeholder server would fail the moment Nexus tried to connect to it on your very next reload, so this just gives you a valid, empty file to add real servers into afterward.`,
|
|
567
1003
|
], previewW);
|
|
1004
|
+
case "add-custom-stdio":
|
|
1005
|
+
return [
|
|
1006
|
+
...this.formatPreview(["Runs a command on your machine over stdio — e.g. an npx-installed MCP server."], previewW),
|
|
1007
|
+
"",
|
|
1008
|
+
colorizeCommandLine(truncateToWidth("/mcp add <name> -- <command> [args...]", previewW, "…")),
|
|
1009
|
+
"",
|
|
1010
|
+
fg(this.t.muted, "Example (real, runnable):"),
|
|
1011
|
+
colorizeCommandLine(truncateToWidth("/mcp add chrome-devtools -- npx -y chrome-devtools-mcp@1.6.0", previewW, "…")),
|
|
1012
|
+
"",
|
|
1013
|
+
...this.formatPreview([
|
|
1014
|
+
'Enter fills in "/mcp add my-server -- " into the chat input below — finish typing there.',
|
|
1015
|
+
"Run `/mcp add` with no arguments (from chat) for the full flag list.",
|
|
1016
|
+
], previewW),
|
|
1017
|
+
];
|
|
1018
|
+
case "add-custom-url":
|
|
1019
|
+
return [
|
|
1020
|
+
...this.formatPreview(["Connects to a remote MCP server over HTTP."], previewW),
|
|
1021
|
+
"",
|
|
1022
|
+
colorizeCommandLine(truncateToWidth("/mcp add <name> --url <url> [--header K=V] [--auth oauth|bearer] [--bearer-env VAR]", previewW, "…")),
|
|
1023
|
+
"",
|
|
1024
|
+
fg(this.t.muted, "Example (real, runnable):"),
|
|
1025
|
+
colorizeCommandLine(truncateToWidth("/mcp add context7 --url https://mcp.context7.com/mcp", previewW, "…")),
|
|
1026
|
+
"",
|
|
1027
|
+
...this.formatPreview([
|
|
1028
|
+
'Enter fills in "/mcp add my-server --url " into the chat input below — finish typing there.',
|
|
1029
|
+
"Run `/mcp add` with no arguments (from chat) for the full flag list.",
|
|
1030
|
+
], previewW),
|
|
1031
|
+
];
|
|
1032
|
+
case "view-installed-server": {
|
|
1033
|
+
const server = action.installed;
|
|
1034
|
+
if (!server)
|
|
1035
|
+
return this.formatPreview(["Add a server from the Presets or Custom tab to see it here."], previewW);
|
|
1036
|
+
return this.formatPreview([
|
|
1037
|
+
`${server.name} — ${server.kind === "stdio" ? "local (stdio)" : "remote (HTTP)"}, currently ${server.disabled ? "disabled" : "active"}${server.authCapable ? (server.signedIn ? ", signed in" : ", NOT signed in yet") : ""}.`,
|
|
1038
|
+
"",
|
|
1039
|
+
`Press Enter to ${server.disabled ? "enable" : "disable"} it here. Takes effect after /reload.`,
|
|
1040
|
+
...(server.authCapable
|
|
1041
|
+
? [
|
|
1042
|
+
"",
|
|
1043
|
+
server.signedIn
|
|
1044
|
+
? 'Press "a" to sign in again (e.g. to switch accounts). This re-opens your browser.'
|
|
1045
|
+
: 'Press "a" to sign in now — opens your browser for this server\'s OAuth login. Tokens refresh automatically after that; you should not need to repeat this unless access is revoked.',
|
|
1046
|
+
]
|
|
1047
|
+
: []),
|
|
1048
|
+
], previewW);
|
|
1049
|
+
}
|
|
568
1050
|
default:
|
|
569
1051
|
return this.formatPreview(["Close the setup flow."], previewW);
|
|
570
1052
|
}
|
|
@@ -589,10 +1071,14 @@ export class McpSetupPanel {
|
|
|
589
1071
|
: "New file will be created. Showing exact content diff.", width));
|
|
590
1072
|
lines.push("");
|
|
591
1073
|
const diffLines = preview.diffText.split("\n");
|
|
592
|
-
const maxLines =
|
|
1074
|
+
const maxLines = 12;
|
|
593
1075
|
const shown = diffLines.slice(0, maxLines);
|
|
594
1076
|
for (const line of shown) {
|
|
595
|
-
|
|
1077
|
+
// Truncate, don't word-wrap: wrapText collapses whitespace and would
|
|
1078
|
+
// destroy JSON's nested indentation.
|
|
1079
|
+
const color = diffLineColor(line);
|
|
1080
|
+
const truncated = truncateToWidth(line, width, "…");
|
|
1081
|
+
lines.push(color ? fg(color, truncated) : colorizeJsonLine(truncated));
|
|
596
1082
|
}
|
|
597
1083
|
if (diffLines.length > maxLines) {
|
|
598
1084
|
lines.push(...wrapText(`… ${diffLines.length - maxLines} more diff line${diffLines.length - maxLines === 1 ? "" : "s"}`, width));
|