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
@@ -2,358 +2,404 @@ import React from "react";
2
2
  import type { ItemRenderer } from "../registry.js";
3
3
  import type { ProfileEntry } from "../../types/index.js";
4
4
  import type { PredefinedProfile } from "../../data/predefined-profiles.js";
5
- import { theme } from "../theme.js";
5
+ import { theme, type UiColor } from "../theme.js";
6
6
 
7
7
  // ─── List item types ───────────────────────────────────────────────────────────
8
8
 
9
9
  export type ProfileListItem =
10
- | { kind: "header"; label: string; color: string; count: number }
11
- | { kind: "predefined"; profile: PredefinedProfile }
12
- | { kind: "saved"; entry: ProfileEntry };
10
+ | { kind: "header"; label: string; color: UiColor; count: number }
11
+ | { kind: "predefined"; profile: PredefinedProfile }
12
+ | { kind: "saved"; entry: ProfileEntry };
13
13
 
14
14
  // ─── Helpers ───────────────────────────────────────────────────────────────────
15
15
 
16
16
  function truncate(s: string, maxLen: number): string {
17
- if (s.length <= maxLen) return s;
18
- return s.slice(0, maxLen - 1) + "\u2026";
17
+ if (s.length <= maxLen) return s;
18
+ return s.slice(0, maxLen - 1) + "\u2026";
19
19
  }
20
20
 
21
21
  export function humanizeValue(_key: string, value: unknown): string {
22
- if (typeof value === "boolean") return value ? "on" : "off";
23
- if (typeof value !== "string") return String(value);
24
- if (value === "claude-sonnet-4-6") return "Sonnet";
25
- if (value === "claude-opus-4-6") return "Opus";
26
- if (value === "claude-haiku-4-6") return "Haiku";
27
- if (value.startsWith("claude-sonnet")) return "Sonnet";
28
- if (value.startsWith("claude-opus")) return "Opus";
29
- if (value.startsWith("claude-haiku")) return "Haiku";
30
- return value;
22
+ if (typeof value === "boolean") return value ? "on" : "off";
23
+ if (typeof value !== "string") return String(value);
24
+ if (value === "claude-sonnet-4-6") return "Sonnet";
25
+ if (value === "claude-opus-4-6") return "Opus";
26
+ if (value === "claude-haiku-4-6") return "Haiku";
27
+ if (value.startsWith("claude-sonnet")) return "Sonnet";
28
+ if (value.startsWith("claude-opus")) return "Opus";
29
+ if (value.startsWith("claude-haiku")) return "Haiku";
30
+ return value;
31
31
  }
32
32
 
33
33
  export function humanizeKey(key: string): string {
34
- const labels: Record<string, string> = {
35
- effortLevel: "Effort",
36
- model: "Model",
37
- outputStyle: "Output",
38
- alwaysThinkingEnabled: "Thinking",
39
- includeGitInstructions: "Git Instructions",
40
- respectGitignore: "Gitignore",
41
- enableAllProjectMcpServers: "Auto MCP",
42
- autoUpdatesChannel: "Updates",
43
- language: "Language",
44
- cleanupPeriodDays: "Cleanup",
45
- };
46
- return labels[key] ?? key;
34
+ const labels: Record<string, string> = {
35
+ effortLevel: "Effort",
36
+ model: "Model",
37
+ outputStyle: "Output",
38
+ alwaysThinkingEnabled: "Thinking",
39
+ includeGitInstructions: "Git Instructions",
40
+ respectGitignore: "Gitignore",
41
+ enableAllProjectMcpServers: "Auto MCP",
42
+ autoUpdatesChannel: "Updates",
43
+ language: "Language",
44
+ cleanupPeriodDays: "Cleanup",
45
+ };
46
+ return labels[key] ?? key;
47
47
  }
48
48
 
49
49
  export function stripSuffix(pluginName: string): string {
50
- return pluginName
51
- .replace(/@magus$/, "")
52
- .replace(/@claude-plugins-official$/, "");
50
+ return pluginName
51
+ .replace(/@magus$/, "")
52
+ .replace(/@claude-plugins-official$/, "");
53
53
  }
