claudeup 4.35.0 → 4.36.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 (63) hide show
  1. package/package.json +4 -4
  2. package/src/__tests__/git-worktree.test.ts +108 -0
  3. package/src/__tests__/scope-squares.test.tsx +165 -0
  4. package/src/__tests__/theme-adaptive-colors.test.ts +307 -0
  5. package/src/__tests__/uppercase-keybindings.test.ts +101 -0
  6. package/src/__tests__/worktree-registry-resolution.test.ts +107 -0
  7. package/src/main.tsx +21 -5
  8. package/src/opentui.d.ts +21 -12
  9. package/src/services/claude-settings.ts +37 -7
  10. package/src/services/git-worktree.ts +129 -0
  11. package/src/services/plugin-manager.ts +10 -1
  12. package/src/ui/App.tsx +19 -12
  13. package/src/ui/adapters/pluginsAdapter.ts +174 -168
  14. package/src/ui/adapters/settingsAdapter.ts +119 -116
  15. package/src/ui/adapters/skillsAdapter.ts +203 -196
  16. package/src/ui/components/CategoryHeader.tsx +9 -8
  17. package/src/ui/components/EmptyFilterState.tsx +10 -5
  18. package/src/ui/components/FlagDetailEditor.tsx +0 -0
  19. package/src/ui/components/ScopeIndicator.tsx +10 -6
  20. package/src/ui/components/ScrollableList.tsx +3 -2
  21. package/src/ui/components/SearchInput.tsx +2 -1
  22. package/src/ui/components/StyledText.tsx +5 -4
  23. package/src/ui/components/TabBar.tsx +4 -3
  24. package/src/ui/components/layout/FooterHints.tsx +37 -30
  25. package/src/ui/components/layout/Panel.tsx +6 -5
  26. package/src/ui/components/layout/ProgressBar.tsx +7 -6
  27. package/src/ui/components/layout/ScopeTabs.tsx +6 -5
  28. package/src/ui/components/layout/ScreenLayout.tsx +27 -24
  29. package/src/ui/components/layout/index.ts +3 -3
  30. package/src/ui/components/modals/ConfirmModal.tsx +12 -11
  31. package/src/ui/components/modals/InputModal.tsx +14 -6
  32. package/src/ui/components/modals/LoadingModal.tsx +6 -5
  33. package/src/ui/components/modals/MessageModal.tsx +9 -8
  34. package/src/ui/components/modals/SelectModal.tsx +11 -7
  35. package/src/ui/components/modals/VersionMismatchModal.tsx +14 -16
  36. package/src/ui/components/primitives/ActionHints.tsx +26 -26
  37. package/src/ui/components/primitives/DetailSection.tsx +13 -12
  38. package/src/ui/components/primitives/KeyValueLine.tsx +9 -8
  39. package/src/ui/components/primitives/ListCategoryRow.tsx +25 -27
  40. package/src/ui/components/primitives/MetaText.tsx +3 -3
  41. package/src/ui/components/primitives/ScopeDetail.tsx +48 -48
  42. package/src/ui/components/primitives/ScopeSquares.tsx +47 -22
  43. package/src/ui/components/primitives/SelectableRow.tsx +22 -16
  44. package/src/ui/hooks/useGitignoreModal.ts +78 -74
  45. package/src/ui/registry.ts +11 -11
  46. package/src/ui/renderers/cliToolRenderers.tsx +260 -203
  47. package/src/ui/renderers/gitignoreRenderers.tsx +43 -42
  48. package/src/ui/renderers/mcpRenderers.tsx +121 -117
  49. package/src/ui/renderers/pluginRenderers.tsx +528 -471
  50. package/src/ui/renderers/profileRenderers.tsx +346 -300
  51. package/src/ui/renderers/settingsRenderers.tsx +183 -176
  52. package/src/ui/renderers/skillRenderers.tsx +410 -326
  53. package/src/ui/screens/AliasScreen.tsx +1336 -1309
  54. package/src/ui/screens/CliToolsScreen.tsx +92 -40
  55. package/src/ui/screens/EnvVarsScreen.tsx +19 -13
  56. package/src/ui/screens/GitignoreScreen.tsx +510 -493
  57. package/src/ui/screens/McpRegistryScreen.tsx +28 -21
  58. package/src/ui/screens/McpScreen.tsx +12 -3
  59. package/src/ui/screens/PluginsScreen.tsx +17 -7
  60. package/src/ui/screens/ProfilesScreen.tsx +39 -23
  61. package/src/ui/screens/SkillsScreen.tsx +832 -688
  62. package/src/ui/theme-mode.ts +73 -0
  63. package/src/ui/theme.ts +147 -53
