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,32 @@
1
+ import React from "react";
2
+ import { theme } from "../theme.js";
3
+
4
+ interface EmptyFilterStateProps {
5
+ query: string;
6
+ entityName?: string; // "plugins", "skills", "MCP servers"
7
+ }
8
+
9
+ export function EmptyFilterState({
10
+ query,
11
+ entityName = "items",
12
+ }: EmptyFilterStateProps) {
13
+ return (
14
+ <box flexDirection="column" marginTop={2} paddingLeft={2} paddingRight={2}>
15
+ <text fg={theme.colors.warning}>
16
+ No {entityName} found for "{query}"
17
+ </text>
18
+ <box marginTop={1}>
19
+ <text fg={theme.colors.muted}>
20
+ Try a different search term, or if you think
21
+ </text>
22
+ </box>
23
+ <text fg={theme.colors.muted}>this is a bug, report it at:</text>
24
+ <box marginTop={1}>
25
+ <text fg={theme.colors.link}>github.com/MadAppGang/magus/issues</text>
26
+ </box>
27
+ <box marginTop={1}>
28
+ <text fg={theme.colors.muted}>Press Esc to clear the filter.</text>
29
+ </box>
30
+ </box>
31
+ );
32
+ }
@@ -0,0 +1,63 @@
1
+ import React from "react";
2
+ import { type UiColor, theme } from "../theme.js";
3
+
4
+ /**
5
+ * Scope indicator for list items.
6
+ * Shows colored letter badges when installed, empty space when not.
7
+ *
8
+ * Installed in user+project: u p
9
+ * Installed in project only: p
10
+ * Not installed: (3 spaces)
11
+ *
12
+ * Colors match the detail panel: cyan=user, green=project, yellow=local
13
+ */
14
+
15
+ export function scopeIndicatorText(
16
+ user?: boolean,
17
+ project?: boolean,
18
+ local?: boolean,
19
+ ): {
20
+ text: string;
21
+ segments: Array<{ char: string; bg: UiColor; fg: UiColor }>;
22
+ } {
23
+ const segments: Array<{ char: string; bg: UiColor; fg: UiColor }> = [
24
+ user
25
+ ? { char: "u", bg: theme.colors.info, fg: theme.hints.fg }
26
+ : { char: " ", bg: "", fg: "" },
27
+ project
28
+ ? { char: "p", bg: theme.colors.success, fg: theme.hints.fg }
29
+ : { char: " ", bg: "", fg: "" },
30
+ local
31
+ ? { char: "l", bg: theme.colors.warning, fg: theme.hints.fg }
32
+ : { char: " ", bg: "", fg: "" },
33
+ ];
34
+
35
+ const text = segments.map((s) => s.char).join("");
36
+ return { text, segments };
37
+ }
38
+
39
+ interface ScopeIndicatorProps {
40
+ user?: boolean;
41
+ project?: boolean;
42
+ local?: boolean;
43
+ }
44
+
45
+ export function ScopeIndicator({ user, project, local }: ScopeIndicatorProps) {
46
+ const { segments } = scopeIndicatorText(user, project, local);
47
+
48
+ return (
49
+ <text fg={theme.colors.text}>
50
+ {segments.map((s, i) =>
51
+ s.bg ? (
52
+ // biome-ignore lint/suspicious/noArrayIndexKey: segments are fixed-position scope squares (user/project/local); the slot is the identity and the list never reorders
53
+ <span key={i} bg={s.bg} fg={s.fg}>
54
+ {s.char}
55
+ </span>
56
+ ) : (
57
+ // biome-ignore lint/suspicious/noArrayIndexKey: same fixed-slot list as above
58
+ <span key={i}> </span>
59
+ ),
60
+ )}
61
+ </text>
62
+ );
63
+ }
@@ -0,0 +1,95 @@
1
+ import type React from "react";
2
+ import { useEffect, useMemo, useState } from "react";
3
+ import { theme } from "../theme.js";
4
+
5
+ interface ScrollableListProps<T> {
6
+ /** Array of items to display */
7
+ items: T[];
8
+ /** Currently selected index */
9
+ selectedIndex: number;
10
+ /** Render function for each item */
11
+ renderItem: (item: T, index: number, isSelected: boolean) => React.ReactNode;
12
+ /** Maximum visible height (number of lines) */
13
+ maxHeight: number;
14
+ /** Show scroll indicators */
15
+ showScrollIndicators?: boolean;
16
+ /** Item key extractor */
17
+ getKey?: (item: T, index: number) => string;
18
+ }
19
+
20
+ export function ScrollableList<T>({
21
+ items,
22
+ selectedIndex,
23
+ renderItem,
24
+ maxHeight,
25
+ showScrollIndicators = true,
26
+ getKey,
27
+ }: ScrollableListProps<T>) {
28
+ const [scrollOffset, setScrollOffset] = useState(0);
29
+
30
+ const hasItemsAbove = scrollOffset > 0;
31
+ const hasItemsBelow = scrollOffset + maxHeight < items.length;
32
+ const indicatorLines =
33
+ (showScrollIndicators && hasItemsAbove ? 1 : 0) +
34
+ (showScrollIndicators && hasItemsBelow ? 1 : 0);
35
+ const effectiveMaxHeight = Math.max(1, maxHeight - indicatorLines);
36
+
37
+ // Adjust scroll offset to keep selected item visible
38
+ useEffect(() => {
39
+ if (selectedIndex < scrollOffset) {
40
+ setScrollOffset(selectedIndex);
41
+ } else if (selectedIndex >= scrollOffset + effectiveMaxHeight) {
42
+ setScrollOffset(selectedIndex - effectiveMaxHeight + 1);
43
+ }
44
+ }, [selectedIndex, effectiveMaxHeight, scrollOffset]);
45
+
46
+ // Reset scroll when items change drastically (e.g. search)
47
+ useEffect(() => {
48
+ if (scrollOffset >= items.length) {
49
+ setScrollOffset(Math.max(0, items.length - effectiveMaxHeight));
50
+ }
51
+ }, [items.length, scrollOffset, effectiveMaxHeight]);
52
+
53
+ const visibleItems = useMemo(() => {
54
+ const start = scrollOffset;
55
+ const end = Math.min(scrollOffset + effectiveMaxHeight, items.length);
56
+ return items.slice(start, end).map((item, idx) => ({
57
+ item,
58
+ originalIndex: start + idx,
59
+ }));
60
+ }, [items, scrollOffset, effectiveMaxHeight]);
61
+
62
+ const itemsBelow = items.length - scrollOffset - effectiveMaxHeight;
63
+
64
+ return (
65
+ <box flexDirection="column">
66
+ {showScrollIndicators && hasItemsAbove && (
67
+ <text fg={theme.colors.info}>↑ {scrollOffset} more</text>
68
+ )}
69
+
70
+ {visibleItems.map(({ item, originalIndex }) => (
71
+ <box
72
+ key={getKey ? getKey(item, originalIndex) : `${originalIndex}`}
73
+ width="100%"
74
+ // height=1 is load-bearing, not cosmetic. overflow="hidden" alone clips
75
+ // CONTENT but lets the box GROW: an over-wide <text> wraps to a second
76
+ // line, the row becomes two lines tall, and every item below shifts —
77
+ // which rendered as a phantom blank line under "Magus Marketing" and
78
+ // the last two plugins composited onto one row. The list's scroll math
79
+ // assumes one line per item; this makes that assumption true.
80
+ height={1}
81
+ flexShrink={0}
82
+ overflow="hidden"
83
+ >
84
+ {renderItem(item, originalIndex, originalIndex === selectedIndex)}
85
+ </box>
86
+ ))}
87
+
88
+ {showScrollIndicators && hasItemsBelow && (
89
+ <text fg={theme.colors.info}>↓ {itemsBelow} more</text>
90
+ )}
91
+ </box>
92
+ );
93
+ }
94
+
95
+ export default ScrollableList;
@@ -0,0 +1,57 @@
1
+ import React from "react";
2
+ import { useKeyboardHandler } from "../hooks/useKeyboardHandler.js";
3
+ import { theme } from "../theme.js";
4
+
5
+ interface SearchInputProps {
6
+ /** Current search value */
7
+ value: string;
8
+ /** Called when value changes */
9
+ onChange: (value: string) => void;
10
+ /** Placeholder text when empty */
11
+ placeholder?: string;
12
+ /** Whether the input is focused/active */
13
+ isActive: boolean;
14
+ /** Called when user presses escape to exit search */
15
+ onExit?: () => void;
16
+ /** Called when user presses enter */
17
+ onSubmit?: () => void;
18
+ }
19
+
20
+ export function SearchInput({
21
+ value,
22
+ onChange,
23
+ placeholder = "Search...",
24
+ isActive,
25
+ onExit,
26
+ onSubmit,
27
+ }: SearchInputProps) {
28
+ // Handle keyboard shortcuts when active
29
+ useKeyboardHandler((_input, key) => {
30
+ if (!isActive) return;
31
+
32
+ if (key.escape) {
33
+ onExit?.();
34
+ return;
35
+ }
36
+
37
+ if (key.return) {
38
+ onSubmit?.();
39
+ return;
40
+ }
41
+ });
42
+
43
+ return (
44
+ <box flexDirection="row">
45
+ <text fg={theme.colors.info}>❯ </text>
46
+ <input
47
+ value={value}
48
+ onChange={onChange}
49
+ placeholder={placeholder}
50
+ focused={isActive}
51
+ width={40}
52
+ />
53
+ </box>
54
+ );
55
+ }
56
+
57
+ export default SearchInput;
@@ -0,0 +1,75 @@
1
+ import type React from "react";
2
+ import { theme } from "../theme.js";
3
+
4
+ interface StyledTextProps {
5
+ /** Text content */
6
+ children: React.ReactNode;
7
+ /** Bold text */
8
+ bold?: boolean;
9
+ /** Foreground color */
10
+ color?: string;
11
+ /** Background color */
12
+ bgColor?: string;
13
+ /** Dimmed text */
14
+ dim?: boolean;
15
+ /** Italic text */
16
+ italic?: boolean;
17
+ /** Underlined text */
18
+ underline?: boolean;
19
+ /** Inverse colors */
20
+ inverse?: boolean;
21
+ }
22
+
23
+ /**
24
+ * StyledText - Utility component to simplify text styling with OpenTUI
25
+ *
26
+ * Converts props to nested tag pattern required by OpenTUI.
27
+ *
28
+ * Example:
29
+ * <StyledText bold color={theme.colors.success}>Success</StyledText>
30
+ * becomes: <text fg={theme.colors.success}><strong>Success</strong></text>
31
+ */
32
+ export function StyledText({
33
+ children,
34
+ bold = false,
35
+ color,
36
+ bgColor,
37
+ dim = false,
38
+ italic = false,
39
+ underline = false,
40
+ inverse = false,
41
+ }: StyledTextProps) {
42
+ let content: React.ReactNode = children;
43
+
44
+ // Apply text modifiers (innermost to outermost)
45
+ if (bold) {
46
+ content = <strong>{content}</strong>;
47
+ }
48
+ if (italic) {
49
+ content = <em>{content}</em>;
50
+ }
51
+ if (underline) {
52
+ content = <u>{content}</u>;
53
+ }
54
+ // Note: dim and inverse use span with colors since OpenTUI doesn't have these as React elements
55
+ if (dim) {
56
+ content = <span fg={theme.colors.muted}>{content}</span>;
57
+ }
58
+ if (inverse) {
59
+ // Inverse effect approximated with contrasting colors
60
+ content = (
61
+ <span fg={theme.hints.fg} bg={theme.colors.text}>
62
+ {content}
63
+ </span>
64
+ );
65
+ }
66
+
67
+ // Apply colors (outer layer)
68
+ const textProps: { fg?: string; bg?: string } = {};
69
+ if (color) textProps.fg = color;
70
+ if (bgColor) textProps.bg = bgColor;
71
+
72
+ return <text {...textProps}>{content}</text>;
73
+ }
74
+
75
+ export default StyledText;
@@ -0,0 +1,101 @@
1
+ import React from "react";
2
+ import { useDimensions } from "../state/DimensionsContext.js";
3
+ import type { Screen } from "../state/types.js";
4
+ import { theme } from "../theme.js";
5
+
6
+ interface Tab {
7
+ key: string;
8
+ label: string;
9
+ screen: Screen;
10
+ }
11
+
12
+ export const TABS: Tab[] = [
13
+ { key: "1", label: "Plugins", screen: "plugins" },
14
+ { key: "2", label: "Skills", screen: "skills" },
15
+ { key: "3", label: "MCP", screen: "mcp" },
16
+ { key: "4", label: "Settings", screen: "settings" },
17
+ { key: "5", label: "Profiles", screen: "profiles" },
18
+ { key: "6", label: "CLI", screen: "cli-tools" },
19
+ { key: "7", label: "Git State", screen: "gitignore" },
20
+ { key: "8", label: "Alias", screen: "alias" },
21
+ { key: "9", label: "Styles", screen: "styles" },
22
+ // Last, and keyed "0" because the digits ran out. The number row reads
23
+ // 1…9 then 0, so a tenth tab keyed 0 sits under the finger that follows 9 —
24
+ // which is the only reason the order here and the order on the keyboard
25
+ // agree.
26
+ { key: "0", label: "Models", screen: "models" },
27
+ ];
28
+
29
+ /** Columns one tab occupies: the text plus its one-space padding each side. */
30
+ function cellWidth(text: string): number {
31
+ return text.length + 2;
32
+ }
33
+
34
+ /** Columns the whole bar occupies: every cell plus the separators between. */
35
+ export function barWidth(texts: string[]): number {
36
+ if (texts.length === 0) return 0;
37
+ return texts.reduce((n, t) => n + cellWidth(t), 0) + (texts.length - 1);
38
+ }
39
+
40
+ /**
41
+ * Choose what each tab shows for the space available.
42
+ *
43
+ * The full bar is ~110 columns at ten tabs, so on a standard 100-column window
44
+ * it overflows and OpenTUI clips each cell — producing a row of truncated stubs
45
+ * ("1:", "4:", "7:Git ") that names nothing. Dropping the inactive labels keeps
46
+ * the bar honest: the number is what you press, and the one tab whose name
47
+ * matters is the one you are on. Compacted, ten tabs need ~46 columns.
48
+ */
49
+ export function layoutTabs(
50
+ tabs: Tab[],
51
+ currentScreen: Screen,
52
+ available: number,
53
+ ): string[] {
54
+ const full = tabs.map((tab) => `${tab.key}:${tab.label}`);
55
+ if (barWidth(full) <= available) return full;
56
+ return tabs.map((tab) =>
57
+ tab.screen === currentScreen ? `${tab.key}:${tab.label}` : tab.key,
58
+ );
59
+ }
60
+
61
+ interface TabBarProps {
62
+ currentScreen: Screen;
63
+ }
64
+
65
+ export function TabBar({ currentScreen }: TabBarProps) {
66
+ const dimensions = useDimensions();
67
+ // ScreenLayout pads the bar by 1 each side, inside a container padded by 1.
68
+ const available = Math.max(10, dimensions.terminalWidth - 4);
69
+ const texts = layoutTabs(TABS, currentScreen, available);
70
+
71
+ return (
72
+ <box flexDirection="row" gap={0}>
73
+ {TABS.map((tab, index) => {
74
+ const isSelected = tab.screen === currentScreen;
75
+ const isLast = index === TABS.length - 1;
76
+ const text = texts[index];
77
+
78
+ return (
79
+ <box key={tab.key} flexDirection="row">
80
+ {/* Tab content */}
81
+ {isSelected ? (
82
+ <box>
83
+ <text bg={theme.colors.accent} fg={theme.hints.fg}>
84
+ <strong> {text} </strong>
85
+ </text>
86
+ </box>
87
+ ) : (
88
+ <box>
89
+ <text fg={theme.colors.muted}> {text} </text>
90
+ </box>
91
+ )}
92
+ {/* Separator */}
93
+ {!isLast && <text fg={theme.colors.muted}>│</text>}
94
+ </box>
95
+ );
96
+ })}
97
+ </box>
98
+ );
99
+ }
100
+
101
+ export default TabBar;
@@ -0,0 +1,78 @@
1
+ import type React from "react";
2
+ import { theme } from "../../theme.js";
3
+
4
+ /** One footer hint: a key (or key group) and the action it performs. */
5
+ export interface FooterHint {
6
+ /** Key(s) for this action, e.g. ["↑", "↓"] or ["U"]. Joined by `joinKeys`. */
7
+ keys: string[];
8
+ label: string;
9
+ }
10
+
11
+ /**
12
+ * Render a key group as one chip's text.
13
+ *
14
+ * Single-character keys butt together, because `↑↓` is how an arrow pair is written
15
+ * everywhere and a separator there would be noise. Anything longer needs a gap, or the
16
+ * names run into each other and read as one key that does not exist — MEASURED on screen:
17
+ * `["Enter", "a"]` rendered as `Entera`, and the layout's own scroll hint
18
+ * `["^U/^D", "PgUp/Dn"]` rendered as `^U/^DPgUp/Dn` on every screen in the app.
19
+ *
20
+ * A space rather than a slash, because several key names already contain one.
21
+ */
22
+ export function joinKeys(keys: string[]): string {
23
+ return keys.every((k) => [...k].length === 1)
24
+ ? keys.join("")
25
+ : keys.join(" ");
26
+ }
27
+
28
+ // The shared chip palette, not a local one. This file used to define its own
29
+ // (`theme.colors.dim` block + terminal-foreground ink), which is why the footer
30
+ // stayed a low-contrast grey-on-grey strip after the palette was made vivid —
31
+ // the new colours simply never reached it. Anything chip-shaped uses theme.hints.
32
+ const KEY_BG = theme.hints.defaultBg;
33
+ const KEY_FG = theme.hints.fg;
34
+ const LABEL_FG = theme.colors.muted;
35
+ const GAP = " ";
36
+
37
+ /**
38
+ * Render a row of footer hints as key badges + labels:
39
+ *
40
+ * [↑↓] move [U] update [/] search
41
+ *
42
+ * Each key is shown in a chip so it reads as a pressable key, not prose.
43
+ * Shared across screens so every footer looks the same.
44
+ */
45
+ export function FooterHints({
46
+ hints,
47
+ }: { hints: FooterHint[] }): React.ReactNode {
48
+ const nodes: React.ReactNode[] = [];
49
+ hints.forEach((hint, i) => {
50
+ // A hint is identified by its key chord plus label, not its position.
51
+ const id = `${joinKeys(hint.keys)}:${hint.label}`;
52
+ if (i > 0) {
53
+ nodes.push(
54
+ <span key={`gap-${id}`} fg={LABEL_FG}>
55
+ {GAP}
56
+ </span>,
57
+ );
58
+ }
59
+ nodes.push(
60
+ <span key={`k-${id}`} bg={KEY_BG} fg={KEY_FG}>
61
+ {` ${joinKeys(hint.keys)} `}
62
+ </span>,
63
+ );
64
+ nodes.push(
65
+ <span key={`l-${id}`} fg={LABEL_FG}>
66
+ {` ${hint.label}`}
67
+ </span>,
68
+ );
69
+ });
70
+ return <text fg={theme.colors.text}>{nodes}</text>;
71
+ }
72
+
73
+ /** Convenience for callers that already have a node: same as <FooterHints>. */
74
+ export function renderFooterHints(hints: FooterHint[]): React.ReactNode {
75
+ return <FooterHints hints={hints} />;
76
+ }
77
+
78
+ export default FooterHints;
@@ -0,0 +1,63 @@
1
+ import type React from "react";
2
+ import { type UiColor, theme } from "../../theme.js";
3
+
4
+ interface PanelProps {
5
+ /** Panel title */
6
+ title?: string;
7
+ /** Panel content */
8
+ children: React.ReactNode;
9
+ /** Border color */
10
+ borderColor?: UiColor;
11
+ /** Title color */
12
+ titleColor?: UiColor;
13
+ /** Panel width - number or percentage like "50%" */
14
+ width?: number | `${number}%` | "auto";
15
+ /** Panel height - number or percentage like "50%" */
16
+ height?: number | `${number}%` | "auto";
17
+ /** Whether to use flexGrow */
18
+ flexGrow?: number;
19
+ /** Whether panel is focused/active */
20
+ focused?: boolean;
21
+ }
22
+
23
+ export function Panel({
24
+ title,
25
+ children,
26
+ borderColor = theme.colors.accent,
27
+ titleColor = theme.colors.accent,
28
+ width,
29
+ height,
30
+ flexGrow = 1,
31
+ focused = false,
32
+ }: PanelProps) {
33
+ const activeColor = focused ? theme.colors.accent : borderColor;
34
+
35
+ return (
36
+ <box
37
+ flexDirection="column"
38
+ width={width}
39
+ height={height}
40
+ flexGrow={flexGrow}
41
+ border
42
+ borderStyle="single"
43
+ borderColor={activeColor}
44
+ padding={1}
45
+ >
46
+ {/* Title row */}
47
+ {title && (
48
+ <box marginBottom={0}>
49
+ <text fg={titleColor}>
50
+ <strong>{title}</strong>
51
+ </text>
52
+ </box>
53
+ )}
54
+
55
+ {/* Content */}
56
+ <box flexDirection="column" flexGrow={1}>
57
+ {children}
58
+ </box>
59
+ </box>
60
+ );
61
+ }
62
+
63
+ export default Panel;
@@ -0,0 +1,44 @@
1
+ import React from "react";
2
+ import { theme } from "../../theme.js";
3
+
4
+ interface ProgressBarProps {
5
+ /** Progress message */
6
+ message: string;
7
+ /** Current progress (if determinate) */
8
+ current?: number;
9
+ /** Total items (if determinate) */
10
+ total?: number;
11
+ }
12
+
13
+ export function ProgressBar({ message, current, total }: ProgressBarProps) {
14
+ const isDeterminate =
15
+ current !== undefined && total !== undefined && total > 0;
16
+
17
+ if (isDeterminate) {
18
+ const barWidth = 20;
19
+ const filled = Math.round((current / total) * barWidth);
20
+ const empty = barWidth - filled;
21
+ const bar = "█".repeat(filled) + "░".repeat(empty);
22
+
23
+ return (
24
+ <box>
25
+ <text fg={theme.colors.info}>⟳</text>
26
+ <text fg={theme.colors.text}> {message} </text>
27
+ <text fg={theme.colors.info}>
28
+ [{bar}] {current}/{total}
29
+ </text>
30
+ </box>
31
+ );
32
+ }
33
+
34
+ // Indeterminate progress
35
+ return (
36
+ <box>
37
+ <text fg={theme.colors.info}>⟳</text>
38
+ <text fg={theme.colors.text}> {message}</text>
39
+ <text fg={theme.colors.muted}> ...</text>
40
+ </box>
41
+ );
42
+ }
43
+
44
+ export default ProgressBar;
@@ -0,0 +1,54 @@
1
+ import React from "react";
2
+ import { theme } from "../../theme.js";
3
+
4
+ interface ScopeTabsProps {
5
+ /** Current scope */
6
+ scope: "project" | "global";
7
+ /** Callback when scope changes */
8
+ onToggle?: () => void;
9
+ /** Hint text for toggle key */
10
+ toggleHint?: string;
11
+ }
12
+
13
+ export function ScopeTabs({
14
+ scope,
15
+ onToggle: _onToggle,
16
+ toggleHint,
17
+ }: ScopeTabsProps) {
18
+ const isProject = scope === "project";
19
+
20
+ return (
21
+ <box marginBottom={1} flexDirection="row" gap={1}>
22
+ {/* Project tab */}
23
+ <box>
24
+ {isProject ? (
25
+ <text bg={theme.colors.info} fg={theme.hints.fg}>
26
+ <strong> ◆ Project </strong>
27
+ </text>
28
+ ) : (
29
+ <text fg={theme.colors.muted}> ○ Project </text>
30
+ )}
31
+ </box>
32
+
33
+ {/* Global tab */}
34
+ <box>
35
+ {!isProject ? (
36
+ <text bg={theme.colors.accent} fg={theme.hints.fg}>
37
+ <strong> ◆ Global </strong>
38
+ </text>
39
+ ) : (
40
+ <text fg={theme.colors.muted}> ○ Global </text>
41
+ )}
42
+ </box>
43
+
44
+ {/* Toggle hint */}
45
+ {toggleHint && (
46
+ <box marginLeft={2}>
47
+ <text fg={theme.colors.muted}>({toggleHint})</text>
48
+ </box>
49
+ )}
50
+ </box>
51
+ );
52
+ }
53
+
54
+ export default ScopeTabs;