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,159 @@
1
+ import React, { useState } from "react";
2
+ import { useKeyboard } from "../../hooks/useKeyboard.js";
3
+ import { useApp } from "../../state/AppContext.js";
4
+ import { ConfirmModal } from "./ConfirmModal.js";
5
+ import { InputModal } from "./InputModal.js";
6
+ import { LoadingModal } from "./LoadingModal.js";
7
+ import { MessageModal } from "./MessageModal.js";
8
+ import { SelectModal } from "./SelectModal.js";
9
+
10
+ /**
11
+ * Container that renders the active modal as an overlay
12
+ * Handles ALL keyboard events when a modal is open to avoid
13
+ * conflicts with multiple useKeyboard hooks in child components
14
+ *
15
+ * Input modal: Enter/Escape handled by OpenTUI <input> onSubmit + ModalContainer escape
16
+ * Select modal: keyboard fully handled here (index tracking + Enter/arrows)
17
+ */
18
+ export function ModalContainer() {
19
+ const { state } = useApp();
20
+ const { modal } = state;
21
+
22
+ // Track select modal index here (lifted from SelectModal)
23
+ const [selectIndex, setSelectIndex] = useState(0);
24
+
25
+ // Reset select index when modal changes
26
+ const modalRef = React.useRef(modal);
27
+ if (modal !== modalRef.current) {
28
+ modalRef.current = modal;
29
+ if (modal?.type === "select") {
30
+ setSelectIndex(modal.defaultIndex ?? 0);
31
+ }
32
+ }
33
+
34
+ // Handle keyboard events for modals
35
+ useKeyboard((key) => {
36
+ if (!modal) return;
37
+ if (modal.type === "loading") return;
38
+
39
+ // Escape — close any modal
40
+ if (key.name === "escape") {
41
+ if (modal.type === "confirm") modal.onCancel();
42
+ else if (modal.type === "input") modal.onCancel();
43
+ else if (modal.type === "select") modal.onCancel();
44
+ else if (modal.type === "message") modal.onDismiss();
45
+ return;
46
+ }
47
+
48
+ // 'q' to close — but NOT for input modals (need to type 'q')
49
+ if (key.name === "q" && modal.type !== "input") {
50
+ if (modal.type === "confirm") modal.onCancel();
51
+ else if (modal.type === "select") modal.onCancel();
52
+ else if (modal.type === "message") modal.onDismiss();
53
+ return;
54
+ }
55
+
56
+ // Input modal — let OpenTUI <input> handle Enter via onSubmit
57
+ if (modal.type === "input") {
58
+ return;
59
+ }
60
+
61
+ // Select modal — handle navigation and selection
62
+ if (modal.type === "select") {
63
+ if (key.name === "return" || key.name === "enter") {
64
+ modal.onSelect(modal.options[selectIndex].value);
65
+ } else if (key.name === "up" || key.name === "k") {
66
+ setSelectIndex((prev) => Math.max(0, prev - 1));
67
+ } else if (key.name === "down" || key.name === "j") {
68
+ setSelectIndex((prev) => Math.min(modal.options.length - 1, prev + 1));
69
+ }
70
+ return;
71
+ }
72
+
73
+ // Message modal — Enter to dismiss
74
+ if (modal.type === "message") {
75
+ if (key.name === "return" || key.name === "enter") {
76
+ modal.onDismiss();
77
+ }
78
+ }
79
+
80
+ // Confirm modal — Enter to confirm
81
+ if (modal.type === "confirm") {
82
+ if (key.name === "return" || key.name === "enter") {
83
+ modal.onConfirm();
84
+ }
85
+ }
86
+ });
87
+
88
+ if (!modal) {
89
+ return null;
90
+ }
91
+
92
+ const renderModal = () => {
93
+ switch (modal.type) {
94
+ case "confirm":
95
+ return (
96
+ <ConfirmModal
97
+ title={modal.title}
98
+ message={modal.message}
99
+ onConfirm={modal.onConfirm}
100
+ onCancel={modal.onCancel}
101
+ />
102
+ );
103
+
104
+ case "input":
105
+ return (
106
+ <InputModal
107
+ title={modal.title}
108
+ label={modal.label}
109
+ defaultValue={modal.defaultValue}
110
+ onSubmit={modal.onSubmit}
111
+ onCancel={modal.onCancel}
112
+ />
113
+ );
114
+
115
+ case "select":
116
+ return (
117
+ <SelectModal
118
+ title={modal.title}
119
+ message={modal.message}
120
+ options={modal.options}
121
+ defaultIndex={selectIndex}
122
+ onSelect={modal.onSelect}
123
+ onCancel={modal.onCancel}
124
+ />
125
+ );
126
+
127
+ case "message":
128
+ return (
129
+ <MessageModal
130
+ title={modal.title}
131
+ message={modal.message}
132
+ variant={modal.variant}
133
+ onDismiss={modal.onDismiss}
134
+ />
135
+ );
136
+
137
+ case "loading":
138
+ return <LoadingModal message={modal.message} />;
139
+
140
+ default:
141
+ return null;
142
+ }
143
+ };
144
+
145
+ // Center the modal on screen
146
+ return (
147
+ <box
148
+ position="absolute"
149
+ width="100%"
150
+ height="100%"
151
+ justifyContent="center"
152
+ alignItems="center"
153
+ >
154
+ {renderModal()}
155
+ </box>
156
+ );
157
+ }
158
+
159
+ export default ModalContainer;
@@ -0,0 +1,90 @@
1
+ import React from "react";
2
+ import { useDimensions } from "../../state/DimensionsContext.js";
3
+ import type { SelectOption } from "../../state/types.js";
4
+ import { theme } from "../../theme.js";
5
+
6
+ interface SelectModalProps {
7
+ /** Modal title */
8
+ title: string;
9
+ /** Modal message */
10
+ message: string;
11
+ /** Select options */
12
+ options: SelectOption[];
13
+ /** Currently selected index (controlled by ModalContainer) */
14
+ defaultIndex?: number;
15
+ /** Callback when option selected */
16
+ onSelect: (value: string) => void;
17
+ /** Callback when cancelled */
18
+ onCancel: () => void;
19
+ }
20
+
21
+ export function SelectModal({
22
+ title,
23
+ message,
24
+ options,
25
+ defaultIndex,
26
+ onSelect: _onSelect,
27
+ onCancel: _onCancel,
28
+ }: SelectModalProps) {
29
+ // Keyboard handling is done by ModalContainer
30
+ // defaultIndex is the live selectedIndex from ModalContainer state
31
+ const selectedIndex = defaultIndex ?? 0;
32
+
33
+ // Size to terminal width: 60% of available, clamped to [50, 90].
34
+ // Previously hardcoded at 50, which forced descriptions and long item
35
+ // labels (e.g. plugin specs) to wrap mid-sentence and produce orphan
36
+ // punctuation lines.
37
+ const dimensions = useDimensions();
38
+ const width = Math.max(
39
+ 50,
40
+ Math.min(90, Math.floor(dimensions.terminalWidth * 0.6)),
41
+ );
42
+
43
+ return (
44
+ <box
45
+ flexDirection="column"
46
+ border
47
+ borderStyle="rounded"
48
+ borderColor={theme.colors.dim}
49
+ backgroundColor={theme.surface.bg}
50
+ paddingLeft={3}
51
+ paddingRight={3}
52
+ paddingTop={1}
53
+ paddingBottom={1}
54
+ width={width}
55
+ >
56
+ <box marginBottom={1}>
57
+ <text fg={theme.colors.text}>
58
+ <strong>{title}</strong>
59
+ </text>
60
+ </box>
61
+
62
+ <box marginBottom={1}>
63
+ <text fg={theme.colors.muted}>{message}</text>
64
+ </box>
65
+
66
+ <box flexDirection="column" paddingLeft={1}>
67
+ {options.map((option, idx) => {
68
+ const isSelected = idx === selectedIndex;
69
+ return (
70
+ <text
71
+ key={option.value}
72
+ fg={isSelected ? theme.colors.text : theme.colors.muted}
73
+ >
74
+ <span fg={isSelected ? theme.colors.text : theme.colors.muted}>
75
+ {isSelected ? "❯ " : " "}
76
+ </span>
77
+ {isSelected ? <strong>{option.label}</strong> : option.label}
78
+ </text>
79
+ );
80
+ })}
81
+ </box>
82
+
83
+ <box marginTop={1}>
84
+ <text fg={theme.colors.muted}>↑↓ Select • ↵ Confirm • Esc Cancel</text>
85
+ </box>
86
+ </box>
87
+ );
88
+ }
89
+
90
+ export default SelectModal;
@@ -0,0 +1,6 @@
1
+ export { ConfirmModal } from "./ConfirmModal.js";
2
+ export { InputModal } from "./InputModal.js";
3
+ export { SelectModal } from "./SelectModal.js";
4
+ export { MessageModal } from "./MessageModal.js";
5
+ export { LoadingModal } from "./LoadingModal.js";
6
+ export { ModalContainer } from "./ModalContainer.js";
@@ -0,0 +1,44 @@
1
+ import React from "react";
2
+ import { theme } from "../../theme.js";
3
+
4
+ export interface Hint {
5
+ key: string;
6
+ label: string;
7
+ tone?: "default" | "primary" | "danger";
8
+ }
9
+
10
+ interface ActionHintsProps {
11
+ hints: Hint[];
12
+ }
13
+
14
+ /**
15
+ * Keyboard shortcut badges for detail panels.
16
+ * Renders: [bg] key [/bg] label
17
+ */
18
+ export function ActionHints({ hints }: ActionHintsProps) {
19
+ return (
20
+ <box flexDirection="column" marginTop={1}>
21
+ {hints.map((hint) => (
22
+ // A box with no flexDirection lays out as a column here, which put
23
+ // every key chip on its own line above its label. The chip and what
24
+ // it does are one unit — the footer hints already render that way.
25
+ <box key={`${hint.key}:${hint.label}`} flexDirection="row">
26
+ <text
27
+ bg={
28
+ hint.tone === "danger"
29
+ ? theme.hints.dangerBg
30
+ : hint.tone === "primary"
31
+ ? theme.hints.primaryBg
32
+ : theme.hints.defaultBg
33
+ }
34
+ fg={theme.hints.fg}
35
+ >
36
+ {" "}
37
+ {hint.key}{" "}
38
+ </text>
39
+ <text fg={theme.colors.muted}> {hint.label}</text>
40
+ </box>
41
+ ))}
42
+ </box>
43
+ );
44
+ }
@@ -0,0 +1,23 @@
1
+ import type React from "react";
2
+ import { theme } from "../../theme.js";
3
+
4
+ interface DetailSectionProps {
5
+ title?: string;
6
+ children: React.ReactNode;
7
+ }
8
+
9
+ /**
10
+ * A labeled section in a detail panel with optional title header.
11
+ */
12
+ export function DetailSection({ title, children }: DetailSectionProps) {
13
+ return (
14
+ <box flexDirection="column" marginTop={1}>
15
+ {title ? (
16
+ <text fg={theme.colors.info}>
17
+ <strong>{title}</strong>
18
+ </text>
19
+ ) : null}
20
+ {children}
21
+ </box>
22
+ );
23
+ }
@@ -0,0 +1,26 @@
1
+ import type React from "react";
2
+ import { theme } from "../../theme.js";
3
+
4
+ interface KeyValueLineProps {
5
+ label: string;
6
+ value: React.ReactNode;
7
+ }
8
+
9
+ /**
10
+ * Aligned label-value pair for detail panels.
11
+ *
12
+ * Labels pad to a 10-column gutter. The trailing space is inside the padding,
13
+ * not appended after it, so a label at or past 10 columns still separates from
14
+ * its value instead of running into it: every caller used to pass a short fixed
15
+ * label, and the first arbitrary one — a skill's `disable-model-invocation`
16
+ * frontmatter key — rendered as `disable-model-invocationtrue`. Labels of 9
17
+ * columns or fewer are unaffected.
18
+ */
19
+ export function KeyValueLine({ label, value }: KeyValueLineProps) {
20
+ return (
21
+ <text fg={theme.colors.text}>
22
+ <span fg={theme.colors.muted}>{`${label} `.padEnd(10)}</span>
23
+ {value}
24
+ </text>
25
+ );
26
+ }
@@ -0,0 +1,45 @@
1
+ import React from "react";
2
+ import { theme } from "../../theme.js";
3
+ import { SelectableRow } from "./SelectableRow.js";
4
+
5
+ interface ListCategoryRowProps {
6
+ title: string;
7
+ expanded?: boolean;
8
+ count?: number;
9
+ badge?: string;
10
+ tone?: keyof typeof theme.category;
11
+ selected: boolean;
12
+ /**
13
+ * Whether the row can be opened and closed. Default true, which is what
14
+ * every collapsible list wants. Set false for a heading that is purely a
15
+ * label — a ▶ on a row that does not respond to Enter reads as a control
16
+ * that is broken rather than as a section title.
17
+ */
18
+ expandable?: boolean;
19
+ }
20
+
21
+ export function ListCategoryRow({
22
+ title,
23
+ expanded = false,
24
+ count,
25
+ badge,
26
+ tone = "gray",
27
+ selected,
28
+ expandable = true,
29
+ }: ListCategoryRowProps) {
30
+ const colors = theme.category[tone];
31
+ const marker = expandable ? (expanded ? "▼ " : "▶ ") : "";
32
+ const label = `${marker}${title}${count !== undefined ? ` (${count})` : ""}`;
33
+
34
+ return (
35
+ <SelectableRow selected={selected}>
36
+ {!selected && (
37
+ <span bg={colors.bg} fg={colors.fg}>
38
+ <strong> {label} </strong>
39
+ </span>
40
+ )}
41
+ {selected && <strong> {label} </strong>}
42
+ {!selected && badge ? <span fg={colors.badgeFg}> {badge}</span> : null}
43
+ </SelectableRow>
44
+ );
45
+ }
@@ -0,0 +1,40 @@
1
+ import React from "react";
2
+ import { theme } from "../../theme.js";
3
+
4
+ interface MetaTextProps {
5
+ text: string;
6
+ tone?: "muted" | "warning" | "success" | "danger";
7
+ /**
8
+ * True when this sits inside a SELECTED row. Not cosmetic.
9
+ *
10
+ * Every `theme.meta` tone is chosen against the terminal's own background,
11
+ * and none of them survives the selection fill: measured on a screenshot,
12
+ * `muted` is 1.11:1 on it, `warning` 1.07:1, `danger` 1.11:1. So a selected
13
+ * row's `(default)`, `(new)`, `(needed)` and version strings were drawn in a
14
+ * colour indistinguishable from the purple behind them.
15
+ *
16
+ * On a selected row the tone's HUE is surrendered — the selection owns the
17
+ * colour channel there — and only the emphasis survives: `fg` for a tone
18
+ * that was saying something, `dim` for one that was only muttering. The
19
+ * label is words, so its meaning does not depend on the hue.
20
+ */
21
+ selected?: boolean;
22
+ }
23
+
24
+ /**
25
+ * Subdued text for versions, stars, status indicators.
26
+ */
27
+ export function MetaText({
28
+ text,
29
+ tone = "muted",
30
+ selected = false,
31
+ }: MetaTextProps) {
32
+ if (selected) {
33
+ return (
34
+ <span fg={tone === "muted" ? theme.selection.dim : theme.selection.fg}>
35
+ {text}
36
+ </span>
37
+ );
38
+ }
39
+ return <span fg={theme.meta[tone]}>{text}</span>;
40
+ }
@@ -0,0 +1,67 @@
1
+ import React from "react";
2
+ import { theme } from "../../theme.js";
3
+
4
+ interface ScopeDetailScope {
5
+ user?: boolean;
6
+ project?: boolean;
7
+ local?: boolean;
8
+ }
9
+
10
+ interface ScopeDetailPaths {
11
+ user?: string;
12
+ project?: string;
13
+ local?: string;
14
+ }
15
+
16
+ interface ScopeDetailProps {
17
+ scopes: ScopeDetailScope;
18
+ paths?: ScopeDetailPaths;
19
+ }
20
+
21
+ /**
22
+ * The u/p/l badges used in detail panels.
23
+ * Renders each scope as: [bg=color] key [/bg] ● or ○ Label path
24
+ */
25
+ export function ScopeDetail({ scopes, paths }: ScopeDetailProps) {
26
+ const items = [
27
+ {
28
+ key: "u",
29
+ label: "User",
30
+ color: theme.scopes.user,
31
+ active: scopes.user,
32
+ path: paths?.user,
33
+ },
34
+ {
35
+ key: "p",
36
+ label: "Project",
37
+ color: theme.scopes.project,
38
+ active: scopes.project,
39
+ path: paths?.project,
40
+ },
41
+ {
42
+ key: "l",
43
+ label: "Local",
44
+ color: theme.scopes.local,
45
+ active: scopes.local,
46
+ path: paths?.local,
47
+ },
48
+ ].filter((i) => i.path !== undefined || i.active !== undefined);
49
+
50
+ return (
51
+ <box flexDirection="column">
52
+ {items.map((item) => (
53
+ <text key={item.key}>
54
+ <span bg={item.color} fg={theme.hints.fg}>
55
+ {" "}
56
+ {item.key}{" "}
57
+ </span>
58
+ <span fg={item.active ? item.color : theme.colors.muted}>
59
+ {item.active ? " ● " : " ○ "}
60
+ </span>
61
+ <span fg={item.color}>{item.label}</span>
62
+ {item.path && <span fg={theme.colors.muted}> {item.path}</span>}
63
+ </text>
64
+ ))}
65
+ </box>
66
+ );
67
+ }
@@ -0,0 +1,58 @@
1
+ import type React from "react";
2
+ import { scopeOffFill } from "../../theme-mode.js";
3
+ import { theme } from "../../theme.js";
4
+
5
+ interface ScopeSquaresProps {
6
+ user: boolean;
7
+ project: boolean;
8
+ local?: boolean;
9
+ selected?: boolean;
10
+ }
11
+
12
+ /** Every segment is a filled block; only the fill colour says lit or unlit. */
13
+ const SEGMENT = "■";
14
+
15
+ /**
16
+ * Scope bar: three positional segments, user / project / local.
17
+ *
18
+ * Reads as a segmented indicator — each segment lit in its scope colour, or
19
+ * unlit in a fill that recedes toward the page. Position identifies the scope;
20
+ * the first segment is always user, the second project, the third local.
21
+ *
22
+ * The unlit fill used to be `theme.colors.muted` (#6B7280), the same mid-dark
23
+ * grey as de-emphasised body text. Against a light page that reads as a *dark*
24
+ * block, giving an unlit segment the same visual weight as a lit one — "non
25
+ * installed state is dark as well - confusing". The unlit fill now comes from
26
+ * `scopeOffFill()`, which steps toward the page the resolved mode names; see
27
+ * theme-mode.ts for why that one value cannot be fixed.
28
+ */
29
+ export function ScopeSquares({
30
+ user,
31
+ project,
32
+ local,
33
+ selected = false,
34
+ }: ScopeSquaresProps) {
35
+ // On a selected row the background is our own accent, so an unlit segment
36
+ // recedes toward that block rather than toward the page.
37
+ const off = selected ? theme.selection.bg : scopeOffFill();
38
+ const lit = (on: boolean, color: string) =>
39
+ on ? (selected ? theme.selection.fg : color) : off;
40
+
41
+ const segments = [
42
+ lit(user, theme.scopes.user),
43
+ lit(project, theme.scopes.project),
44
+ ...(local !== undefined ? [lit(local, theme.scopes.local)] : []),
45
+ ];
46
+
47
+ // Fragments of <span> — safe to embed inside a coloured <text> parent.
48
+ return (
49
+ <>
50
+ {segments.map((fg, i) => (
51
+ // biome-ignore lint/suspicious/noArrayIndexKey: fixed positional segments
52
+ <span key={i} fg={fg}>
53
+ {SEGMENT}
54
+ </span>
55
+ ))}
56
+ </>
57
+ );
58
+ }
@@ -0,0 +1,30 @@
1
+ import type React from "react";
2
+ import { theme } from "../../theme.js";
3
+
4
+ interface SelectableRowProps {
5
+ selected: boolean;
6
+ indent?: number;
7
+ children: React.ReactNode;
8
+ }
9
+
10
+ export function SelectableRow({
11
+ selected,
12
+ indent = 0,
13
+ children,
14
+ }: SelectableRowProps) {
15
+ return (
16
+ <text
17
+ // Unselected: no background, so the row sits on the terminal's own page.
18
+ bg={selected ? theme.selection.bg : undefined}
19
+ // Unselected must still NAME a colour. `undefined` does not mean "the
20
+ // terminal's foreground" — OpenTUI's fallback for an unstyled cell is
21
+ // truecolor white, so every unselected row here rendered white-on-cream
22
+ // and vanished on a light theme. Same root cause as the original bug,
23
+ // reached by omission rather than by a wrong literal.
24
+ fg={selected ? theme.selection.fg : theme.colors.text}
25
+ >
26
+ {" ".repeat(indent)}
27
+ {children}
28
+ </text>
29
+ );
30
+ }
@@ -0,0 +1,9 @@
1
+ export { SelectableRow } from "./SelectableRow.js";
2
+ export { ListCategoryRow } from "./ListCategoryRow.js";
3
+ export { ScopeSquares } from "./ScopeSquares.js";
4
+ export { ScopeDetail } from "./ScopeDetail.js";
5
+ export { ActionHints } from "./ActionHints.js";
6
+ export type { Hint } from "./ActionHints.js";
7
+ export { MetaText } from "./MetaText.js";
8
+ export { KeyValueLine } from "./KeyValueLine.js";
9
+ export { DetailSection } from "./DetailSection.js";
@@ -0,0 +1,2 @@
1
+ export { useKeyboardHandler } from "./useKeyboardHandler.js";
2
+ export { useKeyboard } from "./useKeyboard.js";