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.
- package/package.json +4 -4
- package/src/__tests__/git-worktree.test.ts +108 -0
- package/src/__tests__/scope-squares.test.tsx +165 -0
- package/src/__tests__/theme-adaptive-colors.test.ts +307 -0
- package/src/__tests__/uppercase-keybindings.test.ts +101 -0
- package/src/__tests__/worktree-registry-resolution.test.ts +107 -0
- package/src/main.tsx +21 -5
- package/src/opentui.d.ts +21 -12
- package/src/services/claude-settings.ts +37 -7
- package/src/services/git-worktree.ts +129 -0
- package/src/services/plugin-manager.ts +10 -1
- package/src/ui/App.tsx +19 -12
- package/src/ui/adapters/pluginsAdapter.ts +174 -168
- package/src/ui/adapters/settingsAdapter.ts +119 -116
- package/src/ui/adapters/skillsAdapter.ts +203 -196
- package/src/ui/components/CategoryHeader.tsx +9 -8
- package/src/ui/components/EmptyFilterState.tsx +10 -5
- package/src/ui/components/FlagDetailEditor.tsx +0 -0
- package/src/ui/components/ScopeIndicator.tsx +10 -6
- package/src/ui/components/ScrollableList.tsx +3 -2
- package/src/ui/components/SearchInput.tsx +2 -1
- package/src/ui/components/StyledText.tsx +5 -4
- package/src/ui/components/TabBar.tsx +4 -3
- package/src/ui/components/layout/FooterHints.tsx +37 -30
- package/src/ui/components/layout/Panel.tsx +6 -5
- package/src/ui/components/layout/ProgressBar.tsx +7 -6
- package/src/ui/components/layout/ScopeTabs.tsx +6 -5
- package/src/ui/components/layout/ScreenLayout.tsx +27 -24
- package/src/ui/components/layout/index.ts +3 -3
- package/src/ui/components/modals/ConfirmModal.tsx +12 -11
- package/src/ui/components/modals/InputModal.tsx +14 -6
- package/src/ui/components/modals/LoadingModal.tsx +6 -5
- package/src/ui/components/modals/MessageModal.tsx +9 -8
- package/src/ui/components/modals/SelectModal.tsx +11 -7
- package/src/ui/components/modals/VersionMismatchModal.tsx +14 -16
- package/src/ui/components/primitives/ActionHints.tsx +26 -26
- package/src/ui/components/primitives/DetailSection.tsx +13 -12
- package/src/ui/components/primitives/KeyValueLine.tsx +9 -8
- package/src/ui/components/primitives/ListCategoryRow.tsx +25 -27
- package/src/ui/components/primitives/MetaText.tsx +3 -3
- package/src/ui/components/primitives/ScopeDetail.tsx +48 -48
- package/src/ui/components/primitives/ScopeSquares.tsx +47 -22
- package/src/ui/components/primitives/SelectableRow.tsx +22 -16
- package/src/ui/hooks/useGitignoreModal.ts +78 -74
- package/src/ui/registry.ts +11 -11
- package/src/ui/renderers/cliToolRenderers.tsx +260 -203
- package/src/ui/renderers/gitignoreRenderers.tsx +43 -42
- package/src/ui/renderers/mcpRenderers.tsx +121 -117
- package/src/ui/renderers/pluginRenderers.tsx +528 -471
- package/src/ui/renderers/profileRenderers.tsx +346 -300
- package/src/ui/renderers/settingsRenderers.tsx +183 -176
- package/src/ui/renderers/skillRenderers.tsx +410 -326
- package/src/ui/screens/AliasScreen.tsx +1336 -1309
- package/src/ui/screens/CliToolsScreen.tsx +92 -40
- package/src/ui/screens/EnvVarsScreen.tsx +19 -13
- package/src/ui/screens/GitignoreScreen.tsx +510 -493
- package/src/ui/screens/McpRegistryScreen.tsx +28 -21
- package/src/ui/screens/McpScreen.tsx +12 -3
- package/src/ui/screens/PluginsScreen.tsx +17 -7
- package/src/ui/screens/ProfilesScreen.tsx +39 -23
- package/src/ui/screens/SkillsScreen.tsx +832 -688
- package/src/ui/theme-mode.ts +73 -0
- 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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
18
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
kind: "header";
|
|
88
|
+
label: string;
|
|
89
|
+
color: UiColor;
|
|
90
|
+
count: number;
|
|
91
91
|
}> = {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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<{
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
353
|
+
item: ProfileListItem,
|
|
354
|
+
_index: number,
|
|
355
|
+
isSelected: boolean,
|
|
310
356
|
): React.ReactNode {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
367
|
+
item: ProfileListItem | undefined,
|
|
368
|
+
loading: boolean,
|
|
369
|
+
error: string | undefined,
|
|
324
370
|
): React.ReactNode {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
337
|
-
|
|
382
|
+
profileList: import("../../types/index.js").ProfileEntry[],
|
|
383
|
+
PREDEFINED_PROFILES: PredefinedProfile[],
|
|
338
384
|
): ProfileListItem[] {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
}
|