magus-cli 0.0.0-placeholder → 7.1.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 (330) hide show
  1. package/README.md +309 -0
  2. package/bin/magus.js +170 -0
  3. package/package.json +67 -3
  4. package/scripts/build-binaries.ts +113 -0
  5. package/scripts/capture-builtin.ts +412 -0
  6. package/scripts/check-optional-deps.ts +49 -0
  7. package/scripts/test-isolated.ts +49 -0
  8. package/scripts/verify-binary.ts +95 -0
  9. package/scripts/verify-community-registry.ts +272 -0
  10. package/src/__tests__/active-closure.test.ts +184 -0
  11. package/src/__tests__/alias-adopt.test.ts +365 -0
  12. package/src/__tests__/alias-parser.test.ts +417 -0
  13. package/src/__tests__/alias-shell-writer.test.ts +681 -0
  14. package/src/__tests__/alias-store.test.ts +163 -0
  15. package/src/__tests__/binary-signing.test.ts +120 -0
  16. package/src/__tests__/blackbox-active-closure.test.ts +119 -0
  17. package/src/__tests__/blackbox-convergence.test.ts +334 -0
  18. package/src/__tests__/blackbox-parity.test.ts +725 -0
  19. package/src/__tests__/blackbox-plugin-state.test.ts +545 -0
  20. package/src/__tests__/blackbox-tool-state.test.ts +669 -0
  21. package/src/__tests__/cache-provenance.test.ts +84 -0
  22. package/src/__tests__/capture-builtin.test.ts +92 -0
  23. package/src/__tests__/catalog-cache-store.test.ts +309 -0
  24. package/src/__tests__/catalog-notice.test.ts +155 -0
  25. package/src/__tests__/cli-ansi.test.ts +297 -0
  26. package/src/__tests__/cli-apply-seams.test.ts +288 -0
  27. package/src/__tests__/cli-live.test.ts +391 -0
  28. package/src/__tests__/cli-router.test.ts +240 -0
  29. package/src/__tests__/cli-tool-commands.test.ts +239 -0
  30. package/src/__tests__/cli-tools-adapter.test.ts +294 -0
  31. package/src/__tests__/cli-update-view.test.ts +524 -0
  32. package/src/__tests__/community-fetch.test.ts +545 -0
  33. package/src/__tests__/community-registry.test.ts +269 -0
  34. package/src/__tests__/community-staleness.test.ts +720 -0
  35. package/src/__tests__/component-badge.test.ts +98 -0
  36. package/src/__tests__/component-icons.test.ts +94 -0
  37. package/src/__tests__/content-drift.test.ts +353 -0
  38. package/src/__tests__/conventions-integration.test.ts +830 -0
  39. package/src/__tests__/conventions-manager.test.ts +1236 -0
  40. package/src/__tests__/doctor-bins.test.ts +43 -0
  41. package/src/__tests__/doctor.test.ts +463 -0
  42. package/src/__tests__/dotenv.test.ts +195 -0
  43. package/src/__tests__/enabled-not-installed.test.ts +94 -0
  44. package/src/__tests__/file-locking.test.ts +208 -0
  45. package/src/__tests__/fixtures/tool-probe/brew-list-versions-tmux.txt +2 -0
  46. package/src/__tests__/fixtures/tool-probe/brew-outdated-behind-homebrew-spec.txt +29 -0
  47. package/src/__tests__/fixtures/tool-probe/brew-outdated-tmux-current.txt +7 -0
  48. package/src/__tests__/fixtures/tool-probe/brew-outdated-unknown-formula.txt +6 -0
  49. package/src/__tests__/fixtures/tool-probe/go-version-m-tmux-mcp.txt +27 -0
  50. package/src/__tests__/fixtures/tool-probe/npm-dist-tags-missing-scoped.txt +4 -0
  51. package/src/__tests__/fixtures/tool-probe/npm-dist-tags-openai-codex.txt +4 -0
  52. package/src/__tests__/fixtures/tool-probe/pypi-cowsay.txt +3 -0
  53. package/src/__tests__/fixtures/tool-probe/uv-receipt-browser-use.toml +10 -0
  54. package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-git.toml +8 -0
  55. package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-pinned.toml +8 -0
  56. package/src/__tests__/fixtures/tool-probe/uv-receipt-graphifyy.toml +7 -0
  57. package/src/__tests__/fixtures/tool-probe/uv-receipt-mcp.toml +6 -0
  58. package/src/__tests__/fixtures/tool-probe/uv-receipt-serena-agent.toml +9 -0
  59. package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated-pinned.txt +5 -0
  60. package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated.txt +11 -0
  61. package/src/__tests__/fixtures/tool-probe/uv-tool-list.txt +22 -0
  62. package/src/__tests__/footer-hints.test.ts +40 -0
  63. package/src/__tests__/format-command.test.ts +78 -0
  64. package/src/__tests__/git-worktree.test.ts +108 -0
  65. package/src/__tests__/github-budget.test.ts +206 -0
  66. package/src/__tests__/gitignore-check.test.ts +71 -0
  67. package/src/__tests__/gitignore-detector.test.ts +176 -0
  68. package/src/__tests__/gitignore-fixer.test.ts +317 -0
  69. package/src/__tests__/gitignore-resolver.test.ts +193 -0
  70. package/src/__tests__/gitignore-service.test.ts +137 -0
  71. package/src/__tests__/go-module.test.ts +43 -0
  72. package/src/__tests__/hook-import-policy.test.ts +90 -0
  73. package/src/__tests__/hook-process.test.ts +256 -0
  74. package/src/__tests__/hook-registration.test.ts +224 -0
  75. package/src/__tests__/install-command.test.ts +81 -0
  76. package/src/__tests__/install-plan.test.ts +104 -0
  77. package/src/__tests__/launcher-signal.test.ts +308 -0
  78. package/src/__tests__/local-marketplace-scan.test.ts +248 -0
  79. package/src/__tests__/manifest.test.ts +281 -0
  80. package/src/__tests__/markdown-renderer.test.ts +142 -0
  81. package/src/__tests__/marketplace-badge.test.ts +140 -0
  82. package/src/__tests__/marketplace-refresh.test.ts +340 -0
  83. package/src/__tests__/marketplace-version.test.ts +84 -0
  84. package/src/__tests__/marketplaces.test.ts +128 -0
  85. package/src/__tests__/mate-availability.test.ts +156 -0
  86. package/src/__tests__/mate-catalog.test.ts +295 -0
  87. package/src/__tests__/mcp-registry.test.ts +158 -0
  88. package/src/__tests__/model-visuals.test.tsx +2466 -0
  89. package/src/__tests__/models-adapter.test.ts +332 -0
  90. package/src/__tests__/models-cli.test.ts +273 -0
  91. package/src/__tests__/models-core.test.ts +802 -0
  92. package/src/__tests__/models-manager.test.ts +500 -0
  93. package/src/__tests__/models-presets-marketplace.test.ts +168 -0
  94. package/src/__tests__/models-screen-state.test.ts +315 -0
  95. package/src/__tests__/open-file.test.ts +59 -0
  96. package/src/__tests__/owned-marketplaces.test.ts +93 -0
  97. package/src/__tests__/plugin-checkout.test.ts +66 -0
  98. package/src/__tests__/plugin-cli-argv.test.ts +109 -0
  99. package/src/__tests__/plugin-contents.test.ts +371 -0
  100. package/src/__tests__/plugin-manager-fallback.test.ts +320 -0
  101. package/src/__tests__/plugin-releases.test.ts +98 -0
  102. package/src/__tests__/plugin-requires.test.ts +187 -0
  103. package/src/__tests__/plugin-row-text.test.ts +345 -0
  104. package/src/__tests__/plugin-setup.test.ts +1016 -0
  105. package/src/__tests__/profile-adopt.test.ts +177 -0
  106. package/src/__tests__/profile-command.test.ts +110 -0
  107. package/src/__tests__/profile-materializer.test.ts +238 -0
  108. package/src/__tests__/profile-sync.test.ts +154 -0
  109. package/src/__tests__/registry-version-resolution.test.ts +88 -0
  110. package/src/__tests__/resolve-executable.test.ts +42 -0
  111. package/src/__tests__/resolver.test.ts +351 -0
  112. package/src/__tests__/scope-action.test.ts +134 -0
  113. package/src/__tests__/scope-squares.test.tsx +164 -0
  114. package/src/__tests__/selection-scope.test.ts +100 -0
  115. package/src/__tests__/settings-file.test.ts +179 -0
  116. package/src/__tests__/shell-script-callers.test.ts +92 -0
  117. package/src/__tests__/skill-install-files.test.ts +136 -0
  118. package/src/__tests__/skills-catalog-client.test.ts +48 -0
  119. package/src/__tests__/style-wrap.test.ts +220 -0
  120. package/src/__tests__/styles-manager.test.ts +1098 -0
  121. package/src/__tests__/styles-origins.test.ts +414 -0
  122. package/src/__tests__/styles-screen-state.test.ts +457 -0
  123. package/src/__tests__/styles-status-line.test.ts +72 -0
  124. package/src/__tests__/styles-sync.test.ts +452 -0
  125. package/src/__tests__/symlink-manager.test.ts +180 -0
  126. package/src/__tests__/tabbar-layout.test.ts +100 -0
  127. package/src/__tests__/terminology-filler.test.ts +214 -0
  128. package/src/__tests__/theme-adaptive-colors.test.ts +354 -0
  129. package/src/__tests__/theme-env.test.ts +310 -0
  130. package/src/__tests__/theme-resolve.test.ts +380 -0
  131. package/src/__tests__/tool-probe.test.ts +1400 -0
  132. package/src/__tests__/toolchain.test.ts +232 -0
  133. package/src/__tests__/ui-version-writers.test.ts +88 -0
  134. package/src/__tests__/update-apply.test.ts +637 -0
  135. package/src/__tests__/update-plan.test.ts +1396 -0
  136. package/src/__tests__/uppercase-keybindings.test.ts +101 -0
  137. package/src/__tests__/useGitignoreModal.test.ts +79 -0
  138. package/src/__tests__/version-check-refresh.test.ts +45 -0
  139. package/src/__tests__/version-snapshot.test.ts +212 -0
  140. package/src/__tests__/version-unknown-state.test.ts +403 -0
  141. package/src/__tests__/worktree-registry-resolution.test.ts +107 -0
  142. package/src/cli/ansi.ts +528 -0
  143. package/src/cli/bootstrap.ts +134 -0
  144. package/src/cli/doctor.ts +218 -0
  145. package/src/cli/hook.ts +129 -0
  146. package/src/cli/install.ts +382 -0
  147. package/src/cli/live.ts +374 -0
  148. package/src/cli/models.ts +297 -0
  149. package/src/cli/profile.ts +197 -0
  150. package/src/cli/prompt.ts +32 -0
  151. package/src/cli/router.ts +264 -0
  152. package/src/cli/update-view.ts +717 -0
  153. package/src/cli/update.ts +600 -0
  154. package/src/cli/upgrade.ts +97 -0
  155. package/src/data/alias-flags.ts +301 -0
  156. package/src/data/cli-tools.ts +146 -0
  157. package/src/data/community-styles.ts +531 -0
  158. package/src/data/gitignore-defaults.ts +33 -0
  159. package/src/data/gitignore-reasons.ts +93 -0
  160. package/src/data/gitignore-templates.ts +25 -0
  161. package/src/data/marketplaces.ts +278 -0
  162. package/src/data/mcp-servers.ts +51 -0
  163. package/src/data/models-presets.ts +329 -0
  164. package/src/data/predefined-profiles.ts +252 -0
  165. package/src/data/settings-catalog.ts +714 -0
  166. package/src/data/skill-repos.ts +91 -0
  167. package/src/data/styles/asd-ste100.md +38 -0
  168. package/src/data/styles/calibrated.md +24 -0
  169. package/src/data/styles/direct.md +23 -0
  170. package/src/data/styles/evidence-first.md +22 -0
  171. package/src/data/styles/explanatory.md +23 -0
  172. package/src/data/styles/index.ts +57 -0
  173. package/src/data/styles/no-slop.md +32 -0
  174. package/src/data/styles/plain-language.md +23 -0
  175. package/src/data/styles/structured.md +30 -0
  176. package/src/data/styles/terminology.md +28 -0
  177. package/src/data/styles/terse.md +20 -0
  178. package/src/main.tsx +110 -0
  179. package/src/markdown.d.ts +13 -0
  180. package/src/opentui.d.ts +203 -0
  181. package/src/services/active-closure.ts +117 -0
  182. package/src/services/alias-settings.ts +60 -0
  183. package/src/services/alias-shell-writer.ts +1244 -0
  184. package/src/services/alias-store.ts +172 -0
  185. package/src/services/binary-signing.ts +78 -0
  186. package/src/services/catalog-cache-store.ts +316 -0
  187. package/src/services/claude-cli.ts +304 -0
  188. package/src/services/claude-runner.ts +31 -0
  189. package/src/services/claude-settings.ts +1564 -0
  190. package/src/services/cli-tool-commands.ts +133 -0
  191. package/src/services/community-fetcher.ts +90 -0
  192. package/src/services/community-styles.ts +1194 -0
  193. package/src/services/content-drift.ts +208 -0
  194. package/src/services/conventions-manager.ts +945 -0
  195. package/src/services/doctor-bins.ts +63 -0
  196. package/src/services/doctor.ts +570 -0
  197. package/src/services/dotenv.ts +181 -0
  198. package/src/services/git-worktree.ts +129 -0
  199. package/src/services/github-budget.ts +274 -0
  200. package/src/services/gitignore-check.ts +63 -0
  201. package/src/services/gitignore-detector.ts +167 -0
  202. package/src/services/gitignore-fixer.ts +280 -0
  203. package/src/services/gitignore-resolver.ts +208 -0
  204. package/src/services/gitignore-service.ts +167 -0
  205. package/src/services/hook-registration.ts +218 -0
  206. package/src/services/install-plan.ts +107 -0
  207. package/src/services/local-marketplace.ts +657 -0
  208. package/src/services/manifest.ts +250 -0
  209. package/src/services/marketplace-catalog-git.ts +173 -0
  210. package/src/services/marketplace-catalog.ts +129 -0
  211. package/src/services/marketplace-fetcher.ts +400 -0
  212. package/src/services/marketplace-refresh.ts +245 -0
  213. package/src/services/marketplace-sync.ts +145 -0
  214. package/src/services/mate-availability.ts +133 -0
  215. package/src/services/mate-catalog.ts +265 -0
  216. package/src/services/mcp-registry.ts +171 -0
  217. package/src/services/models-core.ts +969 -0
  218. package/src/services/models-manager.ts +606 -0
  219. package/src/services/plugin-checkout.ts +186 -0
  220. package/src/services/plugin-manager.ts +847 -0
  221. package/src/services/plugin-mcp-config.ts +242 -0
  222. package/src/services/plugin-releases.ts +47 -0
  223. package/src/services/plugin-requires.ts +227 -0
  224. package/src/services/plugin-setup.ts +965 -0
  225. package/src/services/profile-adopt.ts +206 -0
  226. package/src/services/profile-materializer.ts +133 -0
  227. package/src/services/profile-sync.ts +158 -0
  228. package/src/services/profiles.ts +225 -0
  229. package/src/services/resolver.ts +377 -0
  230. package/src/services/settings-file.ts +69 -0
  231. package/src/services/settings-manager.ts +307 -0
  232. package/src/services/skills-manager.ts +1111 -0
  233. package/src/services/skillsmp-client.ts +193 -0
  234. package/src/services/styles-manager.ts +1294 -0
  235. package/src/services/symlink-manager.ts +189 -0
  236. package/src/services/terminology-filler.ts +266 -0
  237. package/src/services/tool-probe.ts +1637 -0
  238. package/src/services/toolchain.ts +431 -0
  239. package/src/services/update-engine.ts +691 -0
  240. package/src/services/update-plan.ts +820 -0
  241. package/src/services/version-check.ts +127 -0
  242. package/src/services/version-snapshot.ts +189 -0
  243. package/src/tui.tsx +112 -0
  244. package/src/types/bun.d.ts +27 -0
  245. package/src/types/gitignore.ts +68 -0
  246. package/src/types/index.ts +447 -0
  247. package/src/ui/App.tsx +535 -0
  248. package/src/ui/adapters/catalogNotice.ts +122 -0
  249. package/src/ui/adapters/cliToolsAdapter.ts +176 -0
  250. package/src/ui/adapters/modelsAdapter.ts +213 -0
  251. package/src/ui/adapters/pluginsAdapter.ts +413 -0
  252. package/src/ui/adapters/settingsAdapter.ts +168 -0
  253. package/src/ui/adapters/skillsAdapter.ts +229 -0
  254. package/src/ui/adapters/stylesAdapter.ts +403 -0
  255. package/src/ui/components/CategoryHeader.tsx +64 -0
  256. package/src/ui/components/EmptyFilterState.tsx +32 -0
  257. package/src/ui/components/FlagDetailEditor.tsx +0 -0
  258. package/src/ui/components/ScopeIndicator.tsx +63 -0
  259. package/src/ui/components/ScrollableList.tsx +95 -0
  260. package/src/ui/components/SearchInput.tsx +57 -0
  261. package/src/ui/components/StyledText.tsx +75 -0
  262. package/src/ui/components/TabBar.tsx +101 -0
  263. package/src/ui/components/layout/FooterHints.tsx +78 -0
  264. package/src/ui/components/layout/Panel.tsx +63 -0
  265. package/src/ui/components/layout/ProgressBar.tsx +44 -0
  266. package/src/ui/components/layout/ScopeTabs.tsx +54 -0
  267. package/src/ui/components/layout/ScreenLayout.tsx +321 -0
  268. package/src/ui/components/layout/index.ts +9 -0
  269. package/src/ui/components/modals/ConfirmModal.tsx +62 -0
  270. package/src/ui/components/modals/InputModal.tsx +75 -0
  271. package/src/ui/components/modals/LoadingModal.tsx +51 -0
  272. package/src/ui/components/modals/MessageModal.tsx +66 -0
  273. package/src/ui/components/modals/ModalContainer.tsx +159 -0
  274. package/src/ui/components/modals/SelectModal.tsx +90 -0
  275. package/src/ui/components/modals/index.ts +6 -0
  276. package/src/ui/components/primitives/ActionHints.tsx +44 -0
  277. package/src/ui/components/primitives/DetailSection.tsx +23 -0
  278. package/src/ui/components/primitives/KeyValueLine.tsx +26 -0
  279. package/src/ui/components/primitives/ListCategoryRow.tsx +45 -0
  280. package/src/ui/components/primitives/MetaText.tsx +40 -0
  281. package/src/ui/components/primitives/ScopeDetail.tsx +67 -0
  282. package/src/ui/components/primitives/ScopeSquares.tsx +58 -0
  283. package/src/ui/components/primitives/SelectableRow.tsx +30 -0
  284. package/src/ui/components/primitives/index.ts +9 -0
  285. package/src/ui/hooks/index.ts +2 -0
  286. package/src/ui/hooks/useGitignoreModal.ts +95 -0
  287. package/src/ui/hooks/useKeyboard.ts +24 -0
  288. package/src/ui/hooks/useKeyboardHandler.ts +63 -0
  289. package/src/ui/registry.ts +33 -0
  290. package/src/ui/renderers/cliToolRenderers.tsx +326 -0
  291. package/src/ui/renderers/gitignoreRenderers.tsx +80 -0
  292. package/src/ui/renderers/markdownRenderer.tsx +302 -0
  293. package/src/ui/renderers/mcpRenderers.tsx +149 -0
  294. package/src/ui/renderers/modelRenderers.tsx +1309 -0
  295. package/src/ui/renderers/modelVisuals.tsx +1402 -0
  296. package/src/ui/renderers/pluginRenderers.tsx +1089 -0
  297. package/src/ui/renderers/pluginRowText.ts +70 -0
  298. package/src/ui/renderers/pluginStatusText.ts +56 -0
  299. package/src/ui/renderers/profileRenderers.tsx +406 -0
  300. package/src/ui/renderers/settingsRenderers.tsx +218 -0
  301. package/src/ui/renderers/skillRenderers.tsx +479 -0
  302. package/src/ui/renderers/styleRenderers.tsx +813 -0
  303. package/src/ui/screens/AliasScreen.tsx +1558 -0
  304. package/src/ui/screens/CliToolsScreen.tsx +367 -0
  305. package/src/ui/screens/EnvVarsScreen.tsx +221 -0
  306. package/src/ui/screens/GitignoreScreen.tsx +562 -0
  307. package/src/ui/screens/McpRegistryScreen.tsx +366 -0
  308. package/src/ui/screens/McpScreen.tsx +256 -0
  309. package/src/ui/screens/ModelsScreen.tsx +542 -0
  310. package/src/ui/screens/PluginsScreen.tsx +1423 -0
  311. package/src/ui/screens/ProfilesScreen.tsx +438 -0
  312. package/src/ui/screens/SkillsScreen.tsx +853 -0
  313. package/src/ui/screens/StylesScreen.tsx +1064 -0
  314. package/src/ui/screens/index.ts +11 -0
  315. package/src/ui/state/AppContext.tsx +233 -0
  316. package/src/ui/state/DimensionsContext.tsx +154 -0
  317. package/src/ui/state/reducer.ts +810 -0
  318. package/src/ui/state/types.ts +503 -0
  319. package/src/ui/theme-mode.ts +440 -0
  320. package/src/ui/theme-resolve.ts +147 -0
  321. package/src/ui/theme.ts +175 -0
  322. package/src/utils/clipboard.ts +58 -0
  323. package/src/utils/command-utils.ts +149 -0
  324. package/src/utils/config-dir.ts +45 -0
  325. package/src/utils/file-locking.ts +146 -0
  326. package/src/utils/fuzzy-search.ts +138 -0
  327. package/src/utils/go-module.ts +36 -0
  328. package/src/utils/open-file.ts +84 -0
  329. package/src/utils/run.ts +67 -0
  330. package/src/utils/string-utils.ts +88 -0
