claudeup 4.35.1 → 4.37.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 (72) hide show
  1. package/package.json +4 -4
  2. package/src/__tests__/catalog-cache-store.test.ts +271 -0
  3. package/src/__tests__/catalog-notice.test.ts +155 -0
  4. package/src/__tests__/github-budget.test.ts +200 -0
  5. package/src/__tests__/plugin-manager-fallback.test.ts +200 -8
  6. package/src/__tests__/scope-squares.test.tsx +165 -0
  7. package/src/__tests__/theme-adaptive-colors.test.ts +307 -0
  8. package/src/__tests__/uppercase-keybindings.test.ts +101 -0
  9. package/src/main.tsx +21 -5
  10. package/src/opentui.d.ts +21 -12
  11. package/src/services/catalog-cache-store.ts +218 -0
  12. package/src/services/github-budget.ts +274 -0
  13. package/src/services/marketplace-catalog-git.ts +170 -0
  14. package/src/services/marketplace-catalog.ts +95 -0
  15. package/src/services/marketplace-fetcher.ts +310 -87
  16. package/src/services/plugin-manager.ts +103 -92
  17. package/src/ui/App.tsx +19 -12
  18. package/src/ui/adapters/catalogNotice.ts +122 -0
  19. package/src/ui/adapters/pluginsAdapter.ts +174 -168
  20. package/src/ui/adapters/settingsAdapter.ts +119 -116
  21. package/src/ui/adapters/skillsAdapter.ts +203 -196
  22. package/src/ui/components/CategoryHeader.tsx +9 -8
  23. package/src/ui/components/EmptyFilterState.tsx +10 -5
  24. package/src/ui/components/FlagDetailEditor.tsx +0 -0
  25. package/src/ui/components/ScopeIndicator.tsx +10 -6
  26. package/src/ui/components/ScrollableList.tsx +3 -2
  27. package/src/ui/components/SearchInput.tsx +2 -1
  28. package/src/ui/components/StyledText.tsx +5 -4
  29. package/src/ui/components/TabBar.tsx +4 -3
  30. package/src/ui/components/layout/FooterHints.tsx +37 -30
  31. package/src/ui/components/layout/Panel.tsx +6 -5
  32. package/src/ui/components/layout/ProgressBar.tsx +7 -6
  33. package/src/ui/components/layout/ScopeTabs.tsx +6 -5
  34. package/src/ui/components/layout/ScreenLayout.tsx +46 -26
  35. package/src/ui/components/layout/index.ts +3 -3
  36. package/src/ui/components/modals/ConfirmModal.tsx +12 -11
  37. package/src/ui/components/modals/InputModal.tsx +14 -6
  38. package/src/ui/components/modals/LoadingModal.tsx +6 -5
  39. package/src/ui/components/modals/MessageModal.tsx +9 -8
  40. package/src/ui/components/modals/SelectModal.tsx +11 -7
  41. package/src/ui/components/modals/VersionMismatchModal.tsx +14 -16
  42. package/src/ui/components/primitives/ActionHints.tsx +26 -26
  43. package/src/ui/components/primitives/DetailSection.tsx +13 -12
  44. package/src/ui/components/primitives/KeyValueLine.tsx +9 -8
  45. package/src/ui/components/primitives/ListCategoryRow.tsx +25 -27
  46. package/src/ui/components/primitives/MetaText.tsx +3 -3
  47. package/src/ui/components/primitives/ScopeDetail.tsx +48 -48
  48. package/src/ui/components/primitives/ScopeSquares.tsx +47 -22
  49. package/src/ui/components/primitives/SelectableRow.tsx +22 -16
  50. package/src/ui/hooks/useGitignoreModal.ts +78 -74
  51. package/src/ui/registry.ts +11 -11
  52. package/src/ui/renderers/cliToolRenderers.tsx +260 -203
  53. package/src/ui/renderers/gitignoreRenderers.tsx +43 -42
  54. package/src/ui/renderers/mcpRenderers.tsx +121 -117
  55. package/src/ui/renderers/pluginRenderers.tsx +566 -471
  56. package/src/ui/renderers/profileRenderers.tsx +346 -300
  57. package/src/ui/renderers/settingsRenderers.tsx +183 -176
  58. package/src/ui/renderers/skillRenderers.tsx +410 -326
  59. package/src/ui/screens/AliasScreen.tsx +1336 -1309
  60. package/src/ui/screens/CliToolsScreen.tsx +92 -40
  61. package/src/ui/screens/EnvVarsScreen.tsx +19 -13
  62. package/src/ui/screens/GitignoreScreen.tsx +510 -493
  63. package/src/ui/screens/McpRegistryScreen.tsx +28 -21
  64. package/src/ui/screens/McpScreen.tsx +12 -3
  65. package/src/ui/screens/PluginsScreen.tsx +152 -33
  66. package/src/ui/screens/ProfilesScreen.tsx +39 -23
  67. package/src/ui/screens/SkillsScreen.tsx +832 -688
  68. package/src/ui/state/reducer.ts +11 -2
  69. package/src/ui/state/types.ts +16 -1
  70. package/src/ui/theme-mode.ts +73 -0
  71. package/src/ui/theme.ts +147 -53
  72. package/src/utils/config-dir.ts +47 -0
