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.
- package/README.md +309 -0
- package/bin/magus.js +170 -0
- package/package.json +67 -3
- package/scripts/build-binaries.ts +113 -0
- package/scripts/capture-builtin.ts +412 -0
- package/scripts/check-optional-deps.ts +49 -0
- package/scripts/test-isolated.ts +49 -0
- package/scripts/verify-binary.ts +95 -0
- package/scripts/verify-community-registry.ts +272 -0
- package/src/__tests__/active-closure.test.ts +184 -0
- package/src/__tests__/alias-adopt.test.ts +365 -0
- package/src/__tests__/alias-parser.test.ts +417 -0
- package/src/__tests__/alias-shell-writer.test.ts +681 -0
- package/src/__tests__/alias-store.test.ts +163 -0
- package/src/__tests__/binary-signing.test.ts +120 -0
- package/src/__tests__/blackbox-active-closure.test.ts +119 -0
- package/src/__tests__/blackbox-convergence.test.ts +334 -0
- package/src/__tests__/blackbox-parity.test.ts +725 -0
- package/src/__tests__/blackbox-plugin-state.test.ts +545 -0
- package/src/__tests__/blackbox-tool-state.test.ts +669 -0
- package/src/__tests__/cache-provenance.test.ts +84 -0
- package/src/__tests__/capture-builtin.test.ts +92 -0
- package/src/__tests__/catalog-cache-store.test.ts +309 -0
- package/src/__tests__/catalog-notice.test.ts +155 -0
- package/src/__tests__/cli-ansi.test.ts +297 -0
- package/src/__tests__/cli-apply-seams.test.ts +288 -0
- package/src/__tests__/cli-live.test.ts +391 -0
- package/src/__tests__/cli-router.test.ts +240 -0
- package/src/__tests__/cli-tool-commands.test.ts +239 -0
- package/src/__tests__/cli-tools-adapter.test.ts +294 -0
- package/src/__tests__/cli-update-view.test.ts +524 -0
- package/src/__tests__/community-fetch.test.ts +545 -0
- package/src/__tests__/community-registry.test.ts +269 -0
- package/src/__tests__/community-staleness.test.ts +720 -0
- package/src/__tests__/component-badge.test.ts +98 -0
- package/src/__tests__/component-icons.test.ts +94 -0
- package/src/__tests__/content-drift.test.ts +353 -0
- package/src/__tests__/conventions-integration.test.ts +830 -0
- package/src/__tests__/conventions-manager.test.ts +1236 -0
- package/src/__tests__/doctor-bins.test.ts +43 -0
- package/src/__tests__/doctor.test.ts +463 -0
- package/src/__tests__/dotenv.test.ts +195 -0
- package/src/__tests__/enabled-not-installed.test.ts +94 -0
- package/src/__tests__/file-locking.test.ts +208 -0
- package/src/__tests__/fixtures/tool-probe/brew-list-versions-tmux.txt +2 -0
- package/src/__tests__/fixtures/tool-probe/brew-outdated-behind-homebrew-spec.txt +29 -0
- package/src/__tests__/fixtures/tool-probe/brew-outdated-tmux-current.txt +7 -0
- package/src/__tests__/fixtures/tool-probe/brew-outdated-unknown-formula.txt +6 -0
- package/src/__tests__/fixtures/tool-probe/go-version-m-tmux-mcp.txt +27 -0
- package/src/__tests__/fixtures/tool-probe/npm-dist-tags-missing-scoped.txt +4 -0
- package/src/__tests__/fixtures/tool-probe/npm-dist-tags-openai-codex.txt +4 -0
- package/src/__tests__/fixtures/tool-probe/pypi-cowsay.txt +3 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-browser-use.toml +10 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-git.toml +8 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-pinned.toml +8 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-graphifyy.toml +7 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-mcp.toml +6 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-serena-agent.toml +9 -0
- package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated-pinned.txt +5 -0
- package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated.txt +11 -0
- package/src/__tests__/fixtures/tool-probe/uv-tool-list.txt +22 -0
- package/src/__tests__/footer-hints.test.ts +40 -0
- package/src/__tests__/format-command.test.ts +78 -0
- package/src/__tests__/git-worktree.test.ts +108 -0
- package/src/__tests__/github-budget.test.ts +206 -0
- package/src/__tests__/gitignore-check.test.ts +71 -0
- package/src/__tests__/gitignore-detector.test.ts +176 -0
- package/src/__tests__/gitignore-fixer.test.ts +317 -0
- package/src/__tests__/gitignore-resolver.test.ts +193 -0
- package/src/__tests__/gitignore-service.test.ts +137 -0
- package/src/__tests__/go-module.test.ts +43 -0
- package/src/__tests__/hook-import-policy.test.ts +90 -0
- package/src/__tests__/hook-process.test.ts +256 -0
- package/src/__tests__/hook-registration.test.ts +224 -0
- package/src/__tests__/install-command.test.ts +81 -0
- package/src/__tests__/install-plan.test.ts +104 -0
- package/src/__tests__/launcher-signal.test.ts +308 -0
- package/src/__tests__/local-marketplace-scan.test.ts +248 -0
- package/src/__tests__/manifest.test.ts +281 -0
- package/src/__tests__/markdown-renderer.test.ts +142 -0
- package/src/__tests__/marketplace-badge.test.ts +140 -0
- package/src/__tests__/marketplace-refresh.test.ts +340 -0
- package/src/__tests__/marketplace-version.test.ts +84 -0
- package/src/__tests__/marketplaces.test.ts +128 -0
- package/src/__tests__/mate-availability.test.ts +156 -0
- package/src/__tests__/mate-catalog.test.ts +295 -0
- package/src/__tests__/mcp-registry.test.ts +158 -0
- package/src/__tests__/model-visuals.test.tsx +2466 -0
- package/src/__tests__/models-adapter.test.ts +332 -0
- package/src/__tests__/models-cli.test.ts +273 -0
- package/src/__tests__/models-core.test.ts +802 -0
- package/src/__tests__/models-manager.test.ts +500 -0
- package/src/__tests__/models-presets-marketplace.test.ts +168 -0
- package/src/__tests__/models-screen-state.test.ts +315 -0
- package/src/__tests__/open-file.test.ts +59 -0
- package/src/__tests__/owned-marketplaces.test.ts +93 -0
- package/src/__tests__/plugin-checkout.test.ts +66 -0
- package/src/__tests__/plugin-cli-argv.test.ts +109 -0
- package/src/__tests__/plugin-contents.test.ts +371 -0
- package/src/__tests__/plugin-manager-fallback.test.ts +320 -0
- package/src/__tests__/plugin-releases.test.ts +98 -0
- package/src/__tests__/plugin-requires.test.ts +187 -0
- package/src/__tests__/plugin-row-text.test.ts +345 -0
- package/src/__tests__/plugin-setup.test.ts +1016 -0
- package/src/__tests__/profile-adopt.test.ts +177 -0
- package/src/__tests__/profile-command.test.ts +110 -0
- package/src/__tests__/profile-materializer.test.ts +238 -0
- package/src/__tests__/profile-sync.test.ts +154 -0
- package/src/__tests__/registry-version-resolution.test.ts +88 -0
- package/src/__tests__/resolve-executable.test.ts +42 -0
- package/src/__tests__/resolver.test.ts +351 -0
- package/src/__tests__/scope-action.test.ts +134 -0
- package/src/__tests__/scope-squares.test.tsx +164 -0
- package/src/__tests__/selection-scope.test.ts +100 -0
- package/src/__tests__/settings-file.test.ts +179 -0
- package/src/__tests__/shell-script-callers.test.ts +92 -0
- package/src/__tests__/skill-install-files.test.ts +136 -0
- package/src/__tests__/skills-catalog-client.test.ts +48 -0
- package/src/__tests__/style-wrap.test.ts +220 -0
- package/src/__tests__/styles-manager.test.ts +1098 -0
- package/src/__tests__/styles-origins.test.ts +414 -0
- package/src/__tests__/styles-screen-state.test.ts +457 -0
- package/src/__tests__/styles-status-line.test.ts +72 -0
- package/src/__tests__/styles-sync.test.ts +452 -0
- package/src/__tests__/symlink-manager.test.ts +180 -0
- package/src/__tests__/tabbar-layout.test.ts +100 -0
- package/src/__tests__/terminology-filler.test.ts +214 -0
- package/src/__tests__/theme-adaptive-colors.test.ts +354 -0
- package/src/__tests__/theme-env.test.ts +310 -0
- package/src/__tests__/theme-resolve.test.ts +380 -0
- package/src/__tests__/tool-probe.test.ts +1400 -0
- package/src/__tests__/toolchain.test.ts +232 -0
- package/src/__tests__/ui-version-writers.test.ts +88 -0
- package/src/__tests__/update-apply.test.ts +637 -0
- package/src/__tests__/update-plan.test.ts +1396 -0
- package/src/__tests__/uppercase-keybindings.test.ts +101 -0
- package/src/__tests__/useGitignoreModal.test.ts +79 -0
- package/src/__tests__/version-check-refresh.test.ts +45 -0
- package/src/__tests__/version-snapshot.test.ts +212 -0
- package/src/__tests__/version-unknown-state.test.ts +403 -0
- package/src/__tests__/worktree-registry-resolution.test.ts +107 -0
- package/src/cli/ansi.ts +528 -0
- package/src/cli/bootstrap.ts +134 -0
- package/src/cli/doctor.ts +218 -0
- package/src/cli/hook.ts +129 -0
- package/src/cli/install.ts +382 -0
- package/src/cli/live.ts +374 -0
- package/src/cli/models.ts +297 -0
- package/src/cli/profile.ts +197 -0
- package/src/cli/prompt.ts +32 -0
- package/src/cli/router.ts +264 -0
- package/src/cli/update-view.ts +717 -0
- package/src/cli/update.ts +600 -0
- package/src/cli/upgrade.ts +97 -0
- package/src/data/alias-flags.ts +301 -0
- package/src/data/cli-tools.ts +146 -0
- package/src/data/community-styles.ts +531 -0
- package/src/data/gitignore-defaults.ts +33 -0
- package/src/data/gitignore-reasons.ts +93 -0
- package/src/data/gitignore-templates.ts +25 -0
- package/src/data/marketplaces.ts +278 -0
- package/src/data/mcp-servers.ts +51 -0
- package/src/data/models-presets.ts +329 -0
- package/src/data/predefined-profiles.ts +252 -0
- package/src/data/settings-catalog.ts +714 -0
- package/src/data/skill-repos.ts +91 -0
- package/src/data/styles/asd-ste100.md +38 -0
- package/src/data/styles/calibrated.md +24 -0
- package/src/data/styles/direct.md +23 -0
- package/src/data/styles/evidence-first.md +22 -0
- package/src/data/styles/explanatory.md +23 -0
- package/src/data/styles/index.ts +57 -0
- package/src/data/styles/no-slop.md +32 -0
- package/src/data/styles/plain-language.md +23 -0
- package/src/data/styles/structured.md +30 -0
- package/src/data/styles/terminology.md +28 -0
- package/src/data/styles/terse.md +20 -0
- package/src/main.tsx +110 -0
- package/src/markdown.d.ts +13 -0
- package/src/opentui.d.ts +203 -0
- package/src/services/active-closure.ts +117 -0
- package/src/services/alias-settings.ts +60 -0
- package/src/services/alias-shell-writer.ts +1244 -0
- package/src/services/alias-store.ts +172 -0
- package/src/services/binary-signing.ts +78 -0
- package/src/services/catalog-cache-store.ts +316 -0
- package/src/services/claude-cli.ts +304 -0
- package/src/services/claude-runner.ts +31 -0
- package/src/services/claude-settings.ts +1564 -0
- package/src/services/cli-tool-commands.ts +133 -0
- package/src/services/community-fetcher.ts +90 -0
- package/src/services/community-styles.ts +1194 -0
- package/src/services/content-drift.ts +208 -0
- package/src/services/conventions-manager.ts +945 -0
- package/src/services/doctor-bins.ts +63 -0
- package/src/services/doctor.ts +570 -0
- package/src/services/dotenv.ts +181 -0
- package/src/services/git-worktree.ts +129 -0
- package/src/services/github-budget.ts +274 -0
- package/src/services/gitignore-check.ts +63 -0
- package/src/services/gitignore-detector.ts +167 -0
- package/src/services/gitignore-fixer.ts +280 -0
- package/src/services/gitignore-resolver.ts +208 -0
- package/src/services/gitignore-service.ts +167 -0
- package/src/services/hook-registration.ts +218 -0
- package/src/services/install-plan.ts +107 -0
- package/src/services/local-marketplace.ts +657 -0
- package/src/services/manifest.ts +250 -0
- package/src/services/marketplace-catalog-git.ts +173 -0
- package/src/services/marketplace-catalog.ts +129 -0
- package/src/services/marketplace-fetcher.ts +400 -0
- package/src/services/marketplace-refresh.ts +245 -0
- package/src/services/marketplace-sync.ts +145 -0
- package/src/services/mate-availability.ts +133 -0
- package/src/services/mate-catalog.ts +265 -0
- package/src/services/mcp-registry.ts +171 -0
- package/src/services/models-core.ts +969 -0
- package/src/services/models-manager.ts +606 -0
- package/src/services/plugin-checkout.ts +186 -0
- package/src/services/plugin-manager.ts +847 -0
- package/src/services/plugin-mcp-config.ts +242 -0
- package/src/services/plugin-releases.ts +47 -0
- package/src/services/plugin-requires.ts +227 -0
- package/src/services/plugin-setup.ts +965 -0
- package/src/services/profile-adopt.ts +206 -0
- package/src/services/profile-materializer.ts +133 -0
- package/src/services/profile-sync.ts +158 -0
- package/src/services/profiles.ts +225 -0
- package/src/services/resolver.ts +377 -0
- package/src/services/settings-file.ts +69 -0
- package/src/services/settings-manager.ts +307 -0
- package/src/services/skills-manager.ts +1111 -0
- package/src/services/skillsmp-client.ts +193 -0
- package/src/services/styles-manager.ts +1294 -0
- package/src/services/symlink-manager.ts +189 -0
- package/src/services/terminology-filler.ts +266 -0
- package/src/services/tool-probe.ts +1637 -0
- package/src/services/toolchain.ts +431 -0
- package/src/services/update-engine.ts +691 -0
- package/src/services/update-plan.ts +820 -0
- package/src/services/version-check.ts +127 -0
- package/src/services/version-snapshot.ts +189 -0
- package/src/tui.tsx +112 -0
- package/src/types/bun.d.ts +27 -0
- package/src/types/gitignore.ts +68 -0
- package/src/types/index.ts +447 -0
- package/src/ui/App.tsx +535 -0
- package/src/ui/adapters/catalogNotice.ts +122 -0
- package/src/ui/adapters/cliToolsAdapter.ts +176 -0
- package/src/ui/adapters/modelsAdapter.ts +213 -0
- package/src/ui/adapters/pluginsAdapter.ts +413 -0
- package/src/ui/adapters/settingsAdapter.ts +168 -0
- package/src/ui/adapters/skillsAdapter.ts +229 -0
- package/src/ui/adapters/stylesAdapter.ts +403 -0
- package/src/ui/components/CategoryHeader.tsx +64 -0
- package/src/ui/components/EmptyFilterState.tsx +32 -0
- package/src/ui/components/FlagDetailEditor.tsx +0 -0
- package/src/ui/components/ScopeIndicator.tsx +63 -0
- package/src/ui/components/ScrollableList.tsx +95 -0
- package/src/ui/components/SearchInput.tsx +57 -0
- package/src/ui/components/StyledText.tsx +75 -0
- package/src/ui/components/TabBar.tsx +101 -0
- package/src/ui/components/layout/FooterHints.tsx +78 -0
- package/src/ui/components/layout/Panel.tsx +63 -0
- package/src/ui/components/layout/ProgressBar.tsx +44 -0
- package/src/ui/components/layout/ScopeTabs.tsx +54 -0
- package/src/ui/components/layout/ScreenLayout.tsx +321 -0
- package/src/ui/components/layout/index.ts +9 -0
- package/src/ui/components/modals/ConfirmModal.tsx +62 -0
- package/src/ui/components/modals/InputModal.tsx +75 -0
- package/src/ui/components/modals/LoadingModal.tsx +51 -0
- package/src/ui/components/modals/MessageModal.tsx +66 -0
- package/src/ui/components/modals/ModalContainer.tsx +159 -0
- package/src/ui/components/modals/SelectModal.tsx +90 -0
- package/src/ui/components/modals/index.ts +6 -0
- package/src/ui/components/primitives/ActionHints.tsx +44 -0
- package/src/ui/components/primitives/DetailSection.tsx +23 -0
- package/src/ui/components/primitives/KeyValueLine.tsx +26 -0
- package/src/ui/components/primitives/ListCategoryRow.tsx +45 -0
- package/src/ui/components/primitives/MetaText.tsx +40 -0
- package/src/ui/components/primitives/ScopeDetail.tsx +67 -0
- package/src/ui/components/primitives/ScopeSquares.tsx +58 -0
- package/src/ui/components/primitives/SelectableRow.tsx +30 -0
- package/src/ui/components/primitives/index.ts +9 -0
- package/src/ui/hooks/index.ts +2 -0
- package/src/ui/hooks/useGitignoreModal.ts +95 -0
- package/src/ui/hooks/useKeyboard.ts +24 -0
- package/src/ui/hooks/useKeyboardHandler.ts +63 -0
- package/src/ui/registry.ts +33 -0
- package/src/ui/renderers/cliToolRenderers.tsx +326 -0
- package/src/ui/renderers/gitignoreRenderers.tsx +80 -0
- package/src/ui/renderers/markdownRenderer.tsx +302 -0
- package/src/ui/renderers/mcpRenderers.tsx +149 -0
- package/src/ui/renderers/modelRenderers.tsx +1309 -0
- package/src/ui/renderers/modelVisuals.tsx +1402 -0
- package/src/ui/renderers/pluginRenderers.tsx +1089 -0
- package/src/ui/renderers/pluginRowText.ts +70 -0
- package/src/ui/renderers/pluginStatusText.ts +56 -0
- package/src/ui/renderers/profileRenderers.tsx +406 -0
- package/src/ui/renderers/settingsRenderers.tsx +218 -0
- package/src/ui/renderers/skillRenderers.tsx +479 -0
- package/src/ui/renderers/styleRenderers.tsx +813 -0
- package/src/ui/screens/AliasScreen.tsx +1558 -0
- package/src/ui/screens/CliToolsScreen.tsx +367 -0
- package/src/ui/screens/EnvVarsScreen.tsx +221 -0
- package/src/ui/screens/GitignoreScreen.tsx +562 -0
- package/src/ui/screens/McpRegistryScreen.tsx +366 -0
- package/src/ui/screens/McpScreen.tsx +256 -0
- package/src/ui/screens/ModelsScreen.tsx +542 -0
- package/src/ui/screens/PluginsScreen.tsx +1423 -0
- package/src/ui/screens/ProfilesScreen.tsx +438 -0
- package/src/ui/screens/SkillsScreen.tsx +853 -0
- package/src/ui/screens/StylesScreen.tsx +1064 -0
- package/src/ui/screens/index.ts +11 -0
- package/src/ui/state/AppContext.tsx +233 -0
- package/src/ui/state/DimensionsContext.tsx +154 -0
- package/src/ui/state/reducer.ts +810 -0
- package/src/ui/state/types.ts +503 -0
- package/src/ui/theme-mode.ts +440 -0
- package/src/ui/theme-resolve.ts +147 -0
- package/src/ui/theme.ts +175 -0
- package/src/utils/clipboard.ts +58 -0
- package/src/utils/command-utils.ts +149 -0
- package/src/utils/config-dir.ts +45 -0
- package/src/utils/file-locking.ts +146 -0
- package/src/utils/fuzzy-search.ts +138 -0
- package/src/utils/go-module.ts +36 -0
- package/src/utils/open-file.ts +84 -0
- package/src/utils/run.ts +67 -0
- 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
|
+
}
|
|
Binary file
|
|
@@ -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;
|