54
54
 
55
55
  export function wrapNames(names: string[], lineMax = 40): string[] {
56
- const lines: string[] = [];
57
- let current = "";
58
- for (const name of names) {
59
- const add = current ? `, ${name}` : name;
60
- if (current && current.length + add.length > lineMax) {
61
- lines.push(current);
62
- current = name;
63
- } else {
64
- current += current ? `, ${name}` : name;
65
- }
66
- }
67
- if (current) lines.push(current);
68
- return lines;
56
+ const lines: string[] = [];
57
+ let current = "";
58
+ for (const name of names) {
59
+ const add = current ? `, ${name}` : name;
60
+ if (current && current.length + add.length > lineMax) {
61
+ lines.push(current);
62
+ current = name;
63
+ } else {
64
+ current += current ? `, ${name}` : name;
65
+ }
66
+ }
67
+ if (current) lines.push(current);
68
+ return lines;
69
69
  }
70
70
 
71
71
  export function formatDate(iso: string): string {
72
- try {
73
- const d = new Date(iso);
74
- return d.toLocaleDateString("en-US", {
75
- month: "short",
76
- day: "numeric",
77
- year: "numeric",
78
- });
79
- } catch {
80
- return iso;
81
- }
72
+ try {
73
+ const d = new Date(iso);
74
+ return d.toLocaleDateString("en-US", {
75
+ month: "short",
76
+ day: "numeric",
77
+ year: "numeric",
78
+ });
79
+ } catch {
80
+ return iso;
81
+ }
82
82
  }
83
83
 
84
84
  // ─── Header renderer ───────────────────────────────────────────────────────────
85
85
 
86
86
  const headerRenderer: ItemRenderer<{
87
- kind: "header";
88
- label: string;
89
- color: string;
90
- count: number;
87
+ kind: "header";
88
+ label: string;
89
+ color: UiColor;
90
+ count: number;
91
91
  }> = {
92
- renderRow: ({ item }) => (
93
- <text bg={item.color} fg="white">
94
- <strong>
95
- {" "}
96
- {item.label} ({item.count}){" "}
97
- </strong>
98
- </text>
99
- ),
100
- renderDetail: () => (
101
- <text fg={theme.colors.muted}>Select a profile to see details</text>
102
- ),
92
+ renderRow: ({ item }) => (
93
+ <text bg={item.color} fg={theme.hints.fg}>
94
+ <strong>
95
+ {" "}
96
+ {item.label} ({item.count}){" "}
97
+ </strong>
98
+ </text>
99
+ ),
100
+ renderDetail: () => (
101
+ <text fg={theme.colors.muted}>Select a profile to see details</text>
102
+ ),
103
103
  };
104
104
 
105
105
  // ─── Predefined renderer ───────────────────────────────────────────────────────
106
106
 
107
107
  const DIVIDER = "────────────────────────";
108
108
 
