@linxiraos/pi-tui 1.2.1 → 1.3.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/CHANGELOG.md +245 -224
- package/README.md +36 -0
- package/THIRD-PARTY-NOTICES.txt +55 -5
- package/dist/types/app-keybindings.d.ts +18 -8
- package/dist/types/apps/git/diff-pane.d.ts +5 -0
- package/dist/types/apps/git/git-tui.d.ts +16 -2
- package/dist/types/apps/git/state.d.ts +11 -0
- package/dist/types/autocomplete.d.ts +2 -0
- package/dist/types/charts/chart-svg.d.ts +7 -13
- package/dist/types/chat/advisor-message.d.ts +4 -1
- package/dist/types/chat/assistant-message.d.ts +9 -4
- package/dist/types/chat/figure-markdown.d.ts +7 -2
- package/dist/types/chat/messages.d.ts +3 -1
- package/dist/types/chat/read-tool-group.d.ts +2 -2
- package/dist/types/chat/table-chart.d.ts +11 -15
- package/dist/types/chat/tool-execution.d.ts +32 -20
- package/dist/types/chat/user-message.d.ts +3 -2
- package/dist/types/chrome/countdown-timer.d.ts +3 -2
- package/dist/types/chrome/diff.d.ts +2 -3
- package/dist/types/chrome/status-notice.d.ts +2 -0
- package/dist/types/chrome/transcript-container.d.ts +2 -0
- package/dist/types/components/image.d.ts +2 -0
- package/dist/types/components/markdown.d.ts +49 -3
- package/dist/types/components/text.d.ts +2 -0
- package/dist/types/debug-server.d.ts +2 -0
- package/dist/types/keybinding-matchers.d.ts +6 -0
- package/dist/types/multiplexers/tmux.d.ts +1 -0
- package/dist/types/multiplexers/types.d.ts +6 -0
- package/dist/types/native/backend.d.ts +5 -4
- package/dist/types/native/blobs.d.ts +24 -12
- package/dist/types/overlays/agent-hub-projection.d.ts +10 -5
- package/dist/types/overlays/annotation-overlay.d.ts +19 -0
- package/dist/types/overlays/approval-dialog.d.ts +79 -0
- package/dist/types/overlays/ask-dialog.d.ts +11 -6
- package/dist/types/overlays/btw-history-panel.d.ts +2 -6
- package/dist/types/overlays/hook-editor.d.ts +7 -2
- package/dist/types/overlays/hook-input.d.ts +12 -4
- package/dist/types/overlays/job-tree.d.ts +50 -0
- package/dist/types/overlays/jobs-panel.d.ts +25 -11
- package/dist/types/overlays/login-dialog.d.ts +8 -3
- package/dist/types/overlays/mcp-add-wizard.d.ts +9 -1
- package/dist/types/overlays/model-browser.d.ts +6 -1
- package/dist/types/overlays/model-picker.d.ts +2 -0
- package/dist/types/overlays/pause-screen.d.ts +5 -0
- package/dist/types/overlays/plan-review-overlay.d.ts +4 -0
- package/dist/types/overlays/report-panel.d.ts +5 -1
- package/dist/types/overlays/reset-usage-selector.d.ts +7 -5
- package/dist/types/overlays/session-account-selector.d.ts +4 -0
- package/dist/types/overlays/session-info-overlay.d.ts +7 -3
- package/dist/types/overlays/session-selector.d.ts +3 -1
- package/dist/types/overlays/usage-dashboard.d.ts +6 -6
- package/dist/types/prompt/composer.d.ts +23 -20
- package/dist/types/prompt/custom-editor.d.ts +20 -10
- package/dist/types/prompt/image-references.d.ts +22 -0
- package/dist/types/prompt/internal-url-autocomplete.d.ts +1 -0
- package/dist/types/prompt/macos-spelling.d.ts +2 -1
- package/dist/types/prompt/prose-gate.d.ts +3 -0
- package/dist/types/prompt/queued-messages.d.ts +1 -1
- package/dist/types/prompt/typing-word.d.ts +25 -0
- package/dist/types/prompt/welcome.d.ts +26 -6
- package/dist/types/render/hanging-wrap.d.ts +7 -0
- package/dist/types/render/hyperlink.d.ts +2 -0
- package/dist/types/render/render-utils.d.ts +9 -1
- package/dist/types/render/utils.d.ts +1 -1
- package/dist/types/setup/scenes/splash.d.ts +6 -5
- package/dist/types/status-line/context-usage.d.ts +4 -1
- package/dist/types/status-line/host.d.ts +6 -4
- package/dist/types/status-line/startup.d.ts +6 -6
- package/dist/types/status-line/types.d.ts +3 -0
- package/dist/types/stdin-buffer.d.ts +7 -0
- package/dist/types/terminal-capabilities.d.ts +16 -1
- package/dist/types/terminal-multiplexer.d.ts +7 -0
- package/dist/types/terminal.d.ts +8 -4
- package/dist/types/theme/symbols.d.ts +1 -1
- package/dist/types/theme/theme.d.ts +11 -0
- package/dist/types/thinking.d.ts +5 -0
- package/dist/types/tmux.d.ts +39 -4
- package/dist/types/tools/ast-edit.d.ts +2 -2
- package/dist/types/tools/ast-grep.d.ts +2 -2
- package/dist/types/tools/bash.d.ts +6 -1
- package/dist/types/tools/edit.d.ts +2 -0
- package/dist/types/tools/eval.d.ts +53 -4
- package/dist/types/tools/find.d.ts +14 -4
- package/dist/types/tools/glob.d.ts +3 -3
- package/dist/types/tools/grep.d.ts +2 -2
- package/dist/types/tools/grouped-file-output.d.ts +9 -4
- package/dist/types/tools/native-view.d.ts +1 -1
- package/dist/types/tools/read.d.ts +32 -10
- package/dist/types/tools/renderer.d.ts +2 -0
- package/dist/types/tools/task.d.ts +31 -0
- package/dist/types/tools/web-search-types.d.ts +1 -0
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/tui.d.ts +20 -3
- package/package.json +10 -10
- package/src/app-keybindings.d.ts +18 -8
- package/src/app-keybindings.ts +20 -8
- package/src/apps/autoresearch-dashboard.ts +444 -27
- package/src/apps/debug/raw-sse.ts +2 -2
- package/src/apps/git/diff-pane.ts +39 -41
- package/src/apps/git/git-tui.ts +80 -14
- package/src/apps/git/sidebar.ts +10 -1
- package/src/apps/git/state.ts +12 -0
- package/src/autocomplete.d.ts +2 -0
- package/src/autocomplete.ts +21 -19
- package/src/bracketed-paste.ts +9 -2
- package/src/charts/chart-svg.d.ts +7 -13
- package/src/charts/chart-svg.ts +19 -22
- package/src/charts/table-data.ts +17 -6
- package/src/chat/advisor-message.d.ts +4 -1
- package/src/chat/advisor-message.ts +77 -24
- package/src/chat/assistant-message.d.ts +9 -4
- package/src/chat/assistant-message.ts +127 -31
- package/src/chat/bash-execution.ts +7 -2
- package/src/chat/figure-markdown.d.ts +7 -2
- package/src/chat/figure-markdown.ts +10 -3
- package/src/chat/image-loading.ts +9 -6
- package/src/chat/messages.d.ts +3 -1
- package/src/chat/messages.ts +4 -1
- package/src/chat/read-tool-group.d.ts +2 -2
- package/src/chat/read-tool-group.ts +97 -42
- package/src/chat/svg-figure.ts +3 -1
- package/src/chat/svg-source.ts +6 -3
- package/src/chat/table-chart.d.ts +11 -15
- package/src/chat/table-chart.ts +24 -57
- package/src/chat/tool-execution.d.ts +32 -20
- package/src/chat/tool-execution.ts +103 -36
- package/src/chat/user-message.d.ts +3 -2
- package/src/chat/user-message.ts +54 -34
- package/src/chrome/countdown-timer.ts +3 -3
- package/src/chrome/diff.d.ts +2 -3
- package/src/chrome/diff.ts +4 -9
- package/src/chrome/status-notice.ts +5 -0
- package/src/chrome/transcript-container.d.ts +2 -0
- package/src/chrome/transcript-container.ts +212 -28
- package/src/components/editor.ts +12 -12
- package/src/components/image.d.ts +2 -0
- package/src/components/image.ts +54 -20
- package/src/components/markdown.d.ts +49 -3
- package/src/components/markdown.ts +482 -12
- package/src/components/settings-list.ts +2 -4
- package/src/components/text.d.ts +2 -0
- package/src/components/text.ts +11 -1
- package/src/debug-server.d.ts +2 -0
- package/src/debug-server.ts +51 -69
- package/src/hotkeys-markdown.ts +2 -0
- package/src/keybinding-matchers.d.ts +6 -0
- package/src/keybinding-matchers.ts +19 -0
- package/src/multiplexers/tmux.d.ts +1 -0
- package/src/multiplexers/tmux.ts +3 -0
- package/src/multiplexers/types.d.ts +6 -0
- package/src/multiplexers/types.ts +6 -0
- package/src/native/backend.d.ts +5 -4
- package/src/native/backend.ts +13 -5
- package/src/native/blobs.d.ts +24 -12
- package/src/native/blobs.ts +56 -7
- package/src/native/reconcile.ts +43 -21
- package/src/overlays/advisor-config.ts +259 -8
- package/src/overlays/agent-hub-projection.d.ts +10 -5
- package/src/overlays/agent-hub-projection.ts +63 -12
- package/src/overlays/annotation-overlay.ts +422 -168
- package/src/overlays/approval-dialog.ts +389 -0
- package/src/overlays/ask-dialog.ts +244 -105
- package/src/overlays/btw-history-panel.ts +208 -147
- package/src/overlays/extensions/extension-list.ts +4 -6
- package/src/overlays/hook-editor.ts +90 -32
- package/src/overlays/hook-input.ts +105 -19
- package/src/overlays/job-tree.ts +177 -0
- package/src/overlays/jobs-panel.ts +222 -142
- package/src/overlays/login-dialog.ts +201 -91
- package/src/overlays/mcp-add-wizard.ts +562 -279
- package/src/overlays/model-browser.ts +32 -7
- package/src/overlays/model-hub.ts +4 -10
- package/src/overlays/model-picker.ts +58 -21
- package/src/overlays/pause-screen.ts +81 -23
- package/src/overlays/plan-review-overlay.ts +16 -0
- package/src/overlays/plugin-selector.ts +17 -1
- package/src/overlays/queue-mode-selector.ts +10 -0
- package/src/overlays/report-panel.ts +6 -2
- package/src/overlays/reset-usage-selector.ts +130 -21
- package/src/overlays/session-account-selector.ts +36 -0
- package/src/overlays/session-info-overlay.ts +199 -76
- package/src/overlays/session-selector.ts +15 -10
- package/src/overlays/settings-selector.ts +3 -0
- package/src/overlays/show-images-selector.ts +1 -0
- package/src/overlays/snapcompact-shape-preview.ts +13 -6
- package/src/overlays/theme-selector.ts +6 -1
- package/src/overlays/thinking-selector.ts +8 -4
- package/src/overlays/tree-selector.ts +117 -2
- package/src/overlays/usage-dashboard.ts +498 -144
- package/src/prompt/composer-cache.ts +4 -1
- package/src/prompt/composer.ts +200 -74
- package/src/prompt/custom-editor.ts +168 -65
- package/src/prompt/image-references.d.ts +22 -0
- package/src/prompt/image-references.ts +79 -2
- package/src/prompt/internal-url-autocomplete.ts +1 -0
- package/src/prompt/macos-spelling.ts +28 -5
- package/src/prompt/prose-gate.ts +3 -0
- package/src/prompt/queued-messages.ts +9 -2
- package/src/prompt/tips.txt +1 -2
- package/src/prompt/typing-word.ts +59 -0
- package/src/prompt/welcome.ts +97 -45
- package/src/render/code-cell.ts +18 -5
- package/src/render/hanging-wrap.d.ts +7 -0
- package/src/render/hanging-wrap.ts +58 -0
- package/src/render/hyperlink.d.ts +2 -0
- package/src/render/hyperlink.ts +5 -0
- package/src/render/render-utils.d.ts +9 -1
- package/src/render/render-utils.ts +16 -2
- package/src/render/utils.d.ts +1 -1
- package/src/render/utils.ts +1 -1
- package/src/render/width-aware-text.ts +6 -5
- package/src/setup/scenes/splash.ts +22 -8
- package/src/status-line/component.ts +79 -11
- package/src/status-line/context-usage.ts +176 -73
- package/src/status-line/footer.ts +18 -35
- package/src/status-line/host.ts +6 -1
- package/src/status-line/segments.ts +16 -5
- package/src/status-line/startup.ts +6 -6
- package/src/status-line/types.ts +1 -0
- package/src/stdin-buffer.d.ts +7 -0
- package/src/stdin-buffer.ts +300 -62
- package/src/terminal-capabilities.d.ts +16 -1
- package/src/terminal-capabilities.ts +66 -28
- package/src/terminal-multiplexer.d.ts +7 -0
- package/src/terminal-multiplexer.ts +14 -0
- package/src/terminal.d.ts +8 -4
- package/src/terminal.ts +190 -63
- package/src/theme/symbols.d.ts +1 -1
- package/src/theme/symbols.ts +1 -0
- package/src/theme/theme-class.ts +1 -0
- package/src/theme/theme.d.ts +11 -0
- package/src/theme/theme.ts +46 -14
- package/src/thinking.d.ts +5 -0
- package/src/thinking.ts +9 -1
- package/src/tmux.d.ts +39 -4
- package/src/tmux.ts +110 -16
- package/src/tools/ask.ts +20 -9
- package/src/tools/ast-edit.d.ts +2 -2
- package/src/tools/ast-edit.ts +30 -7
- package/src/tools/ast-grep.d.ts +2 -2
- package/src/tools/ast-grep.ts +39 -35
- package/src/tools/bash.d.ts +6 -1
- package/src/tools/bash.ts +15 -9
- package/src/tools/debug.ts +2 -1
- package/src/tools/default-renderer.ts +3 -2
- package/src/tools/edit.d.ts +2 -0
- package/src/tools/edit.ts +100 -21
- package/src/tools/eval.d.ts +53 -4
- package/src/tools/eval.ts +173 -137
- package/src/tools/find.d.ts +14 -4
- package/src/tools/find.ts +71 -18
- package/src/tools/glob.d.ts +3 -3
- package/src/tools/glob.ts +51 -17
- package/src/tools/grep.d.ts +2 -2
- package/src/tools/grep.ts +99 -22
- package/src/tools/grouped-file-output.d.ts +9 -4
- package/src/tools/grouped-file-output.ts +21 -6
- package/src/tools/lsp.ts +30 -18
- package/src/tools/mcp.ts +2 -2
- package/src/tools/native-view.d.ts +1 -1
- package/src/tools/native-view.ts +2 -2
- package/src/tools/read.d.ts +32 -10
- package/src/tools/read.ts +89 -43
- package/src/tools/renderer.d.ts +2 -0
- package/src/tools/renderer.ts +2 -0
- package/src/tools/task.d.ts +31 -0
- package/src/tools/task.ts +40 -7
- package/src/tools/todo.ts +3 -2
- package/src/tools/wait.ts +16 -30
- package/src/tools/web-search-types.d.ts +1 -0
- package/src/tools/web-search-types.ts +1 -0
- package/src/tools/web-search.ts +2 -1
- package/src/tools/write.d.ts +2 -0
- package/src/tools/write.ts +36 -12
- package/src/tools/yield.ts +2 -2
- package/src/tui.d.ts +20 -3
- package/src/tui.ts +160 -43
- package/src/utils.ts +5 -3
- package/dist/types/tools/eval-format/code-mode-declarations.d.ts +0 -5
- package/src/tools/eval-format/code-mode-declarations.ts +0 -61
|
@@ -5,19 +5,23 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { Container, matchesKey, replaceTabs, Spacer, Text, truncateToWidth, wrapTextWithAnsi } from "../index";
|
|
7
7
|
import type { Component } from "../tui";
|
|
8
|
+
import * as path from "node:path";
|
|
8
9
|
import { getMCPConfigPath, getProjectDir } from "@linxiraos/pi-utils";
|
|
9
10
|
import { shortenPath } from "../render/render-utils";
|
|
10
11
|
import { getSelectListTheme, theme } from "../theme/theme";
|
|
12
|
+
import type { TspText } from "@linxiraos/pi-wire";
|
|
11
13
|
import { matchesAppInterrupt, matchesSelectDown, matchesSelectUp } from "../keybinding-matchers";
|
|
12
14
|
import { formatKeyHint } from "../app-keybindings";
|
|
13
15
|
import { boundKeys, editorKey, editorKeys, interruptKey } from "../chrome/keybinding-hints";
|
|
16
|
+
import type { KeyName } from "../key-hint-format";
|
|
17
|
+
import { getKeybindings } from "../keybindings";
|
|
14
18
|
import { OverlayPanel } from "../chrome/overlay-box";
|
|
15
19
|
import { TextFormField, type FormFieldTheme } from "../components/form";
|
|
16
20
|
import { SelectList } from "../components/select-list";
|
|
17
21
|
import { WizardStep, type WizardStepKind } from "../components/wizard-step";
|
|
18
|
-
import { col, node, span, text } from "../native/describe";
|
|
22
|
+
import { col, compact, node, row, span, text } from "../native/describe";
|
|
19
23
|
import { type DescribeContext, leafKey, type NativeChild, type NativeNode, type NativeUiEvent } from "../native/node";
|
|
20
|
-
import {
|
|
24
|
+
import { actionBar, actionButton } from "../native/overlay";
|
|
21
25
|
import { plainText } from "../native/spans";
|
|
22
26
|
import { isNativeRendering } from "../native/state";
|
|
23
27
|
|
|
@@ -185,29 +189,106 @@ function choiceHint(escapeAction: string): string {
|
|
|
185
189
|
return `[${editorKeys("tui.select.up", "tui.select.down")} to navigate, ${formatKeyHint("enter")} to select, ${interruptKey()} to ${escapeAction}]`;
|
|
186
190
|
}
|
|
187
191
|
|
|
188
|
-
/** The
|
|
189
|
-
|
|
190
|
-
|
|
192
|
+
/** The wizard's phases, the native progress strip's segments in order. */
|
|
193
|
+
const PHASES = ["Name", "Transport", "Connection", "Auth", "Scope", "Review"] as const;
|
|
194
|
+
const PHASE_NAME = 0;
|
|
195
|
+
const PHASE_TRANSPORT = 1;
|
|
196
|
+
const PHASE_CONNECTION = 2;
|
|
197
|
+
const PHASE_AUTH = 3;
|
|
198
|
+
const PHASE_SCOPE = 4;
|
|
199
|
+
const PHASE_REVIEW = 5;
|
|
200
|
+
|
|
201
|
+
/** The phase each step belongs to. */
|
|
202
|
+
const STEP_PHASE: Readonly<Record<MCPWizardStep, number>> = {
|
|
203
|
+
name: PHASE_NAME,
|
|
204
|
+
transport: PHASE_TRANSPORT,
|
|
205
|
+
command: PHASE_CONNECTION,
|
|
206
|
+
args: PHASE_CONNECTION,
|
|
207
|
+
url: PHASE_CONNECTION,
|
|
208
|
+
"auth-method": PHASE_AUTH,
|
|
209
|
+
"oauth-error": PHASE_AUTH,
|
|
210
|
+
"oauth-auth-url": PHASE_AUTH,
|
|
211
|
+
"oauth-token-url": PHASE_AUTH,
|
|
212
|
+
"oauth-client-id": PHASE_AUTH,
|
|
213
|
+
"oauth-client-secret": PHASE_AUTH,
|
|
214
|
+
"oauth-scopes": PHASE_AUTH,
|
|
215
|
+
apikey: PHASE_AUTH,
|
|
216
|
+
"auth-location": PHASE_AUTH,
|
|
217
|
+
"env-var-name": PHASE_AUTH,
|
|
218
|
+
"header-name": PHASE_AUTH,
|
|
219
|
+
scope: PHASE_SCOPE,
|
|
220
|
+
confirm: PHASE_REVIEW,
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
/** Transport choices in list order. */
|
|
224
|
+
const TRANSPORTS: readonly TransportType[] = ["stdio", "http", "sse"];
|
|
225
|
+
|
|
226
|
+
/** How each transport reads as a choice and in the review. */
|
|
227
|
+
const TRANSPORT_CHOICES: Readonly<Record<TransportType, { icon: string; label: string; description: string }>> = {
|
|
228
|
+
stdio: { icon: "terminal", label: "Local command", description: "omp starts it on this machine" },
|
|
229
|
+
http: { icon: "globe", label: "Remote server", description: "Streamable HTTP endpoint" },
|
|
230
|
+
sse: { icon: "broadcast", label: "Server-sent events", description: "Older HTTP servers" },
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const SCOPE_LABELS: Readonly<Record<Scope, string>> = { user: "Every project", project: "This project" };
|
|
234
|
+
|
|
235
|
+
/** The ANSI heading: the step's title and where it sits among the phases. */
|
|
236
|
+
function ansiHeading(title: string, tone: "accent" | "success" | "warning" | "error" | "muted", phase: number): Text {
|
|
237
|
+
return new Text(`${theme.fg(tone, title)}${theme.fg("dim", ` ${phase + 1}/${PHASES.length}`)}`, 0, 0);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** A status line of a native async step: a spinner while busy, else a toned icon. */
|
|
241
|
+
function statusRow(state: "busy" | "ok" | "warn" | "error" | "off", label: TspText, key = "status"): NativeNode {
|
|
242
|
+
if (state === "busy") {
|
|
243
|
+
return node(
|
|
244
|
+
"row",
|
|
245
|
+
{ role: "zeta.mcpAdd.status", gap: "sm", align: "center" },
|
|
246
|
+
[node("spinner", { style: "orbit", tone: "accent" }), text(label, { wrap: "word" })],
|
|
247
|
+
key,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
const [icon, tone] = (
|
|
251
|
+
{
|
|
252
|
+
ok: ["check", "success"],
|
|
253
|
+
warn: ["warn", "warning"],
|
|
254
|
+
error: ["x-circle", "error"],
|
|
255
|
+
off: ["slash-circle", "muted"],
|
|
256
|
+
} as const
|
|
257
|
+
)[state];
|
|
258
|
+
return node(
|
|
259
|
+
"row",
|
|
260
|
+
{ role: "zeta.mcpAdd.status", tone, gap: "sm", align: "center" },
|
|
261
|
+
[node("icon", { name: icon, tone }), text(label, { wrap: "word" })],
|
|
262
|
+
key,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** A tool's own error message, whole, in a quiet mono well. */
|
|
267
|
+
function errorBlock(message: string, key = "error"): NativeNode {
|
|
268
|
+
return { ...text(message, { role: "zeta.mcpAdd.error", wrap: "word" }), key };
|
|
191
269
|
}
|
|
192
270
|
|
|
193
|
-
/**
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
271
|
+
/** Footer buttons of a native step: what Esc runs (`undefined`: Esc cancels) and what Enter runs (`undefined`: nothing). */
|
|
272
|
+
interface NativeStepActions {
|
|
273
|
+
readonly back?: string;
|
|
274
|
+
/** The key Escape is bound to on this step (Back's, or Cancel's when there is no Back). */
|
|
275
|
+
readonly backKey?: KeyName;
|
|
276
|
+
readonly next?: string;
|
|
277
|
+
/** The key `next` mirrors (Enter in a choice, the field's submit key in an input step). */
|
|
278
|
+
readonly nextKey?: KeyName;
|
|
200
279
|
}
|
|
201
280
|
|
|
202
281
|
/** What the native description shows for the mounted step (the ANSI view mounts a {@link WizardStep}). */
|
|
203
282
|
interface NativeWizardStep {
|
|
204
|
-
readonly
|
|
205
|
-
|
|
206
|
-
readonly
|
|
207
|
-
/**
|
|
283
|
+
readonly phase: number;
|
|
284
|
+
readonly title: string;
|
|
285
|
+
readonly lead?: string;
|
|
286
|
+
/** An optional field: the title carries an `Optional` tag. */
|
|
287
|
+
readonly optional?: boolean;
|
|
288
|
+
/** Nodes, and a text step's `Input`, under the title and over the choices. */
|
|
208
289
|
readonly body: readonly NativeChild[];
|
|
209
290
|
readonly choices?: readonly WizardChoiceOption[];
|
|
210
|
-
readonly
|
|
291
|
+
readonly actions: NativeStepActions;
|
|
211
292
|
}
|
|
212
293
|
|
|
213
294
|
const mcpFormTheme: FormFieldTheme = {
|
|
@@ -219,8 +300,14 @@ const mcpFormTheme: FormFieldTheme = {
|
|
|
219
300
|
|
|
220
301
|
interface WizardChoiceOption {
|
|
221
302
|
readonly label: string;
|
|
222
|
-
/** Shown dimmed beneath the row while the row is not highlighted. */
|
|
303
|
+
/** Shown dimmed beneath the row while the row is not highlighted (ANSI), after the label (native). */
|
|
223
304
|
readonly description?: string;
|
|
305
|
+
/** Native row's detail when it differs from `description` (a mono path). */
|
|
306
|
+
readonly detail?: TspText;
|
|
307
|
+
/** Native row icon. */
|
|
308
|
+
readonly icon?: string;
|
|
309
|
+
/** A short machine tag at the row's end (`stdio`); the ANSI label carries it in parentheses. */
|
|
310
|
+
readonly tag?: string;
|
|
224
311
|
}
|
|
225
312
|
|
|
226
313
|
export class MCPAddWizard extends OverlayPanel {
|
|
@@ -256,11 +343,14 @@ export class MCPAddWizard extends OverlayPanel {
|
|
|
256
343
|
#inputField: TextFormField | null = null;
|
|
257
344
|
#selectedIndex = 0;
|
|
258
345
|
#validationError: string | null = null;
|
|
259
|
-
|
|
260
|
-
#
|
|
261
|
-
/**
|
|
262
|
-
|
|
263
|
-
|
|
346
|
+
/** Why the last OAuth attempt ended: the error (whole for native, fitted for ANSI) and a tip. */
|
|
347
|
+
#oauthFailure: { cancelled: boolean; message: string; ansiMessage: string; tip?: string } | null = null;
|
|
348
|
+
/**
|
|
349
|
+
* Bumped when a connection test starts and whenever the user steps back, so
|
|
350
|
+
* a test that settles after the user left its step doesn't move the wizard.
|
|
351
|
+
*/
|
|
352
|
+
#attempt = 0;
|
|
353
|
+
#nativeStep: NativeWizardStep = { phase: PHASE_NAME, title: "", body: [], actions: {} };
|
|
264
354
|
/** Memoized native description; dropped whenever a step mounts or its body changes. */
|
|
265
355
|
#native: NativeNode | undefined;
|
|
266
356
|
#onCompleteCallback: (name: string, config: MCPAddWizardConfig, scope: Scope) => void;
|
|
@@ -345,78 +435,91 @@ export class MCPAddWizard extends OverlayPanel {
|
|
|
345
435
|
}
|
|
346
436
|
|
|
347
437
|
/**
|
|
348
|
-
* `
|
|
349
|
-
*
|
|
438
|
+
* A text step. `lead` says what the field wants (the ANSI field's label),
|
|
439
|
+
* `placeholder` is an example value (the ANSI view lists it as a detail),
|
|
440
|
+
* `note` a constraint under the field, `escape` what Escape does
|
|
441
|
+
* (`undefined`: an optional step whose only hint is skipping).
|
|
350
442
|
*/
|
|
351
443
|
#inputStep(options: {
|
|
352
|
-
|
|
353
|
-
|
|
444
|
+
title: string;
|
|
445
|
+
lead: string;
|
|
354
446
|
initial: string;
|
|
447
|
+
placeholder?: string;
|
|
355
448
|
note?: string;
|
|
356
449
|
escape?: string;
|
|
357
|
-
details?: readonly string[];
|
|
358
450
|
optional: boolean;
|
|
451
|
+
secret?: boolean;
|
|
359
452
|
error?: string | null;
|
|
360
453
|
}): void {
|
|
361
454
|
this.#stepKind = "input";
|
|
455
|
+
const phase = STEP_PHASE[this.#currentStep];
|
|
362
456
|
const keyHint =
|
|
363
457
|
options.escape === undefined
|
|
364
458
|
? `[Press ${editorKey("tui.input.submit")} to skip or continue]`
|
|
365
459
|
: inputHint(options.escape);
|
|
366
460
|
const field = new TextFormField({
|
|
367
461
|
theme: mcpFormTheme,
|
|
368
|
-
label: options.
|
|
369
|
-
details: options.
|
|
462
|
+
label: options.lead,
|
|
463
|
+
details: options.placeholder ? [new Text(theme.fg("muted", `e.g. ${options.placeholder}`), 0, 0)] : undefined,
|
|
370
464
|
summary: options.error
|
|
371
465
|
? [new Text(theme.fg("error", `✗ ${sanitize(options.error)}`), 0, 0), new Spacer(1)]
|
|
372
466
|
: undefined,
|
|
373
467
|
hint: options.note ? `[${options.note}]\n${keyHint}` : keyHint,
|
|
374
468
|
initialValue: options.initial,
|
|
469
|
+
secret: options.secret,
|
|
375
470
|
empty: options.optional ? "submit" : "reject",
|
|
376
471
|
emptyError: " ",
|
|
377
472
|
onSubmit: () => this.#saveInputAndProceed(),
|
|
378
473
|
onCancel: () => this.#cancelInputStep(),
|
|
379
474
|
});
|
|
475
|
+
field.input.placeholder = options.placeholder;
|
|
380
476
|
this.#inputField = field;
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
if (options.note) body.push(text([span(options.note, "muted")], { wrap: "word" }));
|
|
477
|
+
const caption = options.error
|
|
478
|
+
? text([span(plainText(options.error), "error")], { role: "zeta.mcpAdd.note", wrap: "word" })
|
|
479
|
+
: options.note
|
|
480
|
+
? text(options.note, { role: "zeta.mcpAdd.note", wrap: "word" })
|
|
481
|
+
: undefined;
|
|
482
|
+
const cancelKey = getKeybindings().getKeys("tui.select.cancel")[0] ?? "escape";
|
|
388
483
|
this.#show(
|
|
389
|
-
new WizardStep({
|
|
390
|
-
kind: "input",
|
|
391
|
-
heading: new Text(theme.fg("accent", options.heading), 0, 0),
|
|
392
|
-
content: field,
|
|
393
|
-
}),
|
|
484
|
+
new WizardStep({ kind: "input", heading: ansiHeading(options.title, "accent", phase), content: field }),
|
|
394
485
|
{
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
486
|
+
phase,
|
|
487
|
+
title: options.title,
|
|
488
|
+
lead: options.lead,
|
|
489
|
+
optional: options.optional,
|
|
490
|
+
body: [
|
|
491
|
+
node(
|
|
492
|
+
"col",
|
|
493
|
+
{ role: "zeta.mcpAdd.field", gap: "xs", ...(options.error ? { tone: "error" as const } : {}) },
|
|
494
|
+
compact([field.input, caption]),
|
|
495
|
+
"field",
|
|
496
|
+
),
|
|
497
|
+
],
|
|
498
|
+
actions: {
|
|
499
|
+
back: this.#currentStep === "name" ? undefined : "Back",
|
|
500
|
+
backKey: cancelKey,
|
|
501
|
+
next: "Continue",
|
|
502
|
+
nextKey: getKeybindings().getKeys("tui.input.submit")[0] ?? "enter",
|
|
503
|
+
},
|
|
402
504
|
},
|
|
403
505
|
);
|
|
404
506
|
}
|
|
405
507
|
|
|
406
|
-
/** `escape` names what Escape does; `
|
|
508
|
+
/** `escape` names what Escape does; `intro` replaces the ANSI lead; `body` sits between the lead and the choices. */
|
|
407
509
|
#choiceStep(options: {
|
|
408
|
-
|
|
510
|
+
title: string;
|
|
511
|
+
lead?: string;
|
|
409
512
|
headingTone?: "accent" | "error" | "muted";
|
|
410
513
|
intro?: Component;
|
|
411
|
-
|
|
514
|
+
body?: readonly NativeChild[];
|
|
412
515
|
choices: readonly WizardChoiceOption[];
|
|
413
516
|
escape: string;
|
|
414
|
-
kind?: WizardStepKind;
|
|
415
517
|
}): void {
|
|
416
518
|
const tone = options.headingTone ?? "accent";
|
|
519
|
+
const phase = STEP_PHASE[this.#currentStep];
|
|
417
520
|
const items = options.choices.map((choice, index) => ({
|
|
418
521
|
value: String(index),
|
|
419
|
-
label: choice.label,
|
|
522
|
+
label: choice.tag ? `${choice.label} (${choice.tag})` : choice.label,
|
|
420
523
|
description: choice.description,
|
|
421
524
|
}));
|
|
422
525
|
const list = new SelectList(items, MAX_DISPLAY_WIDTH * Math.max(1, items.length), getSelectListTheme(), {
|
|
@@ -432,80 +535,160 @@ export class MCPAddWizard extends OverlayPanel {
|
|
|
432
535
|
},
|
|
433
536
|
});
|
|
434
537
|
list.setSelectedIndex(this.#selectedIndex);
|
|
435
|
-
this.#stepKind =
|
|
538
|
+
this.#stepKind = "choice";
|
|
436
539
|
this.#show(
|
|
437
540
|
new WizardStep({
|
|
438
|
-
kind:
|
|
439
|
-
heading:
|
|
440
|
-
intro: options.intro,
|
|
541
|
+
kind: "choice",
|
|
542
|
+
heading: ansiHeading(options.title, tone, phase),
|
|
543
|
+
intro: options.intro ?? (options.lead ? new Text(options.lead, 0, 0) : undefined),
|
|
441
544
|
content: list,
|
|
442
545
|
footer: new Text(theme.fg("muted", choiceHint(options.escape)), 0, 0),
|
|
443
546
|
}),
|
|
444
547
|
{
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
548
|
+
phase,
|
|
549
|
+
title: options.title,
|
|
550
|
+
lead: options.lead,
|
|
551
|
+
body: options.body ?? [],
|
|
448
552
|
choices: options.choices,
|
|
449
|
-
|
|
553
|
+
actions: {
|
|
554
|
+
back: this.#currentStep === "name" ? undefined : "Back",
|
|
555
|
+
backKey: boundKeys("app.interrupt", ["escape"])[0] ?? "escape",
|
|
556
|
+
next: "Continue",
|
|
557
|
+
nextKey: "enter",
|
|
558
|
+
},
|
|
450
559
|
},
|
|
451
560
|
);
|
|
452
561
|
}
|
|
453
562
|
|
|
454
|
-
/**
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
563
|
+
/**
|
|
564
|
+
* A step the wizard is working through (connection test, OAuth): `ansi` and
|
|
565
|
+
* `native` describe its body; `back` labels what Escape does there.
|
|
566
|
+
*/
|
|
567
|
+
#asyncStep(options: {
|
|
568
|
+
phase: number;
|
|
569
|
+
title: string;
|
|
570
|
+
tone: "accent" | "success" | "warning" | "error" | "muted";
|
|
571
|
+
lead?: string;
|
|
572
|
+
ansi: Component;
|
|
573
|
+
native: readonly NativeChild[];
|
|
574
|
+
footer?: Component;
|
|
575
|
+
back?: string;
|
|
576
|
+
}): void {
|
|
463
577
|
this.#stepKind = "async";
|
|
464
578
|
this.#inputField = null;
|
|
465
579
|
this.#show(
|
|
466
580
|
new WizardStep({
|
|
467
581
|
kind: "async",
|
|
468
|
-
heading:
|
|
469
|
-
content:
|
|
470
|
-
footer,
|
|
582
|
+
heading: ansiHeading(options.title, options.tone, options.phase),
|
|
583
|
+
content: options.ansi,
|
|
584
|
+
footer: options.footer,
|
|
471
585
|
}),
|
|
472
|
-
{
|
|
586
|
+
{
|
|
587
|
+
phase: options.phase,
|
|
588
|
+
title: options.title,
|
|
589
|
+
lead: options.lead,
|
|
590
|
+
body: options.native,
|
|
591
|
+
actions: { back: options.back ?? "Back", backKey: boundKeys("app.interrupt", ["escape"])[0] ?? "escape" },
|
|
592
|
+
},
|
|
473
593
|
);
|
|
474
594
|
}
|
|
475
595
|
|
|
596
|
+
/**
|
|
597
|
+
* The sheet: the phase strip, the step's title and lead, its body (field,
|
|
598
|
+
* status, summary), its choices, and a footer whose buttons run what
|
|
599
|
+
* Escape, Enter and Ctrl+C run.
|
|
600
|
+
*/
|
|
476
601
|
override describe(_cx: DescribeContext): NativeNode {
|
|
477
602
|
if (this.#native) return this.#native;
|
|
478
603
|
const step = this.#nativeStep;
|
|
604
|
+
const title = row(
|
|
605
|
+
compact([
|
|
606
|
+
text(step.title, { role: "zeta.mcpAdd.title", wrap: "word" }),
|
|
607
|
+
step.optional && text("Optional", { role: "zeta.mcpAdd.tag" }),
|
|
608
|
+
]),
|
|
609
|
+
{ gap: "sm", align: "center" },
|
|
610
|
+
);
|
|
611
|
+
const head = compact([
|
|
612
|
+
title,
|
|
613
|
+
step.lead ? text(step.lead, { role: "zeta.mcpAdd.lead", wrap: "word" }) : undefined,
|
|
614
|
+
]);
|
|
479
615
|
const children: NativeChild[] = [
|
|
480
|
-
|
|
616
|
+
this.#describeProgress(step.phase),
|
|
617
|
+
node("col", { role: "zeta.mcpAdd.head", gap: "xs" }, head, "head"),
|
|
481
618
|
...step.body,
|
|
482
619
|
];
|
|
483
620
|
if (step.choices) {
|
|
484
621
|
const items = step.choices.map((choice, index) =>
|
|
485
622
|
node(
|
|
486
623
|
"item",
|
|
487
|
-
{
|
|
624
|
+
{
|
|
625
|
+
label: choice.label,
|
|
626
|
+
...(choice.detail || choice.description ? { detail: choice.detail ?? choice.description } : {}),
|
|
627
|
+
...(choice.icon ? { icon: choice.icon } : {}),
|
|
628
|
+
...(choice.tag ? { value: [span(choice.tag, "mono")] } : {}),
|
|
629
|
+
},
|
|
488
630
|
undefined,
|
|
489
631
|
String(index),
|
|
490
632
|
),
|
|
491
633
|
);
|
|
492
634
|
// Keyed per step: a double-click's trailing event never lands on the next step's list.
|
|
493
|
-
children.push(
|
|
635
|
+
children.push(
|
|
636
|
+
node(
|
|
637
|
+
"list",
|
|
638
|
+
{ selected: String(this.#selectedIndex), role: "zeta.mcpAdd.choices" },
|
|
639
|
+
items,
|
|
640
|
+
`choices-${this.#currentStep}`,
|
|
641
|
+
),
|
|
642
|
+
);
|
|
494
643
|
}
|
|
495
|
-
children.push(
|
|
644
|
+
children.push(this.#describeActions(step.actions));
|
|
496
645
|
// The wizard replaces the editor in the dock; its sheet hoists into the terminal's layer.
|
|
497
646
|
const sheet = node(
|
|
498
647
|
"overlay",
|
|
499
648
|
{ role: this.nativeRole, head: "Add MCP server", anchor: "center", size: "md", modal: true },
|
|
500
|
-
[col(children, { gap: "
|
|
649
|
+
[col(children, { gap: "lg" })],
|
|
501
650
|
"sheet",
|
|
502
651
|
);
|
|
503
652
|
this.#native = col([sheet]);
|
|
504
653
|
return this.#native;
|
|
505
654
|
}
|
|
506
655
|
|
|
507
|
-
/**
|
|
656
|
+
/** The phase strip: phases behind filled, the current one lit, Auth dashed when the server needed none. */
|
|
657
|
+
#describeProgress(phase: number): NativeNode {
|
|
658
|
+
const authSkipped = phase > PHASE_AUTH && this.#state.authMethod === "none";
|
|
659
|
+
const segments = PHASES.map((label, index) => {
|
|
660
|
+
const skipped = index === PHASE_AUTH && authSkipped;
|
|
661
|
+
const tone = index === phase ? "accent" : index > phase ? "muted" : skipped ? "neutral" : "success";
|
|
662
|
+
return node(
|
|
663
|
+
"col",
|
|
664
|
+
{ role: "zeta.mcpAdd.phase", tone, ...(skipped ? { title: "Auth · not needed" } : {}) },
|
|
665
|
+
[text(label)],
|
|
666
|
+
label,
|
|
667
|
+
);
|
|
668
|
+
});
|
|
669
|
+
return node("row", { role: "zeta.mcpAdd.progress", gap: "xs" }, segments, "progress");
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/** Cancel on the left; Back and the step's one primary action on the right. Keys live in the tooltips. */
|
|
673
|
+
#describeActions(actions: NativeStepActions): NativeNode {
|
|
674
|
+
const buttons: (NativeNode | null)[] = [
|
|
675
|
+
actionButton("Cancel", "cancel", { keys: actions.back === undefined ? actions.backKey : "ctrl+c" }),
|
|
676
|
+
null,
|
|
677
|
+
];
|
|
678
|
+
if (actions.back) buttons.push(actionButton(actions.back, "back", { keys: actions.backKey }));
|
|
679
|
+
if (actions.next) buttons.push(actionButton(actions.next, "next", { keys: actions.nextKey, tone: "accent" }));
|
|
680
|
+
return actionBar(buttons);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* A click on a choice is Enter on it, exactly as highlighting it with the
|
|
685
|
+
* arrows and pressing Enter; the footer's buttons run what their keys run.
|
|
686
|
+
*/
|
|
508
687
|
handleNativeEvent(event: NativeUiEvent): void {
|
|
688
|
+
if (event.type === "action") {
|
|
689
|
+
this.#runAction(event.act);
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
509
692
|
if (event.type !== "select" && event.type !== "activate") return;
|
|
510
693
|
if (leafKey(event.key) !== `choices-${this.#currentStep}`) return;
|
|
511
694
|
if (this.#inputField || this.#stepKind === "async" || this.#oauthAbort) return;
|
|
@@ -516,6 +699,29 @@ export class MCPAddWizard extends OverlayPanel {
|
|
|
516
699
|
this.#requestRender();
|
|
517
700
|
}
|
|
518
701
|
|
|
702
|
+
/** Cancel is Ctrl+C, Back is Escape, Next is Enter. */
|
|
703
|
+
#runAction(act: string): void {
|
|
704
|
+
switch (act) {
|
|
705
|
+
case "cancel":
|
|
706
|
+
this.#oauthAbort?.abort("MCP OAuth flow cancelled by user");
|
|
707
|
+
this.#onCancelCallback();
|
|
708
|
+
return;
|
|
709
|
+
case "back":
|
|
710
|
+
if (this.#oauthAbort) this.#oauthAbort.abort("MCP OAuth flow cancelled by user");
|
|
711
|
+
else if (this.#inputField) this.#cancelInputStep();
|
|
712
|
+
else if (this.#currentStep === "name") this.#onCancelCallback();
|
|
713
|
+
else this.#goBack();
|
|
714
|
+
break;
|
|
715
|
+
case "next":
|
|
716
|
+
if (this.#inputField) this.#inputField.submit();
|
|
717
|
+
else if (this.#stepKind !== "async") this.#selectCurrentOption();
|
|
718
|
+
break;
|
|
719
|
+
default:
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
this.#requestRender();
|
|
723
|
+
}
|
|
724
|
+
|
|
519
725
|
#cancelInputStep(): void {
|
|
520
726
|
if (this.#currentStep === "name") {
|
|
521
727
|
this.#onCancelCallback();
|
|
@@ -589,10 +795,11 @@ export class MCPAddWizard extends OverlayPanel {
|
|
|
589
795
|
|
|
590
796
|
#renderNameStep(): void {
|
|
591
797
|
this.#inputStep({
|
|
592
|
-
|
|
593
|
-
|
|
798
|
+
title: "Name the server",
|
|
799
|
+
lead: "A unique name: it labels the server in /mcp and prefixes its tools.",
|
|
594
800
|
initial: this.#state.name,
|
|
595
|
-
|
|
801
|
+
placeholder: "github",
|
|
802
|
+
note: "Letters, numbers, dash, underscore, dot and colon",
|
|
596
803
|
escape: "cancel",
|
|
597
804
|
optional: false,
|
|
598
805
|
error: this.#validationError,
|
|
@@ -601,23 +808,19 @@ export class MCPAddWizard extends OverlayPanel {
|
|
|
601
808
|
|
|
602
809
|
#renderTransportStep(): void {
|
|
603
810
|
this.#choiceStep({
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
{ label: "stdio (Local process)" },
|
|
609
|
-
{ label: "http (HTTP server)" },
|
|
610
|
-
{ label: "sse (Server-Sent Events)" },
|
|
611
|
-
],
|
|
612
|
-
escape: "cancel",
|
|
811
|
+
title: "How does omp reach it?",
|
|
812
|
+
lead: `Pick how ${this.#state.name} talks to omp.`,
|
|
813
|
+
choices: TRANSPORTS.map(transport => ({ ...TRANSPORT_CHOICES[transport], tag: transport })),
|
|
814
|
+
escape: "go back",
|
|
613
815
|
});
|
|
614
816
|
}
|
|
615
817
|
|
|
616
818
|
#renderCommandStep(): void {
|
|
617
819
|
this.#inputStep({
|
|
618
|
-
|
|
619
|
-
|
|
820
|
+
title: "Command",
|
|
821
|
+
lead: "The program omp starts for this server.",
|
|
620
822
|
initial: this.#state.command,
|
|
823
|
+
placeholder: "npx",
|
|
621
824
|
escape: "go back",
|
|
622
825
|
optional: false,
|
|
623
826
|
});
|
|
@@ -625,19 +828,21 @@ export class MCPAddWizard extends OverlayPanel {
|
|
|
625
828
|
|
|
626
829
|
#renderArgsStep(): void {
|
|
627
830
|
this.#inputStep({
|
|
628
|
-
|
|
629
|
-
|
|
831
|
+
title: "Arguments",
|
|
832
|
+
lead: `Passed to ${this.#state.command}, separated by spaces. omp tries the connection next.`,
|
|
630
833
|
initial: this.#state.args,
|
|
834
|
+
placeholder: "-y @modelcontextprotocol/server-github",
|
|
631
835
|
optional: true,
|
|
632
836
|
});
|
|
633
837
|
}
|
|
634
838
|
|
|
635
839
|
#renderUrlStep(): void {
|
|
636
840
|
this.#inputStep({
|
|
637
|
-
|
|
638
|
-
|
|
841
|
+
title: "Server URL",
|
|
842
|
+
lead: `Where ${this.#state.name} listens. omp tries the connection next.`,
|
|
639
843
|
initial: this.#state.url,
|
|
640
|
-
|
|
844
|
+
placeholder: "https://mcp.example.com/mcp",
|
|
845
|
+
note: "Starts with http:// or https://",
|
|
641
846
|
escape: "go back",
|
|
642
847
|
optional: false,
|
|
643
848
|
error: this.#validationError,
|
|
@@ -646,17 +851,22 @@ export class MCPAddWizard extends OverlayPanel {
|
|
|
646
851
|
|
|
647
852
|
#renderAuthLocationStep(): void {
|
|
648
853
|
this.#choiceStep({
|
|
649
|
-
|
|
650
|
-
|
|
854
|
+
title: "Where does the key go?",
|
|
855
|
+
lead: "How the server expects to receive it.",
|
|
856
|
+
choices: [
|
|
857
|
+
{ icon: "terminal", label: "Environment variable", description: "Set when the server starts" },
|
|
858
|
+
{ icon: "globe", label: "HTTP header", description: "Sent with every request" },
|
|
859
|
+
],
|
|
651
860
|
escape: "go back",
|
|
652
861
|
});
|
|
653
862
|
}
|
|
654
863
|
|
|
655
864
|
#renderEnvVarNameStep(): void {
|
|
656
865
|
this.#inputStep({
|
|
657
|
-
|
|
658
|
-
|
|
866
|
+
title: "Environment variable",
|
|
867
|
+
lead: "The variable the server reads its key from.",
|
|
659
868
|
initial: this.#state.envVarName,
|
|
869
|
+
placeholder: "API_KEY",
|
|
660
870
|
escape: "go back",
|
|
661
871
|
optional: false,
|
|
662
872
|
});
|
|
@@ -664,71 +874,116 @@ export class MCPAddWizard extends OverlayPanel {
|
|
|
664
874
|
|
|
665
875
|
#renderHeaderNameStep(): void {
|
|
666
876
|
this.#inputStep({
|
|
667
|
-
|
|
668
|
-
|
|
877
|
+
title: "Header name",
|
|
878
|
+
lead: "The HTTP header that carries the key.",
|
|
669
879
|
initial: this.#state.headerName,
|
|
880
|
+
placeholder: "Authorization",
|
|
670
881
|
escape: "go back",
|
|
671
882
|
optional: false,
|
|
672
883
|
});
|
|
673
884
|
}
|
|
674
885
|
|
|
675
|
-
|
|
886
|
+
/** Where `scope`'s config file is: relative inside the project, else `~`-shortened. */
|
|
887
|
+
#configPathLabel(scope: Scope): string {
|
|
676
888
|
const cwd = getProjectDir();
|
|
677
|
-
const
|
|
678
|
-
const
|
|
889
|
+
const file = getMCPConfigPath(scope, cwd);
|
|
890
|
+
const relative = path.relative(cwd, file);
|
|
891
|
+
return relative && !relative.startsWith("..") && !path.isAbsolute(relative) ? relative : shortenPath(file);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
#renderScopeStep(): void {
|
|
895
|
+
const userPath = this.#configPathLabel("user");
|
|
896
|
+
const projectPath = this.#configPathLabel("project");
|
|
679
897
|
this.#choiceStep({
|
|
680
|
-
|
|
681
|
-
|
|
898
|
+
title: "Where should it live?",
|
|
899
|
+
lead: "Saved for you everywhere, or only in this project's config.",
|
|
900
|
+
choices: [
|
|
901
|
+
{ icon: "home", label: SCOPE_LABELS.user, description: userPath, detail: [span(userPath, "mono")] },
|
|
902
|
+
{
|
|
903
|
+
icon: "folder",
|
|
904
|
+
label: SCOPE_LABELS.project,
|
|
905
|
+
description: projectPath,
|
|
906
|
+
detail: [span(projectPath, "mono")],
|
|
907
|
+
},
|
|
908
|
+
],
|
|
682
909
|
escape: "go back",
|
|
683
910
|
});
|
|
684
911
|
}
|
|
685
912
|
|
|
686
913
|
#renderConfirmStep(): void {
|
|
687
|
-
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
|
|
914
|
+
const state = this.#state;
|
|
915
|
+
const transport = state.transport ?? "stdio";
|
|
916
|
+
const scope = state.scope ?? "user";
|
|
917
|
+
const scopePath = this.#configPathLabel(scope);
|
|
918
|
+
// `ansi` is the value in the ANSI summary; `v` the native `kv` value (it styles and fits values itself).
|
|
919
|
+
const entries: { k: string; v: TspText; ansi: string }[] = [
|
|
920
|
+
{ k: "Name", v: [span(state.name, "strong")], ansi: theme.fg("accent", state.name) },
|
|
921
|
+
{
|
|
922
|
+
k: "Transport",
|
|
923
|
+
v: [span(`${TRANSPORT_CHOICES[transport].label} `), span(transport, "mono muted")],
|
|
924
|
+
ansi: transport,
|
|
925
|
+
},
|
|
691
926
|
];
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
entries.push({ k: "
|
|
695
|
-
if (this.#state.args) {
|
|
696
|
-
entries.push({ k: "Args", v: this.#state.args });
|
|
697
|
-
}
|
|
927
|
+
if (transport === "stdio") {
|
|
928
|
+
entries.push({ k: "Command", v: [span(state.command, "mono")], ansi: state.command });
|
|
929
|
+
if (state.args) entries.push({ k: "Arguments", v: [span(state.args, "mono")], ansi: state.args });
|
|
698
930
|
} else {
|
|
699
|
-
entries.push({ k: "URL", v: replaceTabs(
|
|
931
|
+
entries.push({ k: "URL", v: [span(replaceTabs(state.url), "mono")], ansi: sanitize(state.url) });
|
|
700
932
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
if (
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
}
|
|
933
|
+
if (state.authMethod === "oauth") {
|
|
934
|
+
entries.push({ k: "Auth", v: "OAuth, signed in", ansi: "OAuth (authenticated)" });
|
|
935
|
+
} else if (state.authMethod === "manual") {
|
|
936
|
+
const [where, name] = state.authLocation === "env" ? ["env", state.envVarName] : ["header", state.headerName];
|
|
937
|
+
entries.push({
|
|
938
|
+
k: "Auth",
|
|
939
|
+
v: [span(where === "env" ? "API key in " : "API key in header "), span(name, "mono")],
|
|
940
|
+
ansi: `API key via ${where} (${name})`,
|
|
941
|
+
});
|
|
942
|
+
} else {
|
|
943
|
+
entries.push({ k: "Auth", v: [span("None", "muted")], ansi: "None" });
|
|
713
944
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
945
|
+
entries.push({
|
|
946
|
+
k: "Saved in",
|
|
947
|
+
v: [span(`${SCOPE_LABELS[scope]} `), span(scopePath, "mono muted")],
|
|
948
|
+
ansi: `${SCOPE_LABELS[scope]} (${scopePath})`,
|
|
949
|
+
});
|
|
717
950
|
|
|
718
951
|
const summary = new Container();
|
|
719
|
-
for (const entry of entries) summary.addChild(new Text(`${entry.k}: ${entry.ansi
|
|
952
|
+
for (const entry of entries) summary.addChild(new Text(`${entry.k}: ${entry.ansi}`, 0, 0));
|
|
720
953
|
summary.addChild(new Spacer(1));
|
|
721
954
|
summary.addChild(new Text("Save this configuration?", 0, 0));
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
955
|
+
this.#stepKind = "confirm";
|
|
956
|
+
this.#show(
|
|
957
|
+
new WizardStep({
|
|
958
|
+
kind: "confirm",
|
|
959
|
+
heading: ansiHeading("Review", "accent", PHASE_REVIEW),
|
|
960
|
+
content: summary,
|
|
961
|
+
footer: new Text(
|
|
962
|
+
theme.fg("muted", `[${formatKeyHint("enter")} to save, ${interruptKey()} to go back]`),
|
|
963
|
+
0,
|
|
964
|
+
0,
|
|
965
|
+
),
|
|
966
|
+
}),
|
|
967
|
+
{
|
|
968
|
+
phase: PHASE_REVIEW,
|
|
969
|
+
title: "Review and save",
|
|
970
|
+
lead: "omp connects to the server as soon as it is saved.",
|
|
971
|
+
body: [
|
|
972
|
+
node(
|
|
973
|
+
"kv",
|
|
974
|
+
{ role: "zeta.mcpAdd.summary", items: entries.map(entry => ({ k: entry.k, v: entry.v })) },
|
|
975
|
+
undefined,
|
|
976
|
+
"summary",
|
|
977
|
+
),
|
|
978
|
+
],
|
|
979
|
+
actions: {
|
|
980
|
+
back: "Back",
|
|
981
|
+
backKey: boundKeys("app.interrupt", ["escape"])[0] ?? "escape",
|
|
982
|
+
next: "Save server",
|
|
983
|
+
nextKey: "enter",
|
|
984
|
+
},
|
|
985
|
+
},
|
|
986
|
+
);
|
|
732
987
|
}
|
|
733
988
|
|
|
734
989
|
handleInput(keyData: string): void {
|
|
@@ -794,7 +1049,8 @@ export class MCPAddWizard extends OverlayPanel {
|
|
|
794
1049
|
|
|
795
1050
|
switch (this.#currentStep) {
|
|
796
1051
|
case "name": {
|
|
797
|
-
// Validate server name
|
|
1052
|
+
// Validate server name; a rejected name stays in the field to be fixed.
|
|
1053
|
+
this.#state.name = value;
|
|
798
1054
|
const nameError = this.#deps.validateServerName(value);
|
|
799
1055
|
if (nameError) {
|
|
800
1056
|
this.#validationError = nameError;
|
|
@@ -802,7 +1058,6 @@ export class MCPAddWizard extends OverlayPanel {
|
|
|
802
1058
|
return;
|
|
803
1059
|
}
|
|
804
1060
|
this.#validationError = null;
|
|
805
|
-
this.#state.name = value;
|
|
806
1061
|
this.#currentStep = "transport";
|
|
807
1062
|
this.#selectedIndex = 0;
|
|
808
1063
|
break;
|
|
@@ -820,7 +1075,8 @@ export class MCPAddWizard extends OverlayPanel {
|
|
|
820
1075
|
void this.#testConnectionAndDetectAuth();
|
|
821
1076
|
return;
|
|
822
1077
|
case "url": {
|
|
823
|
-
// Validate URL
|
|
1078
|
+
// Validate URL; a rejected URL stays in the field to be fixed.
|
|
1079
|
+
this.#state.url = value;
|
|
824
1080
|
if (!value) {
|
|
825
1081
|
this.#validationError = "URL is required";
|
|
826
1082
|
this.#renderStep();
|
|
@@ -840,7 +1096,6 @@ export class MCPAddWizard extends OverlayPanel {
|
|
|
840
1096
|
return;
|
|
841
1097
|
}
|
|
842
1098
|
this.#validationError = null;
|
|
843
|
-
this.#state.url = value;
|
|
844
1099
|
void this.#testConnectionAndDetectAuth();
|
|
845
1100
|
return;
|
|
846
1101
|
}
|
|
@@ -909,8 +1164,7 @@ export class MCPAddWizard extends OverlayPanel {
|
|
|
909
1164
|
#selectCurrentOption(): void {
|
|
910
1165
|
switch (this.#currentStep) {
|
|
911
1166
|
case "transport": {
|
|
912
|
-
|
|
913
|
-
this.#state.transport = transports[this.#selectedIndex];
|
|
1167
|
+
this.#state.transport = TRANSPORTS[this.#selectedIndex] ?? "stdio";
|
|
914
1168
|
this.#currentStep = this.#state.transport === "stdio" ? "command" : "url";
|
|
915
1169
|
break;
|
|
916
1170
|
}
|
|
@@ -928,10 +1182,10 @@ export class MCPAddWizard extends OverlayPanel {
|
|
|
928
1182
|
case "oauth-error":
|
|
929
1183
|
if (this.#selectedIndex === 0) {
|
|
930
1184
|
void this.#launchOAuthFlow();
|
|
931
|
-
|
|
932
|
-
this.#currentStep = "oauth-auth-url";
|
|
1185
|
+
return;
|
|
933
1186
|
}
|
|
934
|
-
|
|
1187
|
+
this.#currentStep = "oauth-auth-url";
|
|
1188
|
+
break;
|
|
935
1189
|
case "auth-location": {
|
|
936
1190
|
const authLocations: Array<"env" | "header"> = ["env", "header"];
|
|
937
1191
|
this.#state.authLocation = authLocations[this.#selectedIndex];
|
|
@@ -949,15 +1203,9 @@ export class MCPAddWizard extends OverlayPanel {
|
|
|
949
1203
|
this.#selectedIndex = 0;
|
|
950
1204
|
break;
|
|
951
1205
|
}
|
|
952
|
-
case "confirm":
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
return;
|
|
956
|
-
}
|
|
957
|
-
this.#currentStep = "scope";
|
|
958
|
-
this.#selectedIndex = this.#state.scope === "user" ? 0 : 1;
|
|
959
|
-
break;
|
|
960
|
-
}
|
|
1206
|
+
case "confirm":
|
|
1207
|
+
this.#complete();
|
|
1208
|
+
return;
|
|
961
1209
|
}
|
|
962
1210
|
|
|
963
1211
|
this.#renderStep();
|
|
@@ -982,15 +1230,15 @@ export class MCPAddWizard extends OverlayPanel {
|
|
|
982
1230
|
return 1; // 2 options
|
|
983
1231
|
case "scope":
|
|
984
1232
|
return 1; // 2 options
|
|
985
|
-
case "confirm":
|
|
986
|
-
return 1; // 2 options
|
|
987
1233
|
default:
|
|
988
1234
|
return 0;
|
|
989
1235
|
}
|
|
990
1236
|
}
|
|
991
1237
|
|
|
992
1238
|
#goBack(): void {
|
|
993
|
-
//
|
|
1239
|
+
// A connection test still running for the step being left must not move the wizard on.
|
|
1240
|
+
this.#attempt++;
|
|
1241
|
+
this.#validationError = null;
|
|
994
1242
|
switch (this.#currentStep) {
|
|
995
1243
|
case "transport":
|
|
996
1244
|
this.#currentStep = "name";
|
|
@@ -1076,10 +1324,11 @@ export class MCPAddWizard extends OverlayPanel {
|
|
|
1076
1324
|
|
|
1077
1325
|
#renderAuthMethodStep(): void {
|
|
1078
1326
|
this.#choiceStep({
|
|
1079
|
-
|
|
1327
|
+
title: "How does it authenticate?",
|
|
1328
|
+
lead: `${this.#state.name} needs credentials before omp can use it.`,
|
|
1080
1329
|
choices: [
|
|
1081
|
-
{ label: "
|
|
1082
|
-
{ label: "
|
|
1330
|
+
{ icon: "log-in", label: "Sign in with OAuth", description: "Opens your browser" },
|
|
1331
|
+
{ icon: "key", label: "API key or token", description: "Paste it, or run a command that prints it" },
|
|
1083
1332
|
],
|
|
1084
1333
|
escape: "go back",
|
|
1085
1334
|
});
|
|
@@ -1087,10 +1336,10 @@ export class MCPAddWizard extends OverlayPanel {
|
|
|
1087
1336
|
|
|
1088
1337
|
#renderOAuthAuthUrlStep(): void {
|
|
1089
1338
|
this.#inputStep({
|
|
1090
|
-
|
|
1091
|
-
|
|
1339
|
+
title: "Authorization URL",
|
|
1340
|
+
lead: "The provider's OAuth authorize endpoint.",
|
|
1092
1341
|
initial: this.#state.oauthAuthUrl,
|
|
1093
|
-
|
|
1342
|
+
placeholder: "https://auth.example.com/oauth/authorize",
|
|
1094
1343
|
escape: "go back",
|
|
1095
1344
|
optional: false,
|
|
1096
1345
|
});
|
|
@@ -1098,10 +1347,10 @@ export class MCPAddWizard extends OverlayPanel {
|
|
|
1098
1347
|
|
|
1099
1348
|
#renderOAuthTokenUrlStep(): void {
|
|
1100
1349
|
this.#inputStep({
|
|
1101
|
-
|
|
1102
|
-
|
|
1350
|
+
title: "Token URL",
|
|
1351
|
+
lead: "The endpoint that exchanges the authorization code for a token.",
|
|
1103
1352
|
initial: this.#state.oauthTokenUrl,
|
|
1104
|
-
|
|
1353
|
+
placeholder: "https://auth.example.com/oauth/token",
|
|
1105
1354
|
escape: "go back",
|
|
1106
1355
|
optional: false,
|
|
1107
1356
|
});
|
|
@@ -1109,8 +1358,8 @@ export class MCPAddWizard extends OverlayPanel {
|
|
|
1109
1358
|
|
|
1110
1359
|
#renderOAuthClientIdStep(): void {
|
|
1111
1360
|
this.#inputStep({
|
|
1112
|
-
|
|
1113
|
-
|
|
1361
|
+
title: "Client ID",
|
|
1362
|
+
lead: "The ID of the OAuth app registered with the provider.",
|
|
1114
1363
|
initial: this.#state.oauthClientId,
|
|
1115
1364
|
escape: "go back",
|
|
1116
1365
|
optional: false,
|
|
@@ -1119,50 +1368,65 @@ export class MCPAddWizard extends OverlayPanel {
|
|
|
1119
1368
|
|
|
1120
1369
|
#renderOAuthClientSecretStep(): void {
|
|
1121
1370
|
this.#inputStep({
|
|
1122
|
-
|
|
1123
|
-
|
|
1371
|
+
title: "Client secret",
|
|
1372
|
+
lead: "Leave it empty for PKCE-only flows.",
|
|
1124
1373
|
initial: this.#state.oauthClientSecret,
|
|
1125
|
-
details: ["(Leave empty for PKCE-only flows)"],
|
|
1126
1374
|
escape: "go back",
|
|
1127
1375
|
optional: true,
|
|
1376
|
+
secret: true,
|
|
1128
1377
|
});
|
|
1129
1378
|
}
|
|
1130
1379
|
|
|
1131
1380
|
#renderOAuthScopesStep(): void {
|
|
1132
1381
|
this.#inputStep({
|
|
1133
|
-
|
|
1134
|
-
|
|
1382
|
+
title: "Scopes",
|
|
1383
|
+
lead: "What omp may access, separated by spaces. Sign-in starts next.",
|
|
1135
1384
|
initial: this.#state.oauthScopes,
|
|
1136
|
-
|
|
1385
|
+
placeholder: "read write",
|
|
1137
1386
|
escape: "go back",
|
|
1138
1387
|
optional: true,
|
|
1139
1388
|
});
|
|
1140
1389
|
}
|
|
1141
1390
|
|
|
1142
1391
|
#renderOAuthErrorStep(): void {
|
|
1392
|
+
const failure = this.#oauthFailure;
|
|
1393
|
+
const cancelled = failure?.cancelled ?? false;
|
|
1143
1394
|
const intro = new Container();
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
intro.addChild(new Text(errorLines[index] ?? "", 0, 0));
|
|
1395
|
+
if (failure) {
|
|
1396
|
+
intro.addChild(new Text(failure.ansiMessage, 0, 0));
|
|
1147
1397
|
intro.addChild(new Spacer(1));
|
|
1398
|
+
if (failure.tip) {
|
|
1399
|
+
intro.addChild(new Text(theme.fg("muted", `Tip: ${failure.tip}`), 0, 0));
|
|
1400
|
+
intro.addChild(new Spacer(1));
|
|
1401
|
+
}
|
|
1148
1402
|
}
|
|
1149
1403
|
intro.addChild(new Text("Choose next action:", 0, 0));
|
|
1404
|
+
const body = compact([
|
|
1405
|
+
failure && !cancelled ? errorBlock(failure.message) : undefined,
|
|
1406
|
+
failure?.tip ? { ...text(failure.tip, { role: "zeta.mcpAdd.note", wrap: "word" }), key: "tip" } : undefined,
|
|
1407
|
+
]);
|
|
1150
1408
|
this.#choiceStep({
|
|
1151
|
-
|
|
1152
|
-
headingTone:
|
|
1409
|
+
title: cancelled ? "Sign-in cancelled" : "Sign-in failed",
|
|
1410
|
+
headingTone: cancelled ? "muted" : "error",
|
|
1411
|
+
lead: cancelled
|
|
1412
|
+
? "The browser sign-in stopped before it finished."
|
|
1413
|
+
: "The provider didn't finish signing you in.",
|
|
1153
1414
|
intro,
|
|
1154
|
-
|
|
1155
|
-
choices: [
|
|
1415
|
+
body,
|
|
1416
|
+
choices: [
|
|
1417
|
+
{ icon: "repeat", label: "Try again", description: "Opens the browser again" },
|
|
1418
|
+
{ icon: "sliders", label: "Edit OAuth settings", description: "Endpoints, client and scopes" },
|
|
1419
|
+
],
|
|
1156
1420
|
escape: "go back",
|
|
1157
1421
|
});
|
|
1158
1422
|
}
|
|
1159
1423
|
|
|
1160
1424
|
#renderApiKeyStep(): void {
|
|
1161
1425
|
this.#inputStep({
|
|
1162
|
-
|
|
1163
|
-
|
|
1426
|
+
title: "API key",
|
|
1427
|
+
lead: "Paste the key or token. Start with ! to run a command that prints it, e.g. a password manager.",
|
|
1164
1428
|
initial: this.#state.apiKey,
|
|
1165
|
-
|
|
1429
|
+
placeholder: "!op read op://dev/github/token",
|
|
1166
1430
|
escape: "go back",
|
|
1167
1431
|
optional: false,
|
|
1168
1432
|
});
|
|
@@ -1182,22 +1446,47 @@ export class MCPAddWizard extends OverlayPanel {
|
|
|
1182
1446
|
return;
|
|
1183
1447
|
}
|
|
1184
1448
|
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1449
|
+
const attempt = ++this.#attempt;
|
|
1450
|
+
const target =
|
|
1451
|
+
testConfig.type === "stdio" ? [testConfig.command, ...(testConfig.args ?? [])].join(" ") : testConfig.url;
|
|
1452
|
+
this.#asyncStep({
|
|
1453
|
+
phase: PHASE_CONNECTION,
|
|
1454
|
+
title: "Trying the connection",
|
|
1455
|
+
tone: "accent",
|
|
1456
|
+
lead: "omp connects once to see whether the server needs credentials.",
|
|
1457
|
+
ansi: new Text(theme.fg("muted", `Connecting to ${sanitize(target)}…`), 0, 0),
|
|
1458
|
+
native: [statusRow("busy", [span("Connecting to "), span(replaceTabs(target), "mono")])],
|
|
1459
|
+
});
|
|
1460
|
+
this.#requestRender();
|
|
1461
|
+
/** After a delay, on to the scope step unless the user stepped back meanwhile. */
|
|
1462
|
+
const continueToScope = (delay: number) =>
|
|
1194
1463
|
setTimeout(() => {
|
|
1464
|
+
if (attempt !== this.#attempt) return;
|
|
1195
1465
|
this.#state.authMethod = "none";
|
|
1196
1466
|
this.#currentStep = "scope";
|
|
1197
1467
|
this.#selectedIndex = 0;
|
|
1198
1468
|
this.#renderStep();
|
|
1199
|
-
|
|
1469
|
+
this.#requestRender();
|
|
1470
|
+
}, delay);
|
|
1471
|
+
|
|
1472
|
+
try {
|
|
1473
|
+
// Try to connect - timeout is handled by the transport layer (5 seconds)
|
|
1474
|
+
await this.#onTestConnectionCallback(testConfig);
|
|
1475
|
+
if (attempt !== this.#attempt) return;
|
|
1476
|
+
|
|
1477
|
+
// Success! No auth required
|
|
1478
|
+
this.#asyncStep({
|
|
1479
|
+
phase: PHASE_CONNECTION,
|
|
1480
|
+
title: "Connected",
|
|
1481
|
+
tone: "success",
|
|
1482
|
+
lead: "Next, choose where to save it.",
|
|
1483
|
+
ansi: new Text(theme.fg("success", "✓ No authentication required"), 0, 0),
|
|
1484
|
+
native: [statusRow("ok", "No sign-in needed")],
|
|
1485
|
+
});
|
|
1486
|
+
this.#requestRender();
|
|
1487
|
+
continueToScope(1000);
|
|
1200
1488
|
} catch (error) {
|
|
1489
|
+
if (attempt !== this.#attempt) return;
|
|
1201
1490
|
// Connection failed - check if it's an auth error
|
|
1202
1491
|
const authResult = this.#deps.analyzeAuthError(error as Error, this.#state.url);
|
|
1203
1492
|
|
|
@@ -1224,6 +1513,7 @@ export class MCPAddWizard extends OverlayPanel {
|
|
|
1224
1513
|
if (scopes) oauth = { ...oauth, scopes };
|
|
1225
1514
|
}
|
|
1226
1515
|
|
|
1516
|
+
if (attempt !== this.#attempt) return;
|
|
1227
1517
|
if (oauth) {
|
|
1228
1518
|
this.#state.oauthAuthUrl = oauth.authorizationUrl;
|
|
1229
1519
|
this.#state.oauthTokenUrl = oauth.tokenUrl;
|
|
@@ -1235,12 +1525,7 @@ export class MCPAddWizard extends OverlayPanel {
|
|
|
1235
1525
|
this.#state.oauthResourceIsFallback = !oauth.resource && this.#state.transport !== "stdio";
|
|
1236
1526
|
this.#state.authMethod = "oauth";
|
|
1237
1527
|
|
|
1238
|
-
|
|
1239
|
-
oauthBody.addChild(new Text("Launching browser for authorization...", 0, 0));
|
|
1240
|
-
this.#asyncStep("✓ OAuth detected", "success", oauthBody, [
|
|
1241
|
-
text("Launching browser for authorization..."),
|
|
1242
|
-
]);
|
|
1243
|
-
|
|
1528
|
+
// #launchOAuthFlow shows its own step at once.
|
|
1244
1529
|
void this.#launchOAuthFlow();
|
|
1245
1530
|
return;
|
|
1246
1531
|
}
|
|
@@ -1248,24 +1533,23 @@ export class MCPAddWizard extends OverlayPanel {
|
|
|
1248
1533
|
// OAuth metadata unavailable: fallback to manual API key.
|
|
1249
1534
|
this.#currentStep = "apikey";
|
|
1250
1535
|
this.#renderStep();
|
|
1536
|
+
this.#requestRender();
|
|
1251
1537
|
} else {
|
|
1252
1538
|
// Not an auth error - just a connection failure
|
|
1253
|
-
const errorMsg = sanitize(error instanceof Error ? error.message : String(error));
|
|
1254
1539
|
const failureBody = new Container();
|
|
1255
|
-
failureBody.addChild(new Text(
|
|
1540
|
+
failureBody.addChild(new Text(sanitize(error instanceof Error ? error.message : String(error)), 0, 0));
|
|
1256
1541
|
failureBody.addChild(new Spacer(1));
|
|
1257
1542
|
failureBody.addChild(new Text(theme.fg("muted", "Adding server anyway..."), 0, 0));
|
|
1258
|
-
this.#asyncStep(
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
}, 2000);
|
|
1543
|
+
this.#asyncStep({
|
|
1544
|
+
phase: PHASE_CONNECTION,
|
|
1545
|
+
title: "Couldn't connect",
|
|
1546
|
+
tone: "warning",
|
|
1547
|
+
lead: "omp saves the server anyway; fix it later with /mcp.",
|
|
1548
|
+
ansi: failureBody,
|
|
1549
|
+
native: [errorBlock(nativeErrorText(error)), statusRow("busy", "Adding the server anyway…")],
|
|
1550
|
+
});
|
|
1551
|
+
this.#requestRender();
|
|
1552
|
+
continueToScope(2000);
|
|
1269
1553
|
}
|
|
1270
1554
|
}
|
|
1271
1555
|
}
|
|
@@ -1351,27 +1635,27 @@ export class MCPAddWizard extends OverlayPanel {
|
|
|
1351
1635
|
|
|
1352
1636
|
async #launchOAuthFlow(): Promise<void> {
|
|
1353
1637
|
if (!this.#onOAuthCallback) {
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1638
|
+
this.#asyncStep({
|
|
1639
|
+
phase: PHASE_AUTH,
|
|
1640
|
+
title: "Sign-in isn't available",
|
|
1641
|
+
tone: "error",
|
|
1642
|
+
ansi: new Text("OAuth login cannot start without a host OAuth handler.", 0, 0),
|
|
1643
|
+
native: [statusRow("error", "OAuth login cannot start without a host OAuth handler.")],
|
|
1644
|
+
});
|
|
1359
1645
|
this.#requestRender();
|
|
1360
1646
|
return;
|
|
1361
1647
|
}
|
|
1362
1648
|
|
|
1363
1649
|
// Validate OAuth configuration
|
|
1364
1650
|
if (!this.#state.oauthAuthUrl || !this.#state.oauthTokenUrl) {
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
"
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
interruptHint("go back"),
|
|
1374
|
-
);
|
|
1651
|
+
this.#asyncStep({
|
|
1652
|
+
phase: PHASE_AUTH,
|
|
1653
|
+
title: "OAuth settings incomplete",
|
|
1654
|
+
tone: "error",
|
|
1655
|
+
ansi: new Text("Authorization and Token URLs are required.", 0, 0),
|
|
1656
|
+
native: [statusRow("error", "Authorization and token URLs are required.")],
|
|
1657
|
+
footer: new Text(theme.fg("muted", `[Press ${interruptKey()} to go back]`), 0, 0),
|
|
1658
|
+
});
|
|
1375
1659
|
this.#requestRender();
|
|
1376
1660
|
return;
|
|
1377
1661
|
}
|
|
@@ -1382,18 +1666,22 @@ export class MCPAddWizard extends OverlayPanel {
|
|
|
1382
1666
|
authBody.addChild(new Text(theme.fg("muted", "Browser will open automatically."), 0, 0));
|
|
1383
1667
|
authBody.addChild(new Spacer(1));
|
|
1384
1668
|
authBody.addChild(new Text(theme.fg("warning", "If browser doesn't open, copy the URL from chat."), 0, 0));
|
|
1385
|
-
this.#
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1669
|
+
const authHost = URL.parse(this.#state.oauthAuthUrl)?.host;
|
|
1670
|
+
this.#asyncStep({
|
|
1671
|
+
phase: PHASE_AUTH,
|
|
1672
|
+
title: "Sign in to continue",
|
|
1673
|
+
tone: "accent",
|
|
1674
|
+
lead: "Approve access in your browser. If it didn't open, copy the link from the chat.",
|
|
1675
|
+
ansi: authBody,
|
|
1676
|
+
native: [
|
|
1677
|
+
statusRow(
|
|
1678
|
+
"busy",
|
|
1679
|
+
authHost ? [span("Waiting for "), span(authHost, "mono"), span("…")] : "Waiting for the browser…",
|
|
1680
|
+
),
|
|
1393
1681
|
],
|
|
1394
|
-
new Text(theme.fg("muted", `(Press ${interruptKey()} to cancel)`), 0, 0),
|
|
1395
|
-
|
|
1396
|
-
);
|
|
1682
|
+
footer: new Text(theme.fg("muted", `(Press ${interruptKey()} to cancel)`), 0, 0),
|
|
1683
|
+
back: "Stop waiting",
|
|
1684
|
+
});
|
|
1397
1685
|
this.#requestRender();
|
|
1398
1686
|
|
|
1399
1687
|
this.#oauthAbort = new AbortController();
|
|
@@ -1432,11 +1720,17 @@ export class MCPAddWizard extends OverlayPanel {
|
|
|
1432
1720
|
const initialFrame = spinnerFrames[0] ?? "|";
|
|
1433
1721
|
const healthText = new Text(theme.fg("muted", `${initialFrame} Checking server connection...`), 0, 0);
|
|
1434
1722
|
healthBody.addChild(healthText);
|
|
1435
|
-
const
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1723
|
+
const signedIn = statusRow("ok", "Signed in", "signed-in");
|
|
1724
|
+
const checks = (health: NativeNode) =>
|
|
1725
|
+
node("col", { role: "zeta.mcpAdd.checks", gap: "sm" }, [signedIn, health], "checks");
|
|
1726
|
+
this.#asyncStep({
|
|
1727
|
+
phase: PHASE_AUTH,
|
|
1728
|
+
title: "Checking the server",
|
|
1729
|
+
tone: "success",
|
|
1730
|
+
lead: "omp tries the new credentials before saving.",
|
|
1731
|
+
ansi: healthBody,
|
|
1732
|
+
native: [checks(statusRow("busy", "Waiting for the server to answer…", "health"))],
|
|
1733
|
+
});
|
|
1440
1734
|
|
|
1441
1735
|
// The frame ticker only repaints; a native terminal animates the described spinner itself.
|
|
1442
1736
|
let spinnerIndex = 0;
|
|
@@ -1481,15 +1775,14 @@ export class MCPAddWizard extends OverlayPanel {
|
|
|
1481
1775
|
clearInterval(spinner);
|
|
1482
1776
|
if (healthPassed) {
|
|
1483
1777
|
healthText.setText(theme.fg("success", "✓ Health check passed"));
|
|
1484
|
-
this.#setNativeBody([
|
|
1778
|
+
this.#setNativeBody([checks(statusRow("ok", "The server answers", "health"))]);
|
|
1485
1779
|
} else {
|
|
1486
1780
|
healthText.setText(theme.fg("warning", "⚠ Health check failed (will still save config)"));
|
|
1487
1781
|
healthBody.addChild(new Spacer(1));
|
|
1488
1782
|
healthBody.addChild(new Text(theme.fg("muted", healthError), 0, 0));
|
|
1489
1783
|
this.#setNativeBody([
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
text([span(nativeHealthError, "muted")], { wrap: "word" }),
|
|
1784
|
+
checks(statusRow("warn", "The server didn't answer; omp saves it anyway", "health")),
|
|
1785
|
+
errorBlock(nativeHealthError),
|
|
1493
1786
|
]);
|
|
1494
1787
|
}
|
|
1495
1788
|
this.#requestRender();
|
|
@@ -1509,32 +1802,22 @@ export class MCPAddWizard extends OverlayPanel {
|
|
|
1509
1802
|
// keeps the "OAuth authentication failed" framing so the existing tips
|
|
1510
1803
|
// stay meaningful. Name-matching avoids importing controller types.
|
|
1511
1804
|
const cancelled = error instanceof Error && error.name === "MCPOAuthCancelledError";
|
|
1512
|
-
const
|
|
1805
|
+
const ansiMessage = sanitize(error instanceof Error ? error.message : String(error));
|
|
1513
1806
|
let tip: string | undefined;
|
|
1514
1807
|
if (cancelled) {
|
|
1515
|
-
tip = "
|
|
1516
|
-
} else if (
|
|
1517
|
-
tip = "
|
|
1518
|
-
} else if (
|
|
1519
|
-
tip = "
|
|
1520
|
-
} else if (
|
|
1521
|
-
tip = "
|
|
1522
|
-
}
|
|
1523
|
-
const tipLines: string[] = [errorMsg];
|
|
1524
|
-
const nativeLines = [text(nativeErrorText(error), { wrap: "word" })];
|
|
1525
|
-
if (tip) {
|
|
1526
|
-
tipLines.push(theme.fg("muted", tip));
|
|
1527
|
-
nativeLines.push(text([span(tip, "muted")], { wrap: "word" }));
|
|
1808
|
+
tip = "Try again reopens the browser.";
|
|
1809
|
+
} else if (ansiMessage.includes("timeout") || ansiMessage.includes("timed out")) {
|
|
1810
|
+
tip = "Complete authorization faster next time.";
|
|
1811
|
+
} else if (ansiMessage.includes("Invalid OAuth URLs")) {
|
|
1812
|
+
tip = "Check that the OAuth URLs are correct.";
|
|
1813
|
+
} else if (ansiMessage.includes("ECONNREFUSED")) {
|
|
1814
|
+
tip = "Verify the OAuth server is accessible.";
|
|
1528
1815
|
}
|
|
1529
|
-
this.#
|
|
1816
|
+
this.#oauthFailure = { cancelled, message: nativeErrorText(error), ansiMessage, tip };
|
|
1530
1817
|
|
|
1531
1818
|
// Set up as a selector step
|
|
1532
1819
|
this.#selectedIndex = 0;
|
|
1533
1820
|
this.#currentStep = "oauth-error";
|
|
1534
|
-
this.#oauthErrorHeading = cancelled
|
|
1535
|
-
? { text: "○ OAuth cancelled", tone: "muted" }
|
|
1536
|
-
: { text: "✗ OAuth authentication failed", tone: "error" };
|
|
1537
|
-
this.#oauthErrorLines = tipLines;
|
|
1538
1821
|
this.#renderStep();
|
|
1539
1822
|
this.#requestRender();
|
|
1540
1823
|
} finally {
|