@@ -0,0 +1,302 @@
1
+ import type React from "react";
2
+ import { theme } from "../theme.js";
3
+
4
+ /**
5
+ * Markdown → OpenTUI elements, for the detail pane.
6
+ *
7
+ * Why not a library. `glow` is a Go binary: it cannot ship inside
8
+ * `bun build --compile`, and shelling out to something the user may not have
9
+ * installed turns a display detail into a runtime dependency. The JS options
10
+ * (`marked-terminal` and friends) emit ANSI escape sequences, which is the
11
+ * wrong output format — OpenTUI composes `<text>`/`<span>` elements with theme
12
+ * colours, and feeding it raw escapes bypasses the theme and breaks on a
13
+ * re-render. This module is ~200 lines and emits elements directly.
14
+ *
15
+ * Deliberately partial. It covers what SKILL.md files actually contain:
16
+ * headings, fenced code, tables, rules, lists, blockquotes, and inline
17
+ * bold/code. Anything unrecognised falls through as plain text, which is what
18
+ * the pane showed for everything before.
19
+ */
20
+
21
+ export interface MarkdownRenderResult {
22
+ nodes: React.ReactNode[];
23
+ /** Lines beyond `maxLines` that were not rendered. */
24
+ withheld: number;
25
+ }
26
+
27
+ interface RenderOptions {
28
+ /** Columns available to the pane, used to size table columns. */
29
+ width: number;
30
+ /** Render-cost guard; the pane itself scrolls. */
31
+ maxLines: number;
32
+ }
33
+
34
+ // ─── Inline spans ────────────────────────────────────────────────────────────
35
+
36
+ /**
37
+ * Split a line into `code`, **bold** and plain runs.
38
+ *
39
+ * One pass over both markers rather than nested passes: a nested pass would
40
+ * re-scan text already claimed by a code span and turn `` `a**b` `` into a bold
41
+ * run inside code.
42
+ */
43
+ function inlineSpans(text: string, keyPrefix: string): React.ReactNode[] {
44
+ const nodes: React.ReactNode[] = [];
45
+ const pattern = /(`[^`]+`)|(\*\*[^*]+\*\*)/g;
46
+ let last = 0;
47
+ let match: RegExpExecArray | null = pattern.exec(text);
48
+ let i = 0;
49
+
50
+ while (match) {
51
+ if (match.index > last) {
52
+ nodes.push(
53
+ <span key={`${keyPrefix}-t${i}`}>{text.slice(last, match.index)}</span>,
54
+ );
55
+ }
56
+ if (match[1]) {
57
+ nodes.push(
58
+ <span key={`${keyPrefix}-c${i}`} fg={theme.colors.link}>
59
+ {match[1].slice(1, -1)}
60
+ </span>,
61
+ );
62
+ } else {
63
+ nodes.push(
64
+ <strong key={`${keyPrefix}-b${i}`}>{match[2].slice(2, -2)}</strong>,
65
+ );
66
+ }
67
+ last = match.index + match[0].length;
68
+ i += 1;
69
+ match = pattern.exec(text);
70
+ }
71
+
72
+ if (last < text.length) {
73
+ nodes.push(<span key={`${keyPrefix}-t${i}`}>{text.slice(last)}</span>);
74
+ }
75
+ return nodes;
76
+ }
77
+
78
+ // ─── Tables ──────────────────────────────────────────────────────────────────
79
+
80
+ /**
81
+ * Strip inline markers from a table cell.
82
+ *
83
+ * Cells are padded to a column width, so they cannot carry `<span>` children
84
+ * the way body text does without the padding and the spans disagreeing about
85
+ * length. Removing the markers is the honest trade: a cell reading
86
+ * `` `context`, `callers` `` printed its backticks verbatim, which is the raw
87
+ * source leaking into a rendered view.
88
+ */
89
+ function plainCell(cell: string): string {
90
+ return cell
91
+ .trim()
92
+ .replace(/`([^`]+)`/g, "$1")
93
+ .replace(/\*\*([^*]+)\*\*/g, "$1")
94
+ .replace(/<br\s*\/?>/gi, " ");
95
+ }
96
+
97
+ function splitRow(line: string): string[] {
98
+ return line
99
+ .replace(/^\s*\|/, "")
100
+ .replace(/\|\s*$/, "")
101
+ .split("|")
102
+ .map(plainCell);
103
+ }
104
+
105
+ const isSeparatorRow = (line: string): boolean =>
106
+ /^\s*\|?[\s:|-]+\|[\s:|-]*$/.test(line) && line.includes("-");
107
+
108
+ /**
109
+ * Render a table as aligned columns with a rule under the header.
110
+ *
111
+ * Pipes are dropped rather than drawn. In a pane this narrow the vertical bars
112
+ * cost a column each and, once a cell wraps, line up with nothing — which is
113
+ * exactly how the raw source read before.
114
+ */
115
+ function renderTable(
116
+ rows: string[][],
117
+ width: number,
118
+ key: string,
119
+ ): React.ReactNode[] {
120
+ const columns = Math.max(...rows.map((r) => r.length));
121
+ const natural: number[] = [];
122
+ for (let c = 0; c < columns; c++) {
123
+ natural.push(Math.max(1, ...rows.map((r) => (r[c] ?? "").length)));
124
+ }
125
+
126
+ // Shrink the widest column until the row fits. Shrinking the widest keeps a
127
+ // narrow key column (Mode, Level) intact and takes the space out of the prose
128
+ // column, which is the one that can afford it.
129
+ const gap = 2;
130
+ const budget = Math.max(20, width - 1);
131
+ const widths = [...natural];
132
+ const total = () => widths.reduce((a, b) => a + b, 0) + gap * (columns - 1);
133
+ while (total() > budget) {
134
+ const widest = widths.indexOf(Math.max(...widths));
135
+ if (widths[widest] <= 6) break;
136
+ widths[widest] -= 1;
137
+ }
138
+
139
+ const fit = (cell: string, w: number) =>
140
+ cell.length <= w ? cell.padEnd(w) : `${cell.slice(0, Math.max(1, w - 1))}…`;
141
+
142
+ const nodes: React.ReactNode[] = [];
143
+ rows.forEach((row, r) => {
144
+ const line = widths
145
+ .map((w, c) => fit(row[c] ?? "", w))
146
+ .join(" ".repeat(gap))
147
+ .trimEnd();
148
+
149
+ nodes.push(
150
+ // biome-ignore lint/suspicious/noArrayIndexKey: table rows never reorder
151
+ <text key={`${key}-r${r}`} fg={theme.colors.muted}>
152
+ {" "}
153
+ {r === 0 ? <strong>{line}</strong> : line}
154
+ </text>,
155
+ );
156
+
157
+ if (r === 0) {
158
+ nodes.push(
159
+ <text key={`${key}-rule`} fg={theme.colors.dim}>
160
+ {" "}
161
+ {widths.map((w) => "─".repeat(w)).join(" ".repeat(gap))}
162
+ </text>,
163
+ );
164
+ }
165
+ });
166
+ return nodes;
167
+ }
168
+
169
+ // ─── Document ────────────────────────────────────────────────────────────────
170
+
171
+ const HEADING_COLORS = [
172
+ theme.colors.accent,
173
+ theme.colors.accent,
174
+ theme.colors.info,
175
+ theme.colors.info,
176
+ theme.colors.muted,
177
+ theme.colors.muted,
178
+ ];
179
+
180
+ export function renderMarkdown(
181
+ source: string,
182
+ { width, maxLines }: RenderOptions,
183
+ ): MarkdownRenderResult {
184
+ const lines = source.split("\n");
185
+ const nodes: React.ReactNode[] = [];
186
+ let i = 0;
187
+ let emitted = 0;
188
+
189
+ const push = (node: React.ReactNode) => {
190
+ nodes.push(node);
191
+ emitted += 1;
192
+ };
193
+
194
+ while (i < lines.length && emitted < maxLines) {
195
+ const line = lines[i];
196
+
197
+ // Fenced code. The fence markers are dropped and the block is tinted, so
198
+ // the reader sees a code block rather than three backticks they have to
199
+ // mentally strip.
200
+ const fence = line.match(/^\s*```\s*(\S*)/);
201
+ if (fence) {
202
+ const lang = fence[1];
203
+ i += 1;
204
+ if (lang) {
205
+ push(
206
+ <text key={`f${i}`} fg={theme.colors.dim}>
207
+ {" "}
208
+ {lang}
209
+ </text>,
210
+ );
211
+ }
212
+ while (i < lines.length && !/^\s*```/.test(lines[i])) {
213
+ const code = lines[i];
214
+ push(
215
+ <text key={`fc${i}`} fg={theme.colors.link}>
216
+ {` ${code}` || " "}
217
+ </text>,
218
+ );
219
+ i += 1;
220
+ if (emitted >= maxLines) break;
221
+ }
222
+ i += 1; // closing fence
223
+ continue;
224
+ }
225
+
226
+ // Table: a pipe row whose successor is a separator row.
227
+ if (
228
+ line.trim().startsWith("|") &&
229
+ i + 1 < lines.length &&
230
+ isSeparatorRow(lines[i + 1])
231
+ ) {
232
+ const rows: string[][] = [splitRow(line)];
233
+ i += 2;
234
+ while (i < lines.length && lines[i].trim().startsWith("|")) {
235
+ rows.push(splitRow(lines[i]));
236
+ i += 1;
237
+ }
238
+ for (const node of renderTable(rows, width, `tb${i}`)) push(node);
239
+ continue;
240
+ }
241
+
242
+ // Horizontal rule. Drawn, rather than left as three hyphens that read as
243
+ // a stray line of text.
244
+ if (/^\s*(---+|\*\*\*+|___+)\s*$/.test(line)) {
245
+ push(
246
+ <text key={`hr${i}`} fg={theme.colors.dim}>
247
+ {"─".repeat(Math.max(4, Math.min(width - 1, 60)))}
248
+ </text>,
249
+ );
250
+ i += 1;
251
+ continue;
252
+ }
253
+
254
+ const heading = line.match(/^(#{1,6})\s+(.*)$/);
255
+ if (heading) {
256
+ const level = heading[1].length;
257
+ push(
258
+ <text key={`h${i}`} fg={HEADING_COLORS[level - 1]}>
259
+ <strong>{heading[2]}</strong>
260
+ </text>,
261
+ );
262
+ i += 1;
263
+ continue;
264
+ }
265
+
266
+ const quote = line.match(/^\s*>\s?(.*)$/);
267
+ if (quote) {
268
+ push(
269
+ <text key={`q${i}`} fg={theme.colors.muted}>
270
+ <span fg={theme.colors.dim}>{" │ "}</span>
271
+ {inlineSpans(quote[1], `q${i}`)}
272
+ </text>,
273
+ );
274
+ i += 1;
275
+ continue;
276
+ }
277
+
278
+ const list = line.match(/^(\s*)([-*+]|\d+\.)\s+(.*)$/);
279
+ if (list) {
280
+ const indent = " ".repeat(Math.min(list[1].length, 8));
281
+ const bullet = /\d/.test(list[2]) ? list[2] : "•";
282
+ push(
283
+ <text key={`li${i}`} fg={theme.colors.muted}>
284
+ {` ${indent}`}
285
+ <span fg={theme.colors.dim}>{bullet} </span>
286
+ {inlineSpans(list[3], `li${i}`)}
287
+ </text>,
288
+ );
289
+ i += 1;
290
+ continue;
291
+ }
292
+
293
+ push(
294
+ <text key={`p${i}`} fg={theme.colors.muted}>
295
+ {line ? inlineSpans(line, `p${i}`) : " "}
296
+ </text>,
297
+ );
298
+ i += 1;
299
+ }
300
+
301
+ return { nodes, withheld: Math.max(0, lines.length - i) };
302
+ }
@@ -0,0 +1,149 @@
1
+ import type React from "react";
2
+ import type { McpServer } from "../../types/index.js";
3
+ import { theme } from "../theme.js";
4
+
5
+ // ─── List item types ───────────────────────────────────────────────────────────
6
+
7
+ export type McpListItem =
8
+ | { kind: "category"; label: string }
9
+ | { kind: "server"; server: McpServer; isInstalled: boolean };
10
+
11
+ // ─── Row renderers ─────────────────────────────────────────────────────────────
12
+
13
+ export function renderMcpRow(
14
+ item: McpListItem,
15
+ _index: number,
16
+ isSelected: boolean,
17
+ ): React.ReactNode {
18
+ if (item.kind === "category") {
19
+ return (
20
+ <text fg={theme.selection.bg}>
21
+ <strong>
22
+ {"▸ "}
23
+ {item.label}
24
+ </strong>
25
+ </text>
26
+ );
27
+ }
28
+
29
+ const { server, isInstalled } = item;
30
+ const icon = isInstalled ? "●" : "○";
31
+ const iconColor = isInstalled ? theme.colors.success : theme.colors.muted;
32
+
33
+ if (isSelected) {
34
+ return (
35
+ <text bg={theme.selection.bg} fg={theme.selection.fg}>
36
+ {" "}
37
+ {icon} {server.name}{" "}
38
+ </text>
39
+ );
40
+ }
41
+
42
+ return (
43
+ <text fg={theme.colors.text}>
44
+ <span fg={iconColor}>{icon}</span>
45
+ <span fg={theme.colors.text}> {server.name}</span>
46
+ {server.requiresConfig ? <span fg={theme.colors.warning}> ⚙</span> : null}
47
+ </text>
48
+ );
49
+ }
50
+
51
+ // ─── Detail renderer ───────────────────────────────────────────────────────────
52
+
53
+ export function renderMcpDetail(
54
+ item: McpListItem | undefined,
55
+ loading: boolean,
56
+ ): React.ReactNode {
57
+ if (loading) {
58
+ return <text fg={theme.colors.muted}>Loading MCP servers...</text>;
59
+ }
60
+
61
+ if (!item || item.kind === "category") {
62
+ return (
63
+ <box
64
+ flexDirection="column"
65
+ alignItems="center"
66
+ justifyContent="center"
67
+ flexGrow={1}
68
+ >
69
+ <text fg={theme.colors.muted}>Select a server to see details</text>
70
+ </box>
71
+ );
72
+ }
73
+
74
+ const { server, isInstalled } = item;
75
+
76
+ return (
77
+ <box flexDirection="column">
78
+ <box marginBottom={1}>
79
+ <text fg={theme.colors.info}>
80
+ <strong>
81
+ {"⚡ "}
82
+ {server.name}
83
+ </strong>
84
+ </text>
85
+ {server.requiresConfig ? (
86
+ <text fg={theme.colors.warning}> ⚙</text>
87
+ ) : null}
88
+ </box>
89
+
90
+ <text fg={theme.colors.muted}>{server.description}</text>
91
+
92
+ <box marginTop={1} flexDirection="column">
93
+ <box>
94
+ <text fg={theme.colors.muted}>{"Status "}</text>
95
+ {isInstalled ? (
96
+ <text fg={theme.colors.success}>{"● Installed"}</text>
97
+ ) : (
98
+ <text fg={theme.colors.muted}>{"○ Not installed"}</text>
99
+ )}
100
+ </box>
101
+ <box>
102
+ <text fg={theme.colors.muted}>{"Type "}</text>
103
+ <text fg={theme.colors.text}>
104
+ {server.type === "http" ? "HTTP" : "Command"}
105
+ </text>
106
+ </box>
107
+ {server.type === "http" ? (
108
+ <box>
109
+ <text fg={theme.colors.muted}>{"URL "}</text>
110
+ <text fg={theme.colors.link}>{server.url}</text>
111
+ </box>
112
+ ) : (
113
+ <box>
114
+ <text fg={theme.colors.muted}>{"Command "}</text>
115
+ <text fg={theme.colors.info}>{server.command}</text>
116
+ </box>
117
+ )}
118
+ {server.requiresConfig ? (
119
+ <box>
120
+ <text fg={theme.colors.muted}>{"Config "}</text>
121
+ <text fg={theme.colors.warning}>
122
+ {server.configFields?.length ?? 0} fields required
123
+ </text>
124
+ </box>
125
+ ) : null}
126
+ </box>
127
+
128
+ <box marginTop={2}>
129
+ {isInstalled ? (
130
+ <box>
131
+ <text bg={theme.colors.danger} fg={theme.hints.fg}>
132
+ {" "}
133
+ Enter{" "}
134
+ </text>
135
+ <text fg={theme.colors.muted}> Remove server</text>
136
+ </box>
137
+ ) : (
138
+ <box>
139
+ <text bg={theme.colors.success} fg={theme.hints.fg}>
140
+ {" "}
141
+ Enter{" "}
142
+ </text>
143
+ <text fg={theme.colors.muted}> Install server</text>
144
+ </box>
145
+ )}
146
+ </box>
147
+ </box>
148
+ );
149
+ }