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,440 @@
|
|
|
1
|
+
import type { ThemeMode } from "@opentui/core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The one place magus needs to know whether the terminal is light or dark.
|
|
5
|
+
*
|
|
6
|
+
* Everything else in the palette avoids this question on purpose: accents are
|
|
7
|
+
* chosen to clear 3:1 against both backgrounds, and body text uses the terminal's
|
|
8
|
+
* own foreground. A *disabled row tint* is the exception, because "subtle" is
|
|
9
|
+
* defined relative to the page — a wash that sits gently on cream is a bright
|
|
10
|
+
* band on near-black, and there is no third colour that is quiet on both.
|
|
11
|
+
*
|
|
12
|
+
* ANSI cannot help here either: there is no alpha channel, so the tint cannot be
|
|
13
|
+
* expressed as "the page, 8% toward the foreground". OpenTUI's alpha blending
|
|
14
|
+
* lives only in its 3d module, not the text renderer.
|
|
15
|
+
*
|
|
16
|
+
* So we resolve, once at startup, through `theme-resolve.ts`: flag, then
|
|
17
|
+
* `MAGUS_THEME`, then `TERM_THEME`, then the bounded OSC probe, then
|
|
18
|
+
* `COLORFGBG`, then dark. **There is no unknown.** Resolution always yields a
|
|
19
|
+
* mode; when nothing answered it is dark, because that is the user's stated
|
|
20
|
+
* default, not because dark was detected.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
let detected: ThemeMode = "dark";
|
|
24
|
+
|
|
25
|
+
/** Set once at startup from `resolveTheme()`. */
|
|
26
|
+
export function setThemeMode(mode: ThemeMode): void {
|
|
27
|
+
detected = mode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getThemeMode(): ThemeMode {
|
|
31
|
+
return detected;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Test seam — return to the pre-resolution default between cases. */
|
|
35
|
+
export function resetThemeMode(): void {
|
|
36
|
+
detected = "dark";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Fill for an UNLIT segment of the three-square scope bar.
|
|
41
|
+
*
|
|
42
|
+
* The bar reads as three segments, each lit (in its scope colour) or unlit. Unlit
|
|
43
|
+
* used to be the same mid-grey as body de-emphasis, `#6B7280`, which is dark —
|
|
44
|
+
* so an unlit segment carried the same visual weight as a lit one and the bar
|
|
45
|
+
* read as three similar blobs. Reported directly: "non installed state is dark as
|
|
46
|
+
* well - confusing".
|
|
47
|
+
*
|
|
48
|
+
* An unlit segment should recede toward the page, and "toward the page" is the
|
|
49
|
+
* one thing a fixed colour cannot express: a pale fill that sits quietly on cream
|
|
50
|
+
* is a bright block on near-black. Hence the two values, chosen per resolved
|
|
51
|
+
* mode — this is the only place magus asks.
|
|
52
|
+
*/
|
|
53
|
+
export const SCOPE_OFF_FILL = {
|
|
54
|
+
/** Just off a light page — a filled but quiet segment. */
|
|
55
|
+
light: "#D9D9CC",
|
|
56
|
+
/** Just off a dark page. */
|
|
57
|
+
dark: "#3A3A40",
|
|
58
|
+
} as const;
|
|
59
|
+
|
|
60
|
+
/** Fill colour for an unlit scope segment. Always a colour — never undefined,
|
|
61
|
+
* because the segment is a filled block whatever the terminal turns out to be. */
|
|
62
|
+
export function scopeOffFill(): string {
|
|
63
|
+
return SCOPE_OFF_FILL[detected];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Badge colours for the four plugin component kinds.
|
|
68
|
+
*
|
|
69
|
+
* These are chips, not accents: they sit on hundreds of rows at once, so the
|
|
70
|
+
* fill has to be a *wash* of the hue rather than the hue itself. That runs into
|
|
71
|
+
* the same wall `SCOPE_OFF_FILL` documents above — a wash is defined relative to
|
|
72
|
+
* the page, ANSI has no alpha, so the two ends are picked by hand.
|
|
73
|
+
*
|
|
74
|
+
* Hues are deliberately far apart (purple / teal / blue / amber) for the reason
|
|
75
|
+
* the scope squares are: one character of colour carries no readable hue shift,
|
|
76
|
+
* only a readable hue *difference*.
|
|
77
|
+
*
|
|
78
|
+
* The glyph colour is set per mode too. Reusing the vivid accent over a dark
|
|
79
|
+
* wash would drop it below the contrast the palette guarantees against the
|
|
80
|
+
* page — the accent's measured ratio is against `CONTRAST_REFERENCE`, not
|
|
81
|
+
* against another tint laid on top of it.
|
|
82
|
+
*/
|
|
83
|
+
export const COMPONENT_BADGE = {
|
|
84
|
+
skill: {
|
|
85
|
+
light: { bg: "#E9DCF5", fg: "#6B21A8" },
|
|
86
|
+
dark: { bg: "#2E2140", fg: "#C4A5F0" },
|
|
87
|
+
},
|
|
88
|
+
command: {
|
|
89
|
+
light: { bg: "#D3E7EC", fg: "#0E5F73" },
|
|
90
|
+
dark: { bg: "#123037", fg: "#7DD3E0" },
|
|
91
|
+
},
|
|
92
|
+
agent: {
|
|
93
|
+
light: { bg: "#DCE4F7", fg: "#1D4ED8" },
|
|
94
|
+
dark: { bg: "#1B2740", fg: "#A5C0F5" },
|
|
95
|
+
},
|
|
96
|
+
mcp: {
|
|
97
|
+
light: { bg: "#F0E2CE", fg: "#92400E" },
|
|
98
|
+
dark: { bg: "#3A2A15", fg: "#E5B877" },
|
|
99
|
+
},
|
|
100
|
+
} as const;
|
|
101
|
+
|
|
102
|
+
export type ComponentBadgeKind = keyof typeof COMPONENT_BADGE;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Badge and chart colours for the four Claude models, the three external-model slots, and
|
|
106
|
+
* `inherit`.
|
|
107
|
+
*
|
|
108
|
+
* Four strengths, because a badge and a bar cannot share an ink: twenty characters of a
|
|
109
|
+
* colour weigh far more than six, so what reads as a label reads as a slab when it is a
|
|
110
|
+
* chart. `bg`/`fg` are the chip; `bar` is the distribution bar's fill; `label` is the ink a
|
|
111
|
+
* word takes when it is written ON that fill.
|
|
112
|
+
*
|
|
113
|
+
* ## Why `label` exists, and why `fg` could not do its job
|
|
114
|
+
*
|
|
115
|
+
* A segment of the workflow chart is one block of one colour with its model's name written
|
|
116
|
+
* on it. The name used to be drawn as the CHIP — `fg` on `bg`, inset into the bar — which
|
|
117
|
+
* put a second, visibly different rectangle inside a block that is supposed to read as one
|
|
118
|
+
* thing. Reported exactly that way: "do not add additional background colour to model
|
|
119
|
+
* segments".
|
|
120
|
+
*
|
|
121
|
+
* Deleting the chip and keeping `fg` was not an option: MEASURED, `fg` on the same model's
|
|
122
|
+
* `bar` runs 1.74:1 (sonnet dark) to 2.99:1 (haiku dark), which is a smudge rather than a
|
|
123
|
+
* word. `fg` is built to sit on `bg`, and `bg` is not what a bar is painted with.
|
|
124
|
+
*
|
|
125
|
+
* So each model owns a fourth ink, chosen against its OWN bar and nothing else. Every one is
|
|
126
|
+
* the model's own hue taken deep — the same family, not a fifth palette — and every one
|
|
127
|
+
* clears 4.5:1 with margin; the measured value is on each line below. `inherit` is the one
|
|
128
|
+
* that runs the other way: its bar is a dark recessive grey rather than a bright mid-tone,
|
|
129
|
+
* so its label is near-white on the dark page. That is forced by the fill, not a stylistic
|
|
130
|
+
* exception.
|
|
131
|
+
*
|
|
132
|
+
* ## The two pages are built differently, deliberately
|
|
133
|
+
*
|
|
134
|
+
* `COMPONENT_BADGE` above explains why a chip needs two ends at all. This map goes one step
|
|
135
|
+
* further and does not MIRROR them. Cream already carries the light, so a light chip is a
|
|
136
|
+
* pale wash under mid-tone ink and stays quiet. A near-black page carries none, so the same
|
|
137
|
+
* construction turns grey and the screen goes flat — a symmetric palette was built, shown,
|
|
138
|
+
* and rejected on sight as "dark looks like the light theme". A dark chip is therefore a
|
|
139
|
+
* SATURATED MID-TONE under near-white ink: a wash toward black is dark, a wash toward white
|
|
140
|
+
* is the light theme, and neither of those is a colour.
|
|
141
|
+
*
|
|
142
|
+
* ## The dark fills clear a page we cannot see
|
|
143
|
+
*
|
|
144
|
+
* `CONTRAST_REFERENCE_KEYS.dark` is `#1C1C1E`, but a real terminal may be pure black or a
|
|
145
|
+
* slate like One Dark's `#282C34`. MEASURED, the `COMPONENT_BADGE` dark washes sit at
|
|
146
|
+
* 1.00–1.07:1 against that slate — the same luminance as the page — so the chip loses its
|
|
147
|
+
* edge and reads as a hole punched in the row. These fills clear every ground: 3.1–4.3:1 on
|
|
148
|
+
* black, 2.8–2.9 on slate, and they still clear 4.5:1 on cream, so even a mis-detected theme
|
|
149
|
+
* is merely odd rather than unreadable.
|
|
150
|
+
*
|
|
151
|
+
* Hues stay far apart for the same reason the scope squares' do: one or two characters of
|
|
152
|
+
* colour carry no readable hue SHIFT, only a readable hue DIFFERENCE.
|
|
153
|
+
*
|
|
154
|
+
* ## The mates are ONE hue family, and that is the exception that proves the rule
|
|
155
|
+
*
|
|
156
|
+
* `mate1`, `mate2` and `kangaroo` are three different external models, so they need three
|
|
157
|
+
* distinguishable inks — but they all mean the same thing (someone else's model, reached
|
|
158
|
+
* through claudish), and four unrelated hues would say the opposite. They therefore share a
|
|
159
|
+
* warm outback band nothing else on this screen occupies: measured hue angles 39° / 23° / 6°
|
|
160
|
+
* on the dark page and 41° / 25° / 10° on the light one — ochre, terracotta, red earth. The
|
|
161
|
+
* four Claude models sit at teal 190°, green 150°, purple 265° and grey, so a mate cannot be
|
|
162
|
+
* mistaken for one of them at a glance, and a mate cannot be mistaken for another mate
|
|
163
|
+
* either, because ~17° of hue at full saturation is a readable difference where ~17° of
|
|
164
|
+
* LUMINANCE would not be.
|
|
165
|
+
*
|
|
166
|
+
* Measured contrast, ink on its own fill (the number the chip has to clear):
|
|
167
|
+
*
|
|
168
|
+
* mate1 light 5.60:1 dark 4.82:1
|
|
169
|
+
* mate2 light 5.82:1 dark 4.96:1
|
|
170
|
+
* kangaroo light 5.98:1 dark 5.68:1
|
|
171
|
+
*
|
|
172
|
+
* And the dark fills against every plausible dark ground — pure black,
|
|
173
|
+
* `CONTRAST_REFERENCE_KEYS.dark`, One Dark's `#282C34` — plus the cream page a mis-detected
|
|
174
|
+
* theme would put them on:
|
|
175
|
+
*
|
|
176
|
+
* mate1 4.09 / 3.32 / 2.73 cream 4.81
|
|
177
|
+
* mate2 3.83 / 3.10 / 2.55 cream 5.14
|
|
178
|
+
* kangaroo 3.31 / 2.68 / 2.21 cream 5.94
|
|
179
|
+
*
|
|
180
|
+
* which is the same band the four Claude fills occupy, so a mate chip carries the same weight
|
|
181
|
+
* as the models beside it rather than reading as a second-class one.
|
|
182
|
+
*
|
|
183
|
+
* There is a FOURTH warm entry, `mates`, and it is not a slot: it is the family's own ink,
|
|
184
|
+
* worn by the one collapsed segment a workflow bar draws for all external work at once. Its
|
|
185
|
+
* own note below says why a slot's ink could not do that job.
|
|
186
|
+
*/
|
|
187
|
+
export const MODEL_BADGE = {
|
|
188
|
+
opus: {
|
|
189
|
+
/** label 4.88:1 on its bar */
|
|
190
|
+
light: { bg: "#D3E7EC", fg: "#0E5F73", bar: "#92CEDC", label: "#125463" },
|
|
191
|
+
/** label 4.93:1 on its bar */
|
|
192
|
+
dark: { bg: "#0E7C94", fg: "#F0FDFF", bar: "#48C8E0", label: "#044B58" },
|
|
193
|
+
},
|
|
194
|
+
sonnet: {
|
|
195
|
+
/** label 4.84:1 on its bar */
|
|
196
|
+
light: { bg: "#D6EEDD", fg: "#166534", bar: "#93D9B2", label: "#145D34" },
|
|
197
|
+
/** label 4.84:1 on its bar */
|
|
198
|
+
dark: { bg: "#12804F", fg: "#EFFFF6", bar: "#4FD98F", label: "#07572C" },
|
|
199
|
+
},
|
|
200
|
+
haiku: {
|
|
201
|
+
/** label 4.81:1 on its bar */
|
|
202
|
+
light: { bg: "#E6E6E0", fg: "#4B5563", bar: "#C9CCD2", label: "#4B5362" },
|
|
203
|
+
/** label 4.91:1 on its bar — the dark grey bar is the dimmest of the four, so
|
|
204
|
+
* its label is the closest to black of them. */
|
|
205
|
+
dark: { bg: "#697180", fg: "#F8FAFC", bar: "#8A92A2", label: "#21252E" },
|
|
206
|
+
},
|
|
207
|
+
fable: {
|
|
208
|
+
/** label 4.86:1 on its bar */
|
|
209
|
+
light: { bg: "#E9DCF5", fg: "#6B21A8", bar: "#C3A6EC", label: "#520FB0" },
|
|
210
|
+
/** label 4.81:1 on its bar */
|
|
211
|
+
dark: { bg: "#7B4CD6", fg: "#F6F0FF", bar: "#A87BE8", label: "#310570" },
|
|
212
|
+
},
|
|
213
|
+
/** Ochre. The first external slot, and the lightest step of the warm family.
|
|
214
|
+
* label 4.87:1 light / 4.81:1 dark, each on its own bar. */
|
|
215
|
+
mate1: {
|
|
216
|
+
light: { bg: "#F6E6C4", fg: "#7A5210", bar: "#E2C27A", label: "#654909" },
|
|
217
|
+
dark: { bg: "#926511", fg: "#FFF7E8", bar: "#D8A445", label: "#533807" },
|
|
218
|
+
},
|
|
219
|
+
/** Terracotta — the middle step, a clear ~17° of hue from either neighbour.
|
|
220
|
+
* label 4.80:1 light / 4.83:1 dark, each on its own bar. */
|
|
221
|
+
mate2: {
|
|
222
|
+
light: { bg: "#F7DDCB", fg: "#8A3E12", bar: "#EAB08A", label: "#793406" },
|
|
223
|
+
dark: { bg: "#A8501B", fg: "#FFF1E8", bar: "#E08A52", label: "#552404" },
|
|
224
|
+
},
|
|
225
|
+
/** Red earth. The third slot wears the reddest step, and it is still nowhere near
|
|
226
|
+
* `theme.colors.danger` — this is a desaturated earth tone, not a warning.
|
|
227
|
+
* label 4.86:1 light / 4.82:1 dark, each on its own bar. */
|
|
228
|
+
kangaroo: {
|
|
229
|
+
light: { bg: "#F6D6D0", fg: "#8E2F26", bar: "#E5A096", label: "#7B1C0E" },
|
|
230
|
+
dark: { bg: "#A83A2E", fg: "#FFEFEC", bar: "#E07A6C", label: "#571107" },
|
|
231
|
+
},
|
|
232
|
+
/**
|
|
233
|
+
* THE FAMILY, not a fourth slot — the ink of the collapsed `mates` bar segment.
|
|
234
|
+
*
|
|
235
|
+
* A workflow bar draws every mate-routed agent as ONE run, because the question a bar
|
|
236
|
+
* answers is "how much of this workflow leaves Claude Code" and that is one number. That
|
|
237
|
+
* run cannot wear a slot's ink: painting a two-slot segment in `mate1`'s ochre says the
|
|
238
|
+
* work went to `mate1`, and painting it in whichever slot contributes most makes the same
|
|
239
|
+
* segment change colour when a count changes — the identical label in two hues down one
|
|
240
|
+
* chart. So the family gets an ink of its own: honey amber, hue 35° light / 31° dark.
|
|
241
|
+
*
|
|
242
|
+
* It does NOT try to be a fourth distinguishable step, and it could not be. The warm band
|
|
243
|
+
* is 0–50° and already holds three slots at ~17° spacing, so the widest gap left is about
|
|
244
|
+
* 11° — under the separation the slots themselves needed. It does not have to be: the
|
|
245
|
+
* family tone and the slot tones never appear in ONE graphic. The chart and the colour key
|
|
246
|
+
* under it use only this; the tier table and the agent table use only the slots.
|
|
247
|
+
*
|
|
248
|
+
* MEASURED, same battery as the slots above:
|
|
249
|
+
* label on its own bar light 4.97:1 dark 4.78:1
|
|
250
|
+
* fg on its own bg light 5.87:1 dark 4.83:1
|
|
251
|
+
* dark bg vs grounds 4.03 (black) / 3.27 (#1C1C1E) / 2.69 (#282C34) cream 4.88
|
|
252
|
+
*/
|
|
253
|
+
mates: {
|
|
254
|
+
light: { bg: "#F6E3C9", fg: "#80480F", bar: "#E5B77F", label: "#6B3D07" },
|
|
255
|
+
dark: { bg: "#9E5D16", fg: "#FFF5E8", bar: "#DB9A4C", label: "#553106" },
|
|
256
|
+
},
|
|
257
|
+
/** Not a model — the absence of a routing decision. Grey on both pages, so it never
|
|
258
|
+
* reads as another choice sitting beside the real ones. A mate is NOT this: a mate is
|
|
259
|
+
* a decision to route elsewhere, which is why it gets a colour and this does not.
|
|
260
|
+
*
|
|
261
|
+
* Its label is a NEUTRAL grey at 4.82:1 light / 4.83:1 dark, and on the dark page it
|
|
262
|
+
* is the only label that is pale rather than deep — this is the one bar that is itself
|
|
263
|
+
* dark, so nothing darker could be read on it. */
|
|
264
|
+
inherit: {
|
|
265
|
+
light: { bg: "#EFEFE6", fg: "#8A8A80", bar: "#DEDED2", label: "#5E5E57" },
|
|
266
|
+
dark: { bg: "#4A4A54", fg: "#D0D2D8", bar: "#4E4E58", label: "#C6C6CA" },
|
|
267
|
+
},
|
|
268
|
+
} as const;
|
|
269
|
+
|
|
270
|
+
export type ModelBadgeKind = keyof typeof MODEL_BADGE;
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Fill, ink and page-text for the report's state badges — one set per page.
|
|
274
|
+
*
|
|
275
|
+
* Every value here is LIFTED from `MODEL_BADGE` above rather than invented, so
|
|
276
|
+
* the contrast measurements recorded there carry over. Ink on fill, and the
|
|
277
|
+
* page text on its own ground, both measured here:
|
|
278
|
+
*
|
|
279
|
+
* update `sonnet` green ink/fill 5.82 light, 4.80 dark
|
|
280
|
+
* text 6.68 on cream, 9.44 on #1C1C1E
|
|
281
|
+
* deprecated `kangaroo` red earth label 4.86 light on its own bar
|
|
282
|
+
* fix `fable` purple label 4.86 light on its own bar
|
|
283
|
+
*
|
|
284
|
+
* `text` is the tone for a heading or glyph drawn ON THE PAGE, which is not the
|
|
285
|
+
* same question as ink on a fill: on cream it is the deep member of the family,
|
|
286
|
+
* on near-black the pale one. Using the fill for both put a mid-amber heading on
|
|
287
|
+
* a dark page, where it reads as dirt.
|
|
288
|
+
*
|
|
289
|
+
* Green, not ochre, because green is what an update wears elsewhere in magus
|
|
290
|
+
* and the report was the odd one out. It is a like-for-like swap inside the same
|
|
291
|
+
* measured system: the green fill separates from the page by 1.15 on cream and
|
|
292
|
+
* 3.42 on near-black, against the ochre's 1.15 and 3.32.
|
|
293
|
+
*
|
|
294
|
+
* Amber is NOT freed up by this — `unknown` still wears it, and that is the
|
|
295
|
+
* distinction worth keeping: green says "there is a newer version", amber says
|
|
296
|
+
* "we could not tell".
|
|
297
|
+
*
|
|
298
|
+
* Purple carries "fix required" because the warm band is spoken for and the
|
|
299
|
+
* measured set has no blue. Hue, not meaning — the badge says the word.
|
|
300
|
+
*/
|
|
301
|
+
const REPORT_BADGE = {
|
|
302
|
+
update: {
|
|
303
|
+
light: { fill: "#D6EEDD", ink: "#166534", text: "#166534" },
|
|
304
|
+
dark: { fill: "#12804F", ink: "#EFFFF6", text: "#4FD98F" },
|
|
305
|
+
},
|
|
306
|
+
deprecated: {
|
|
307
|
+
light: { fill: "#F6D6D0", ink: "#8E2F26", text: "#8E2F26" },
|
|
308
|
+
dark: { fill: "#A83A2E", ink: "#FFEFEC", text: "#E07A6C" },
|
|
309
|
+
},
|
|
310
|
+
fix: {
|
|
311
|
+
light: { fill: "#E9DCF5", ink: "#6B21A8", text: "#6B21A8" },
|
|
312
|
+
dark: { fill: "#7B4CD6", ink: "#F6F0FF", text: "#A87BE8" },
|
|
313
|
+
},
|
|
314
|
+
} as const;
|
|
315
|
+
|
|
316
|
+
export type ReportBadgeKind = keyof typeof REPORT_BADGE;
|
|
317
|
+
|
|
318
|
+
export interface ReportInk {
|
|
319
|
+
/** The badge's fill. */
|
|
320
|
+
fill: string;
|
|
321
|
+
/** Ink for the word written on `fill`. */
|
|
322
|
+
ink: string;
|
|
323
|
+
/** The tone for a heading or glyph on the page itself. */
|
|
324
|
+
text: string;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** Badge colours for one report state, for the resolved page. */
|
|
328
|
+
export function reportBadge(kind: ReportBadgeKind): ReportInk {
|
|
329
|
+
return REPORT_BADGE[kind][detected];
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* The unfilled part of a bar — `░` — for the page this run resolved.
|
|
334
|
+
*
|
|
335
|
+
* A track is CHROME. It exists so the eye can see how long the bar could have
|
|
336
|
+
* been, and it must never compete with the fill. "Quiet" is only definable
|
|
337
|
+
* against a page, which is why this lives here and not beside the ramps.
|
|
338
|
+
*
|
|
339
|
+
* It was a single dark grey, `darken(brand.muted, 0.68)` = #222429, chosen
|
|
340
|
+
* against a near-black terminal and applied on both. MEASURED against the two
|
|
341
|
+
* reference grounds:
|
|
342
|
+
*
|
|
343
|
+
* #222429 (was) this table
|
|
344
|
+
* dark 1.10:1 quiet 1.10:1 quiet
|
|
345
|
+
* cream 14.54:1 LOUDEST 1.24:1 quiet
|
|
346
|
+
*
|
|
347
|
+
* 14.54:1 is higher contrast than the body text. On a light terminal the empty
|
|
348
|
+
* half of every step bar was the most prominent thing in the report — a dense
|
|
349
|
+
* stipple that read as hatched texture and buried a 1 ms step's actual fill.
|
|
350
|
+
* The light value steps from the page TOWARD `brand.muted` rather than scaling
|
|
351
|
+
* a grey, so it stays in the page's own hue instead of laying cold grey on warm
|
|
352
|
+
* cream.
|
|
353
|
+
*/
|
|
354
|
+
const BAR_TRACK = { light: "#E0E2C3", dark: "#222429" } as const;
|
|
355
|
+
|
|
356
|
+
export function barTrack(): string {
|
|
357
|
+
return BAR_TRACK[detected];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export interface ModelInk {
|
|
361
|
+
bg: string;
|
|
362
|
+
fg: string;
|
|
363
|
+
bar: string;
|
|
364
|
+
/** Ink for a word written ON `bar`. Never `fg` — see the note above `MODEL_BADGE`. */
|
|
365
|
+
label: string;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Chip and bar colours for one model, for the resolved mode.
|
|
370
|
+
*
|
|
371
|
+
* Total by construction: a name this build does not know gets `inherit`'s grey, which reads
|
|
372
|
+
* as "magus does not recognise this" rather than as a missing colour. A hand-edited
|
|
373
|
+
* config can name anything.
|
|
374
|
+
*/
|
|
375
|
+
export function modelBadge(kind: string): ModelInk {
|
|
376
|
+
const key = (kind in MODEL_BADGE ? kind : "inherit") as ModelBadgeKind;
|
|
377
|
+
return MODEL_BADGE[key][detected];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* The UNLIT track: the `·` behind an effort dot, the `░` behind a bar.
|
|
382
|
+
*
|
|
383
|
+
* Light reuses `SCOPE_OFF_FILL`, which already solves this there. Dark does NOT: at
|
|
384
|
+
* `#3A3A40` the track measures 1.51:1 on the reference page and simply vanishes, so a bar
|
|
385
|
+
* reads as a floating block with no scale and the effort dots lose the denominator that
|
|
386
|
+
* makes them a level rather than a count. `#4A4A52` is 1.94:1 — still recessive, still
|
|
387
|
+
* visible.
|
|
388
|
+
*/
|
|
389
|
+
export const TRACK_FILL = {
|
|
390
|
+
light: SCOPE_OFF_FILL.light,
|
|
391
|
+
dark: "#4A4A52",
|
|
392
|
+
} as const;
|
|
393
|
+
|
|
394
|
+
/** Fill for an unlit track segment, for the resolved mode. */
|
|
395
|
+
export function trackFill(): string {
|
|
396
|
+
return TRACK_FILL[detected];
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Effort's colour ramp — TEXT only, never a fill, so a row never paints two blocks.
|
|
401
|
+
*
|
|
402
|
+
* Effort is an ORDINAL, so these are a ramp rather than four unrelated hues. The ramp avoids
|
|
403
|
+
* green and red, which already mean "on" and "failed" elsewhere on the same screen: one
|
|
404
|
+
* colour, one meaning, app-wide. Dark runs brighter than light for the reason `MODEL_BADGE`
|
|
405
|
+
* gives — a dim colour on a dark page is a grey one.
|
|
406
|
+
*/
|
|
407
|
+
export const EFFORT_INK = {
|
|
408
|
+
low: { light: "#6B7280", dark: "#9AA0AC" },
|
|
409
|
+
medium: { light: "#0E7490", dark: "#6FD9F0" },
|
|
410
|
+
high: { light: "#B45309", dark: "#F2B25E" },
|
|
411
|
+
xhigh: { light: "#A21CAF", dark: "#E49BFF" },
|
|
412
|
+
} as const;
|
|
413
|
+
|
|
414
|
+
export type EffortInkKind = keyof typeof EFFORT_INK;
|
|
415
|
+
|
|
416
|
+
/** Ink for one effort level, for the resolved mode. Unknown words take the lowest step. */
|
|
417
|
+
export function effortInk(effort: string): string {
|
|
418
|
+
const key = (effort in EFFORT_INK ? effort : "low") as EffortInkKind;
|
|
419
|
+
return EFFORT_INK[key][detected];
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* The two reference pages, mirrored from `theme.ts`'s `CONTRAST_REFERENCE`.
|
|
424
|
+
*
|
|
425
|
+
* Duplicated deliberately: `theme.ts` imports opentui's RGBA, which drags the
|
|
426
|
+
* renderer into anything that touches it. This module is pure data by design so
|
|
427
|
+
* the colour tests can run without a terminal. A test asserts the two agree.
|
|
428
|
+
*/
|
|
429
|
+
export const CONTRAST_REFERENCE_KEYS = {
|
|
430
|
+
light: "#FAFAD2",
|
|
431
|
+
dark: "#1C1C1E",
|
|
432
|
+
} as const;
|
|
433
|
+
|
|
434
|
+
/** Chip colours for one component kind, for the resolved mode. */
|
|
435
|
+
export function componentBadge(kind: ComponentBadgeKind): {
|
|
436
|
+
bg: string;
|
|
437
|
+
fg: string;
|
|
438
|
+
} {
|
|
439
|
+
return COMPONENT_BADGE[kind][detected];
|
|
440
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { ThemeMode } from "@opentui/core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Terminal theme resolution — the one order magus follows, decided once at
|
|
5
|
+
* process start, first answer wins:
|
|
6
|
+
*
|
|
7
|
+
* 1. `--theme` flag
|
|
8
|
+
* 2. `MAGUS_THEME`
|
|
9
|
+
* 3. `TERM_THEME`
|
|
10
|
+
* 4. OSC 10/11 probe (bounded, interactive tty only)
|
|
11
|
+
* 5. `COLORFGBG`
|
|
12
|
+
* 6. dark
|
|
13
|
+
*
|
|
14
|
+
* A step with no answer says nothing and the next one runs; nothing before
|
|
15
|
+
* step 6 ever asserts dark. Every theme word is compared exactly — the byte
|
|
16
|
+
* strings `light` and `dark`, lowercase, untrimmed — because the same test has
|
|
17
|
+
* to run in a POSIX `case` and a tmux format, and only the exact comparison is
|
|
18
|
+
* free in all three.
|
|
19
|
+
*
|
|
20
|
+
* This module is pure by design: no `process` access except as a *default
|
|
21
|
+
* argument* to `snapshotThemeEnv`, no I/O, no renderer. Everything it needs is
|
|
22
|
+
* passed in, so the whole contract runs in a test without a terminal.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export type ThemeSource =
|
|
26
|
+
| "flag"
|
|
27
|
+
| "variable"
|
|
28
|
+
| "TERM_THEME"
|
|
29
|
+
| "osc11"
|
|
30
|
+
| "COLORFGBG"
|
|
31
|
+
| "default";
|
|
32
|
+
|
|
33
|
+
export interface ThemeResolution {
|
|
34
|
+
/** Never null — when nothing answered, it is dark by decision. */
|
|
35
|
+
mode: ThemeMode;
|
|
36
|
+
source: ThemeSource;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The three variables the resolver reads, captured BEFORE .env is merged. */
|
|
40
|
+
export interface ThemeEnv {
|
|
41
|
+
TERM_THEME?: string;
|
|
42
|
+
MAGUS_THEME?: string;
|
|
43
|
+
COLORFGBG?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ResolveThemeInput {
|
|
47
|
+
/** Already validated by the router: only "light" | "dark" | undefined arrive here. */
|
|
48
|
+
flag?: ThemeMode;
|
|
49
|
+
env: ThemeEnv;
|
|
50
|
+
/** stdin AND stdout are TTYs. Decided by the caller; the resolver never touches process. */
|
|
51
|
+
isInteractive: boolean;
|
|
52
|
+
/** Bounded OSC 10/11 probe. Called at most once, and only when step 4 is reached. */
|
|
53
|
+
probe: () => Promise<ThemeMode | null>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Exact match on "light" | "dark"; everything else is no opinion. */
|
|
57
|
+
export function parseThemeWord(value: string | undefined): ThemeMode | null {
|
|
58
|
+
if (value === "light") return "light";
|
|
59
|
+
if (value === "dark") return "dark";
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The literal background fields that answer — the same set as the statusline's `case`. */
|
|
64
|
+
const LIGHT_BACKGROUNDS: ReadonlySet<string> = new Set(["7", "15"]);
|
|
65
|
+
const DARK_BACKGROUNDS: ReadonlySet<string> = new Set([
|
|
66
|
+
"0",
|
|
67
|
+
"1",
|
|
68
|
+
"2",
|
|
69
|
+
"3",
|
|
70
|
+
"4",
|
|
71
|
+
"5",
|
|
72
|
+
"6",
|
|
73
|
+
"8",
|
|
74
|
+
"9",
|
|
75
|
+
"10",
|
|
76
|
+
"11",
|
|
77
|
+
"12",
|
|
78
|
+
"13",
|
|
79
|
+
"14",
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Last ";"-field is the background: 7|15 → light, 0-6|8-14 → dark, else null.
|
|
84
|
+
*
|
|
85
|
+
* Exact string membership, not numeric parsing: the statusline's
|
|
86
|
+
* `parse_colorfgbg` is a POSIX `case` over the literal words, so `07`, `015`
|
|
87
|
+
* and `00` are no opinion there, and that literal set is the contract the two
|
|
88
|
+
* tools share. `Number("07")` would make magus say light where the
|
|
89
|
+
* statusline says nothing.
|
|
90
|
+
*
|
|
91
|
+
* A value without a `;` is not `<fg>;<bg>` at all — a lone `7` is no opinion,
|
|
92
|
+
* matching the statusline's `case "$1" in *\;*)` guard so the two tools agree.
|
|
93
|
+
*/
|
|
94
|
+
export function parseColorFgBg(value: string | undefined): ThemeMode | null {
|
|
95
|
+
if (value === undefined || !value.includes(";")) return null;
|
|
96
|
+
const bg = value.slice(value.lastIndexOf(";") + 1);
|
|
97
|
+
if (LIGHT_BACKGROUNDS.has(bg)) return "light";
|
|
98
|
+
if (DARK_BACKGROUNDS.has(bg)) return "dark";
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Copy the three variables out of `env` — call this before loadProjectDotenv.
|
|
104
|
+
*
|
|
105
|
+
* Copies, never references: the returned object must not observe later writes
|
|
106
|
+
* to `process.env`, which is exactly what a merged `.env` would be.
|
|
107
|
+
*/
|
|
108
|
+
export function snapshotThemeEnv(
|
|
109
|
+
env: Readonly<Record<string, string | undefined>> = process.env,
|
|
110
|
+
): ThemeEnv {
|
|
111
|
+
const snapshot: ThemeEnv = {};
|
|
112
|
+
if (env.TERM_THEME !== undefined) snapshot.TERM_THEME = env.TERM_THEME;
|
|
113
|
+
if (env.MAGUS_THEME !== undefined) snapshot.MAGUS_THEME = env.MAGUS_THEME;
|
|
114
|
+
if (env.COLORFGBG !== undefined) snapshot.COLORFGBG = env.COLORFGBG;
|
|
115
|
+
return snapshot;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function resolveTheme(
|
|
119
|
+
input: ResolveThemeInput,
|
|
120
|
+
): Promise<ThemeResolution> {
|
|
121
|
+
if (input.flag !== undefined) {
|
|
122
|
+
return { mode: input.flag, source: "flag" };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const variable = parseThemeWord(input.env.MAGUS_THEME);
|
|
126
|
+
if (variable !== null) return { mode: variable, source: "variable" };
|
|
127
|
+
|
|
128
|
+
const termTheme = parseThemeWord(input.env.TERM_THEME);
|
|
129
|
+
if (termTheme !== null) return { mode: termTheme, source: "TERM_THEME" };
|
|
130
|
+
|
|
131
|
+
// Steps 1-3 answered nothing, so this is the only path that probes. Not
|
|
132
|
+
// reaching here is what keeps first paint undelayed when a word answered.
|
|
133
|
+
if (input.isInteractive) {
|
|
134
|
+
let probed: ThemeMode | null = null;
|
|
135
|
+
try {
|
|
136
|
+
probed = await input.probe();
|
|
137
|
+
} catch {
|
|
138
|
+
probed = null;
|
|
139
|
+
}
|
|
140
|
+
if (probed !== null) return { mode: probed, source: "osc11" };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const colorFgBg = parseColorFgBg(input.env.COLORFGBG);
|
|
144
|
+
if (colorFgBg !== null) return { mode: colorFgBg, source: "COLORFGBG" };
|
|
145
|
+
|
|
146
|
+
return { mode: "dark", source: "default" };
|
|
147
|
+
}
|