@linxiraos/pi-tui 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. package/CHANGELOG.md +188 -120
  2. package/README.md +36 -0
  3. package/THIRD-PARTY-NOTICES.txt +55 -5
  4. package/dist/types/app-keybindings.d.ts +18 -8
  5. package/dist/types/apps/git/diff-pane.d.ts +5 -0
  6. package/dist/types/apps/git/git-tui.d.ts +16 -2
  7. package/dist/types/apps/git/state.d.ts +11 -0
  8. package/dist/types/autocomplete.d.ts +2 -0
  9. package/dist/types/charts/chart-svg.d.ts +7 -13
  10. package/dist/types/chat/advisor-message.d.ts +4 -1
  11. package/dist/types/chat/assistant-message.d.ts +9 -4
  12. package/dist/types/chat/figure-markdown.d.ts +7 -2
  13. package/dist/types/chat/messages.d.ts +3 -1
  14. package/dist/types/chat/read-tool-group.d.ts +2 -2
  15. package/dist/types/chat/table-chart.d.ts +11 -15
  16. package/dist/types/chat/tool-execution.d.ts +32 -20
  17. package/dist/types/chat/user-message.d.ts +3 -2
  18. package/dist/types/chrome/countdown-timer.d.ts +3 -2
  19. package/dist/types/chrome/diff.d.ts +2 -3
  20. package/dist/types/chrome/transcript-container.d.ts +2 -0
  21. package/dist/types/components/image.d.ts +2 -0
  22. package/dist/types/components/markdown.d.ts +49 -3
  23. package/dist/types/components/text.d.ts +2 -0
  24. package/dist/types/debug-server.d.ts +2 -0
  25. package/dist/types/keybinding-matchers.d.ts +6 -0
  26. package/dist/types/multiplexers/tmux.d.ts +1 -0
  27. package/dist/types/multiplexers/types.d.ts +6 -0
  28. package/dist/types/native/backend.d.ts +5 -4
  29. package/dist/types/native/blobs.d.ts +24 -12
  30. package/dist/types/overlays/agent-hub-projection.d.ts +10 -5
  31. package/dist/types/overlays/annotation-overlay.d.ts +19 -0
  32. package/dist/types/overlays/approval-dialog.d.ts +79 -0
  33. package/dist/types/overlays/ask-dialog.d.ts +11 -6
  34. package/dist/types/overlays/btw-history-panel.d.ts +2 -6
  35. package/dist/types/overlays/hook-editor.d.ts +7 -2
  36. package/dist/types/overlays/hook-input.d.ts +12 -4
  37. package/dist/types/overlays/job-tree.d.ts +50 -0
  38. package/dist/types/overlays/jobs-panel.d.ts +25 -11
  39. package/dist/types/overlays/login-dialog.d.ts +8 -3
  40. package/dist/types/overlays/mcp-add-wizard.d.ts +9 -1
  41. package/dist/types/overlays/model-browser.d.ts +6 -1
  42. package/dist/types/overlays/model-picker.d.ts +2 -0
  43. package/dist/types/overlays/pause-screen.d.ts +5 -0
  44. package/dist/types/overlays/plan-review-overlay.d.ts +4 -0
  45. package/dist/types/overlays/report-panel.d.ts +5 -1
  46. package/dist/types/overlays/reset-usage-selector.d.ts +7 -5
  47. package/dist/types/overlays/session-account-selector.d.ts +4 -0
  48. package/dist/types/overlays/session-info-overlay.d.ts +7 -3
  49. package/dist/types/overlays/session-selector.d.ts +3 -1
  50. package/dist/types/overlays/usage-dashboard.d.ts +6 -6
  51. package/dist/types/prompt/composer.d.ts +22 -26
  52. package/dist/types/prompt/custom-editor.d.ts +20 -10
  53. package/dist/types/prompt/image-references.d.ts +22 -0
  54. package/dist/types/prompt/internal-url-autocomplete.d.ts +1 -0
  55. package/dist/types/prompt/macos-spelling.d.ts +2 -1
  56. package/dist/types/prompt/prose-gate.d.ts +3 -0
  57. package/dist/types/prompt/queued-messages.d.ts +1 -1
  58. package/dist/types/prompt/typing-word.d.ts +25 -0
  59. package/dist/types/prompt/welcome.d.ts +35 -43
  60. package/dist/types/render/hanging-wrap.d.ts +7 -0
  61. package/dist/types/render/hyperlink.d.ts +2 -0
  62. package/dist/types/render/render-utils.d.ts +9 -1
  63. package/dist/types/render/utils.d.ts +1 -1
  64. package/dist/types/setup/scenes/splash.d.ts +6 -5
  65. package/dist/types/status-line/context-usage.d.ts +4 -1
  66. package/dist/types/status-line/host.d.ts +6 -4
  67. package/dist/types/status-line/startup.d.ts +6 -6
  68. package/dist/types/status-line/types.d.ts +3 -0
  69. package/dist/types/stdin-buffer.d.ts +7 -0
  70. package/dist/types/terminal-capabilities.d.ts +16 -1
  71. package/dist/types/terminal-multiplexer.d.ts +7 -0
  72. package/dist/types/terminal.d.ts +8 -4
  73. package/dist/types/theme/symbols.d.ts +1 -1
  74. package/dist/types/theme/theme.d.ts +11 -0
  75. package/dist/types/thinking.d.ts +5 -0
  76. package/dist/types/tmux.d.ts +39 -4
  77. package/dist/types/tools/ast-edit.d.ts +2 -2
  78. package/dist/types/tools/ast-grep.d.ts +2 -2
  79. package/dist/types/tools/bash.d.ts +6 -1
  80. package/dist/types/tools/edit.d.ts +2 -0
  81. package/dist/types/tools/eval.d.ts +53 -4
  82. package/dist/types/tools/find.d.ts +14 -4
  83. package/dist/types/tools/glob.d.ts +3 -3
  84. package/dist/types/tools/grep.d.ts +2 -2
  85. package/dist/types/tools/grouped-file-output.d.ts +9 -4
  86. package/dist/types/tools/native-view.d.ts +1 -1
  87. package/dist/types/tools/read.d.ts +32 -10
  88. package/dist/types/tools/renderer.d.ts +2 -0
  89. package/dist/types/tools/task.d.ts +31 -0
  90. package/dist/types/tools/write.d.ts +2 -0
  91. package/dist/types/tui.d.ts +20 -3
  92. package/package.json +10 -10
  93. package/src/app-keybindings.d.ts +18 -8
  94. package/src/app-keybindings.ts +20 -8
  95. package/src/apps/autoresearch-dashboard.ts +444 -27
  96. package/src/apps/debug/raw-sse.ts +2 -2
  97. package/src/apps/git/diff-pane.ts +39 -41
  98. package/src/apps/git/git-tui.ts +80 -14
  99. package/src/apps/git/sidebar.ts +10 -1
  100. package/src/apps/git/state.ts +12 -0
  101. package/src/autocomplete.d.ts +2 -0
  102. package/src/autocomplete.ts +21 -19
  103. package/src/bracketed-paste.ts +9 -2
  104. package/src/charts/chart-svg.d.ts +7 -13
  105. package/src/charts/chart-svg.ts +19 -22
  106. package/src/chat/advisor-message.d.ts +4 -1
  107. package/src/chat/advisor-message.ts +77 -24
  108. package/src/chat/assistant-message.d.ts +9 -4
  109. package/src/chat/assistant-message.ts +127 -31
  110. package/src/chat/bash-execution.ts +7 -2
  111. package/src/chat/figure-markdown.d.ts +7 -2
  112. package/src/chat/figure-markdown.ts +10 -3
  113. package/src/chat/image-loading.ts +9 -6
  114. package/src/chat/messages.d.ts +3 -1
  115. package/src/chat/messages.ts +4 -1
  116. package/src/chat/read-tool-group.d.ts +2 -2
  117. package/src/chat/read-tool-group.ts +97 -42
  118. package/src/chat/svg-figure.ts +3 -1
  119. package/src/chat/svg-source.ts +6 -3
  120. package/src/chat/table-chart.d.ts +11 -15
  121. package/src/chat/table-chart.ts +24 -57
  122. package/src/chat/tool-execution.d.ts +32 -20
  123. package/src/chat/tool-execution.ts +103 -36
  124. package/src/chat/user-message.d.ts +3 -2
  125. package/src/chat/user-message.ts +54 -34
  126. package/src/chrome/countdown-timer.ts +3 -3
  127. package/src/chrome/diff.d.ts +2 -3
  128. package/src/chrome/diff.ts +4 -9
  129. package/src/chrome/transcript-container.d.ts +2 -0
  130. package/src/chrome/transcript-container.ts +212 -28
  131. package/src/components/editor.ts +12 -12
  132. package/src/components/image.d.ts +2 -0
  133. package/src/components/image.ts +54 -20
  134. package/src/components/markdown.d.ts +49 -3
  135. package/src/components/markdown.ts +461 -11
  136. package/src/components/settings-list.ts +2 -4
  137. package/src/components/text.d.ts +2 -0
  138. package/src/components/text.ts +11 -1
  139. package/src/debug-server.d.ts +2 -0
  140. package/src/debug-server.ts +51 -69
  141. package/src/hotkeys-markdown.ts +2 -0
  142. package/src/keybinding-matchers.d.ts +6 -0
  143. package/src/keybinding-matchers.ts +19 -0
  144. package/src/multiplexers/tmux.d.ts +1 -0
  145. package/src/multiplexers/tmux.ts +3 -0
  146. package/src/multiplexers/types.d.ts +6 -0
  147. package/src/multiplexers/types.ts +6 -0
  148. package/src/native/backend.d.ts +5 -4
  149. package/src/native/backend.ts +13 -5
  150. package/src/native/blobs.d.ts +24 -12
  151. package/src/native/blobs.ts +56 -7
  152. package/src/native/reconcile.ts +43 -21
  153. package/src/overlays/advisor-config.ts +259 -8
  154. package/src/overlays/agent-hub-projection.d.ts +10 -5
  155. package/src/overlays/agent-hub-projection.ts +63 -12
  156. package/src/overlays/agent-hub.ts +2 -2
  157. package/src/overlays/annotation-overlay.ts +422 -168
  158. package/src/overlays/approval-dialog.ts +389 -0
  159. package/src/overlays/ask-dialog.ts +244 -105
  160. package/src/overlays/btw-history-panel.ts +208 -147
  161. package/src/overlays/extensions/extension-list.ts +4 -6
  162. package/src/overlays/hook-editor.ts +90 -32
  163. package/src/overlays/hook-input.ts +105 -19
  164. package/src/overlays/job-tree.ts +177 -0
  165. package/src/overlays/jobs-panel.ts +222 -142
  166. package/src/overlays/login-dialog.ts +201 -91
  167. package/src/overlays/mcp-add-wizard.ts +562 -279
  168. package/src/overlays/model-browser.ts +32 -7
  169. package/src/overlays/model-hub.ts +4 -10
  170. package/src/overlays/model-picker.ts +58 -21
  171. package/src/overlays/pause-screen.ts +81 -23
  172. package/src/overlays/plan-review-overlay.ts +16 -0
  173. package/src/overlays/plugin-selector.ts +17 -1
  174. package/src/overlays/plugin-settings.ts +8 -2
  175. package/src/overlays/queue-mode-selector.ts +10 -0
  176. package/src/overlays/report-panel.ts +6 -2
  177. package/src/overlays/reset-usage-selector.ts +130 -21
  178. package/src/overlays/session-account-selector.ts +36 -0
  179. package/src/overlays/session-info-overlay.ts +199 -76
  180. package/src/overlays/session-selector.ts +15 -10
  181. package/src/overlays/settings-selector.ts +3 -0
  182. package/src/overlays/show-images-selector.ts +1 -0
  183. package/src/overlays/snapcompact-shape-preview.ts +13 -6
  184. package/src/overlays/theme-selector.ts +6 -1
  185. package/src/overlays/thinking-selector.ts +8 -4
  186. package/src/overlays/tree-selector.ts +117 -2
  187. package/src/overlays/usage-dashboard.ts +498 -144
  188. package/src/prompt/composer-cache.ts +4 -6
  189. package/src/prompt/composer.ts +196 -96
  190. package/src/prompt/custom-editor.ts +168 -65
  191. package/src/prompt/image-references.d.ts +22 -0
  192. package/src/prompt/image-references.ts +79 -2
  193. package/src/prompt/internal-url-autocomplete.ts +1 -0
  194. package/src/prompt/macos-spelling.ts +28 -5
  195. package/src/prompt/prose-gate.ts +3 -0
  196. package/src/prompt/queued-messages.ts +9 -2
  197. package/src/prompt/tips.txt +1 -2
  198. package/src/prompt/typing-word.ts +59 -0
  199. package/src/prompt/welcome.ts +256 -423
  200. package/src/render/code-cell.ts +18 -5
  201. package/src/render/hanging-wrap.d.ts +7 -0
  202. package/src/render/hanging-wrap.ts +58 -0
  203. package/src/render/hyperlink.d.ts +2 -0
  204. package/src/render/hyperlink.ts +5 -0
  205. package/src/render/render-utils.d.ts +9 -1
  206. package/src/render/render-utils.ts +16 -2
  207. package/src/render/utils.d.ts +1 -1
  208. package/src/render/utils.ts +1 -1
  209. package/src/render/width-aware-text.ts +6 -5
  210. package/src/setup/scenes/splash.ts +137 -60
  211. package/src/status-line/component.ts +23 -10
  212. package/src/status-line/context-usage.ts +176 -73
  213. package/src/status-line/footer.ts +18 -35
  214. package/src/status-line/host.ts +6 -1
  215. package/src/status-line/segments.ts +16 -5
  216. package/src/status-line/startup.ts +6 -6
  217. package/src/status-line/types.ts +1 -0
  218. package/src/stdin-buffer.d.ts +7 -0
  219. package/src/stdin-buffer.ts +11 -1
  220. package/src/terminal-capabilities.d.ts +16 -1
  221. package/src/terminal-capabilities.ts +66 -28
  222. package/src/terminal-multiplexer.d.ts +7 -0
  223. package/src/terminal-multiplexer.ts +14 -0
  224. package/src/terminal.d.ts +8 -4
  225. package/src/terminal.ts +160 -46
  226. package/src/theme/symbols.d.ts +1 -1
  227. package/src/theme/symbols.ts +1 -0
  228. package/src/theme/theme-class.ts +1 -0
  229. package/src/theme/theme.d.ts +11 -0
  230. package/src/theme/theme.ts +46 -14
  231. package/src/thinking.d.ts +5 -0
  232. package/src/thinking.ts +9 -1
  233. package/src/tmux.d.ts +39 -4
  234. package/src/tmux.ts +110 -16
  235. package/src/tools/ask.ts +20 -9
  236. package/src/tools/ast-edit.d.ts +2 -2
  237. package/src/tools/ast-edit.ts +30 -7
  238. package/src/tools/ast-grep.d.ts +2 -2
  239. package/src/tools/ast-grep.ts +39 -35
  240. package/src/tools/bash.d.ts +6 -1
  241. package/src/tools/bash.ts +15 -9
  242. package/src/tools/debug.ts +2 -1
  243. package/src/tools/default-renderer.ts +3 -2
  244. package/src/tools/edit.d.ts +2 -0
  245. package/src/tools/edit.ts +100 -21
  246. package/src/tools/eval.d.ts +53 -4
  247. package/src/tools/eval.ts +173 -137
  248. package/src/tools/find.d.ts +14 -4
  249. package/src/tools/find.ts +71 -18
  250. package/src/tools/glob.d.ts +3 -3
  251. package/src/tools/glob.ts +51 -17
  252. package/src/tools/grep.d.ts +2 -2
  253. package/src/tools/grep.ts +99 -22
  254. package/src/tools/grouped-file-output.d.ts +9 -4
  255. package/src/tools/grouped-file-output.ts +21 -6
  256. package/src/tools/lsp.ts +30 -18
  257. package/src/tools/mcp.ts +2 -2
  258. package/src/tools/native-view.d.ts +1 -1
  259. package/src/tools/native-view.ts +2 -2
  260. package/src/tools/read.d.ts +32 -10
  261. package/src/tools/read.ts +89 -43
  262. package/src/tools/renderer.d.ts +2 -0
  263. package/src/tools/renderer.ts +2 -0
  264. package/src/tools/task.d.ts +31 -0
  265. package/src/tools/task.ts +40 -7
  266. package/src/tools/todo.ts +3 -2
  267. package/src/tools/wait.ts +16 -30
  268. package/src/tools/web-search.ts +2 -1
  269. package/src/tools/write.d.ts +2 -0
  270. package/src/tools/write.ts +36 -12
  271. package/src/tools/yield.ts +2 -2
  272. package/src/tui.d.ts +20 -3
  273. package/src/tui.ts +140 -36
  274. package/src/utils.ts +5 -3
  275. package/dist/types/tools/eval-format/code-mode-declarations.d.ts +0 -5
  276. 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 { actionHint, hintsRow, type NativeHint } from "../native/overlay";
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 `app.interrupt` key (raw Escape when unbound) as a footer hint. */
189
- function interruptHint(label: string): NativeHint {
190
- return { keys: boundKeys("app.interrupt", ["escape"]).slice(0, 1), label };
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
- /** Native form of {@link choiceHint}. */
194
- function choiceHints(escapeAction: string): readonly (NativeHint | undefined)[] {
195
- return [
196
- actionHint(["tui.select.up", "tui.select.down"], "navigate"),
197
- { keys: ["enter"], label: "select" },
198
- interruptHint(escapeAction),
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 heading: string;
205
- /** Heading colour token. */
206
- readonly tone: string;
207
- /** Nodes, and a text step's `Input`, between the heading and the choices. */
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 hints: readonly (NativeHint | undefined)[];
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
- #oauthErrorLines: readonly string[] | null = null;
260
- #oauthErrorHeading: { text: string; tone: "error" | "muted" } | null = null;
261
- /** Native form of {@link #oauthErrorLines}: the full error and its tip. */
262
- #oauthErrorNative: readonly NativeNode[] = [];
263
- #nativeStep: NativeWizardStep = { heading: "", tone: "accent", body: [], hints: [] };
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
- * `note` is a constraint shown above the key hint; `escape` names what
349
- * Escape does (`undefined`: an optional step whose only hint is skipping).
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
- heading: string;
353
- prompt: string;
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.prompt,
369
- details: options.details?.map(detail => new Text(theme.fg("muted", detail), 0, 0)),
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 body: NativeChild[] = [text(options.prompt, { wrap: "word" })];
382
- for (const detail of options.details ?? []) body.push(text([span(detail, "muted")], { wrap: "word" }));
383
- body.push(field.input);
384
- if (options.error) {
385
- body.push(text([span(`✗ ${plainText(options.error)}`, "error")], { wrap: "word" }));
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
- heading: options.heading,
396
- tone: "accent",
397
- body,
398
- hints:
399
- options.escape === undefined
400
- ? [actionHint("tui.input.submit", "skip or continue")]
401
- : [actionHint("tui.input.submit", "continue"), actionHint("tui.select.cancel", options.escape)],
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; `nativeIntro` is `intro`'s native description. */
508
+ /** `escape` names what Escape does; `intro` replaces the ANSI lead; `body` sits between the lead and the choices. */
407
509
  #choiceStep(options: {
408
- heading: string;
510
+ title: string;
511
+ lead?: string;
409
512
  headingTone?: "accent" | "error" | "muted";
410
513
  intro?: Component;
411
- nativeIntro?: readonly NativeChild[];
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 = options.kind ?? "choice";
538
+ this.#stepKind = "choice";
436
539
  this.#show(
437
540
  new WizardStep({
438
- kind: this.#stepKind,
439
- heading: new Text(theme.fg(tone, options.heading), 0, 0),
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
- heading: options.heading,
446
- tone,
447
- body: options.nativeIntro ?? [],
548
+ phase,
549
+ title: options.title,
550
+ lead: options.lead,
551
+ body: options.body ?? [],
448
552
  choices: options.choices,
449
- hints: choiceHints(options.escape),
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
- /** `nativeBody` describes `body`; `escapeHint` is the native form of `footer`. */
455
- #asyncStep(
456
- heading: string,
457
- headingTone: "accent" | "success" | "warning" | "error" | "muted",
458
- body: Component,
459
- nativeBody: readonly NativeChild[],
460
- footer?: Component,
461
- escapeHint?: NativeHint,
462
- ): void {
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: new Text(theme.fg(headingTone, heading), 0, 0),
469
- content: body,
470
- footer,
582
+ heading: ansiHeading(options.title, options.tone, options.phase),
583
+ content: options.ansi,
584
+ footer: options.footer,
471
585
  }),
472
- { heading, tone: headingTone, body: nativeBody, hints: escapeHint ? [escapeHint] : [] },
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
- text([span(step.heading, `${step.tone} strong`)], { wrap: "word" }),
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
- { label: choice.label, ...(choice.description ? { detail: choice.description } : {}) },
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(node("list", { selected: String(this.#selectedIndex) }, items, `choices-${this.#currentStep}`));
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(hintsRow(step.hints));
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: "md" })],
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
- /** A click on a choice is Enter on it, exactly as highlighting it with the arrows and pressing Enter. */
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
- heading: "Step 1: Server Name",
593
- prompt: "Enter a unique name for this server:",
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
- note: "Only letters, numbers, dash, underscore, dot, colon",
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
- heading: "Step 2: Transport Type",
605
- intro: new Text("Select the transport type:", 0, 0),
606
- nativeIntro: [text("Select the transport type:")],
607
- choices: [
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
- heading: "Step 3: Command",
619
- prompt: "Enter the command to run:",
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
- heading: "Step 4: Arguments (Optional)",
629
- prompt: "Enter command arguments (space-separated):",
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
- heading: "Step 3: Server URL",
638
- prompt: "Enter the server URL:",
841
+ title: "Server URL",
842
+ lead: `Where ${this.#state.name} listens. omp tries the connection next.`,
639
843
  initial: this.#state.url,
640
- note: "Must start with http:// or https://",
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
- heading: "Step: How to provide the key?",
650
- choices: [{ label: "Environment variable" }, { label: "HTTP header" }],
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
- heading: "Step: Environment Variable Name",
658
- prompt: "Enter the environment variable name:",
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
- heading: "Step: HTTP Header Name",
668
- prompt: "Enter the HTTP header name:",
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
- #renderScopeStep(): void {
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 userPathLabel = shortenPath(getMCPConfigPath("user", cwd));
678
- const projectPathLabel = shortenPath(getMCPConfigPath("project", cwd));
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
- heading: "Step: Configuration Scope",
681
- choices: [{ label: `User level (${userPathLabel})` }, { label: `Project level (${projectPathLabel})` }],
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
- // `ansi` overrides the value in the ANSI summary (the native `kv` styles and fits values itself).
688
- const entries: { k: string; v: string; ansi?: string }[] = [
689
- { k: "Name", v: this.#state.name, ansi: theme.fg("accent", this.#state.name) },
690
- { k: "Type", v: `${this.#state.transport}` },
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
- if (this.#state.transport === "stdio") {
694
- entries.push({ k: "Command", v: this.#state.command });
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(this.#state.url), ansi: sanitize(this.#state.url) });
931
+ entries.push({ k: "URL", v: [span(replaceTabs(state.url), "mono")], ansi: sanitize(state.url) });
700
932
  }
701
-
702
- // Auth info
703
- if (this.#state.authMethod === "none") {
704
- entries.push({ k: "Auth", v: "None" });
705
- } else if (this.#state.authMethod === "oauth") {
706
- entries.push({ k: "Auth", v: "OAuth (authenticated)" });
707
- } else if (this.#state.authMethod === "manual") {
708
- if (this.#state.authLocation === "env") {
709
- entries.push({ k: "Auth", v: `API key via env (${this.#state.envVarName})` });
710
- } else {
711
- entries.push({ k: "Auth", v: `API key via header (${this.#state.headerName})` });
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
- const scopeLabel = this.#state.scope === "user" ? "User level" : "Project level";
716
- entries.push({ k: "Scope", v: scopeLabel });
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 ?? entry.v}`, 0, 0));
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
- const items = entries.map(entry => ({ k: entry.k, v: entry.k === "Name" ? [span(entry.v, "accent")] : entry.v }));
723
-
724
- this.#choiceStep({
725
- heading: "Review Configuration",
726
- intro: summary,
727
- nativeIntro: [node("kv", { items }, undefined, "summary"), text("Save this configuration?")],
728
- choices: [{ label: "Yes" }, { label: "No" }],
729
- escape: "go back",
730
- kind: "confirm",
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
- const transports: TransportType[] = ["stdio", "http", "sse"];
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
- } else {
932
- this.#currentStep = "oauth-auth-url";
1185
+ return;
933
1186
  }
934
- return;
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
- if (this.#selectedIndex === 0) {
954
- this.#complete();
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
- // Navigate to previous step
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
- heading: "Step: Authentication Method",
1327
+ title: "How does it authenticate?",
1328
+ lead: `${this.#state.name} needs credentials before omp can use it.`,
1080
1329
  choices: [
1081
- { label: "OAuth flow (web-based)", description: "(opens browser)" },
1082
- { label: "Manual API key/token", description: "(paste or use shell command)" },
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
- heading: "OAuth: Authorization URL",
1091
- prompt: "Enter the OAuth authorization endpoint:",
1339
+ title: "Authorization URL",
1340
+ lead: "The provider's OAuth authorize endpoint.",
1092
1341
  initial: this.#state.oauthAuthUrl,
1093
- details: ["e.g., https://auth.example.com/oauth/authorize"],
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
- heading: "OAuth: Token URL",
1102
- prompt: "Enter the OAuth token endpoint:",
1350
+ title: "Token URL",
1351
+ lead: "The endpoint that exchanges the authorization code for a token.",
1103
1352
  initial: this.#state.oauthTokenUrl,
1104
- details: ["e.g., https://auth.example.com/oauth/token"],
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
- heading: "OAuth: Client ID",
1113
- prompt: "Enter your OAuth client ID:",
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
- heading: "OAuth: Client Secret (Optional)",
1123
- prompt: "Enter your OAuth client secret:",
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
- heading: "OAuth: Scopes (Optional)",
1134
- prompt: "Enter OAuth scopes (space-separated):",
1382
+ title: "Scopes",
1383
+ lead: "What omp may access, separated by spaces. Sign-in starts next.",
1135
1384
  initial: this.#state.oauthScopes,
1136
- details: ["e.g., read write"],
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
- const errorLines = this.#oauthErrorLines ?? [];
1145
- for (let index = 0; index < errorLines.length; index++) {
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
- heading: this.#oauthErrorHeading?.text ?? "OAuth authentication failed",
1152
- headingTone: this.#oauthErrorHeading?.tone ?? "error",
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
- nativeIntro: [...this.#oauthErrorNative, text("Choose next action:")],
1155
- choices: [{ label: "Retry OAuth authentication" }, { label: "Edit OAuth settings" }],
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
- heading: "API Key Required",
1163
- prompt: "Enter your API key or token:",
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
- details: ["(Supports !command for password manager)"],
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
- try {
1186
- // Try to connect - timeout is handled by the transport layer (5 seconds)
1187
- await this.#onTestConnectionCallback(testConfig);
1188
-
1189
- // Success! No auth required
1190
- const successBody = new Container();
1191
- successBody.addChild(new Text("No authentication required", 0, 0));
1192
- this.#asyncStep("✓ Connection successful!", "success", successBody, [text("No authentication required")]);
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
- }, 1000);
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
- const oauthBody = new Container();
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(errorMsg, 0, 0));
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("✗ Connection failed", "error", failureBody, [
1259
- text(nativeErrorText(error), { wrap: "word" }),
1260
- text([span("Adding server anyway...", "muted")]),
1261
- ]);
1262
-
1263
- setTimeout(() => {
1264
- this.#state.authMethod = "none";
1265
- this.#currentStep = "scope";
1266
- this.#selectedIndex = 0;
1267
- this.#renderStep();
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
- const unavailableBody = new Container();
1355
- unavailableBody.addChild(new Text("OAuth login cannot start without a host OAuth handler.", 0, 0));
1356
- this.#asyncStep("OAuth flow not available", "error", unavailableBody, [
1357
- text("OAuth login cannot start without a host OAuth handler.", { wrap: "word" }),
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
- const incompleteBody = new Container();
1366
- incompleteBody.addChild(new Text("Authorization and Token URLs are required.", 0, 0));
1367
- this.#asyncStep(
1368
- "OAuth configuration incomplete",
1369
- "error",
1370
- incompleteBody,
1371
- [text("Authorization and Token URLs are required.")],
1372
- new Text(theme.fg("muted", `[Press ${interruptKey()} to go back]`), 0, 0),
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.#asyncStep(
1386
- "OAuth Authentication",
1387
- "accent",
1388
- authBody,
1389
- [
1390
- text("Launching OAuth flow..."),
1391
- text([span("Browser will open automatically.", "muted")]),
1392
- text([span("If browser doesn't open, copy the URL from chat.", "warning")], { wrap: "word" }),
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
- interruptHint("cancel"),
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 healthIntro = text([span("Running connection health check...", "muted")]);
1436
- this.#asyncStep("✓ Authentication successful!", "success", healthBody, [
1437
- healthIntro,
1438
- node("spinner", { label: [span("Checking server connection...", "muted")] }, undefined, "health"),
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([healthIntro, text([span("✓ Health check passed", "success")])]);
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
- healthIntro,
1491
- text([span("⚠ Health check failed (will still save config)", "warning")]),
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 errorMsg = sanitize(error instanceof Error ? error.message : String(error));
1805
+ const ansiMessage = sanitize(error instanceof Error ? error.message : String(error));
1513
1806
  let tip: string | undefined;
1514
1807
  if (cancelled) {
1515
- tip = "Tip: Choose Retry to launch the browser again.";
1516
- } else if (errorMsg.includes("timeout") || errorMsg.includes("timed out")) {
1517
- tip = "Tip: Complete authorization faster next time";
1518
- } else if (errorMsg.includes("Invalid OAuth URLs")) {
1519
- tip = "Tip: Check that the OAuth URLs are correct";
1520
- } else if (errorMsg.includes("ECONNREFUSED")) {
1521
- tip = "Tip: Verify the OAuth server is accessible";
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.#oauthErrorNative = nativeLines;
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 {