@@ -0,0 +1,73 @@
1
+ import type { ThemeMode } from "@opentui/core";
2
+
3
+ /**
4
+ * The one place claudeup 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 ask. OpenTUI queries the terminal over OSC and reports `themeMode`; we
17
+ * read it once at startup. **Unknown means no tint** — a terminal that does not
18
+ * answer the query gets today's behaviour rather than a guess that could paint a
19
+ * bright band across half the list.
20
+ */
21
+
22
+ let detected: ThemeMode | null = null;
23
+
24
+ /** Set once at startup from `renderer.themeMode`. */
25
+ export function setThemeMode(mode: ThemeMode | null): void {
26
+ detected = mode;
27
+ }
28
+
29
+ export function getThemeMode(): ThemeMode | null {
30
+ return detected;
31
+ }
32
+
33
+ /** Test seam — reset detection between cases. */
34
+ export function resetThemeMode(): void {
35
+ detected = null;
36
+ }
37
+
38
+ /**
39
+ * Fill for an UNLIT segment of the three-square scope bar.
40
+ *
41
+ * The bar reads as three segments, each lit (in its scope colour) or unlit. Unlit
42
+ * used to be the same mid-grey as body de-emphasis, `#6B7280`, which is dark —
43
+ * so an unlit segment carried the same visual weight as a lit one and the bar
44
+ * read as three similar blobs. Reported directly: "non installed state is dark as
45
+ * well - confusing".
46
+ *
47
+ * An unlit segment should recede toward the page, and "toward the page" is the
48
+ * one thing a fixed colour cannot express: a pale fill that sits quietly on cream
49
+ * is a bright block on near-black. Hence the two values, chosen per detected
50
+ * terminal — this is the only place claudeup asks.
51
+ */
52
+ export const SCOPE_OFF_FILL = {
53
+ /** Just off a light page — a filled but quiet segment. */
54
+ light: "#D9D9CC",
55
+ /** Just off a dark page. */
56
+ dark: "#3A3A40",
57
+ } as const;
58
+
59
+ /**
60
+ * Neutral fallback when the terminal never answered the OSC query. A mid grey is
61
+ * visible on both pages — not as quiet as the tuned values, but never invisible
62
+ * and never a bright block. Failing to a wrong extreme would be worse than
63
+ * failing to something merely unremarkable.
64
+ */
65
+ export const SCOPE_OFF_FALLBACK = "#9CA3AF";
66
+
67
+ /** Fill colour for an unlit scope segment. Always a colour — never undefined,
68
+ * because the segment is a filled block whatever the terminal turns out to be. */
69
+ export function scopeOffFill(): string {
70
+ if (detected === "light") return SCOPE_OFF_FILL.light;
71
+ if (detected === "dark") return SCOPE_OFF_FILL.dark;
72
+ return SCOPE_OFF_FALLBACK;
73
+ }
package/src/ui/theme.ts CHANGED
@@ -1,55 +1,149 @@
1
+ import { RGBA } from "@opentui/core";
2
+
3
+ /**
4
+ * claudeup's palette: our own vivid accents, on the terminal's own canvas.
5
+ *
6
+ * Three rules, and the reason for each:
7
+ *
8
+ * 1. **Backgrounds are never ours.** Surfaces and modals use the terminal's
9
+ * background. Painting an absolute fill boxes the UI into whichever theme we
10
+ * guessed, and it was the original sin here — modals hardcoded #1C1C1E.
11
+ * 2. **Body text is never ours.** Normal text uses the terminal's foreground,
12
+ * which is the only colour guaranteed to clear a text-grade contrast ratio
13
+ * against that terminal's own background. `text` used to be the string
14
+ * `"white"` — OpenTUI resolves that to rgb(255,255,255), not to "the
15
+ * terminal's white" — so on a light theme every installed plugin was drawn
16
+ * white-on-cream and vanished while the *un*installed ones, in grey, stayed
17
+ * readable. The list read exactly backwards.
18
+ * 3. **Accents ARE ours, and vivid.** Deferring accents to ANSI slots 0-15 made
19
+ * everything legible but flattened it: a theme that renders slots 2 and 3 as
20
+ * neighbouring olives leaves user/project/local scope squares indistinguishable,
21
+ * and slot 8 chips melt into the page. Distinction is a product decision, so
22
+ * these are chosen, not inherited.
23
+ *
24
+ * ## How the accents were chosen
25
+ *
26
+ * By measurement, not taste. A single colour CANNOT reach 4.5:1 against both a
27
+ * light and a dark terminal — clearing 4.5:1 on cream needs relative luminance
28
+ * <= ~0.17, clearing it on near-black needs >= ~0.22, and those do not overlap.
29
+ * So the bar is 3:1, WCAG's threshold for UI components and large text, which
30
+ * admits a band of mid-dark saturated colours. Every accent below clears 3:1
31
+ * against BOTH reference backgrounds, verified in theme-adaptive-colors.test.ts —
32
+ * so a colour that only works on dark cannot be added without failing the build.
33
+ *
34
+ * Where we own a badge's background we also own its ink, so `ink` is plain white
35
+ * and its contrast is deterministic rather than a function of the user's theme.
36
+ * That is why badge ink is not slot 0 or the terminal background: on a light
37
+ * theme, terminal-background ink on a yellow chip is unreadable.
38
+ */
39
+
40
+ /** Reference backgrounds every accent is measured against. */
41
+ export const CONTRAST_REFERENCE = {
42
+ light: "#FAFAD2",
43
+ dark: "#1C1C1E",
44
+ } as const;
45
+
46
+ /**
47
+ * The vivid set. Ratios are (on light / on dark), measured — see the test.
48
+ * Hues for the three scopes are deliberately far apart (blue / green / amber)
49
+ * because they appear as single-character squares, where a subtle hue shift is
50
+ * not readable at all.
51
+ */
52
+ export const brand = {
53
+ accent: "#9333EA", // 5.04 / 3.16 — brand purple
54
+ link: "#2563EB", // 4.84 / 3.29
55
+ info: "#0E7490", // 5.02 / 3.18 — teal
56
+ success: "#15803D", // 4.70 / 3.39
57
+ warning: "#B45309", // 4.70 / 3.39 — amber
58
+ danger: "#DC2626", // 4.52 / 3.52
59
+ muted: "#6B7280", // 4.53 / 3.52 — de-emphasis, still legible on both
60
+ /** Ink on OUR badges. We own both sides, so contrast is deterministic. */
61
+ ink: "#FFFFFF",
62
+ } as const;
63
+
64
+ /** Any colour a component may hand to a JSX colour prop. */
65
+ export type UiColor = string | RGBA;
66
+
67
+ /** The terminal's own foreground — the only honest "normal text" colour. */
68
+ const fg = () => RGBA.defaultForeground();
69
+
70
+ /** The terminal's own background — for surfaces, never for ink. */
71
+ const bg = () => RGBA.defaultBackground();
72
+
1
73
  export const theme = {
2
- colors: {
3
- text: "white",
4
- muted: "#666666",
5
- dim: "#333333",
6
- border: "#444444",
7
- link: "#5c9aff",
8
- accent: "#7e57c2",
9
- /** Low-key brand tint for bylines — readable, deliberately not attention-grabbing. */
10
- byline: "#8a7fa8",
11
- success: "green",
12
- warning: "yellow",
13
- danger: "red",
14
- info: "cyan",
15
- },
16
-
17
- selection: {
18
- bg: "magenta",
19
- fg: "white",
20
- },
21
-
22
- scopes: {
23
- user: "cyan",
24
- project: "green",
25
- local: "yellow",
26
- },
27
-
28
- category: {
29
- purple: { bg: "#7e57c2", fg: "white", badgeFg: "#d1c4e9" },
30
- green: { bg: "#2e7d32", fg: "white", badgeFg: "#c8e6c9" },
31
- teal: { bg: "#00695c", fg: "white", badgeFg: "#b2dfdb" },
32
- yellow: { bg: "#8d6e00", fg: "white", badgeFg: "#ffe082" },
33
- gray: { bg: "#455a64", fg: "white", badgeFg: "#cfd8dc" },
34
- red: { bg: "#b71c1c", fg: "white", badgeFg: "#ffcdd2" },
35
- },
36
-
37
- meta: {
38
- muted: "gray",
39
- warning: "yellow",
40
- success: "green",
41
- danger: "red",
42
- },
43
-
44
- hints: {
45
- defaultBg: "white",
46
- primaryBg: "cyan",
47
- dangerBg: "red",
48
- },
49
-
50
- spacing: {
51
- rowIndent: 1,
52
- sectionGap: 1,
53
- panelPadding: 1,
54
- },
74
+ colors: {
75
+ /** Adaptive by rule 2 — never make this one of ours. */
76
+ text: fg(),
77
+ muted: brand.muted,
78
+ /** Same grey as `muted`; a third level should come from layout, not colour. */
79
+ dim: brand.muted,
80
+ border: brand.muted,
81
+ link: brand.link,
82
+ accent: brand.accent,
83
+ /** Low-key brand tint for bylines — readable, deliberately not attention-grabbing. */
84
+ byline: brand.accent,
85
+ success: brand.success,
86
+ warning: brand.warning,
87
+ danger: brand.danger,
88
+ info: brand.info,
89
+ },
90
+
91
+ selection: {
92
+ bg: brand.accent,
93
+ fg: brand.ink,
94
+ },
95
+
96
+ /** Blue / green / amber: the widest hue separation available, because these
97
+ * render as one-character squares where a subtle shift reads as noise. */
98
+ scopes: {
99
+ user: brand.link,
100
+ project: brand.success,
101
+ local: brand.warning,
102
+ },
103
+
104
+ /** Badge colours. `fg` is the ink ON the block; `badgeFg` is the same hue used
105
+ * as ink on the normal background instead. */
106
+ category: {
107
+ purple: { bg: brand.accent, fg: brand.ink, badgeFg: brand.accent },
108
+ green: { bg: brand.success, fg: brand.ink, badgeFg: brand.success },
109
+ teal: { bg: brand.info, fg: brand.ink, badgeFg: brand.info },
110
+ yellow: { bg: brand.warning, fg: brand.ink, badgeFg: brand.warning },
111
+ gray: { bg: brand.muted, fg: brand.ink, badgeFg: brand.muted },
112
+ red: { bg: brand.danger, fg: brand.ink, badgeFg: brand.danger },
113
+ },
114
+
115
+ meta: {
116
+ muted: brand.muted,
117
+ warning: brand.warning,
118
+ success: brand.success,
119
+ danger: brand.danger,
120
+ },
121
+
122
+ /**
123
+ * Footer key chips. A vivid block with white ink, because the point of a chip
124
+ * is to read as a pressable key at a glance — the previous neutral-grey chip
125
+ * melted into a light page. Dark slates were measured and rejected: they clear
126
+ * 3:1 on cream but drop to ~2.2 on a dark terminal, i.e. an invisible block.
127
+ */
128
+ hints: {
129
+ defaultBg: brand.accent,
130
+ primaryBg: brand.info,
131
+ dangerBg: brand.danger,
132
+ fg: brand.ink,
133
+ },
134
+
135
+ /** Panels and modals sit on the terminal's own background and are separated by
136
+ * their border, never by an absolute fill that fights the user's theme. */
137
+ surface: {
138
+ bg: bg(),
139
+ border: brand.muted,
140
+ },
141
+
142
+ spacing: {
143
+ rowIndent: 1,
144
+ sectionGap: 1,
145
+ panelPadding: 1,
146
+ },
55
147
  } as const;
148
+
149
+ export { fg as defaultFg, bg as defaultBg };