109
- const predefinedRenderer: ItemRenderer<{ kind: "predefined"; profile: PredefinedProfile }> = {
110
- renderRow: ({ item, isSelected }) => {
111
- const { profile } = item;
112
- const pluginCount =
113
- profile.magusPlugins.length + profile.anthropicPlugins.length;
114
- const skillCount = profile.skills.length;
115
- const countStr = `${pluginCount}p ${skillCount}s`;
116
-
117
- if (isSelected) {
118
- return (
119
- <text bg={theme.selection.bg} fg={theme.selection.fg}>
120
- {" "}{profile.name} {countStr}{" "}
121
- </text>
122
- );
123
- }
124
-
125
- return (
126
- <text>
127
- <span fg="white">{" "}{profile.name}</span>
128
- <span fg={theme.colors.dim}> {countStr}</span>
129
- </text>
130
- );
131
- },
132
-
133
- renderDetail: ({ item }) => {
134
- const { profile } = item;
135
- const settingEntries = Object.entries(profile.settings).filter(
136
- ([k]) => k !== "env",
137
- );
138
- const envMap =
139
- (profile.settings["env"] as Record<string, string> | undefined) ?? {};
140
- const tasksOn = envMap["CLAUDE_CODE_ENABLE_TASKS"] === "true";
141
- const teamsOn = envMap["CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS"] === "true";
142
-
143
- return (
144
- <box flexDirection="column">
145
- <box>
146
- <text bg={theme.colors.accent} fg="white">
147
- <strong> {profile.name} </strong>
148
- </text>
149
- </box>
150
- <box marginTop={1}>
151
- <text fg={theme.colors.muted}>{profile.description}</text>
152
- </box>
153
-
154
- {/* Magus plugins */}
155
- <box><text fg={theme.colors.muted}>{`\nMagus (${profile.magusPlugins.length})`}</text></box>
156
- <box><text fg="#00bfa5">{profile.magusPlugins.map((p) => ` ■ ${p}`).join("\n")}</text></box>
157
-
158
- {/* Anthropic plugins */}
159
- <box><text fg={theme.colors.muted}>{`\nAnthropic (${profile.anthropicPlugins.length})`}</text></box>
160
- <box><text fg="#b39ddb">{profile.anthropicPlugins.map((p) => ` ■ ${p}`).join("\n")}</text></box>
161
-
162
- {/* Skills */}
163
- {profile.skills.length > 0 && (
164
- <>
165
- <box><text fg={theme.colors.muted}>{`\nSkills (${profile.skills.length})`}</text></box>
166
- <box><text fg="#ffd54f">{profile.skills.map((s) => ` ■ ${s}`).join("\n")}</text></box>
167
- </>
168
- )}
169
-
170
- {/* Settings */}
171
- <box><text fg={theme.colors.dim}>{`\n${DIVIDER}`}</text></box>
172
- <box><text fg={theme.colors.muted}>{[
173
- ...settingEntries.map(([k, v]) => ` ${humanizeKey(k).padEnd(18)}${humanizeValue(k, v)}`),
174
- ...(tasksOn ? [` ${"Tasks".padEnd(18)}on`] : []),
175
- ...(teamsOn ? [` ${"Agent Teams".padEnd(18)}on`] : []),
176
- ].join("\n")}</text></box>
177
-
178
- {/* Action */}
179
- <box><text fg={theme.colors.dim}>{`\n${DIVIDER}`}</text></box>
180
- <box><text fg={theme.colors.info}> Enter/a to apply</text></box>
181
- </box>
182
- );
183
- },
109
+ const predefinedRenderer: ItemRenderer<{
110
+ kind: "predefined";
111
+ profile: PredefinedProfile;
112
+ }> = {
113
+ renderRow: ({ item, isSelected }) => {
114
+ const { profile } = item;
115
+ const pluginCount =
116
+ profile.magusPlugins.length + profile.anthropicPlugins.length;
117
+ const skillCount = profile.skills.length;
118
+ const countStr = `${pluginCount}p ${skillCount}s`;
119
+
120
+ if (isSelected) {
121
+ return (
122
+ <text bg={theme.selection.bg} fg={theme.selection.fg}>
123
+ {" "}
124
+ {profile.name} {countStr}{" "}
125
+ </text>
126
+ );
127
+ }
128
+
129
+ return (
130
+ <text fg={theme.colors.text}>
131
+ <span fg={theme.colors.text}>
132
+ {" "}
133
+ {profile.name}
134
+ </span>
135
+ <span fg={theme.colors.dim}> {countStr}</span>
136
+ </text>
137
+ );
138
+ },
139
+
140
+ renderDetail: ({ item }) => {
141
+ const { profile } = item;
142
+ const settingEntries = Object.entries(profile.settings).filter(
143
+ ([k]) => k !== "env",
144
+ );
145
+ const envMap =
146
+ (profile.settings["env"] as Record<string, string> | undefined) ?? {};
147
+ const tasksOn = envMap["CLAUDE_CODE_ENABLE_TASKS"] === "true";
148
+ const teamsOn = envMap["CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS"] === "true";
149
+
150
+ return (
151
+ <box flexDirection="column">
152
+ <box>
153
+ <text bg={theme.colors.accent} fg={theme.hints.fg}>
154
+ <strong> {profile.name} </strong>
155
+ </text>
156
+ </box>
157
+ <box marginTop={1}>
158
+ <text fg={theme.colors.muted}>{profile.description}</text>
159
+ </box>
160
+
161
+ {/* Magus plugins */}
162
+ <box>
163
+ <text
164
+ fg={theme.colors.muted}
165
+ >{`\nMagus (${profile.magusPlugins.length})`}</text>
166
+ </box>
167
+ <box>
168
+ <text fg={theme.colors.info}>
169
+ {profile.magusPlugins.map((p) => ` ■ ${p}`).join("\n")}
170
+ </text>
171
+ </box>
172
+
173
+ {/* Anthropic plugins */}
174
+ <box>
175
+ <text
176
+ fg={theme.colors.muted}
177
+ >{`\nAnthropic (${profile.anthropicPlugins.length})`}</text>
178
+ </box>
179
+ <box>
180
+ <text fg={theme.colors.accent}>
181
+ {profile.anthropicPlugins.map((p) => ` ■ ${p}`).join("\n")}
182
+ </text>
183
+ </box>
184
+
185
+ {/* Skills */}
186
+ {profile.skills.length > 0 && (
187
+ <>
188
+ <box>
189
+ <text
190
+ fg={theme.colors.muted}
191
+ >{`\nSkills (${profile.skills.length})`}</text>
192
+ </box>
193
+ <box>
194
+ <text fg={theme.colors.warning}>
195
+ {profile.skills.map((s) => ` ■ ${s}`).join("\n")}
196
+ </text>
197
+ </box>
198
+ </>
199
+ )}
200
+
201
+ {/* Settings */}
202
+ <box>
203
+ <text fg={theme.colors.dim}>{`\n${DIVIDER}`}</text>
204
+ </box>
205
+ <box>
206
+ <text fg={theme.colors.muted}>
207
+ {[
208
+ ...settingEntries.map(
209
+ ([k, v]) =>
210
+ ` ${humanizeKey(k).padEnd(18)}${humanizeValue(k, v)}`,
211
+ ),
212
+ ...(tasksOn ? [` ${"Tasks".padEnd(18)}on`] : []),
213
+ ...(teamsOn ? [` ${"Agent Teams".padEnd(18)}on`] : []),
214
+ ].join("\n")}
215
+ </text>
216
+ </box>
217
+
218
+ {/* Action */}
219
+ <box>
220
+ <text fg={theme.colors.dim}>{`\n${DIVIDER}`}</text>
221
+ </box>
222
+ <box>
223
+ <text fg={theme.colors.info}> Enter/a to apply</text>
224
+ </box>
225
+ </box>
226
+ );
227
+ },
184
228
  };
185
229
 
186
230
  // ─── Saved renderer ────────────────────────────────────────────────────────────
187
231
 
188
232
  const savedRenderer: ItemRenderer<{ kind: "saved"; entry: ProfileEntry }> = {
189
- renderRow: ({ item, isSelected }) => {
190
- const { entry } = item;
191
- const pluginCount = Object.keys(entry.plugins).length;
192
- const dateStr = formatDate(entry.updatedAt);
193
- const label = truncate(
194
- `${entry.name} — ${pluginCount} plugin${pluginCount !== 1 ? "s" : ""} · ${dateStr}`,
195
- 45,
196
- );
197
-
198
- if (isSelected) {
199
- return (
200
- <text bg={theme.selection.bg} fg={theme.selection.fg}>
201
- {" "}
202
- {label}{" "}
203
- </text>
204
- );
205
- }
206
-
207
- const scopeColor =
208
- entry.scope === "user" ? theme.scopes.user : theme.scopes.project;
209
- const scopeLabel = entry.scope === "user" ? "[user]" : "[proj]";
210
-
211
- return (
212
- <text>
213
- <span fg={scopeColor}>{scopeLabel} </span>
214
- <span fg={theme.colors.text}>{label}</span>
215
- </text>
216
- );
217
- },
218
-
219
- renderDetail: ({ item }) => {
220
- const { entry: selectedProfile } = item;
221
- const plugins = Object.keys(selectedProfile.plugins);
222
- const cleanPlugins = plugins.map(stripSuffix);
223
- const scopeColor =
224
- selectedProfile.scope === "user" ? theme.scopes.user : theme.scopes.project;
225
- const scopeLabel =
226
- selectedProfile.scope === "user"
227
- ? "User (~/.claude/profiles.json)"
228
- : "Project (.claude/profiles.json committed to git)";
229
-
230
- return (
231
- <box flexDirection="column">
232
- <text fg={theme.colors.info}>
233
- <strong>{selectedProfile.name}</strong>
234
- </text>
235
- <box marginTop={1}>
236
- <text fg={theme.colors.muted}>Scope: </text>
237
- <text fg={scopeColor}>{scopeLabel}</text>
238
- </box>
239
- <box marginTop={1}>
240
- <text fg={theme.colors.muted}>
241
- Created: {formatDate(selectedProfile.createdAt)} · Updated:{" "}
242
- {formatDate(selectedProfile.updatedAt)}
243
- </text>
244
- </box>
245
- <box marginTop={1} flexDirection="column">
246
- <text fg={theme.colors.muted}>
247
- Plugins ({plugins.length}
248
- {plugins.length === 0 ? " — applying will disable all plugins" : ""}
249
- ):
250
- </text>
251
- {cleanPlugins.length === 0 ? (
252
- <text fg={theme.colors.warning}> (none)</text>
253
- ) : (
254
- wrapNames(cleanPlugins).map((line, i) => (
255
- <text key={i} fg={theme.colors.text}>
256
- {" "}
257
- {line}
258
- </text>
259
- ))
260
- )}
261
- </box>
262
- <box marginTop={2} flexDirection="column">
263
- <box>
264
- <text bg={theme.selection.bg} fg={theme.selection.fg}>
265
- {" "}
266
- Enter/a{" "}
267
- </text>
268
- <text fg={theme.colors.muted}> Apply profile</text>
269
- </box>
270
- <box marginTop={1}>
271
- <text bg={theme.colors.dim} fg="white">
272
- {" "}
273
- r{" "}
274
- </text>
275
- <text fg={theme.colors.muted}> Rename</text>
276
- </box>
277
- <box marginTop={1}>
278
- <text bg={theme.colors.danger} fg="white">
279
- {" "}
280
- d{" "}
281
- </text>
282
- <text fg={theme.colors.muted}> Delete</text>
283
- </box>
284
- <box marginTop={1}>
285
- <text bg="blue" fg="white">
286
- {" "}
287
- c{" "}
288
- </text>
289
- <text fg={theme.colors.muted}> Copy JSON to clipboard</text>
290
- </box>
291
- <box marginTop={1}>
292
- <text bg={theme.colors.success} fg="white">
293
- {" "}
294
- i{" "}
295
- </text>
296
- <text fg={theme.colors.muted}> Import from clipboard</text>
297
- </box>
298
- </box>
299
- </box>
300
- );
301
- },
233
+ renderRow: ({ item, isSelected }) => {
234
+ const { entry } = item;
235
+ const pluginCount = Object.keys(entry.plugins).length;
236
+ const dateStr = formatDate(entry.updatedAt);
237
+ const label = truncate(
238
+ `${entry.name} — ${pluginCount} plugin${pluginCount !== 1 ? "s" : ""} · ${dateStr}`,
239
+ 45,
240
+ );
241
+
242
+ if (isSelected) {
243
+ return (
244
+ <text bg={theme.selection.bg} fg={theme.selection.fg}>
245
+ {" "}
246
+ {label}{" "}
247
+ </text>
248
+ );
249
+ }
250
+
251
+ const scopeColor =
252
+ entry.scope === "user" ? theme.scopes.user : theme.scopes.project;
253
+ const scopeLabel = entry.scope === "user" ? "[user]" : "[proj]";
254
+
255
+ return (
256
+ <text fg={theme.colors.text}>
257
+ <span fg={scopeColor}>{scopeLabel} </span>
258
+ <span fg={theme.colors.text}>{label}</span>
259
+ </text>
260
+ );
261
+ },
262
+
263
+ renderDetail: ({ item }) => {
264
+ const { entry: selectedProfile } = item;
265
+ const plugins = Object.keys(selectedProfile.plugins);
266
+ const cleanPlugins = plugins.map(stripSuffix);
267
+ const scopeColor =
268
+ selectedProfile.scope === "user"
269
+ ? theme.scopes.user
270
+ : theme.scopes.project;
271
+ const scopeLabel =
272
+ selectedProfile.scope === "user"
273
+ ? "User (~/.claude/profiles.json)"
274
+ : "Project (.claude/profiles.json — committed to git)";
275
+
276
+ return (
277
+ <box flexDirection="column">
278
+ <text fg={theme.colors.info}>
279
+ <strong>{selectedProfile.name}</strong>
280
+ </text>
281
+ <box marginTop={1}>
282
+ <text fg={theme.colors.muted}>Scope: </text>
283
+ <text fg={scopeColor}>{scopeLabel}</text>
284
+ </box>
285
+ <box marginTop={1}>
286
+ <text fg={theme.colors.muted}>
287
+ Created: {formatDate(selectedProfile.createdAt)} · Updated:{" "}
288
+ {formatDate(selectedProfile.updatedAt)}
289
+ </text>
290
+ </box>
291
+ <box marginTop={1} flexDirection="column">
292
+ <text fg={theme.colors.muted}>
293
+ Plugins ({plugins.length}
294
+ {plugins.length === 0 ? " — applying will disable all plugins" : ""}
295
+ ):
296
+ </text>
297
+ {cleanPlugins.length === 0 ? (
298
+ <text fg={theme.colors.warning}> (none)</text>
299
+ ) : (
300
+ wrapNames(cleanPlugins).map((line, i) => (
301
+ <text key={i} fg={theme.colors.text}>
302
+ {" "}
303
+ {line}
304
+ </text>
305
+ ))
306
+ )}
307
+ </box>
308
+ <box marginTop={2} flexDirection="column">
309
+ <box>
310
+ <text bg={theme.selection.bg} fg={theme.selection.fg}>
311
+ {" "}
312
+ Enter/a{" "}
313
+ </text>
314
+ <text fg={theme.colors.muted}> Apply profile</text>
315
+ </box>
316
+ <box marginTop={1}>
317
+ <text bg={theme.colors.dim} fg={theme.hints.fg}>
318
+ {" "}
319
+ r{" "}
320
+ </text>
321
+ <text fg={theme.colors.muted}> Rename</text>
322
+ </box>
323
+ <box marginTop={1}>
324
+ <text bg={theme.colors.danger} fg={theme.hints.fg}>
325
+ {" "}
326
+ d{" "}
327
+ </text>
328
+ <text fg={theme.colors.muted}> Delete</text>
329
+ </box>
330
+ <box marginTop={1}>
331
+ <text bg={theme.colors.link} fg={theme.hints.fg}>
332
+ {" "}
333
+ c{" "}
334
+ </text>
335
+ <text fg={theme.colors.muted}> Copy JSON to clipboard</text>
336
+ </box>
337
+ <box marginTop={1}>
338
+ <text bg={theme.colors.success} fg={theme.hints.fg}>
339
+ {" "}
340
+ i{" "}
341
+ </text>
342
+ <text fg={theme.colors.muted}> Import from clipboard</text>
343
+ </box>
344
+ </box>
345
+ </box>
346
+ );
347
+ },
302
348
  };
303
349
 
304
350
  // ─── Dispatch helpers ──────────────────────────────────────────────────────────
305
351
 
306
352
  export function renderProfileRow(
307
- item: ProfileListItem,
308
- _index: number,
309
- isSelected: boolean,
353
+ item: ProfileListItem,
354
+ _index: number,
355
+ isSelected: boolean,
310
356
  ): React.ReactNode {
311
- if (item.kind === "header") {
312
- return headerRenderer.renderRow({ item, isSelected });
313
- }
314
- if (item.kind === "predefined") {
315
- return predefinedRenderer.renderRow({ item, isSelected });
316
- }
317
- return savedRenderer.renderRow({ item, isSelected });
357
+ if (item.kind === "header") {
358
+ return headerRenderer.renderRow({ item, isSelected });
359
+ }
360
+ if (item.kind === "predefined") {
361
+ return predefinedRenderer.renderRow({ item, isSelected });
362
+ }
363
+ return savedRenderer.renderRow({ item, isSelected });
318
364
  }
319
365
 
320
366
  export function renderProfileDetail(
321
- item: ProfileListItem | undefined,
322
- loading: boolean,
323
- error: string | undefined,
367
+ item: ProfileListItem | undefined,
368
+ loading: boolean,
369
+ error: string | undefined,
324
370
  ): React.ReactNode {
325
- if (loading) return <text fg={theme.colors.muted}>Loading profiles...</text>;
326
- if (error) return <text fg={theme.colors.danger}>Error: {error}</text>;
327
- if (!item || item.kind === "header")
328
- return <text fg={theme.colors.muted}>Select a profile to see details</text>;
329
- if (item.kind === "predefined") {
330
- return predefinedRenderer.renderDetail({ item });
331
- }
332
- return savedRenderer.renderDetail({ item });
371
+ if (loading) return <text fg={theme.colors.muted}>Loading profiles...</text>;
372
+ if (error) return <text fg={theme.colors.danger}>Error: {error}</text>;
373
+ if (!item || item.kind === "header")
374
+ return <text fg={theme.colors.muted}>Select a profile to see details</text>;
375
+ if (item.kind === "predefined") {
376
+ return predefinedRenderer.renderDetail({ item });
377
+ }
378
+ return savedRenderer.renderDetail({ item });
333
379
  }
334
380
 
335
381
  export function buildProfileListItems(
336
- profileList: import("../../types/index.js").ProfileEntry[],
337
- PREDEFINED_PROFILES: PredefinedProfile[],
382
+ profileList: import("../../types/index.js").ProfileEntry[],
383
+ PREDEFINED_PROFILES: PredefinedProfile[],
338
384
  ): ProfileListItem[] {
339
- const items: ProfileListItem[] = [];
340
- items.push({
341
- kind: "header",
342
- label: "Presets",
343
- color: "#4527a0",
344
- count: PREDEFINED_PROFILES.length,
345
- });
346
- for (const p of PREDEFINED_PROFILES) {
347
- items.push({ kind: "predefined", profile: p });
348
- }
349
- items.push({
350
- kind: "header",
351
- label: "Your Profiles",
352
- color: "#00695c",
353
- count: profileList.length,
354
- });
355
- for (const e of profileList) {
356
- items.push({ kind: "saved", entry: e });
357
- }
358
- return items;
385
+ const items: ProfileListItem[] = [];
386
+ items.push({
387
+ kind: "header",
388
+ label: "Presets",
389
+ color: theme.colors.accent,
390
+ count: PREDEFINED_PROFILES.length,
391
+ });
392
+ for (const p of PREDEFINED_PROFILES) {
393
+ items.push({ kind: "predefined", profile: p });
394
+ }
395
+ items.push({
396
+ kind: "header",
397
+ label: "Your Profiles",
398
+ color: theme.colors.info,
399
+ count: profileList.length,
400
+ });
401
+ for (const e of profileList) {
402
+ items.push({ kind: "saved", entry: e });
403
+ }
404
+ return items;
359
405
  }