@@ -1,5 +1,6 @@
1
1
  import React from "react";
2
2
  import type { VersionMismatchInfo } from "../../../services/plugin-version-check.js";
3
+ import { theme } from "../../theme.js";
3
4
 
4
5
  interface VersionMismatchModalProps {
5
6
  /** List of version mismatches detected */
@@ -8,15 +9,15 @@ interface VersionMismatchModalProps {
8
9
  defaultIndex?: number;
9
10
  }
10
11
 
11
- const COLOR_BG = "#1C1C1E";
12
- const COLOR_BORDER = "#525252";
13
- const COLOR_TITLE = "#EDEDED";
14
- const COLOR_MUTED = "#A1A1AA";
15
- const COLOR_DIM = "#71717A";
16
- const COLOR_WARN = "#FBBF24";
17
- const COLOR_BAD = "#F87171";
18
- const COLOR_GOOD = "#4ADE80";
19
- const COLOR_ACCENT = "#F4F4F5";
12
+ const COLOR_BG = theme.surface.bg;
13
+ const COLOR_BORDER = theme.colors.dim;
14
+ const COLOR_TITLE = theme.colors.text;
15
+ const COLOR_MUTED = theme.colors.muted;
16
+ const COLOR_DIM = theme.colors.muted;
17
+ const COLOR_WARN = theme.colors.warning;
18
+ const COLOR_BAD = theme.colors.danger;
19
+ const COLOR_GOOD = theme.colors.success;
20
+ const COLOR_ACCENT = theme.colors.text;
20
21
 
21
22
  /** Truncate a plugin name to fit the column width */
22
23
  function shortName(pluginId: string, max: number): string {
@@ -80,7 +81,8 @@ export function VersionMismatchModal({
80
81
  </strong>
81
82
  </text>
82
83
  <text fg={COLOR_BORDER}>
83
- {"─".repeat(NAME_W - 1)} {"─".repeat(VER_W - 1)} {"".repeat(VER_W - 1)}
84
+ {"─".repeat(NAME_W - 1)} {"─".repeat(VER_W - 1)}{" "}
85
+ {"─".repeat(VER_W - 1)}
84
86
  </text>
85
87
 
86
88
  {/* Table rows */}
@@ -107,9 +109,7 @@ export function VersionMismatchModal({
107
109
 
108
110
  {/* Options */}
109
111
  <box flexDirection="column" paddingLeft={1}>
110
- <text
111
- fg={selectedIndex === 0 ? COLOR_ACCENT : COLOR_MUTED}
112
- >
112
+ <text fg={selectedIndex === 0 ? COLOR_ACCENT : COLOR_MUTED}>
113
113
  <span fg={selectedIndex === 0 ? COLOR_ACCENT : COLOR_DIM}>
114
114
  {selectedIndex === 0 ? "❯ " : " "}
115
115
  </span>
@@ -119,9 +119,7 @@ export function VersionMismatchModal({
119
119
  "Fix all projects"
120
120
  )}
121
121
  </text>
122
- <text
123
- fg={selectedIndex === 1 ? COLOR_ACCENT : COLOR_MUTED}
124
- >
122
+ <text fg={selectedIndex === 1 ? COLOR_ACCENT : COLOR_MUTED}>
125
123
  <span fg={selectedIndex === 1 ? COLOR_ACCENT : COLOR_DIM}>
126
124
  {selectedIndex === 1 ? "❯ " : " "}
127
125
  </span>
@@ -2,13 +2,13 @@ import React from "react";
2
2
  import { theme } from "../../theme.js";
3
3
 
4
4
  export interface Hint {
5
- key: string;
6
- label: string;
7
- tone?: "default" | "primary" | "danger";
5
+ key: string;
6
+ label: string;
7
+ tone?: "default" | "primary" | "danger";
8
8
  }
9
9
 
10
10
  interface ActionHintsProps {
11
- hints: Hint[];
11
+ hints: Hint[];
12
12
  }
13
13
 
14
14
  /**
@@ -16,26 +16,26 @@ interface ActionHintsProps {
16
16
  * Renders: [bg] key [/bg] label
17
17
  */
18
18
  export function ActionHints({ hints }: ActionHintsProps) {
19
- return (
20
- <box flexDirection="column" marginTop={1}>
21
- {hints.map((hint) => (
22
- <box key={`${hint.key}:${hint.label}`}>
23
- <text
24
- bg={
25
- hint.tone === "danger"
26
- ? theme.hints.dangerBg
27
- : hint.tone === "primary"
28
- ? theme.hints.primaryBg
29
- : theme.hints.defaultBg
30
- }
31
- fg="black"
32
- >
33
- {" "}
34
- {hint.key}{" "}
35
- </text>
36
- <text fg="gray"> {hint.label}</text>
37
- </box>
38
- ))}
39
- </box>
40
- );
19
+ return (
20
+ <box flexDirection="column" marginTop={1}>
21
+ {hints.map((hint) => (
22
+ <box key={`${hint.key}:${hint.label}`}>
23
+ <text
24
+ bg={
25
+ hint.tone === "danger"
26
+ ? theme.hints.dangerBg
27
+ : hint.tone === "primary"
28
+ ? theme.hints.primaryBg
29
+ : theme.hints.defaultBg
30
+ }
31
+ fg={theme.hints.fg}
32
+ >
33
+ {" "}
34
+ {hint.key}{" "}
35
+ </text>
36
+ <text fg={theme.colors.muted}> {hint.label}</text>
37
+ </box>
38
+ ))}
39
+ </box>
40
+ );
41
41
  }
@@ -1,22 +1,23 @@
1
1
  import React from "react";
2
+ import { theme } from "../../theme.js";
2
3
 
3
4
  interface DetailSectionProps {
4
- title?: string;
5
- children: React.ReactNode;
5
+ title?: string;
6
+ children: React.ReactNode;
6
7
  }
7
8
 
8
9
  /**
9
10
  * A labeled section in a detail panel with optional title header.
10
11
  */
11
12
  export function DetailSection({ title, children }: DetailSectionProps) {
12
- return (
13
- <box flexDirection="column" marginTop={1}>
14
- {title ? (
15
- <text fg="cyan">
16
- <strong>{title}</strong>
17
- </text>
18
- ) : null}
19
- {children}
20
- </box>
21
- );
13
+ return (
14
+ <box flexDirection="column" marginTop={1}>
15
+ {title ? (
16
+ <text fg={theme.colors.info}>
17
+ <strong>{title}</strong>
18
+ </text>
19
+ ) : null}
20
+ {children}
21
+ </box>
22
+ );
22
23
  }
@@ -1,8 +1,9 @@
1
1
  import React from "react";
2
+ import { theme } from "../../theme.js";
2
3
 
3
4
  interface KeyValueLineProps {
4
- label: string;
5
- value: React.ReactNode;
5
+ label: string;
6
+ value: React.ReactNode;
6
7
  }
7
8
 
8
9
  /**
@@ -10,10 +11,10 @@ interface KeyValueLineProps {
10
11
  * Label is padded to 10 chars for consistent alignment.
11
12
  */
12
13
  export function KeyValueLine({ label, value }: KeyValueLineProps) {
13
- return (
14
- <text>
15
- <span fg="gray">{label.padEnd(10)}</span>
16
- {value}
17
- </text>
18
- );
14
+ return (
15
+ <text fg={theme.colors.text}>
16
+ <span fg={theme.colors.muted}>{label.padEnd(10)}</span>
17
+ {value}
18
+ </text>
19
+ );
19
20
  }
@@ -3,36 +3,34 @@ import { theme } from "../../theme.js";
3
3
  import { SelectableRow } from "./SelectableRow.js";
4
4
 
5
5
  interface ListCategoryRowProps {
6
- title: string;
7
- expanded?: boolean;
8
- count?: number;
9
- badge?: string;
10
- tone?: keyof typeof theme.category;
11
- selected: boolean;
6
+ title: string;
7
+ expanded?: boolean;
8
+ count?: number;
9
+ badge?: string;
10
+ tone?: keyof typeof theme.category;
11
+ selected: boolean;
12
12
  }
13
13
 
14
14
  export function ListCategoryRow({
15
- title,
16
- expanded = false,
17
- count,
18
- badge,
19
- tone = "gray",
20
- selected,
15
+ title,
16
+ expanded = false,
17
+ count,
18
+ badge,
19
+ tone = "gray",
20
+ selected,
21
21
  }: ListCategoryRowProps) {
22
- const colors = theme.category[tone];
23
- const label = `${expanded ? "▼" : "▶"} ${title}${count !== undefined ? ` (${count})` : ""}`;
22
+ const colors = theme.category[tone];
23
+ const label = `${expanded ? "▼" : "▶"} ${title}${count !== undefined ? ` (${count})` : ""}`;
24
24
 
25
- return (
26
- <SelectableRow selected={selected}>
27
- {!selected && (
28
- <span bg={colors.bg} fg={colors.fg}>
29
- <strong> {label} </strong>
30
- </span>
31
- )}
32
- {selected && <strong> {label} </strong>}
33
- {!selected && badge ? (
34
- <span fg={colors.badgeFg}> {badge}</span>
35
- ) : null}
36
- </SelectableRow>
37
- );
25
+ return (
26
+ <SelectableRow selected={selected}>
27
+ {!selected && (
28
+ <span bg={colors.bg} fg={colors.fg}>
29
+ <strong> {label} </strong>
30
+ </span>
31
+ )}
32
+ {selected && <strong> {label} </strong>}
33
+ {!selected && badge ? <span fg={colors.badgeFg}> {badge}</span> : null}
34
+ </SelectableRow>
35
+ );
38
36
  }
@@ -2,13 +2,13 @@ import React from "react";
2
2
  import { theme } from "../../theme.js";
3
3
 
4
4
  interface MetaTextProps {
5
- text: string;
6
- tone?: "muted" | "warning" | "success" | "danger";
5
+ text: string;
6
+ tone?: "muted" | "warning" | "success" | "danger";
7
7
  }
8
8
 
9
9
  /**
10
10
  * Subdued text for versions, stars, status indicators.
11
11
  */
12
12
  export function MetaText({ text, tone = "muted" }: MetaTextProps) {
13
- return <span fg={theme.meta[tone]}>{text}</span>;
13
+ return <span fg={theme.meta[tone]}>{text}</span>;
14
14
  }
@@ -2,20 +2,20 @@ import React from "react";
2
2
  import { theme } from "../../theme.js";
3
3
 
4
4
  interface ScopeDetailScope {
5
- user?: boolean;
6
- project?: boolean;
7
- local?: boolean;
5
+ user?: boolean;
6
+ project?: boolean;
7
+ local?: boolean;
8
8
  }
9
9
 
10
10
  interface ScopeDetailPaths {
11
- user?: string;
12
- project?: string;
13
- local?: string;
11
+ user?: string;
12
+ project?: string;
13
+ local?: string;
14
14
  }
15
15
 
16
16
  interface ScopeDetailProps {
17
- scopes: ScopeDetailScope;
18
- paths?: ScopeDetailPaths;
17
+ scopes: ScopeDetailScope;
18
+ paths?: ScopeDetailPaths;
19
19
  }
20
20
 
21
21
  /**
@@ -23,45 +23,45 @@ interface ScopeDetailProps {
23
23
  * Renders each scope as: [bg=color] key [/bg] ● or ○ Label path
24
24
  */
25
25
  export function ScopeDetail({ scopes, paths }: ScopeDetailProps) {
26
- const items = [
27
- {
28
- key: "u",
29
- label: "User",
30
- color: theme.scopes.user,
31
- active: scopes.user,
32
- path: paths?.user,
33
- },
34
- {
35
- key: "p",
36
- label: "Project",
37
- color: theme.scopes.project,
38
- active: scopes.project,
39
- path: paths?.project,
40
- },
41
- {
42
- key: "l",
43
- label: "Local",
44
- color: theme.scopes.local,
45
- active: scopes.local,
46
- path: paths?.local,
47
- },
48
- ].filter((i) => i.path !== undefined || i.active !== undefined);
26
+ const items = [
27
+ {
28
+ key: "u",
29
+ label: "User",
30
+ color: theme.scopes.user,
31
+ active: scopes.user,
32
+ path: paths?.user,
33
+ },
34
+ {
35
+ key: "p",
36
+ label: "Project",
37
+ color: theme.scopes.project,
38
+ active: scopes.project,
39
+ path: paths?.project,
40
+ },
41
+ {
42
+ key: "l",
43
+ label: "Local",
44
+ color: theme.scopes.local,
45
+ active: scopes.local,
46
+ path: paths?.local,
47
+ },
48
+ ].filter((i) => i.path !== undefined || i.active !== undefined);
49
49
 
50
- return (
51
- <box flexDirection="column">
52
- {items.map((item) => (
53
- <text key={item.key}>
54
- <span bg={item.color} fg="black">
55
- {" "}
56
- {item.key}{" "}
57
- </span>
58
- <span fg={item.active ? item.color : "gray"}>
59
- {item.active ? " ● " : " ○ "}
60
- </span>
61
- <span fg={item.color}>{item.label}</span>
62
- {item.path && <span fg="gray"> {item.path}</span>}
63
- </text>
64
- ))}
65
- </box>
66
- );
50
+ return (
51
+ <box flexDirection="column">
52
+ {items.map((item) => (
53
+ <text key={item.key}>
54
+ <span bg={item.color} fg={theme.hints.fg}>
55
+ {" "}
56
+ {item.key}{" "}
57
+ </span>
58
+ <span fg={item.active ? item.color : theme.colors.muted}>
59
+ {item.active ? " ● " : " ○ "}
60
+ </span>
61
+ <span fg={item.color}>{item.label}</span>
62
+ {item.path && <span fg={theme.colors.muted}> {item.path}</span>}
63
+ </text>
64
+ ))}
65
+ </box>
66
+ );
67
67
  }
@@ -1,33 +1,58 @@
1
- import React from "react";
1
+ import type React from "react";
2
+ import { scopeOffFill } from "../../theme-mode.js";
2
3
  import { theme } from "../../theme.js";
3
4
 
4
5
  interface ScopeSquaresProps {
5
- user: boolean;
6
- project: boolean;
7
- local?: boolean;
8
- selected?: boolean;
6
+ user: boolean;
7
+ project: boolean;
8
+ local?: boolean;
9
+ selected?: boolean;
9
10
  }
10
11
 
12
+ /** Every segment is a filled block; only the fill colour says lit or unlit. */
13
+ const SEGMENT = "■";
14
+
11
15
  /**
12
- * Colored filled/empty squares for scope display in list items.
13
- * ■ = installed in that scope, □ = not installed.
16
+ * Scope bar: three positional segments, user / project / local.
17
+ *
18
+ * Reads as a segmented indicator — each segment lit in its scope colour, or
19
+ * unlit in a fill that recedes toward the page. Position identifies the scope;
20
+ * the first segment is always user, the second project, the third local.
21
+ *
22
+ * The unlit fill used to be `theme.colors.muted` (#6B7280), the same mid-dark
23
+ * grey as de-emphasised body text. Against a light page that reads as a *dark*
24
+ * block, giving an unlit segment the same visual weight as a lit one — "non
25
+ * installed state is dark as well - confusing". The unlit fill now comes from
26
+ * `scopeOffFill()`, which steps toward whichever page the terminal actually has;
27
+ * see theme-mode.ts for why that one value cannot be fixed.
14
28
  */
15
29
  export function ScopeSquares({
16
- user,
17
- project,
18
- local,
19
- selected = false,
30
+ user,
31
+ project,
32
+ local,
33
+ selected = false,
20
34
  }: ScopeSquaresProps) {
21
- const inactive = selected ? "white" : theme.colors.dim;
35
+ // On a selected row the background is our own accent, so an unlit segment
36
+ // recedes toward that block rather than toward the page.
37
+ const off = selected ? theme.selection.bg : scopeOffFill();
38
+ const lit = (on: boolean, color: string) =>
39
+ on ? (selected ? theme.selection.fg : color) : off;
40
+
41
+ const segments = [
42
+ lit(user, theme.scopes.user),
43
+ lit(project, theme.scopes.project),
44
+ ...(local !== undefined ? [lit(local, theme.scopes.local)] : []),
45
+ ];
22
46
 
23
- // Returns fragments of <span> — safe to embed inside a <text> parent
24
- return (
25
- <>
26
- <span fg={user ? theme.scopes.user : inactive}>■</span>
27
- <span fg={project ? theme.scopes.project : inactive}>■</span>
28
- {local !== undefined && (
29
- <span fg={local ? theme.scopes.local : inactive}>■</span>
30
- )}
31
- </>
32
- );
47
+ // Fragments of <span> — safe to embed inside a coloured <text> parent.
48
+ return (
49
+ <>
50
+ {segments.map((fg, i) => (
51
+ // biome-ignore lint/suspicious/noArrayIndexKey: fixed positional segments
52
+ <span key={i} fg={fg}>
53
+ {SEGMENT}
54
+ </span>
55
+ ))}
56
+ </>
57
+ );
33
58
  }
@@ -1,24 +1,30 @@
1
- import React from "react";
1
+ import type React from "react";
2
2
  import { theme } from "../../theme.js";
3
3
 
4
4
  interface SelectableRowProps {
5
- selected: boolean;
6
- indent?: number;
7
- children: React.ReactNode;
5
+ selected: boolean;
6
+ indent?: number;
7
+ children: React.ReactNode;
8
8
  }
9
9
 
10
10
  export function SelectableRow({
11
- selected,
12
- indent = 0,
13
- children,
11
+ selected,
12
+ indent = 0,
13
+ children,
14
14
  }: SelectableRowProps) {
15
- return (
16
- <text
17
- bg={selected ? theme.selection.bg : undefined}
18
- fg={selected ? theme.selection.fg : undefined}
19
- >
20
- {" ".repeat(indent)}
21
- {children}
22
- </text>
23
- );
15
+ return (
16
+ <text
17
+ // Unselected: no background, so the row sits on the terminal's own page.
18
+ bg={selected ? theme.selection.bg : undefined}
19
+ // Unselected must still NAME a colour. `undefined` does not mean "the
20
+ // terminal's foreground" — OpenTUI's fallback for an unstyled cell is
21
+ // truecolor white, so every unselected row here rendered white-on-cream
22
+ // and vanished on a light theme. Same root cause as the original bug,
23
+ // reached by omission rather than by a wrong literal.
24
+ fg={selected ? theme.selection.fg : theme.colors.text}
25
+ >
26
+ {" ".repeat(indent)}
27
+ {children}
28
+ </text>
29
+ );
24
30
  }