@akon-labs/nexus-cli 0.1.10 → 0.1.11-rc.2

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 (83) hide show
  1. package/dist/cli/require-login.d.ts.map +1 -1
  2. package/dist/cli/require-login.js +2 -2
  3. package/dist/cli/require-login.js.map +1 -1
  4. package/dist/cli.js +1919 -1040
  5. package/dist/core/agent-session.d.ts +3 -1
  6. package/dist/core/agent-session.d.ts.map +1 -1
  7. package/dist/core/agent-session.js +4 -0
  8. package/dist/core/agent-session.js.map +1 -1
  9. package/dist/core/extensions/index.d.ts +1 -1
  10. package/dist/core/extensions/index.d.ts.map +1 -1
  11. package/dist/core/extensions/index.js.map +1 -1
  12. package/dist/core/extensions/loader.d.ts.map +1 -1
  13. package/dist/core/extensions/loader.js +6 -0
  14. package/dist/core/extensions/loader.js.map +1 -1
  15. package/dist/core/extensions/runner.d.ts +7 -1
  16. package/dist/core/extensions/runner.d.ts.map +1 -1
  17. package/dist/core/extensions/runner.js +14 -0
  18. package/dist/core/extensions/runner.js.map +1 -1
  19. package/dist/core/extensions/types.d.ts +18 -0
  20. package/dist/core/extensions/types.d.ts.map +1 -1
  21. package/dist/core/extensions/types.js.map +1 -1
  22. package/dist/core/tools/find.d.ts.map +1 -1
  23. package/dist/core/tools/find.js +1 -1
  24. package/dist/core/tools/find.js.map +1 -1
  25. package/dist/core/tools/grep.d.ts.map +1 -1
  26. package/dist/core/tools/grep.js +1 -1
  27. package/dist/core/tools/grep.js.map +1 -1
  28. package/dist/extensions/gitnexus/config.d.ts +15 -1
  29. package/dist/extensions/gitnexus/config.d.ts.map +1 -1
  30. package/dist/extensions/gitnexus/config.js +86 -2
  31. package/dist/extensions/gitnexus/config.js.map +1 -1
  32. package/dist/extensions/gitnexus/index.d.ts.map +1 -1
  33. package/dist/extensions/gitnexus/index.js +2 -2
  34. package/dist/extensions/gitnexus/index.js.map +1 -1
  35. package/dist/extensions/gitnexus/observatory.d.ts.map +1 -1
  36. package/dist/extensions/gitnexus/observatory.js +2 -2
  37. package/dist/extensions/gitnexus/observatory.js.map +1 -1
  38. package/dist/extensions/index.d.ts.map +1 -1
  39. package/dist/extensions/index.js +3 -0
  40. package/dist/extensions/index.js.map +1 -1
  41. package/dist/extensions/mcp/commands.d.ts +22 -2
  42. package/dist/extensions/mcp/commands.d.ts.map +1 -1
  43. package/dist/extensions/mcp/commands.js +201 -9
  44. package/dist/extensions/mcp/commands.js.map +1 -1
  45. package/dist/extensions/mcp/config.d.ts +2 -0
  46. package/dist/extensions/mcp/config.d.ts.map +1 -1
  47. package/dist/extensions/mcp/config.js +6 -1
  48. package/dist/extensions/mcp/config.js.map +1 -1
  49. package/dist/extensions/mcp/index.d.ts.map +1 -1
  50. package/dist/extensions/mcp/index.js +54 -5
  51. package/dist/extensions/mcp/index.js.map +1 -1
  52. package/dist/extensions/mcp/mcp-setup-panel.d.ts +63 -2
  53. package/dist/extensions/mcp/mcp-setup-panel.d.ts.map +1 -1
  54. package/dist/extensions/mcp/mcp-setup-panel.js +563 -77
  55. package/dist/extensions/mcp/mcp-setup-panel.js.map +1 -1
  56. package/dist/extensions/subagent/index.d.ts +2 -1
  57. package/dist/extensions/subagent/index.d.ts.map +1 -1
  58. package/dist/extensions/subagent/index.js +19 -6
  59. package/dist/extensions/subagent/index.js.map +1 -1
  60. package/dist/extensions/subagent-render.d.ts +3 -0
  61. package/dist/extensions/subagent-render.d.ts.map +1 -0
  62. package/dist/extensions/subagent-render.js +39 -0
  63. package/dist/extensions/subagent-render.js.map +1 -0
  64. package/dist/index.js +1951 -1072
  65. package/dist/modes/interactive/components/tool-execution.d.ts +3 -2
  66. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  67. package/dist/modes/interactive/components/tool-execution.js +12 -2
  68. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  69. package/dist/modes/interactive/interactive-mode.d.ts +2 -0
  70. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  71. package/dist/modes/interactive/interactive-mode.js +7 -3
  72. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  73. package/dist/rpc-entry.js +1919 -1040
  74. package/dist/utils/hidden-spawn.d.ts +29 -0
  75. package/dist/utils/hidden-spawn.d.ts.map +1 -0
  76. package/dist/utils/hidden-spawn.js +101 -0
  77. package/dist/utils/hidden-spawn.js.map +1 -0
  78. package/dist/vendor/ai.js +233 -233
  79. package/native/win32/build.mjs +95 -0
  80. package/native/win32/prebuilds/win32-x64/hidden-spawn.exe +0 -0
  81. package/native/win32/src/hidden-spawn.c +157 -0
  82. package/npm-shrinkwrap.json +2 -37
  83. package/package.json +7 -3
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-setup-panel.d.ts","sourceRoot":"","sources":["../../../src/extensions/mcp/mcp-setup-panel.ts"],"names":[],"mappings":"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","sourcesContent":["import { matchesKey, truncateToWidth, visibleWidth } from \"@earendil-works/pi-tui\";\nimport { getConfigDirName } from \"./agent-dir.ts\";\nimport {\n\ttype ConfigWritePreview,\n\tKNOWN_SERVER_PRESETS,\n\ttype KnownServerPreset,\n\ttype McpDiscoverySummary,\n} from \"./config.ts\";\nimport type { McpOnboardingState } from \"./onboarding-state.ts\";\nimport { createPanelKeys, type PanelKeybindings, type PanelKeys } from \"./panel-keys.ts\";\nimport type { ImportKind } from \"./types.ts\";\n\ninterface SetupTheme {\n\tborder: string;\n\ttitle: string;\n\tselected: string;\n\thint: string;\n\tsuccess: string;\n\twarning: string;\n\tmuted: string;\n}\n\nconst DEFAULT_THEME: SetupTheme = {\n\tborder: \"2\",\n\ttitle: \"36\",\n\tselected: \"32\",\n\thint: \"2\",\n\tsuccess: \"32\",\n\twarning: \"33\",\n\tmuted: \"2;3\",\n};\n\nconst MIN_PANEL_WIDTH = 24;\nconst COMPACT_WIDTH = 60;\nconst COMPACT_ACTION_ROWS = 7;\nconst DESKTOP_PREVIEW_WIDTH = 74;\n\nfunction fg(code: string, text: string): string {\n\treturn code ? `\\x1b[${code}m${text}\\x1b[0m` : text;\n}\n\nfunction wrapText(text: string, width: number): string[] {\n\tif (width <= 8) return [text];\n\tconst words = text.split(/\\s+/).filter(Boolean);\n\tconst lines: string[] = [];\n\tlet current = \"\";\n\tfor (const word of words) {\n\t\tconst candidate = current ? `${current} ${word}` : word;\n\t\tif (visibleWidth(candidate) <= width) {\n\t\t\tcurrent = candidate;\n\t\t\tcontinue;\n\t\t}\n\t\tif (current) lines.push(current);\n\t\tcurrent = word;\n\t}\n\tif (current) lines.push(current);\n\treturn lines.length > 0 ? lines : [\"\"];\n}\n\nexport interface SetupPanelCallbacks {\n\tpreviewImports: (imports: ImportKind[]) => ConfigWritePreview;\n\tpreviewStarterProject: () => ConfigWritePreview;\n\tpreviewRepoPrompt: () => ConfigWritePreview | null;\n\tpreviewKnownServer: (preset: KnownServerPreset) => ConfigWritePreview;\n\tadoptImports: (imports: ImportKind[]) => Promise<{ added: ImportKind[]; path: string }>;\n\tscaffoldProjectConfig: () => Promise<{ path: string }>;\n\taddRepoPrompt: () => Promise<{ path: string; serverName: string }>;\n\taddKnownServer: (preset: KnownServerPreset) => Promise<{ path: string; serverName: string }>;\n\topenPath: (path: string) => Promise<void>;\n\tmarkSetupCompleted: () => void;\n}\n\nexport interface SetupPanelOptions {\n\tmode: \"empty\" | \"setup\";\n\tonboardingState: McpOnboardingState;\n\tkeybindings?: PanelKeybindings;\n}\n\ntype Screen = \"empty\" | \"setup\" | \"imports\" | \"paths\";\n\ntype ActionId =\n\t| \"run-setup\"\n\t| \"adopt-imports\"\n\t| \"view-example\"\n\t| \"show-precedence\"\n\t| \"open-paths\"\n\t| \"add-repoprompt\"\n\t| \"add-known-server\"\n\t| \"scaffold-project\"\n\t| \"close\";\n\ninterface Action {\n\tid: ActionId;\n\tlabel: string;\n\tdescription: string;\n\tpreset?: KnownServerPreset;\n}\n\nexport class McpSetupPanel {\n\tprivate screen: Screen;\n\tprivate actionCursor = 0;\n\tprivate importCursor = 0;\n\tprivate pathCursor = 0;\n\tprivate selectedImports = new Set<ImportKind>();\n\tprivate busy = false;\n\tprivate notice: { text: string; tone: \"success\" | \"warning\" | \"muted\" } | null = null;\n\tprivate tui: { requestRender(): void };\n\tprivate t = DEFAULT_THEME;\n\tprivate keys: PanelKeys;\n\tprivate inactivityTimeout: ReturnType<typeof setTimeout> | null = null;\n\tprivate discovery: McpDiscoverySummary;\n\tprivate callbacks: SetupPanelCallbacks;\n\tprivate options: SetupPanelOptions;\n\tprivate done: () => void;\n\tprivate static readonly INACTIVITY_MS = 60_000;\n\n\tconstructor(\n\t\tdiscovery: McpDiscoverySummary,\n\t\tcallbacks: SetupPanelCallbacks,\n\t\toptions: SetupPanelOptions,\n\t\ttui: { requestRender(): void },\n\t\tdone: () => void,\n\t) {\n\t\tthis.discovery = discovery;\n\t\tthis.callbacks = callbacks;\n\t\tthis.options = options;\n\t\tthis.done = done;\n\t\tthis.tui = tui;\n\t\tthis.keys = createPanelKeys(options.keybindings);\n\t\tthis.screen = options.mode;\n\t\tfor (const entry of discovery.imports) {\n\t\t\tthis.selectedImports.add(entry.kind);\n\t\t}\n\t\tthis.resetInactivityTimeout();\n\t}\n\n\tprivate resetInactivityTimeout(): void {\n\t\tif (this.inactivityTimeout) clearTimeout(this.inactivityTimeout);\n\t\tthis.inactivityTimeout = setTimeout(() => {\n\t\t\tthis.cleanup();\n\t\t\tthis.done();\n\t\t}, McpSetupPanel.INACTIVITY_MS);\n\t}\n\n\tprivate cleanup(): void {\n\t\tif (this.inactivityTimeout) {\n\t\t\tclearTimeout(this.inactivityTimeout);\n\t\t\tthis.inactivityTimeout = null;\n\t\t}\n\t}\n\n\tprivate getActions(): Action[] {\n\t\tconst actions: Action[] = [];\n\t\tif (this.screen === \"empty\") {\n\t\t\tactions.push({\n\t\t\t\tid: \"run-setup\",\n\t\t\t\tlabel: \"Run setup\",\n\t\t\t\tdescription: \"Inspect detected configs, adopt imports, and scaffold a minimal `.nexus/mcp.json`.\",\n\t\t\t});\n\t\t}\n\t\tif (this.discovery.imports.length > 0) {\n\t\t\tactions.push({\n\t\t\t\tid: \"adopt-imports\",\n\t\t\t\tlabel: \"Adopt detected compatibility imports\",\n\t\t\t\tdescription: `Choose which host-specific MCP configs Nexus should import into its own override file. ${this.discovery.imports.length} source${this.discovery.imports.length === 1 ? \"\" : \"s\"} found.`,\n\t\t\t});\n\t\t}\n\t\tactions.push({\n\t\t\tid: \"view-example\",\n\t\t\tlabel: \"View example `.nexus/mcp.json`\",\n\t\t\tdescription: \"Preview a working Nexus MCP config you can paste or adapt.\",\n\t\t});\n\t\tif (!this.discovery.sources.some((source) => source.id === \"nexus-project\" && source.exists)) {\n\t\t\tactions.push({\n\t\t\t\tid: \"scaffold-project\",\n\t\t\t\tlabel: \"Scaffold project `.nexus/mcp.json`\",\n\t\t\t\tdescription: \"Write a minimal Nexus project MCP config, then reload Nexus.\",\n\t\t\t});\n\t\t}\n\t\tactions.push({\n\t\t\tid: \"show-precedence\",\n\t\t\tlabel: \"Explain config precedence\",\n\t\t\tdescription: \"Show the read order and where Nexus writes compatibility settings.\",\n\t\t});\n\t\tif (this.getDetectedPaths().length > 0) {\n\t\t\tactions.push({\n\t\t\t\tid: \"open-paths\",\n\t\t\t\tlabel: \"Open detected config paths\",\n\t\t\t\tdescription: \"Browse the actual config files that Nexus discovered on this machine.\",\n\t\t\t});\n\t\t}\n\t\tfor (const preset of KNOWN_SERVER_PRESETS) {\n\t\t\tactions.push({ id: \"add-known-server\", label: preset.name, description: preset.summary, preset });\n\t\t}\n\t\tif (\n\t\t\t!this.discovery.repoPrompt.configured &&\n\t\t\tthis.discovery.repoPrompt.executablePath &&\n\t\t\tthis.discovery.repoPrompt.targetPath &&\n\t\t\tthis.discovery.repoPrompt.entry &&\n\t\t\tthis.discovery.repoPrompt.serverName\n\t\t) {\n\t\t\tactions.push({\n\t\t\t\tid: \"add-repoprompt\",\n\t\t\t\tlabel: \"Add RepoPrompt to Nexus MCP config\",\n\t\t\t\tdescription:\n\t\t\t\t\t\"Write an MCP entry for RepoPrompt to the recommended Nexus target, then reload MCP in-session.\",\n\t\t\t});\n\t\t}\n\t\tactions.push({ id: \"close\", label: \"Close\", description: \"Exit the onboarding flow.\" });\n\t\treturn actions;\n\t}\n\n\tprivate getDetectedPaths(): string[] {\n\t\tconst paths = [\n\t\t\t...this.discovery.sources.filter((source) => source.exists).map((source) => source.path),\n\t\t\t...this.discovery.imports.map((entry) => entry.path),\n\t\t];\n\t\treturn [...new Set(paths)];\n\t}\n\n\tprivate getSelectedAction(): Action | undefined {\n\t\tconst actions = this.getActions();\n\t\treturn actions[this.actionCursor];\n\t}\n\n\thandleInput(data: string): void {\n\t\tthis.resetInactivityTimeout();\n\t\tif (!this.busy) this.notice = null;\n\n\t\tif (matchesKey(data, \"ctrl+c\")) {\n\t\t\tthis.cleanup();\n\t\t\tthis.done();\n\t\t\treturn;\n\t\t}\n\n\t\tif (matchesKey(data, \"escape\")) {\n\t\t\tif (this.screen === \"imports\" || this.screen === \"paths\") {\n\t\t\t\tthis.screen = this.discovery.hasAnyConfig ? \"setup\" : \"empty\";\n\t\t\t\tthis.tui.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.cleanup();\n\t\t\tthis.done();\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.busy) return;\n\n\t\tif (this.screen === \"imports\") {\n\t\t\tthis.handleImportsInput(data);\n\t\t\treturn;\n\t\t}\n\t\tif (this.screen === \"paths\") {\n\t\t\tthis.handlePathsInput(data);\n\t\t\treturn;\n\t\t}\n\n\t\tconst actions = this.getActions();\n\t\tif (this.keys.selectUp(data)) {\n\t\t\tthis.actionCursor = Math.max(0, this.actionCursor - 1);\n\t\t\tthis.tui.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (this.keys.selectDown(data)) {\n\t\t\tthis.actionCursor = Math.min(actions.length - 1, this.actionCursor + 1);\n\t\t\tthis.tui.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (this.keys.selectConfirm(data)) {\n\t\t\tconst selected = this.getSelectedAction();\n\t\t\tif (selected) void this.runAction(selected);\n\t\t}\n\t}\n\n\tprivate handleImportsInput(data: string): void {\n\t\tconst imports = this.discovery.imports;\n\t\tif (this.keys.selectUp(data)) {\n\t\t\tthis.importCursor = Math.max(0, this.importCursor - 1);\n\t\t\tthis.tui.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (this.keys.selectDown(data)) {\n\t\t\tthis.importCursor = Math.min(imports.length - 1, this.importCursor + 1);\n\t\t\tthis.tui.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (matchesKey(data, \"space\")) {\n\t\t\tconst current = imports[this.importCursor];\n\t\t\tif (!current) return;\n\t\t\tif (this.selectedImports.has(current.kind)) {\n\t\t\t\tthis.selectedImports.delete(current.kind);\n\t\t\t} else {\n\t\t\t\tthis.selectedImports.add(current.kind);\n\t\t\t}\n\t\t\tthis.tui.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (this.keys.selectConfirm(data)) {\n\t\t\tvoid this.applySelectedImports();\n\t\t}\n\t}\n\n\tprivate handlePathsInput(data: string): void {\n\t\tconst paths = this.getDetectedPaths();\n\t\tif (this.keys.selectUp(data)) {\n\t\t\tthis.pathCursor = Math.max(0, this.pathCursor - 1);\n\t\t\tthis.tui.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (this.keys.selectDown(data)) {\n\t\t\tthis.pathCursor = Math.min(paths.length - 1, this.pathCursor + 1);\n\t\t\tthis.tui.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (this.keys.selectConfirm(data)) {\n\t\t\tconst selected = paths[this.pathCursor];\n\t\t\tif (!selected) return;\n\t\t\tvoid this.runBusy(async () => {\n\t\t\t\tawait this.callbacks.openPath(selected);\n\t\t\t\tthis.notice = { text: `Opened ${selected}`, tone: \"success\" };\n\t\t\t});\n\t\t}\n\t}\n\n\tprivate async runAction(action: Action): Promise<void> {\n\t\tif (action.id === \"run-setup\") {\n\t\t\tthis.screen = \"setup\";\n\t\t\tthis.actionCursor = 0;\n\t\t\tthis.tui.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (action.id === \"adopt-imports\") {\n\t\t\tthis.screen = \"imports\";\n\t\t\tthis.importCursor = 0;\n\t\t\tthis.tui.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (action.id === \"open-paths\") {\n\t\t\tthis.screen = \"paths\";\n\t\t\tthis.pathCursor = 0;\n\t\t\tthis.tui.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (action.id === \"scaffold-project\") {\n\t\t\tawait this.runBusy(async () => {\n\t\t\t\tconst result = await this.callbacks.scaffoldProjectConfig();\n\t\t\t\tthis.callbacks.markSetupCompleted();\n\t\t\t\tthis.notice = {\n\t\t\t\t\ttext: `Wrote starter config to ${result.path}. Nexus will reload after this panel closes.`,\n\t\t\t\t\ttone: \"success\",\n\t\t\t\t};\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tif (action.id === \"add-repoprompt\") {\n\t\t\tawait this.runBusy(async () => {\n\t\t\t\tconst result = await this.callbacks.addRepoPrompt();\n\t\t\t\tthis.callbacks.markSetupCompleted();\n\t\t\t\tthis.notice = {\n\t\t\t\t\ttext: `Added ${result.serverName} to ${result.path}. Nexus will reload after this panel closes.`,\n\t\t\t\t\ttone: \"success\",\n\t\t\t\t};\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tif (action.id === \"add-known-server\" && action.preset) {\n\t\t\tconst preset = action.preset;\n\t\t\tawait this.runBusy(async () => {\n\t\t\t\tconst result = await this.callbacks.addKnownServer(preset);\n\t\t\t\tthis.callbacks.markSetupCompleted();\n\t\t\t\tthis.notice = {\n\t\t\t\t\ttext: `Added ${result.serverName} to ${result.path}. Nexus will reload after this panel closes.`,\n\t\t\t\t\ttone: \"success\",\n\t\t\t\t};\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tif (action.id === \"close\") {\n\t\t\tthis.cleanup();\n\t\t\tthis.done();\n\t\t\treturn;\n\t\t}\n\n\t\tthis.notice = {\n\t\t\ttext: \"Review the details below. Press Enter on an action with a side effect to apply it.\",\n\t\t\ttone: \"muted\",\n\t\t};\n\t\tthis.tui.requestRender();\n\t}\n\n\tprivate async applySelectedImports(): Promise<void> {\n\t\tconst selected = this.discovery.imports\n\t\t\t.filter((entry) => this.selectedImports.has(entry.kind))\n\t\t\t.map((entry) => entry.kind);\n\t\tif (selected.length === 0) {\n\t\t\tthis.notice = { text: \"Select at least one compatibility import first.\", tone: \"warning\" };\n\t\t\tthis.tui.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\tawait this.runBusy(async () => {\n\t\t\tconst result = await this.callbacks.adoptImports(selected);\n\t\t\tthis.callbacks.markSetupCompleted();\n\t\t\tthis.notice =\n\t\t\t\tresult.added.length > 0\n\t\t\t\t\t? {\n\t\t\t\t\t\t\ttext: `Added ${result.added.join(\", \")} to ${result.path}. Nexus will reload after this panel closes.`,\n\t\t\t\t\t\t\ttone: \"success\",\n\t\t\t\t\t\t}\n\t\t\t\t\t: { text: `No changes needed in ${result.path}.`, tone: \"muted\" };\n\t\t\tthis.screen = this.discovery.hasAnyConfig ? \"setup\" : \"empty\";\n\t\t\tthis.actionCursor = 0;\n\t\t});\n\t}\n\n\tprivate async runBusy(fn: () => Promise<void>): Promise<void> {\n\t\tthis.busy = true;\n\t\tthis.notice = { text: \"Working...\", tone: \"muted\" };\n\t\tthis.tui.requestRender();\n\t\ttry {\n\t\t\tawait fn();\n\t\t} catch (error) {\n\t\t\tthis.notice = {\n\t\t\t\ttext: error instanceof Error ? error.message : String(error),\n\t\t\t\ttone: \"warning\",\n\t\t\t};\n\t\t} finally {\n\t\t\tthis.busy = false;\n\t\t\tthis.tui.requestRender();\n\t\t}\n\t}\n\n\trender(width: number): string[] {\n\t\tconst panelW = Math.max(MIN_PANEL_WIDTH, width);\n\t\tconst innerW = panelW - 2;\n\t\tconst contentW = this.contentWidth(innerW);\n\t\tconst lines: string[] = [];\n\t\tconst border = fg(this.t.border, \"─\".repeat(innerW));\n\t\tlines.push(`┌${border}┐`);\n\t\tlines.push(this.padLine(fg(this.t.title, \"MCP setup\"), innerW));\n\t\tfor (const line of wrapText(this.discoverySummaryLine(), contentW)) {\n\t\t\tlines.push(this.padLine(line, innerW));\n\t\t}\n\t\tfor (const line of wrapText(this.secondarySummaryLine(), contentW)) {\n\t\t\tlines.push(this.padLine(fg(this.t.muted, line), innerW));\n\t\t}\n\t\tlines.push(this.padLine(\"\", innerW));\n\n\t\tif (this.notice) {\n\t\t\tconst tone =\n\t\t\t\tthis.notice.tone === \"success\"\n\t\t\t\t\t? this.t.success\n\t\t\t\t\t: this.notice.tone === \"warning\"\n\t\t\t\t\t\t? this.t.warning\n\t\t\t\t\t\t: this.t.hint;\n\t\t\tfor (const line of wrapText(this.notice.text, contentW)) {\n\t\t\t\tlines.push(this.padLine(fg(tone, line), innerW));\n\t\t\t}\n\t\t\tlines.push(this.padLine(\"\", innerW));\n\t\t}\n\n\t\tlines.push(`├${border}┤`);\n\n\t\tif (this.screen === \"imports\") {\n\t\t\tlines.push(...this.renderImports(innerW));\n\t\t} else if (this.screen === \"paths\") {\n\t\t\tlines.push(...this.renderPaths(innerW));\n\t\t} else {\n\t\t\tlines.push(...this.renderActions(innerW));\n\t\t}\n\n\t\tlines.push(`└${border}┘`);\n\t\treturn lines;\n\t}\n\n\tprivate renderActions(innerW: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tconst actions = this.getActions();\n\t\tconst compact = innerW < COMPACT_WIDTH;\n\t\tconst { start, end } = compact ? this.visibleActionRange(actions.length) : { start: 0, end: actions.length };\n\n\t\tif (start > 0) {\n\t\t\tlines.push(this.padLine(fg(this.t.muted, `… ${start} more above`), innerW));\n\t\t}\n\t\tfor (let index = start; index < end; index++) {\n\t\t\tconst action = actions[index];\n\t\t\tif (!action) continue;\n\t\t\tif (action.id === \"add-known-server\" && (index === start || actions[index - 1]?.id !== \"add-known-server\")) {\n\t\t\t\tlines.push(this.padLine(fg(this.t.title, \"Add a known server\"), innerW));\n\t\t\t}\n\t\t\tconst selected = index === this.actionCursor;\n\t\t\tconst cursor = selected ? fg(this.t.selected, \"›\") : \" \";\n\t\t\tlines.push(this.padLine(`${cursor} ${truncateToWidth(action.label, this.contentWidth(innerW) - 2)}`, innerW));\n\t\t}\n\t\tif (end < actions.length) {\n\t\t\tlines.push(this.padLine(fg(this.t.muted, `… ${actions.length - end} more below`), innerW));\n\t\t}\n\t\tlines.push(this.padLine(\"\", innerW));\n\n\t\tconst preview = this.getActionPreview(this.getSelectedAction(), this.previewWidth(innerW));\n\t\tfor (const line of preview) {\n\t\t\tlines.push(this.padLine(line, innerW));\n\t\t}\n\t\tlines.push(this.padLine(\"\", innerW));\n\t\tconst hint = compact ? \"Enter select · Esc back\" : \"Enter selects, Esc goes back, Ctrl+C closes.\";\n\t\tlines.push(this.padLine(fg(this.t.muted, hint), innerW));\n\t\treturn lines;\n\t}\n\n\tprivate renderImports(innerW: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tlines.push(this.padLine(\"Select compatibility imports. Space toggles, Enter saves, Esc goes back.\", innerW));\n\t\tlines.push(this.padLine(\"\", innerW));\n\t\tfor (let index = 0; index < this.discovery.imports.length; index++) {\n\t\t\tconst entry = this.discovery.imports[index];\n\t\t\tif (!entry) continue;\n\t\t\tconst selected = this.selectedImports.has(entry.kind) ? \"[x]\" : \"[ ]\";\n\t\t\tconst cursor = index === this.importCursor ? fg(this.t.selected, \"›\") : \" \";\n\t\t\tlines.push(this.padLine(`${cursor} ${selected} ${entry.kind} ${entry.path}`, innerW));\n\t\t}\n\t\tlines.push(this.padLine(\"\", innerW));\n\t\tconst selected = this.discovery.imports\n\t\t\t.filter((entry) => this.selectedImports.has(entry.kind))\n\t\t\t.map((entry) => entry.kind);\n\t\tconst preview = this.callbacks.previewImports(selected);\n\t\tfor (const line of this.formatWritePreview(\n\t\t\t\"Compatibility import write preview\",\n\t\t\tpreview,\n\t\t\t[],\n\t\t\tthis.previewWidth(innerW),\n\t\t)) {\n\t\t\tlines.push(this.padLine(line, innerW));\n\t\t}\n\t\treturn lines;\n\t}\n\n\tprivate renderPaths(innerW: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tlines.push(this.padLine(\"Select a detected config path to open. Enter opens it, Esc goes back.\", innerW));\n\t\tlines.push(this.padLine(\"\", innerW));\n\t\tconst paths = this.getDetectedPaths();\n\t\tfor (let index = 0; index < paths.length; index++) {\n\t\t\tconst cursor = index === this.pathCursor ? fg(this.t.selected, \"›\") : \" \";\n\t\t\tconst path = paths[index];\n\t\t\tif (path !== undefined) lines.push(this.padLine(`${cursor} ${path}`, innerW));\n\t\t}\n\t\treturn lines;\n\t}\n\n\tprivate discoverySummaryLine(): string {\n\t\tif (!this.discovery.hasAnyConfig) {\n\t\t\treturn fg(\n\t\t\t\tthis.t.warning,\n\t\t\t\tthis.options.onboardingState.setupCompleted\n\t\t\t\t\t? \"No MCP servers are active right now.\"\n\t\t\t\t\t: \"No MCP config is active yet.\",\n\t\t\t);\n\t\t}\n\n\t\tif (\n\t\t\tthis.discovery.totalServerCount === 0 &&\n\t\t\t(this.discovery.imports.length > 0 || !!this.discovery.repoPrompt.executablePath)\n\t\t) {\n\t\t\treturn fg(this.t.warning, \"Nexus found MCP-related setup options, but none are active in Nexus yet.\");\n\t\t}\n\n\t\tconst configuredSources = this.discovery.sources.filter((source) => source.serverCount > 0).length;\n\t\treturn fg(\n\t\t\tthis.t.hint,\n\t\t\t`Detected ${this.discovery.totalServerCount} configured servers across ${configuredSources} Nexus source${configuredSources === 1 ? \"\" : \"s\"}.`,\n\t\t);\n\t}\n\n\tprivate secondarySummaryLine(): string {\n\t\tconst hostNote =\n\t\t\tthis.discovery.hostConfigs.length > 0\n\t\t\t\t? ` Host discovery is ${this.discovery.hostConfigDiscovery}; ${this.discovery.hostConfigs.length} host source${this.discovery.hostConfigs.length === 1 ? \"\" : \"s\"} detected.`\n\t\t\t\t: \"\";\n\t\tconst conflictNote =\n\t\t\tthis.discovery.conflicts.length > 0\n\t\t\t\t? ` ${this.discovery.conflicts.length} same-name conflict${this.discovery.conflicts.length === 1 ? \"\" : \"s\"} reported.`\n\t\t\t\t: \"\";\n\t\tif (!this.discovery.hasAnyConfig) {\n\t\t\treturn `Create .nexus/mcp.json, adopt host imports, or quick-add RepoPrompt from this screen.${hostNote}${conflictNote}`;\n\t\t}\n\t\tif (this.discovery.totalServerCount === 0 && this.discovery.imports.length > 0) {\n\t\t\treturn `Detected ${this.discovery.imports.length} compatibility import source${this.discovery.imports.length === 1 ? \"\" : \"s\"}. Adopt them into Nexus or inspect the underlying files.${hostNote}${conflictNote}`;\n\t\t}\n\t\treturn `Nexus MCP files contain server definitions, compatibility imports, and MCP-specific overrides.${hostNote}${conflictNote}`;\n\t}\n\n\tprivate visibleActionRange(total: number): { start: number; end: number } {\n\t\tif (total <= COMPACT_ACTION_ROWS) return { start: 0, end: total };\n\t\tconst half = Math.floor(COMPACT_ACTION_ROWS / 2);\n\t\tconst start = Math.min(Math.max(0, this.actionCursor - half), Math.max(0, total - COMPACT_ACTION_ROWS));\n\t\treturn { start, end: Math.min(total, start + COMPACT_ACTION_ROWS) };\n\t}\n\n\tprivate contentWidth(innerW: number): number {\n\t\treturn Math.max(8, innerW - 4);\n\t}\n\n\tprivate previewWidth(innerW: number): number {\n\t\treturn Math.max(12, Math.min(DESKTOP_PREVIEW_WIDTH, this.contentWidth(innerW)));\n\t}\n\n\tprivate getActionPreview(action?: Action, previewW = DESKTOP_PREVIEW_WIDTH): string[] {\n\t\tswitch (action?.id) {\n\t\t\tcase \"run-setup\":\n\t\t\t\treturn this.formatPreview(\n\t\t\t\t\t[\n\t\t\t\t\t\t\"Run setup to adopt host-specific imports, inspect detected paths, and scaffold a minimal `.nexus/mcp.json` if needed.\",\n\t\t\t\t\t],\n\t\t\t\t\tpreviewW,\n\t\t\t\t);\n\t\t\tcase \"adopt-imports\":\n\t\t\t\treturn this.formatWritePreview(\n\t\t\t\t\t\"Compatibility import write preview\",\n\t\t\t\t\tthis.callbacks.previewImports(\n\t\t\t\t\t\tthis.discovery.imports\n\t\t\t\t\t\t\t.filter((entry) => this.selectedImports.has(entry.kind))\n\t\t\t\t\t\t\t.map((entry) => entry.kind),\n\t\t\t\t\t),\n\t\t\t\t\t[\n\t\t\t\t\t\t`Detected imports: ${this.discovery.imports.map((entry) => `${entry.kind} (${entry.serverCount} servers)`).join(\", \")}`,\n\t\t\t\t\t\t\"Selected imports are written to the Nexus global MCP configuration shown above.\",\n\t\t\t\t\t],\n\t\t\t\t\tpreviewW,\n\t\t\t\t);\n\t\t\tcase \"view-example\":\n\t\t\t\treturn this.formatPreview(\n\t\t\t\t\t[\n\t\t\t\t\t\t\"Example Nexus `.nexus/mcp.json`:\",\n\t\t\t\t\t\t\"{\",\n\t\t\t\t\t\t' \"mcpServers\": {',\n\t\t\t\t\t\t' \"chrome-devtools\": {',\n\t\t\t\t\t\t' \"command\": \"npx\",',\n\t\t\t\t\t\t' \"args\": [\"-y\", \"chrome-devtools-mcp@1.6.0\"]',\n\t\t\t\t\t\t\" }\",\n\t\t\t\t\t\t\" }\",\n\t\t\t\t\t\t\"}\",\n\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\"Use Scaffold project `.nexus/mcp.json` when you want a safe empty shell instead of a live example server.\",\n\t\t\t\t\t],\n\t\t\t\t\tpreviewW,\n\t\t\t\t);\n\t\t\tcase \"show-precedence\":\n\t\t\t\treturn this.formatPreview(\n\t\t\t\t\t[\n\t\t\t\t\t\t\"Read order (later entries win):\",\n\t\t\t\t\t\t\"0. detected host configs (opt-in lowest-precedence fallback)\",\n\t\t\t\t\t\t\"1. ~/.config/nexus/mcp.json\",\n\t\t\t\t\t\t`2. ${getConfigDirName()}/mcp.json`,\n\t\t\t\t\t\t`Host discovery: ${this.discovery.hostConfigDiscovery}. Conflicts reported: ${this.discovery.conflicts.length}.`,\n\t\t\t\t\t\t...this.discovery.conflicts\n\t\t\t\t\t\t\t.slice(0, 8)\n\t\t\t\t\t\t\t.map(\n\t\t\t\t\t\t\t\t(conflict) =>\n\t\t\t\t\t\t\t\t\t`${conflict.serverName}: ${conflict.sources.map((source) => source.path).join(\" -> \")} (winner: ${conflict.winner.path})`,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\"Nexus writes global MCP state to ~/.config/nexus/mcp.json and project state to .nexus/mcp.json.\",\n\t\t\t\t\t],\n\t\t\t\t\tpreviewW,\n\t\t\t\t);\n\t\t\tcase \"open-paths\":\n\t\t\t\treturn this.formatPreview(\n\t\t\t\t\tthis.getDetectedPaths().length > 0\n\t\t\t\t\t\t? [\"Detected paths:\", ...this.getDetectedPaths()]\n\t\t\t\t\t\t: [\"No config paths were detected.\"],\n\t\t\t\t\tpreviewW,\n\t\t\t\t);\n\t\t\tcase \"add-repoprompt\": {\n\t\t\t\tconst repoPrompt = this.discovery.repoPrompt;\n\t\t\t\tconst preview = this.callbacks.previewRepoPrompt();\n\t\t\t\tif (!preview) {\n\t\t\t\t\treturn this.formatPreview([\"RepoPrompt is not available to add from this setup screen.\"], previewW);\n\t\t\t\t}\n\t\t\t\treturn this.formatWritePreview(\n\t\t\t\t\t\"RepoPrompt write preview\",\n\t\t\t\t\tpreview,\n\t\t\t\t\t[\n\t\t\t\t\t\t`Executable: ${repoPrompt.executablePath ?? \"not found\"}`,\n\t\t\t\t\t\t`Target: ${repoPrompt.targetPath ?? \"n/a\"}`,\n\t\t\t\t\t\t`Server name: ${repoPrompt.serverName ?? \"repoprompt\"}`,\n\t\t\t\t\t],\n\t\t\t\t\tpreviewW,\n\t\t\t\t);\n\t\t\t}\n\t\t\tcase \"add-known-server\": {\n\t\t\t\tconst preset = action.preset;\n\t\t\t\tif (!preset) return this.formatPreview([\"Known server preset is unavailable.\"], previewW);\n\t\t\t\treturn this.formatWritePreview(\n\t\t\t\t\t`${preset.name} write preview`,\n\t\t\t\t\tthis.callbacks.previewKnownServer(preset),\n\t\t\t\t\t[preset.summary],\n\t\t\t\t\tpreviewW,\n\t\t\t\t);\n\t\t\t}\n\t\t\tcase \"scaffold-project\":\n\t\t\t\treturn this.formatWritePreview(\n\t\t\t\t\t\"Starter project `.nexus/mcp.json` write preview\",\n\t\t\t\t\tthis.callbacks.previewStarterProject(),\n\t\t\t\t\t[\n\t\t\t\t\t\t\"This writes a minimal `.nexus/mcp.json` in the current project using the Nexus MCP layout.\",\n\t\t\t\t\t\t\"It intentionally avoids adding a fake placeholder server that would fail on first reload.\",\n\t\t\t\t\t],\n\t\t\t\t\tpreviewW,\n\t\t\t\t);\n\t\t\tdefault:\n\t\t\t\treturn this.formatPreview([\"Close the setup flow.\"], previewW);\n\t\t}\n\t}\n\n\tprivate formatPreview(lines: string[], width = DESKTOP_PREVIEW_WIDTH): string[] {\n\t\tconst preview: string[] = [];\n\t\tfor (const line of lines) {\n\t\t\tpreview.push(...wrapText(line, width));\n\t\t}\n\t\treturn preview;\n\t}\n\n\tprivate formatWritePreview(\n\t\ttitle: string,\n\t\tpreview: ConfigWritePreview,\n\t\tintro: string[] = [],\n\t\twidth = DESKTOP_PREVIEW_WIDTH,\n\t): string[] {\n\t\tconst lines: string[] = [];\n\t\tfor (const line of intro) {\n\t\t\tlines.push(...wrapText(line, width));\n\t\t}\n\t\tif (intro.length > 0) lines.push(\"\");\n\t\tlines.push(...wrapText(`${title}: ${preview.path}`, width));\n\t\tlines.push(\n\t\t\t...wrapText(\n\t\t\t\tpreview.existed\n\t\t\t\t\t? \"Existing file detected. Showing exact before/after diff.\"\n\t\t\t\t\t: \"New file will be created. Showing exact content diff.\",\n\t\t\t\twidth,\n\t\t\t),\n\t\t);\n\t\tlines.push(\"\");\n\t\tconst diffLines = preview.diffText.split(\"\\n\");\n\t\tconst maxLines = 18;\n\t\tconst shown = diffLines.slice(0, maxLines);\n\t\tfor (const line of shown) {\n\t\t\tlines.push(...wrapText(line, width));\n\t\t}\n\t\tif (diffLines.length > maxLines) {\n\t\t\tlines.push(\n\t\t\t\t...wrapText(\n\t\t\t\t\t`… ${diffLines.length - maxLines} more diff line${diffLines.length - maxLines === 1 ? \"\" : \"s\"}`,\n\t\t\t\t\twidth,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t\treturn lines;\n\t}\n\n\tprivate padLine(text: string, innerW: number): string {\n\t\tconst inset = 2;\n\t\tconst contentW = Math.max(0, innerW - inset * 2);\n\t\tconst fitted = truncateToWidth(text, contentW, \"…\", true);\n\t\tconst plainWidth = visibleWidth(fitted);\n\t\tconst padding = Math.max(0, contentW - plainWidth);\n\t\treturn `│${\" \".repeat(inset)}${fitted}${\" \".repeat(padding)}${\" \".repeat(inset)}│`;\n\t}\n\n\tinvalidate(): void {}\n\n\tdispose(): void {\n\t\tthis.cleanup();\n\t}\n}\n\nexport function createMcpSetupPanel(\n\tdiscovery: McpDiscoverySummary,\n\tcallbacks: SetupPanelCallbacks,\n\toptions: SetupPanelOptions,\n\ttui: { requestRender(): void },\n\tdone: () => void,\n): McpSetupPanel & { dispose(): void } {\n\treturn new McpSetupPanel(discovery, callbacks, options, tui, done);\n}\n"]}
1
+ {"version":3,"file":"mcp-setup-panel.d.ts","sourceRoot":"","sources":["../../../src/extensions/mcp/mcp-setup-panel.ts"],"names":[],"mappings":"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","sourcesContent":["import { matchesKey, truncateToWidth, visibleWidth } from \"@earendil-works/pi-tui\";\nimport { getConfigDirName } from \"./agent-dir.ts\";\nimport {\n\ttype ConfigWritePreview,\n\tKNOWN_SERVER_PRESETS,\n\ttype KnownServerPreset,\n\ttype McpDiscoverySummary,\n} from \"./config.ts\";\nimport type { McpOnboardingState } from \"./onboarding-state.ts\";\nimport { createPanelKeys, type PanelKeybindings, type PanelKeys } from \"./panel-keys.ts\";\nimport type { ImportKind, ServerEntry } from \"./types.ts\";\n\ninterface SetupTheme {\n\tborder: string;\n\ttitle: string;\n\tselected: string;\n\thint: string;\n\tsuccess: string;\n\twarning: string;\n\tmuted: string;\n}\n\nconst DEFAULT_THEME: SetupTheme = {\n\tborder: \"2\",\n\ttitle: \"36\",\n\tselected: \"32\",\n\thint: \"2\",\n\tsuccess: \"32\",\n\twarning: \"33\",\n\tmuted: \"2;3\",\n};\n\nconst MIN_PANEL_WIDTH = 24;\nconst COMPACT_WIDTH = 60;\nconst COMPACT_ACTION_ROWS = 7;\nconst DESKTOP_PREVIEW_WIDTH = 74;\n\nfunction fg(code: string, text: string): string {\n\treturn code ? `\\x1b[${code}m${text}\\x1b[0m` : text;\n}\n\nfunction wrapText(text: string, width: number): string[] {\n\tif (width <= 8) return [text];\n\tconst words = text.split(/\\s+/).filter(Boolean);\n\tconst lines: string[] = [];\n\tlet current = \"\";\n\tfor (const word of words) {\n\t\tconst candidate = current ? `${current} ${word}` : word;\n\t\tif (visibleWidth(candidate) <= width) {\n\t\t\tcurrent = candidate;\n\t\t\tcontinue;\n\t\t}\n\t\tif (current) lines.push(current);\n\t\tcurrent = word;\n\t}\n\tif (current) lines.push(current);\n\treturn lines.length > 0 ? lines : [\"\"];\n}\n\nexport interface SetupPanelCallbacks {\n\tpreviewImports: (imports: ImportKind[]) => ConfigWritePreview;\n\tpreviewStarterProject: () => ConfigWritePreview;\n\tpreviewRepoPrompt: () => ConfigWritePreview | null;\n\tpreviewKnownServer: (preset: KnownServerPreset) => ConfigWritePreview;\n\tadoptImports: (imports: ImportKind[]) => Promise<{ added: ImportKind[]; path: string }>;\n\tscaffoldProjectConfig: () => Promise<{ path: string }>;\n\taddRepoPrompt: () => Promise<{ path: string; serverName: string }>;\n\taddKnownServer: (preset: KnownServerPreset) => Promise<{ path: string; serverName: string }>;\n\topenPath: (path: string) => Promise<void>;\n\tmarkSetupCompleted: () => void;\n\t/** Replace the main chat input with `text`, so the user lands on a ready-to-edit command after this panel closes. */\n\tseedEditor: (text: string) => void;\n\t/** Flip a configured server's disabled flag in the project config. */\n\ttoggleServerDisabled: (serverName: string, disabled: boolean) => Promise<{ path: string; changed: boolean }>;\n\t/** Run the actual OAuth sign-in flow (opens a browser) for an already-loaded, OAuth-capable server. */\n\t/** `fallbackEntry` lets a server just written this session (not yet in the live config) authenticate immediately, before /reload. */\n\tauthenticateServer: (serverName: string, fallbackEntry?: ServerEntry) => Promise<{ ok: boolean; message: string }>;\n}\n\nexport interface InstalledServerEntry {\n\tname: string;\n\tdisabled: boolean;\n\tkind: \"stdio\" | \"http\";\n\t/** Whether this server is configured for OAuth, so the Installed tab can offer a sign-in action. */\n\tauthCapable: boolean;\n\t/** Whether OAuth tokens actually exist for this server right now. Meaningless (false) when authCapable is false. */\n\tsignedIn: boolean;\n}\n\nexport interface SetupPanelOptions {\n\tmode: \"empty\" | \"setup\";\n\tonboardingState: McpOnboardingState;\n\tkeybindings?: PanelKeybindings;\n\t/** Effective, already-configured servers (from every source), for the Installed tab and the Presets \"active\" marker. */\n\tinstalledServers: InstalledServerEntry[];\n}\n\ntype Screen = \"empty\" | \"setup\" | \"imports\" | \"paths\" | \"confirm-preset\" | \"advanced\";\n\ntype SetupTab = \"presets\" | \"custom\" | \"installed\";\n\nconst SETUP_TABS: ReadonlyArray<{ id: SetupTab; label: string }> = [\n\t{ id: \"presets\", label: \"Presets\" },\n\t{ id: \"custom\", label: \"Custom\" },\n\t{ id: \"installed\", label: \"Installed\" },\n];\n\ntype ActionId =\n\t| \"adopt-imports\"\n\t| \"view-example\"\n\t| \"show-precedence\"\n\t| \"open-paths\"\n\t| \"add-repoprompt\"\n\t| \"add-known-server\"\n\t| \"add-custom-stdio\"\n\t| \"add-custom-url\"\n\t| \"open-advanced\"\n\t| \"scaffold-project\"\n\t| \"view-installed-server\";\n\ninterface Action {\n\tid: ActionId;\n\tlabel: string;\n\tdescription: string;\n\tpreset?: KnownServerPreset;\n\tinstalled?: InstalledServerEntry;\n}\n\n/** Server-type tag shown next to a preset row; color must survive any terminal, so it's always plain colored text. */\n/** Color a JSON-ish line: keys cyan, string values green, braces/brackets dim. Safe only for short, unwrapped lines. */\nfunction colorizeJsonLine(line: string): string {\n\tlet result = \"\";\n\tlet i = 0;\n\twhile (i < line.length) {\n\t\tconst ch = line[i]!;\n\t\tif (ch === '\"') {\n\t\t\tlet end = i + 1;\n\t\t\twhile (end < line.length && line[end] !== '\"') end++;\n\t\t\tconst str = line.slice(i, Math.min(end + 1, line.length));\n\t\t\tconst isKey = line\n\t\t\t\t.slice(i + str.length)\n\t\t\t\t.trimStart()\n\t\t\t\t.startsWith(\":\");\n\t\t\tresult += fg(isKey ? \"36\" : \"32\", str);\n\t\t\ti += str.length;\n\t\t\tcontinue;\n\t\t}\n\t\tif (ch === \"{\" || ch === \"}\" || ch === \"[\" || ch === \"]\") {\n\t\t\tresult += fg(\"2\", ch);\n\t\t\ti++;\n\t\t\tcontinue;\n\t\t}\n\t\tresult += ch;\n\t\ti++;\n\t}\n\treturn result;\n}\n\n/** Diff-line color by prefix, or undefined for a context line that needs no color. */\nfunction diffLineColor(line: string): string | undefined {\n\tif (line.startsWith(\"+++\") || line.startsWith(\"---\")) return \"2\";\n\tif (line.startsWith(\"+\")) return \"32\";\n\tif (line.startsWith(\"-\")) return \"31\";\n\treturn undefined;\n}\n\n/** Color `/mcp add` command syntax: the command itself cyan, --flags yellow, <placeholders> dim italic. Leaves spacing untouched. */\nfunction colorizeCommandLine(line: string): string {\n\treturn line.replace(/(\\/mcp add\\b|--[a-zA-Z-]+|<[^>]+>|\\[[^\\]]+\\])/g, (match) => {\n\t\tif (match.startsWith(\"<\") || match.startsWith(\"[\")) return fg(\"2;3\", match);\n\t\tif (match.startsWith(\"--\")) return fg(\"33\", match);\n\t\treturn fg(\"36\", match);\n\t});\n}\n\n/** Per-preset accent color for the name, so the list scans by identity, not just by type. 256-color codes, distinct from the type-tag palette above. */\nconst PRESET_ACCENT_COLORS: Record<string, string> = {\n\tdeepwiki: \"38;5;111\", // soft blue\n\tcontext7: \"38;5;80\", // teal\n\tnotion: \"38;5;141\", // purple\n\tgithub: \"38;5;215\", // orange\n\t\"chrome-devtools\": \"38;5;211\", // pink\n};\n\nfunction presetAccent(preset: KnownServerPreset): string {\n\treturn PRESET_ACCENT_COLORS[preset.id] ?? \"37\";\n}\n\nexport class McpSetupPanel {\n\tprivate screen: Screen;\n\tprivate activeTab: SetupTab;\n\tprivate actionCursor = 0;\n\tprivate importCursor = 0;\n\tprivate pathCursor = 0;\n\tprivate advancedCursor = 0;\n\tprivate selectedImports = new Set<ImportKind>();\n\tprivate busy = false;\n\tprivate notice: { text: string; tone: \"success\" | \"warning\" | \"muted\" } | null = null;\n\tprivate pendingAction: Action | undefined;\n\tprivate maxHeight: number | undefined;\n\tprivate tui: { requestRender(): void };\n\tprivate t = DEFAULT_THEME;\n\tprivate keys: PanelKeys;\n\tprivate inactivityTimeout: ReturnType<typeof setTimeout> | null = null;\n\tprivate discovery: McpDiscoverySummary;\n\tprivate callbacks: SetupPanelCallbacks;\n\tprivate options: SetupPanelOptions;\n\t/** Mutable copy of options.installedServers, updated in-place after a successful add so the Installed tab and Presets \"active\" tag reflect it immediately, without needing to close and reopen the panel. */\n\tprivate installedServers: InstalledServerEntry[];\n\tprivate done: () => void;\n\tprivate static readonly INACTIVITY_MS = 60_000;\n\n\tconstructor(\n\t\tdiscovery: McpDiscoverySummary,\n\t\tcallbacks: SetupPanelCallbacks,\n\t\toptions: SetupPanelOptions,\n\t\ttui: { requestRender(): void },\n\t\tdone: () => void,\n\t) {\n\t\tthis.discovery = discovery;\n\t\tthis.callbacks = callbacks;\n\t\tthis.options = options;\n\t\tthis.installedServers = [...options.installedServers];\n\t\tthis.done = done;\n\t\tthis.tui = tui;\n\t\tthis.keys = createPanelKeys(options.keybindings);\n\t\tthis.screen = options.mode;\n\t\tthis.activeTab = \"presets\";\n\t\tfor (const entry of discovery.imports) {\n\t\t\tthis.selectedImports.add(entry.kind);\n\t\t}\n\t\tthis.resetInactivityTimeout();\n\t}\n\n\tprivate resetInactivityTimeout(): void {\n\t\tif (this.inactivityTimeout) clearTimeout(this.inactivityTimeout);\n\t\tthis.inactivityTimeout = setTimeout(() => {\n\t\t\tthis.cleanup();\n\t\t\tthis.done();\n\t\t}, McpSetupPanel.INACTIVITY_MS);\n\t}\n\n\tprivate cleanup(): void {\n\t\tif (this.inactivityTimeout) {\n\t\t\tclearTimeout(this.inactivityTimeout);\n\t\t\tthis.inactivityTimeout = null;\n\t\t}\n\t}\n\n\t/** The old \"Setup\" tab's contents — now a sub-screen opened from Custom's \"Advanced options...\", not a permanent top-level tab. */\n\tprivate getAdvancedActions(): Action[] {\n\t\tconst actions: Action[] = [];\n\t\tif (this.discovery.imports.length > 0) {\n\t\t\tactions.push({\n\t\t\t\tid: \"adopt-imports\",\n\t\t\t\tlabel: \"Adopt detected compatibility imports\",\n\t\t\t\tdescription: `Choose which host-specific MCP configs Nexus should import into its own override file. ${this.discovery.imports.length} source${this.discovery.imports.length === 1 ? \"\" : \"s\"} found.`,\n\t\t\t});\n\t\t}\n\t\tactions.push({\n\t\t\tid: \"view-example\",\n\t\t\tlabel: \"View example `.nexus/mcp.json`\",\n\t\t\tdescription: \"Preview a working Nexus MCP config you can paste or adapt.\",\n\t\t});\n\t\tif (!this.discovery.sources.some((source) => source.id === \"nexus-project\" && source.exists)) {\n\t\t\tactions.push({\n\t\t\t\tid: \"scaffold-project\",\n\t\t\t\tlabel: \"Scaffold project `.nexus/mcp.json`\",\n\t\t\t\tdescription: \"Write a minimal Nexus project MCP config, then reload Nexus.\",\n\t\t\t});\n\t\t}\n\t\tactions.push({\n\t\t\tid: \"show-precedence\",\n\t\t\tlabel: \"Explain config precedence\",\n\t\t\tdescription: \"Show the read order and where Nexus writes compatibility settings.\",\n\t\t});\n\t\tif (this.getDetectedPaths().length > 0) {\n\t\t\tactions.push({\n\t\t\t\tid: \"open-paths\",\n\t\t\t\tlabel: \"Open detected config paths\",\n\t\t\t\tdescription: \"Browse the actual config files that Nexus discovered on this machine.\",\n\t\t\t});\n\t\t}\n\t\treturn actions;\n\t}\n\n\tprivate getPresetsTabActions(): Action[] {\n\t\tconst actions: Action[] = [];\n\t\tfor (const preset of KNOWN_SERVER_PRESETS) {\n\t\t\tactions.push({ id: \"add-known-server\", label: preset.name, description: preset.summary, preset });\n\t\t}\n\t\tif (\n\t\t\t!this.discovery.repoPrompt.configured &&\n\t\t\tthis.discovery.repoPrompt.executablePath &&\n\t\t\tthis.discovery.repoPrompt.targetPath &&\n\t\t\tthis.discovery.repoPrompt.entry &&\n\t\t\tthis.discovery.repoPrompt.serverName\n\t\t) {\n\t\t\tactions.push({\n\t\t\t\tid: \"add-repoprompt\",\n\t\t\t\tlabel: \"Add RepoPrompt to Nexus MCP config\",\n\t\t\t\tdescription:\n\t\t\t\t\t\"Write an MCP entry for RepoPrompt to the recommended Nexus target, then reload MCP in-session.\",\n\t\t\t});\n\t\t}\n\t\treturn actions;\n\t}\n\n\tprivate getCustomTabActions(): Action[] {\n\t\treturn [\n\t\t\t{\n\t\t\t\tid: \"add-custom-stdio\",\n\t\t\t\tlabel: \"Add a local server (stdio)...\",\n\t\t\t\tdescription: \"Runs a command on your machine, e.g. an npx-installed MCP server.\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"add-custom-url\",\n\t\t\t\tlabel: \"Add a remote server (URL)...\",\n\t\t\t\tdescription: \"Connects to an MCP server over HTTP.\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"open-advanced\",\n\t\t\t\tlabel: \"Advanced options...\",\n\t\t\t\tdescription: \"Import from other tools, scaffold an empty config, or check how config precedence works.\",\n\t\t\t},\n\t\t];\n\t}\n\n\tprivate getInstalledTabActions(): Action[] {\n\t\tconst servers = this.installedServers;\n\t\tif (servers.length === 0) {\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\tid: \"view-installed-server\",\n\t\t\t\t\tlabel: \"No servers configured yet.\",\n\t\t\t\t\tdescription: \"Add one from the Presets or Custom tab.\",\n\t\t\t\t},\n\t\t\t];\n\t\t}\n\t\treturn servers.map((server) => ({\n\t\t\tid: \"view-installed-server\" as const,\n\t\t\tlabel: server.name,\n\t\t\tdescription: server.disabled\n\t\t\t\t? \"Disabled in the effective config.\"\n\t\t\t\t: server.authCapable && !server.signedIn\n\t\t\t\t\t? \"Active, but not signed in yet.\"\n\t\t\t\t\t: \"Active in the effective config.\",\n\t\t\tinstalled: server,\n\t\t}));\n\t}\n\n\t/**\n\t * Single-glyph prefix instead of a tag chip: ✓ for a preset you already have\n\t * (dim if you've disabled it), ✓/⊘ for an Installed-tab row's active state.\n\t * No mark at all for a preset you don't have yet, to keep the list scannable.\n\t */\n\tprivate getRowMark(action: Action): string {\n\t\tif (action.preset) {\n\t\t\tconst installed = this.installedServers.find((server) => server.name === action.preset!.id);\n\t\t\treturn installed ? fg(installed.disabled ? this.t.muted : this.t.success, \"✓ \") : \" \";\n\t\t}\n\t\tif (action.installed) {\n\t\t\tif (action.installed.disabled) return fg(this.t.muted, \"⊘ \");\n\t\t\tif (action.installed.authCapable && !action.installed.signedIn) return fg(this.t.warning, \"! \");\n\t\t\treturn fg(this.t.success, \"✓ \");\n\t\t}\n\t\treturn \"\";\n\t}\n\n\tprivate getActions(): Action[] {\n\t\tswitch (this.activeTab) {\n\t\t\tcase \"presets\":\n\t\t\t\treturn this.getPresetsTabActions();\n\t\t\tcase \"custom\":\n\t\t\t\treturn this.getCustomTabActions();\n\t\t\tcase \"installed\":\n\t\t\t\treturn this.getInstalledTabActions();\n\t\t}\n\t}\n\n\tprivate cycleTab(direction: 1 | -1): void {\n\t\tconst order = SETUP_TABS.map((tab) => tab.id);\n\t\tconst index = order.indexOf(this.activeTab);\n\t\tthis.activeTab = order[(index + direction + order.length) % order.length] as SetupTab;\n\t\tthis.actionCursor = 0;\n\t\tthis.notice = null;\n\t\tthis.tui.requestRender();\n\t}\n\n\tprivate getDetectedPaths(): string[] {\n\t\tconst paths = [\n\t\t\t...this.discovery.sources.filter((source) => source.exists).map((source) => source.path),\n\t\t\t...this.discovery.imports.map((entry) => entry.path),\n\t\t];\n\t\treturn [...new Set(paths)];\n\t}\n\n\thandleInput(data: string): void {\n\t\tthis.resetInactivityTimeout();\n\t\tif (!this.busy) this.notice = null;\n\n\t\tif (matchesKey(data, \"ctrl+c\")) {\n\t\t\tthis.cleanup();\n\t\t\tthis.done();\n\t\t\treturn;\n\t\t}\n\n\t\tif (matchesKey(data, \"escape\")) {\n\t\t\tif (this.screen === \"imports\" || this.screen === \"paths\") {\n\t\t\t\t// Both are only reachable from the Advanced sub-screen now — back out one level, not all the way to the tabs.\n\t\t\t\tthis.screen = \"advanced\";\n\t\t\t\tthis.tui.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (this.screen === \"confirm-preset\" || this.screen === \"advanced\") {\n\t\t\t\tthis.pendingAction = undefined;\n\t\t\t\tthis.screen = this.discovery.hasAnyConfig ? \"setup\" : \"empty\";\n\t\t\t\tthis.tui.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.cleanup();\n\t\t\tthis.done();\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.busy) return;\n\n\t\tif (this.screen === \"imports\") {\n\t\t\tthis.handleImportsInput(data);\n\t\t\treturn;\n\t\t}\n\t\tif (this.screen === \"paths\") {\n\t\t\tthis.handlePathsInput(data);\n\t\t\treturn;\n\t\t}\n\t\tif (this.screen === \"confirm-preset\") {\n\t\t\tthis.handleConfirmPresetInput(data);\n\t\t\treturn;\n\t\t}\n\t\tif (this.screen === \"advanced\") {\n\t\t\tthis.handleAdvancedInput(data);\n\t\t\treturn;\n\t\t}\n\n\t\tif (matchesKey(data, \"left\")) {\n\t\t\tthis.cycleTab(-1);\n\t\t\treturn;\n\t\t}\n\t\tif (matchesKey(data, \"right\")) {\n\t\t\tthis.cycleTab(1);\n\t\t\treturn;\n\t\t}\n\n\t\tconst actions = this.getActions();\n\t\tif (this.keys.selectUp(data)) {\n\t\t\tthis.actionCursor = Math.max(0, this.actionCursor - 1);\n\t\t\tthis.tui.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (this.keys.selectDown(data)) {\n\t\t\tthis.actionCursor = Math.min(actions.length - 1, this.actionCursor + 1);\n\t\t\tthis.tui.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (data === \"o\" || data === \"O\") {\n\t\t\tthis.openSelectedDocs();\n\t\t\treturn;\n\t\t}\n\t\tif ((data === \"a\" || data === \"A\") && this.activeTab === \"installed\") {\n\t\t\tthis.authenticateSelected();\n\t\t\treturn;\n\t\t}\n\t\tif (this.keys.selectConfirm(data)) {\n\t\t\tconst selected = this.getSelectedAction();\n\t\t\tif (selected) void this.runAction(selected);\n\t\t}\n\t}\n\n\tprivate handleImportsInput(data: string): void {\n\t\tconst imports = this.discovery.imports;\n\t\tif (this.keys.selectUp(data)) {\n\t\t\tthis.importCursor = Math.max(0, this.importCursor - 1);\n\t\t\tthis.tui.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (this.keys.selectDown(data)) {\n\t\t\tthis.importCursor = Math.min(imports.length - 1, this.importCursor + 1);\n\t\t\tthis.tui.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (matchesKey(data, \"space\")) {\n\t\t\tconst current = imports[this.importCursor];\n\t\t\tif (!current) return;\n\t\t\tif (this.selectedImports.has(current.kind)) {\n\t\t\t\tthis.selectedImports.delete(current.kind);\n\t\t\t} else {\n\t\t\t\tthis.selectedImports.add(current.kind);\n\t\t\t}\n\t\t\tthis.tui.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (this.keys.selectConfirm(data)) {\n\t\t\tvoid this.applySelectedImports();\n\t\t}\n\t}\n\n\tprivate handlePathsInput(data: string): void {\n\t\tconst paths = this.getDetectedPaths();\n\t\tif (this.keys.selectUp(data)) {\n\t\t\tthis.pathCursor = Math.max(0, this.pathCursor - 1);\n\t\t\tthis.tui.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (this.keys.selectDown(data)) {\n\t\t\tthis.pathCursor = Math.min(paths.length - 1, this.pathCursor + 1);\n\t\t\tthis.tui.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (this.keys.selectConfirm(data)) {\n\t\t\tconst selected = paths[this.pathCursor];\n\t\t\tif (!selected) return;\n\t\t\tvoid this.runBusy(async () => {\n\t\t\t\tawait this.callbacks.openPath(selected);\n\t\t\t\tthis.notice = { text: `Opened ${selected}`, tone: \"success\" };\n\t\t\t});\n\t\t}\n\t}\n\n\tprivate handleAdvancedInput(data: string): void {\n\t\tconst actions = this.getAdvancedActions();\n\t\tif (this.keys.selectUp(data)) {\n\t\t\tthis.advancedCursor = Math.max(0, this.advancedCursor - 1);\n\t\t\tthis.tui.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (this.keys.selectDown(data)) {\n\t\t\tthis.advancedCursor = Math.min(actions.length - 1, this.advancedCursor + 1);\n\t\t\tthis.tui.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (this.keys.selectConfirm(data)) {\n\t\t\tconst selected = actions[this.advancedCursor];\n\t\t\tif (selected) void this.runAction(selected);\n\t\t}\n\t}\n\n\tprivate handleConfirmPresetInput(data: string): void {\n\t\tif (data === \"o\" || data === \"O\") {\n\t\t\tthis.openSelectedDocs();\n\t\t\treturn;\n\t\t}\n\t\tif (this.keys.selectConfirm(data)) {\n\t\t\tvoid this.confirmPendingAction();\n\t\t}\n\t}\n\n\t/** Getter used by openSelectedDocs while the confirm screen is up (no action-list cursor there). */\n\tprivate getSelectedAction(): Action | undefined {\n\t\tif (this.screen === \"confirm-preset\") return this.pendingAction;\n\t\tconst actions = this.getActions();\n\t\treturn actions[this.actionCursor];\n\t}\n\n\t/** Add or replace an entry so the Installed tab and Presets \"active\" tag update immediately, without closing the panel. */\n\tprivate upsertInstalledServer(entry: InstalledServerEntry): void {\n\t\tconst index = this.installedServers.findIndex((server) => server.name === entry.name);\n\t\tif (index === -1) this.installedServers.push(entry);\n\t\telse this.installedServers[index] = entry;\n\t}\n\n\tprivate async confirmPendingAction(): Promise<void> {\n\t\tconst action = this.pendingAction;\n\t\tif (!action) return;\n\t\tif (action.id === \"add-repoprompt\") {\n\t\t\tawait this.runBusy(async () => {\n\t\t\t\tconst result = await this.callbacks.addRepoPrompt();\n\t\t\t\tthis.callbacks.markSetupCompleted();\n\t\t\t\tthis.upsertInstalledServer({\n\t\t\t\t\tname: result.serverName,\n\t\t\t\t\tdisabled: false,\n\t\t\t\t\tkind: \"stdio\",\n\t\t\t\t\tauthCapable: false,\n\t\t\t\t\tsignedIn: false,\n\t\t\t\t});\n\t\t\t\tthis.notice = {\n\t\t\t\t\ttext: `Added ${result.serverName} to ${result.path}. Nexus will reload after this panel closes.`,\n\t\t\t\t\ttone: \"success\",\n\t\t\t\t};\n\t\t\t});\n\t\t} else if (action.id === \"add-known-server\" && action.preset) {\n\t\t\tconst preset = action.preset;\n\t\t\tconst authCapable = preset.entry.auth === \"oauth\";\n\t\t\tawait this.runBusy(async () => {\n\t\t\t\tconst result = await this.callbacks.addKnownServer(preset);\n\t\t\t\tthis.callbacks.markSetupCompleted();\n\t\t\t\tconst kind = preset.entry.command ? (\"stdio\" as const) : (\"http\" as const);\n\t\t\t\tthis.upsertInstalledServer({\n\t\t\t\t\tname: result.serverName,\n\t\t\t\t\tdisabled: false,\n\t\t\t\t\tkind,\n\t\t\t\t\tauthCapable,\n\t\t\t\t\tsignedIn: false,\n\t\t\t\t});\n\t\t\t\tif (!authCapable) {\n\t\t\t\t\tthis.notice = {\n\t\t\t\t\t\ttext: `Added ${result.serverName} to ${result.path}. Nexus will reload after this panel closes.`,\n\t\t\t\t\t\ttone: \"success\",\n\t\t\t\t\t};\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// Sign in right now, in the same flow — this is the whole point of a preset:\n\t\t\t\t// don't make the user hunt down a separate command after the fact.\n\t\t\t\tthis.notice = { text: `Added ${result.serverName}. Opening your browser to sign in…`, tone: \"muted\" };\n\t\t\t\tthis.tui.requestRender();\n\t\t\t\tconst authResult = await this.callbacks.authenticateServer(result.serverName, preset.entry);\n\t\t\t\tthis.upsertInstalledServer({\n\t\t\t\t\tname: result.serverName,\n\t\t\t\t\tdisabled: false,\n\t\t\t\t\tkind,\n\t\t\t\t\tauthCapable,\n\t\t\t\t\tsignedIn: authResult.ok,\n\t\t\t\t});\n\t\t\t\tthis.notice = authResult.ok\n\t\t\t\t\t? {\n\t\t\t\t\t\t\ttext: `Added and signed in to ${result.serverName} (${result.path}). Nexus will reload after this panel closes.`,\n\t\t\t\t\t\t\ttone: \"success\",\n\t\t\t\t\t\t}\n\t\t\t\t\t: {\n\t\t\t\t\t\t\ttext: `Added ${result.serverName} to ${result.path}, but sign-in didn't finish: ${authResult.message} Retry anytime from the Installed tab (press \"a\").`,\n\t\t\t\t\t\t\ttone: \"warning\",\n\t\t\t\t\t\t};\n\t\t\t});\n\t\t}\n\t\tthis.pendingAction = undefined;\n\t\tthis.screen = this.discovery.hasAnyConfig ? \"setup\" : \"empty\";\n\t\tthis.tui.requestRender();\n\t}\n\n\tprivate async runAction(action: Action): Promise<void> {\n\t\tif (action.id === \"adopt-imports\") {\n\t\t\tthis.screen = \"imports\";\n\t\t\tthis.importCursor = 0;\n\t\t\tthis.tui.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (action.id === \"open-paths\") {\n\t\t\tthis.screen = \"paths\";\n\t\t\tthis.pathCursor = 0;\n\t\t\tthis.tui.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (action.id === \"scaffold-project\") {\n\t\t\tawait this.runBusy(async () => {\n\t\t\t\tconst result = await this.callbacks.scaffoldProjectConfig();\n\t\t\t\tthis.callbacks.markSetupCompleted();\n\t\t\t\tthis.notice = {\n\t\t\t\t\ttext: `Wrote starter config to ${result.path}. Nexus will reload after this panel closes.`,\n\t\t\t\t\ttone: \"success\",\n\t\t\t\t};\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tif (action.id === \"add-repoprompt\" || (action.id === \"add-known-server\" && action.preset)) {\n\t\t\tthis.pendingAction = action;\n\t\t\tthis.screen = \"confirm-preset\";\n\t\t\tthis.tui.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (action.id === \"add-custom-stdio\" || action.id === \"add-custom-url\") {\n\t\t\tconst template = action.id === \"add-custom-stdio\" ? \"/mcp add my-server -- \" : \"/mcp add my-server --url \";\n\t\t\tthis.callbacks.seedEditor(template);\n\t\t\tthis.cleanup();\n\t\t\tthis.done();\n\t\t\treturn;\n\t\t}\n\t\tif (action.id === \"open-advanced\") {\n\t\t\tthis.screen = \"advanced\";\n\t\t\tthis.advancedCursor = 0;\n\t\t\tthis.tui.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (action.id === \"view-installed-server\" && action.installed) {\n\t\t\tconst server = action.installed;\n\t\t\tconst nextDisabled = !server.disabled;\n\t\t\tawait this.runBusy(async () => {\n\t\t\t\tconst result = await this.callbacks.toggleServerDisabled(server.name, nextDisabled);\n\t\t\t\tthis.upsertInstalledServer({ ...server, disabled: nextDisabled });\n\t\t\t\tthis.notice = {\n\t\t\t\t\ttext: `${nextDisabled ? \"Disabled\" : \"Enabled\"} \"${server.name}\" in ${result.path} — run /reload to apply.`,\n\t\t\t\t\ttone: \"success\",\n\t\t\t\t};\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tthis.notice = {\n\t\t\ttext: \"Review the details below. Press Enter on an action with a side effect to apply it.\",\n\t\t\ttone: \"muted\",\n\t\t};\n\t\tthis.tui.requestRender();\n\t}\n\n\tprivate async applySelectedImports(): Promise<void> {\n\t\tconst selected = this.discovery.imports\n\t\t\t.filter((entry) => this.selectedImports.has(entry.kind))\n\t\t\t.map((entry) => entry.kind);\n\t\tif (selected.length === 0) {\n\t\t\tthis.notice = { text: \"Select at least one compatibility import first.\", tone: \"warning\" };\n\t\t\tthis.tui.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\tawait this.runBusy(async () => {\n\t\t\tconst result = await this.callbacks.adoptImports(selected);\n\t\t\tthis.callbacks.markSetupCompleted();\n\t\t\tthis.notice =\n\t\t\t\tresult.added.length > 0\n\t\t\t\t\t? {\n\t\t\t\t\t\t\ttext: `Added ${result.added.join(\", \")} to ${result.path}. Nexus will reload after this panel closes.`,\n\t\t\t\t\t\t\ttone: \"success\",\n\t\t\t\t\t\t}\n\t\t\t\t\t: { text: `No changes needed in ${result.path}.`, tone: \"muted\" };\n\t\t\tthis.screen = \"advanced\";\n\t\t\tthis.advancedCursor = 0;\n\t\t});\n\t}\n\n\tprivate openSelectedDocs(): void {\n\t\tconst selected = this.getSelectedAction();\n\t\tconst docsUrl = selected?.preset?.docsUrl;\n\t\tif (!docsUrl) {\n\t\t\tthis.notice = { text: \"No docs link for this item.\", tone: \"muted\" };\n\t\t\tthis.tui.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tvoid this.runBusy(async () => {\n\t\t\tawait this.callbacks.openPath(docsUrl);\n\t\t\tthis.notice = { text: `Opened ${docsUrl}`, tone: \"success\" };\n\t\t});\n\t}\n\n\tprivate authenticateSelected(): void {\n\t\tconst selected = this.getSelectedAction();\n\t\tconst server = selected?.installed;\n\t\tif (!server) return;\n\t\tif (!server.authCapable) {\n\t\t\tthis.notice = { text: `\"${server.name}\" isn't configured for OAuth sign-in.`, tone: \"muted\" };\n\t\t\tthis.tui.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tvoid this.runBusy(async () => {\n\t\t\tthis.notice = { text: `Opening your browser to sign in to \"${server.name}\"…`, tone: \"muted\" };\n\t\t\tthis.tui.requestRender();\n\t\t\tconst result = await this.callbacks.authenticateServer(server.name);\n\t\t\tthis.upsertInstalledServer({ ...server, signedIn: result.ok });\n\t\t\tthis.notice = { text: result.message, tone: result.ok ? \"success\" : \"warning\" };\n\t\t});\n\t}\n\n\tprivate async runBusy(fn: () => Promise<void>): Promise<void> {\n\t\tthis.busy = true;\n\t\tthis.notice = { text: \"Working...\", tone: \"muted\" };\n\t\tthis.tui.requestRender();\n\t\ttry {\n\t\t\tawait fn();\n\t\t} catch (error) {\n\t\t\tthis.notice = {\n\t\t\t\ttext: error instanceof Error ? error.message : String(error),\n\t\t\t\ttone: \"warning\",\n\t\t\t};\n\t\t} finally {\n\t\t\tthis.busy = false;\n\t\t\tthis.tui.requestRender();\n\t\t}\n\t}\n\n\trender(width: number): string[] {\n\t\tconst panelW = Math.max(MIN_PANEL_WIDTH, width);\n\t\tconst innerW = panelW - 2;\n\t\tconst contentW = this.contentWidth(innerW);\n\t\tconst lines: string[] = [];\n\t\tconst border = fg(this.t.border, \"─\".repeat(innerW));\n\t\tlines.push(`┌${border}┐`);\n\t\tlines.push(this.padLine(fg(this.t.title, \"MCP setup\"), innerW));\n\t\tfor (const line of wrapText(this.discoverySummaryLine(), contentW)) {\n\t\t\tlines.push(this.padLine(line, innerW));\n\t\t}\n\t\tfor (const line of wrapText(this.secondarySummaryLine(), contentW)) {\n\t\t\tlines.push(this.padLine(fg(this.t.muted, line), innerW));\n\t\t}\n\t\tlines.push(this.padLine(\"\", innerW));\n\n\t\tif (this.notice) {\n\t\t\tconst tone =\n\t\t\t\tthis.notice.tone === \"success\"\n\t\t\t\t\t? this.t.success\n\t\t\t\t\t: this.notice.tone === \"warning\"\n\t\t\t\t\t\t? this.t.warning\n\t\t\t\t\t\t: this.t.hint;\n\t\t\tfor (const line of wrapText(this.notice.text, contentW)) {\n\t\t\t\tlines.push(this.padLine(fg(tone, line), innerW));\n\t\t\t}\n\t\t\tlines.push(this.padLine(\"\", innerW));\n\t\t}\n\n\t\tlines.push(`├${border}┤`);\n\n\t\tif (this.screen === \"imports\") {\n\t\t\tlines.push(...this.renderImports(innerW));\n\t\t} else if (this.screen === \"paths\") {\n\t\t\tlines.push(...this.renderPaths(innerW));\n\t\t} else if (this.screen === \"confirm-preset\") {\n\t\t\tlines.push(...this.renderConfirmPreset(innerW));\n\t\t} else if (this.screen === \"advanced\") {\n\t\t\tlines.push(...this.renderAdvanced(innerW));\n\t\t} else {\n\t\t\tlines.push(...this.renderActions(innerW));\n\t\t}\n\n\t\tlines.push(`└${border}┘`);\n\t\treturn this.clampHeight(lines, innerW, border);\n\t}\n\n\tsetMaxHeight(lines: number): void {\n\t\tthis.maxHeight = lines;\n\t}\n\n\t/**\n\t * The overlay host may cap total rows and, absent this hook, would slice\n\t * blindly from the bottom — cutting off the closing border and leaving the\n\t * box looking broken. Truncate the content instead, keeping the header and\n\t * action list (the interactive part) and dropping preview/diff lines first.\n\t */\n\tprivate clampHeight(lines: string[], innerW: number, _border: string): string[] {\n\t\tif (this.maxHeight === undefined || lines.length <= this.maxHeight) return lines;\n\t\tconst budget = Math.max(5, this.maxHeight);\n\t\t// The last line is the closing border, the one before it is the hint/controls\n\t\t// line — both must always stay visible, or the panel loses its own instructions.\n\t\tconst closing = lines[lines.length - 1]!;\n\t\tconst footer = lines[lines.length - 2]!;\n\t\tconst head = lines.slice(0, Math.max(1, budget - 3));\n\t\tconst hidden = lines.length - head.length - 2;\n\t\tif (hidden <= 0) return lines;\n\t\treturn [\n\t\t\t...head,\n\t\t\tthis.padLine(\n\t\t\t\tfg(this.t.muted, `… ${hidden} more line${hidden === 1 ? \"\" : \"s\"} hidden to fit the window`),\n\t\t\t\tinnerW,\n\t\t\t),\n\t\t\tfooter,\n\t\t\tclosing,\n\t\t];\n\t}\n\n\tprivate renderTabBar(): string {\n\t\treturn SETUP_TABS.map((tab) =>\n\t\t\ttab.id === this.activeTab ? fg(this.t.selected, `[ ${tab.label} ]`) : fg(this.t.muted, ` ${tab.label} `),\n\t\t).join(\" \");\n\t}\n\n\tprivate renderActions(innerW: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tconst actions = this.getActions();\n\t\tconst compact = innerW < COMPACT_WIDTH;\n\t\tconst { start, end } = compact ? this.visibleActionRange(actions.length) : { start: 0, end: actions.length };\n\n\t\tlines.push(this.padLine(this.renderTabBar(), innerW));\n\t\tlines.push(this.padLine(\"\", innerW));\n\n\t\tif (start > 0) {\n\t\t\tlines.push(this.padLine(fg(this.t.muted, `… ${start} more above`), innerW));\n\t\t}\n\t\tfor (let index = start; index < end; index++) {\n\t\t\tconst action = actions[index];\n\t\t\tif (!action) continue;\n\t\t\tconst selected = index === this.actionCursor;\n\t\t\tconst cursor = selected ? fg(this.t.selected, \"›\") : \" \";\n\t\t\tconst mark = this.getRowMark(action);\n\t\t\tconst label = truncateToWidth(action.label, Math.max(1, this.contentWidth(innerW) - 2 - visibleWidth(mark)));\n\t\t\tconst installedColor = action.installed\n\t\t\t\t? action.installed.disabled\n\t\t\t\t\t? this.t.muted\n\t\t\t\t\t: action.installed.authCapable && !action.installed.signedIn\n\t\t\t\t\t\t? this.t.warning\n\t\t\t\t\t\t: this.t.success\n\t\t\t\t: undefined;\n\t\t\tconst labelRendered = action.preset\n\t\t\t\t? fg(presetAccent(action.preset), label)\n\t\t\t\t: installedColor\n\t\t\t\t\t? fg(installedColor, label)\n\t\t\t\t\t: label;\n\t\t\tlines.push(this.padLine(`${cursor} ${mark}${labelRendered}`, innerW));\n\t\t}\n\t\tif (end < actions.length) {\n\t\t\tlines.push(this.padLine(fg(this.t.muted, `… ${actions.length - end} more below`), innerW));\n\t\t}\n\t\tlines.push(this.padLine(\"\", innerW));\n\n\t\t// Presets stay a clean, scannable list — no inline summary/diff while browsing.\n\t\t// The write preview only shows on the confirm screen, after pressing Enter.\n\t\tif (this.activeTab !== \"presets\") {\n\t\t\tconst preview = this.getActionPreview(this.getSelectedAction(), this.previewWidth(innerW));\n\t\t\tfor (const line of preview) {\n\t\t\t\tlines.push(this.padLine(line, innerW));\n\t\t\t}\n\t\t\tlines.push(this.padLine(\"\", innerW));\n\t\t}\n\t\tconst docsHint = this.activeTab === \"presets\" ? \" · o docs\" : \"\";\n\t\tconst confirmHint = this.activeTab === \"presets\" ? \" · enter to review & confirm\" : \"\";\n\t\tconst checkHint = this.activeTab === \"presets\" && this.installedServers.length > 0 ? \" · ✓ already added\" : \"\";\n\t\tconst authHint =\n\t\t\tthis.activeTab === \"installed\" && this.installedServers.some((server) => server.authCapable)\n\t\t\t\t? \" · a sign in\"\n\t\t\t\t: \"\";\n\t\tconst hint = compact\n\t\t\t? `Enter select · ←/→ tabs${docsHint} · Esc back`\n\t\t\t: `Enter selects${confirmHint}, ←/→ switch tabs${docsHint}${checkHint}${authHint}, Esc goes back, Ctrl+C closes.`;\n\t\tlines.push(this.padLine(fg(this.t.muted, hint), innerW));\n\t\treturn lines;\n\t}\n\n\tprivate renderImports(innerW: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tlines.push(this.padLine(\"Select compatibility imports. Space toggles, Enter saves, Esc goes back.\", innerW));\n\t\tlines.push(this.padLine(\"\", innerW));\n\t\tfor (let index = 0; index < this.discovery.imports.length; index++) {\n\t\t\tconst entry = this.discovery.imports[index];\n\t\t\tif (!entry) continue;\n\t\t\tconst selected = this.selectedImports.has(entry.kind) ? \"[x]\" : \"[ ]\";\n\t\t\tconst cursor = index === this.importCursor ? fg(this.t.selected, \"›\") : \" \";\n\t\t\tlines.push(this.padLine(`${cursor} ${selected} ${entry.kind} ${entry.path}`, innerW));\n\t\t}\n\t\tlines.push(this.padLine(\"\", innerW));\n\t\tconst selected = this.discovery.imports\n\t\t\t.filter((entry) => this.selectedImports.has(entry.kind))\n\t\t\t.map((entry) => entry.kind);\n\t\tconst preview = this.callbacks.previewImports(selected);\n\t\tfor (const line of this.formatWritePreview(\n\t\t\t\"Compatibility import write preview\",\n\t\t\tpreview,\n\t\t\t[],\n\t\t\tthis.previewWidth(innerW),\n\t\t)) {\n\t\t\tlines.push(this.padLine(line, innerW));\n\t\t}\n\t\treturn lines;\n\t}\n\n\tprivate renderPaths(innerW: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tlines.push(this.padLine(\"Select a detected config path to open. Enter opens it, Esc goes back.\", innerW));\n\t\tlines.push(this.padLine(\"\", innerW));\n\t\tconst paths = this.getDetectedPaths();\n\t\tfor (let index = 0; index < paths.length; index++) {\n\t\t\tconst cursor = index === this.pathCursor ? fg(this.t.selected, \"›\") : \" \";\n\t\t\tconst path = paths[index];\n\t\t\tif (path !== undefined) lines.push(this.padLine(`${cursor} ${path}`, innerW));\n\t\t}\n\t\treturn lines;\n\t}\n\n\tprivate renderAdvanced(innerW: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tconst actions = this.getAdvancedActions();\n\t\tlines.push(this.padLine(`${fg(this.t.muted, \"Custom\")} > ${fg(this.t.title, \"Advanced options\")}`, innerW));\n\t\tlines.push(this.padLine(\"\", innerW));\n\t\tif (actions.length === 0) {\n\t\t\tlines.push(this.padLine(fg(this.t.muted, \"Nothing here right now — nice, config is already tidy.\"), innerW));\n\t\t}\n\t\tfor (let index = 0; index < actions.length; index++) {\n\t\t\tconst action = actions[index];\n\t\t\tif (!action) continue;\n\t\t\tconst cursor = index === this.advancedCursor ? fg(this.t.selected, \"›\") : \" \";\n\t\t\tconst label = truncateToWidth(action.label, Math.max(1, this.contentWidth(innerW) - 2));\n\t\t\tlines.push(this.padLine(`${cursor} ${label}`, innerW));\n\t\t}\n\t\tlines.push(this.padLine(\"\", innerW));\n\t\t// This is the one screen where showing the full explanation on highlight is the point —\n\t\t// you only ever get here by deliberately asking for \"Advanced options\".\n\t\tconst preview = this.getActionPreview(actions[this.advancedCursor], this.previewWidth(innerW));\n\t\tfor (const line of preview) {\n\t\t\tlines.push(this.padLine(line, innerW));\n\t\t}\n\t\tlines.push(this.padLine(\"\", innerW));\n\t\tlines.push(this.padLine(fg(this.t.muted, \"Enter selects, Esc back to Custom, Ctrl+C closes.\"), innerW));\n\t\treturn lines;\n\t}\n\n\tprivate renderConfirmPreset(innerW: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tconst action = this.pendingAction;\n\t\tconst name = action?.preset?.name ?? (action?.id === \"add-repoprompt\" ? \"RepoPrompt\" : \"this server\");\n\t\tlines.push(this.padLine(fg(this.t.title, `Confirm: add ${name}`), innerW));\n\t\tlines.push(this.padLine(\"\", innerW));\n\t\tconst preview = this.getActionPreview(action, this.previewWidth(innerW));\n\t\tfor (const line of preview) {\n\t\t\tlines.push(this.padLine(line, innerW));\n\t\t}\n\t\tlines.push(this.padLine(\"\", innerW));\n\t\tconst docsHint = action?.preset?.docsUrl ? \" · o docs\" : \"\";\n\t\tlines.push(this.padLine(fg(this.t.muted, `Enter writes this file${docsHint} · Esc goes back`), innerW));\n\t\treturn lines;\n\t}\n\n\tprivate discoverySummaryLine(): string {\n\t\tif (!this.discovery.hasAnyConfig) {\n\t\t\treturn fg(\n\t\t\t\tthis.t.warning,\n\t\t\t\tthis.options.onboardingState.setupCompleted\n\t\t\t\t\t? \"No MCP servers are active right now.\"\n\t\t\t\t\t: \"No MCP config is active yet.\",\n\t\t\t);\n\t\t}\n\n\t\tif (\n\t\t\tthis.discovery.totalServerCount === 0 &&\n\t\t\t(this.discovery.imports.length > 0 || !!this.discovery.repoPrompt.executablePath)\n\t\t) {\n\t\t\treturn fg(this.t.warning, \"Nexus found MCP-related setup options, but none are active in Nexus yet.\");\n\t\t}\n\n\t\tconst configuredSources = this.discovery.sources.filter((source) => source.serverCount > 0).length;\n\t\treturn fg(\n\t\t\tthis.t.hint,\n\t\t\t`Detected ${this.discovery.totalServerCount} configured servers across ${configuredSources} Nexus source${configuredSources === 1 ? \"\" : \"s\"}.`,\n\t\t);\n\t}\n\n\tprivate secondarySummaryLine(): string {\n\t\tconst hostNote =\n\t\t\tthis.discovery.hostConfigs.length > 0\n\t\t\t\t? ` Host discovery is ${this.discovery.hostConfigDiscovery}; ${this.discovery.hostConfigs.length} host source${this.discovery.hostConfigs.length === 1 ? \"\" : \"s\"} detected.`\n\t\t\t\t: \"\";\n\t\tconst conflictNote =\n\t\t\tthis.discovery.conflicts.length > 0\n\t\t\t\t? ` ${this.discovery.conflicts.length} same-name conflict${this.discovery.conflicts.length === 1 ? \"\" : \"s\"} reported.`\n\t\t\t\t: \"\";\n\t\tif (!this.discovery.hasAnyConfig) {\n\t\t\treturn `Create .nexus/mcp.json, adopt host imports, or quick-add RepoPrompt from this screen.${hostNote}${conflictNote}`;\n\t\t}\n\t\tif (this.discovery.totalServerCount === 0 && this.discovery.imports.length > 0) {\n\t\t\treturn `Detected ${this.discovery.imports.length} compatibility import source${this.discovery.imports.length === 1 ? \"\" : \"s\"}. Adopt them into Nexus or inspect the underlying files.${hostNote}${conflictNote}`;\n\t\t}\n\t\treturn `Nexus MCP files contain server definitions, compatibility imports, and MCP-specific overrides.${hostNote}${conflictNote}`;\n\t}\n\n\tprivate visibleActionRange(total: number): { start: number; end: number } {\n\t\tif (total <= COMPACT_ACTION_ROWS) return { start: 0, end: total };\n\t\tconst half = Math.floor(COMPACT_ACTION_ROWS / 2);\n\t\tconst start = Math.min(Math.max(0, this.actionCursor - half), Math.max(0, total - COMPACT_ACTION_ROWS));\n\t\treturn { start, end: Math.min(total, start + COMPACT_ACTION_ROWS) };\n\t}\n\n\tprivate contentWidth(innerW: number): number {\n\t\treturn Math.max(8, innerW - 4);\n\t}\n\n\tprivate previewWidth(innerW: number): number {\n\t\treturn Math.max(12, Math.min(DESKTOP_PREVIEW_WIDTH, this.contentWidth(innerW)));\n\t}\n\n\tprivate getActionPreview(action?: Action, previewW = DESKTOP_PREVIEW_WIDTH): string[] {\n\t\tswitch (action?.id) {\n\t\t\tcase \"adopt-imports\":\n\t\t\t\treturn this.formatWritePreview(\n\t\t\t\t\t\"Compatibility import write preview\",\n\t\t\t\t\tthis.callbacks.previewImports(\n\t\t\t\t\t\tthis.discovery.imports\n\t\t\t\t\t\t\t.filter((entry) => this.selectedImports.has(entry.kind))\n\t\t\t\t\t\t\t.map((entry) => entry.kind),\n\t\t\t\t\t),\n\t\t\t\t\t[\n\t\t\t\t\t\t`Detected imports: ${this.discovery.imports.map((entry) => `${entry.kind} (${entry.serverCount} servers)`).join(\", \")}`,\n\t\t\t\t\t\t\"Selected imports are written to the Nexus global MCP configuration shown above.\",\n\t\t\t\t\t],\n\t\t\t\t\tpreviewW,\n\t\t\t\t);\n\t\t\tcase \"view-example\":\n\t\t\t\treturn [\n\t\t\t\t\t...this.formatPreview([\"Example Nexus `.nexus/mcp.json`:\"], previewW),\n\t\t\t\t\t// Truncate, don't word-wrap: wrapText would collapse JSON's indentation.\n\t\t\t\t\t...[\n\t\t\t\t\t\t\"{\",\n\t\t\t\t\t\t' \"mcpServers\": {',\n\t\t\t\t\t\t' \"chrome-devtools\": {',\n\t\t\t\t\t\t' \"command\": \"npx\",',\n\t\t\t\t\t\t' \"args\": [\"-y\", \"chrome-devtools-mcp@1.6.0\"]',\n\t\t\t\t\t\t\" }\",\n\t\t\t\t\t\t\" }\",\n\t\t\t\t\t\t\"}\",\n\t\t\t\t\t].map((line) => colorizeJsonLine(truncateToWidth(line, previewW, \"…\"))),\n\t\t\t\t\t...this.formatPreview(\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\t\"Use Scaffold project `.nexus/mcp.json` when you want a safe empty shell instead of a live example server.\",\n\t\t\t\t\t\t],\n\t\t\t\t\t\tpreviewW,\n\t\t\t\t\t),\n\t\t\t\t];\n\t\t\tcase \"show-precedence\": {\n\t\t\t\tconst hostNote =\n\t\t\t\t\tthis.discovery.hostConfigDiscovery === \"off\"\n\t\t\t\t\t\t? \"Configs from other tools (Cursor, Claude Code, VS Code, etc.) are NOT being read right now — Nexus only looks at its own two files above. You can turn that on if you'd rather not redefine servers you already set up elsewhere.\"\n\t\t\t\t\t\t: this.discovery.hostConfigDiscovery === \"prompt\"\n\t\t\t\t\t\t\t? \"Nexus will ask before reading configs from other tools (Cursor, Claude Code, VS Code, etc.). If you say yes, they're used as a fallback — only for a server name that isn't already defined in either file above.\"\n\t\t\t\t\t\t\t: \"Nexus is also reading configs from other tools (Cursor, Claude Code, VS Code, etc.) as a fallback — they only apply to a server name that isn't already defined in either file above.\";\n\t\t\t\tconst conflictNote =\n\t\t\t\t\tthis.discovery.conflicts.length === 0\n\t\t\t\t\t\t? \"Right now no server name is defined in more than one place, so there's nothing actually being overridden.\"\n\t\t\t\t\t\t: `Right now ${this.discovery.conflicts.length} server name${this.discovery.conflicts.length === 1 ? \" is\" : \"s are\"} defined in more than one place — whichever file is read last wins, and the loser is silently ignored:`;\n\t\t\t\treturn this.formatPreview(\n\t\t\t\t\t[\n\t\t\t\t\t\t\"Nexus can load MCP servers from a few different places, so when the same server name shows up twice, something has to decide which definition actually gets used.\",\n\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\"It reads, in this order — each one can override a server name defined before it:\",\n\t\t\t\t\t\t\" 1. Global — ~/.config/nexus/mcp.json (applies to every project on this machine)\",\n\t\t\t\t\t\t` 2. Project — ${getConfigDirName()}/mcp.json (just this project; wins over global for the same name)`,\n\t\t\t\t\t\t\"\",\n\t\t\t\t\t\thostNote,\n\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\"Everything you add from this panel (a preset, a custom server) gets written to the project file, so it's scoped to this project and takes priority over anything global.\",\n\t\t\t\t\t\t\"\",\n\t\t\t\t\t\tconflictNote,\n\t\t\t\t\t\t...this.discovery.conflicts\n\t\t\t\t\t\t\t.slice(0, 8)\n\t\t\t\t\t\t\t.map(\n\t\t\t\t\t\t\t\t(conflict) =>\n\t\t\t\t\t\t\t\t\t` ${conflict.serverName}: ${conflict.sources.map((source) => source.path).join(\" then \")} — \"${conflict.winner.path}\" wins`,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t],\n\t\t\t\t\tpreviewW,\n\t\t\t\t);\n\t\t\t}\n\t\t\tcase \"open-paths\":\n\t\t\t\treturn this.formatPreview(\n\t\t\t\t\tthis.getDetectedPaths().length > 0\n\t\t\t\t\t\t? [\"Detected paths:\", ...this.getDetectedPaths()]\n\t\t\t\t\t\t: [\"No config paths were detected.\"],\n\t\t\t\t\tpreviewW,\n\t\t\t\t);\n\t\t\tcase \"add-repoprompt\": {\n\t\t\t\tconst repoPrompt = this.discovery.repoPrompt;\n\t\t\t\tconst preview = this.callbacks.previewRepoPrompt();\n\t\t\t\tif (!preview) {\n\t\t\t\t\treturn this.formatPreview([\"RepoPrompt is not available to add from this setup screen.\"], previewW);\n\t\t\t\t}\n\t\t\t\treturn this.formatWritePreview(\n\t\t\t\t\t\"RepoPrompt write preview\",\n\t\t\t\t\tpreview,\n\t\t\t\t\t[\n\t\t\t\t\t\t`Executable: ${repoPrompt.executablePath ?? \"not found\"}`,\n\t\t\t\t\t\t`Target: ${repoPrompt.targetPath ?? \"n/a\"}`,\n\t\t\t\t\t\t`Server name: ${repoPrompt.serverName ?? \"repoprompt\"}`,\n\t\t\t\t\t],\n\t\t\t\t\tpreviewW,\n\t\t\t\t);\n\t\t\t}\n\t\t\tcase \"add-known-server\": {\n\t\t\t\tconst preset = action.preset;\n\t\t\t\tif (!preset) return this.formatPreview([\"Known server preset is unavailable.\"], previewW);\n\t\t\t\treturn this.formatWritePreview(\n\t\t\t\t\t`${preset.name} write preview`,\n\t\t\t\t\tthis.callbacks.previewKnownServer(preset),\n\t\t\t\t\t[preset.summary, ...(preset.docsUrl ? [`Docs: ${preset.docsUrl}`, 'Press \"o\" to open docs.'] : [])],\n\t\t\t\t\tpreviewW,\n\t\t\t\t);\n\t\t\t}\n\t\t\tcase \"scaffold-project\":\n\t\t\t\treturn this.formatWritePreview(\n\t\t\t\t\t\"Starter project `.nexus/mcp.json` write preview\",\n\t\t\t\t\tthis.callbacks.previewStarterProject(),\n\t\t\t\t\t[\n\t\t\t\t\t\t`This creates ${getConfigDirName()}/mcp.json right here in this project — the file everything you add from the Presets or Custom tab actually gets written into. If you already have one, this action doesn't show up (check the Installed tab instead).`,\n\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t`It starts out empty on purpose (just an empty \"mcpServers\" list). A fake placeholder server would fail the moment Nexus tried to connect to it on your very next reload, so this just gives you a valid, empty file to add real servers into afterward.`,\n\t\t\t\t\t],\n\t\t\t\t\tpreviewW,\n\t\t\t\t);\n\t\t\tcase \"add-custom-stdio\":\n\t\t\t\treturn [\n\t\t\t\t\t...this.formatPreview(\n\t\t\t\t\t\t[\"Runs a command on your machine over stdio — e.g. an npx-installed MCP server.\"],\n\t\t\t\t\t\tpreviewW,\n\t\t\t\t\t),\n\t\t\t\t\t\"\",\n\t\t\t\t\tcolorizeCommandLine(truncateToWidth(\"/mcp add <name> -- <command> [args...]\", previewW, \"…\")),\n\t\t\t\t\t\"\",\n\t\t\t\t\tfg(this.t.muted, \"Example (real, runnable):\"),\n\t\t\t\t\tcolorizeCommandLine(\n\t\t\t\t\t\ttruncateToWidth(\"/mcp add chrome-devtools -- npx -y chrome-devtools-mcp@1.6.0\", previewW, \"…\"),\n\t\t\t\t\t),\n\t\t\t\t\t\"\",\n\t\t\t\t\t...this.formatPreview(\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\t'Enter fills in \"/mcp add my-server -- \" into the chat input below — finish typing there.',\n\t\t\t\t\t\t\t\"Run `/mcp add` with no arguments (from chat) for the full flag list.\",\n\t\t\t\t\t\t],\n\t\t\t\t\t\tpreviewW,\n\t\t\t\t\t),\n\t\t\t\t];\n\t\t\tcase \"add-custom-url\":\n\t\t\t\treturn [\n\t\t\t\t\t...this.formatPreview([\"Connects to a remote MCP server over HTTP.\"], previewW),\n\t\t\t\t\t\"\",\n\t\t\t\t\tcolorizeCommandLine(\n\t\t\t\t\t\ttruncateToWidth(\n\t\t\t\t\t\t\t\"/mcp add <name> --url <url> [--header K=V] [--auth oauth|bearer] [--bearer-env VAR]\",\n\t\t\t\t\t\t\tpreviewW,\n\t\t\t\t\t\t\t\"…\",\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t\t\"\",\n\t\t\t\t\tfg(this.t.muted, \"Example (real, runnable):\"),\n\t\t\t\t\tcolorizeCommandLine(\n\t\t\t\t\t\ttruncateToWidth(\"/mcp add context7 --url https://mcp.context7.com/mcp\", previewW, \"…\"),\n\t\t\t\t\t),\n\t\t\t\t\t\"\",\n\t\t\t\t\t...this.formatPreview(\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\t'Enter fills in \"/mcp add my-server --url \" into the chat input below — finish typing there.',\n\t\t\t\t\t\t\t\"Run `/mcp add` with no arguments (from chat) for the full flag list.\",\n\t\t\t\t\t\t],\n\t\t\t\t\t\tpreviewW,\n\t\t\t\t\t),\n\t\t\t\t];\n\t\t\tcase \"view-installed-server\": {\n\t\t\t\tconst server = action.installed;\n\t\t\t\tif (!server)\n\t\t\t\t\treturn this.formatPreview([\"Add a server from the Presets or Custom tab to see it here.\"], previewW);\n\t\t\t\treturn this.formatPreview(\n\t\t\t\t\t[\n\t\t\t\t\t\t`${server.name} — ${server.kind === \"stdio\" ? \"local (stdio)\" : \"remote (HTTP)\"}, currently ${server.disabled ? \"disabled\" : \"active\"}${server.authCapable ? (server.signedIn ? \", signed in\" : \", NOT signed in yet\") : \"\"}.`,\n\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t`Press Enter to ${server.disabled ? \"enable\" : \"disable\"} it here. Takes effect after /reload.`,\n\t\t\t\t\t\t...(server.authCapable\n\t\t\t\t\t\t\t? [\n\t\t\t\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\t\t\tserver.signedIn\n\t\t\t\t\t\t\t\t\t\t? 'Press \"a\" to sign in again (e.g. to switch accounts). This re-opens your browser.'\n\t\t\t\t\t\t\t\t\t\t: 'Press \"a\" to sign in now — opens your browser for this server\\'s OAuth login. Tokens refresh automatically after that; you should not need to repeat this unless access is revoked.',\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t: []),\n\t\t\t\t\t],\n\t\t\t\t\tpreviewW,\n\t\t\t\t);\n\t\t\t}\n\t\t\tdefault:\n\t\t\t\treturn this.formatPreview([\"Close the setup flow.\"], previewW);\n\t\t}\n\t}\n\n\tprivate formatPreview(lines: string[], width = DESKTOP_PREVIEW_WIDTH): string[] {\n\t\tconst preview: string[] = [];\n\t\tfor (const line of lines) {\n\t\t\tpreview.push(...wrapText(line, width));\n\t\t}\n\t\treturn preview;\n\t}\n\n\tprivate formatWritePreview(\n\t\ttitle: string,\n\t\tpreview: ConfigWritePreview,\n\t\tintro: string[] = [],\n\t\twidth = DESKTOP_PREVIEW_WIDTH,\n\t): string[] {\n\t\tconst lines: string[] = [];\n\t\tfor (const line of intro) {\n\t\t\tlines.push(...wrapText(line, width));\n\t\t}\n\t\tif (intro.length > 0) lines.push(\"\");\n\t\tlines.push(...wrapText(`${title}: ${preview.path}`, width));\n\t\tlines.push(\n\t\t\t...wrapText(\n\t\t\t\tpreview.existed\n\t\t\t\t\t? \"Existing file detected. Showing exact before/after diff.\"\n\t\t\t\t\t: \"New file will be created. Showing exact content diff.\",\n\t\t\t\twidth,\n\t\t\t),\n\t\t);\n\t\tlines.push(\"\");\n\t\tconst diffLines = preview.diffText.split(\"\\n\");\n\t\tconst maxLines = 12;\n\t\tconst shown = diffLines.slice(0, maxLines);\n\t\tfor (const line of shown) {\n\t\t\t// Truncate, don't word-wrap: wrapText collapses whitespace and would\n\t\t\t// destroy JSON's nested indentation.\n\t\t\tconst color = diffLineColor(line);\n\t\t\tconst truncated = truncateToWidth(line, width, \"…\");\n\t\t\tlines.push(color ? fg(color, truncated) : colorizeJsonLine(truncated));\n\t\t}\n\t\tif (diffLines.length > maxLines) {\n\t\t\tlines.push(\n\t\t\t\t...wrapText(\n\t\t\t\t\t`… ${diffLines.length - maxLines} more diff line${diffLines.length - maxLines === 1 ? \"\" : \"s\"}`,\n\t\t\t\t\twidth,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t\treturn lines;\n\t}\n\n\tprivate padLine(text: string, innerW: number): string {\n\t\tconst inset = 2;\n\t\tconst contentW = Math.max(0, innerW - inset * 2);\n\t\tconst fitted = truncateToWidth(text, contentW, \"…\", true);\n\t\tconst plainWidth = visibleWidth(fitted);\n\t\tconst padding = Math.max(0, contentW - plainWidth);\n\t\treturn `│${\" \".repeat(inset)}${fitted}${\" \".repeat(padding)}${\" \".repeat(inset)}│`;\n\t}\n\n\tinvalidate(): void {}\n\n\tdispose(): void {\n\t\tthis.cleanup();\n\t}\n}\n\nexport function createMcpSetupPanel(\n\tdiscovery: McpDiscoverySummary,\n\tcallbacks: SetupPanelCallbacks,\n\toptions: SetupPanelOptions,\n\ttui: { requestRender(): void },\n\tdone: () => void,\n): McpSetupPanel & { dispose(): void } {\n\treturn new McpSetupPanel(discovery, callbacks, options, tui, done);\n}\n"]}