claudeup 3.7.2 → 3.9.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 +1 -1
- package/src/data/settings-catalog.js +612 -0
- package/src/data/settings-catalog.ts +689 -0
- package/src/data/skill-repos.js +86 -0
- package/src/data/skill-repos.ts +97 -0
- package/src/services/plugin-manager.js +2 -0
- package/src/services/plugin-manager.ts +3 -0
- package/src/services/profiles.js +161 -0
- package/src/services/profiles.ts +225 -0
- package/src/services/settings-manager.js +108 -0
- package/src/services/settings-manager.ts +140 -0
- package/src/services/skills-manager.js +239 -0
- package/src/services/skills-manager.ts +328 -0
- package/src/services/skillsmp-client.js +67 -0
- package/src/services/skillsmp-client.ts +89 -0
- package/src/types/index.ts +101 -1
- package/src/ui/App.js +23 -18
- package/src/ui/App.tsx +27 -23
- package/src/ui/components/TabBar.js +9 -8
- package/src/ui/components/TabBar.tsx +15 -19
- package/src/ui/components/layout/ScreenLayout.js +8 -14
- package/src/ui/components/layout/ScreenLayout.tsx +51 -58
- package/src/ui/components/modals/ModalContainer.js +43 -11
- package/src/ui/components/modals/ModalContainer.tsx +44 -12
- package/src/ui/components/modals/SelectModal.js +4 -18
- package/src/ui/components/modals/SelectModal.tsx +10 -21
- package/src/ui/screens/CliToolsScreen.js +2 -2
- package/src/ui/screens/CliToolsScreen.tsx +8 -8
- package/src/ui/screens/EnvVarsScreen.js +248 -116
- package/src/ui/screens/EnvVarsScreen.tsx +419 -184
- package/src/ui/screens/McpRegistryScreen.tsx +18 -6
- package/src/ui/screens/McpScreen.js +1 -1
- package/src/ui/screens/McpScreen.tsx +15 -5
- package/src/ui/screens/ModelSelectorScreen.js +3 -5
- package/src/ui/screens/ModelSelectorScreen.tsx +12 -16
- package/src/ui/screens/PluginsScreen.js +154 -66
- package/src/ui/screens/PluginsScreen.tsx +280 -97
- package/src/ui/screens/ProfilesScreen.js +255 -0
- package/src/ui/screens/ProfilesScreen.tsx +487 -0
- package/src/ui/screens/SkillsScreen.js +325 -0
- package/src/ui/screens/SkillsScreen.tsx +574 -0
- package/src/ui/screens/StatusLineScreen.js +2 -2
- package/src/ui/screens/StatusLineScreen.tsx +10 -12
- package/src/ui/screens/index.js +3 -2
- package/src/ui/screens/index.ts +3 -2
- package/src/ui/state/AppContext.js +2 -1
- package/src/ui/state/AppContext.tsx +2 -0
- package/src/ui/state/reducer.js +151 -19
- package/src/ui/state/reducer.ts +167 -19
- package/src/ui/state/types.ts +58 -14
- package/src/utils/clipboard.js +56 -0
- package/src/utils/clipboard.ts +58 -0
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
import React, { useEffect, useCallback } from "react";
|
|
2
|
+
import { useApp, useModal } from "../state/AppContext.js";
|
|
3
|
+
import { useDimensions } from "../state/DimensionsContext.js";
|
|
4
|
+
import { useKeyboard } from "../hooks/useKeyboard.js";
|
|
5
|
+
import { ScreenLayout } from "../components/layout/index.js";
|
|
6
|
+
import { ScrollableList } from "../components/ScrollableList.js";
|
|
7
|
+
import {
|
|
8
|
+
listProfiles,
|
|
9
|
+
applyProfile,
|
|
10
|
+
renameProfile,
|
|
11
|
+
deleteProfile,
|
|
12
|
+
exportProfileToJson,
|
|
13
|
+
importProfileFromJson,
|
|
14
|
+
} from "../../services/profiles.js";
|
|
15
|
+
import {
|
|
16
|
+
writeClipboard,
|
|
17
|
+
readClipboard,
|
|
18
|
+
ClipboardUnavailableError,
|
|
19
|
+
} from "../../utils/clipboard.js";
|
|
20
|
+
import type { ProfileEntry } from "../../types/index.js";
|
|
21
|
+
|
|
22
|
+
export function ProfilesScreen() {
|
|
23
|
+
const { state, dispatch } = useApp();
|
|
24
|
+
const { profiles: profilesState } = state;
|
|
25
|
+
const modal = useModal();
|
|
26
|
+
const dimensions = useDimensions();
|
|
27
|
+
|
|
28
|
+
// Fetch data
|
|
29
|
+
const fetchData = useCallback(async () => {
|
|
30
|
+
dispatch({ type: "PROFILES_DATA_LOADING" });
|
|
31
|
+
try {
|
|
32
|
+
const entries = await listProfiles(state.projectPath);
|
|
33
|
+
dispatch({ type: "PROFILES_DATA_SUCCESS", profiles: entries });
|
|
34
|
+
} catch (error) {
|
|
35
|
+
dispatch({
|
|
36
|
+
type: "PROFILES_DATA_ERROR",
|
|
37
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}, [dispatch, state.projectPath]);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
fetchData();
|
|
44
|
+
}, [fetchData, state.dataRefreshVersion]);
|
|
45
|
+
|
|
46
|
+
const profileList =
|
|
47
|
+
profilesState.profiles.status === "success"
|
|
48
|
+
? profilesState.profiles.data
|
|
49
|
+
: [];
|
|
50
|
+
|
|
51
|
+
const selectedProfile: ProfileEntry | undefined =
|
|
52
|
+
profileList[profilesState.selectedIndex];
|
|
53
|
+
|
|
54
|
+
// Keyboard handling
|
|
55
|
+
useKeyboard((event) => {
|
|
56
|
+
if (state.isSearching || state.modal) return;
|
|
57
|
+
|
|
58
|
+
if (event.name === "up" || event.name === "k") {
|
|
59
|
+
const newIndex = Math.max(0, profilesState.selectedIndex - 1);
|
|
60
|
+
dispatch({ type: "PROFILES_SELECT", index: newIndex });
|
|
61
|
+
} else if (event.name === "down" || event.name === "j") {
|
|
62
|
+
const newIndex = Math.min(
|
|
63
|
+
Math.max(0, profileList.length - 1),
|
|
64
|
+
profilesState.selectedIndex + 1,
|
|
65
|
+
);
|
|
66
|
+
dispatch({ type: "PROFILES_SELECT", index: newIndex });
|
|
67
|
+
} else if (event.name === "enter" || event.name === "a") {
|
|
68
|
+
handleApply();
|
|
69
|
+
} else if (event.name === "r") {
|
|
70
|
+
handleRename();
|
|
71
|
+
} else if (event.name === "d") {
|
|
72
|
+
handleDelete();
|
|
73
|
+
} else if (event.name === "c") {
|
|
74
|
+
handleCopy();
|
|
75
|
+
} else if (event.name === "i") {
|
|
76
|
+
handleImport();
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const handleApply = async () => {
|
|
81
|
+
if (!selectedProfile) return;
|
|
82
|
+
|
|
83
|
+
const scopeChoice = await modal.select(
|
|
84
|
+
"Apply Profile",
|
|
85
|
+
`Apply "${selectedProfile.name}" to which scope?`,
|
|
86
|
+
[
|
|
87
|
+
{ label: "User — ~/.claude/settings.json (global)", value: "user" },
|
|
88
|
+
{
|
|
89
|
+
label: "Project — .claude/settings.json (this project)",
|
|
90
|
+
value: "project",
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
);
|
|
94
|
+
if (scopeChoice === null) return;
|
|
95
|
+
|
|
96
|
+
const targetScope = scopeChoice as "user" | "project";
|
|
97
|
+
const scopeLabel =
|
|
98
|
+
targetScope === "user"
|
|
99
|
+
? "~/.claude/settings.json"
|
|
100
|
+
: ".claude/settings.json";
|
|
101
|
+
|
|
102
|
+
const pluginCount = Object.keys(selectedProfile.plugins).length;
|
|
103
|
+
const emptyWarning =
|
|
104
|
+
pluginCount === 0
|
|
105
|
+
? "\n\nWARNING: This profile has no plugins — applying will disable all plugins."
|
|
106
|
+
: "";
|
|
107
|
+
|
|
108
|
+
const confirmed = await modal.confirm(
|
|
109
|
+
"Confirm Apply",
|
|
110
|
+
`This will REPLACE all enabledPlugins in ${scopeLabel}.${emptyWarning}\n\nContinue?`,
|
|
111
|
+
);
|
|
112
|
+
if (!confirmed) return;
|
|
113
|
+
|
|
114
|
+
modal.loading(`Applying "${selectedProfile.name}"...`);
|
|
115
|
+
try {
|
|
116
|
+
await applyProfile(
|
|
117
|
+
selectedProfile.id,
|
|
118
|
+
selectedProfile.scope,
|
|
119
|
+
targetScope,
|
|
120
|
+
state.projectPath,
|
|
121
|
+
);
|
|
122
|
+
modal.hideModal();
|
|
123
|
+
// Trigger PluginsScreen to refetch
|
|
124
|
+
dispatch({ type: "DATA_REFRESH_COMPLETE" });
|
|
125
|
+
await modal.message(
|
|
126
|
+
"Applied",
|
|
127
|
+
`Profile "${selectedProfile.name}" applied to ${scopeLabel}.`,
|
|
128
|
+
"success",
|
|
129
|
+
);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
modal.hideModal();
|
|
132
|
+
await modal.message(
|
|
133
|
+
"Error",
|
|
134
|
+
`Failed to apply profile: ${error}`,
|
|
135
|
+
"error",
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const handleRename = async () => {
|
|
141
|
+
if (!selectedProfile) return;
|
|
142
|
+
|
|
143
|
+
const newName = await modal.input(
|
|
144
|
+
"Rename Profile",
|
|
145
|
+
"New name:",
|
|
146
|
+
selectedProfile.name,
|
|
147
|
+
);
|
|
148
|
+
if (newName === null || !newName.trim()) return;
|
|
149
|
+
|
|
150
|
+
modal.loading("Renaming...");
|
|
151
|
+
try {
|
|
152
|
+
await renameProfile(
|
|
153
|
+
selectedProfile.id,
|
|
154
|
+
newName.trim(),
|
|
155
|
+
selectedProfile.scope,
|
|
156
|
+
state.projectPath,
|
|
157
|
+
);
|
|
158
|
+
modal.hideModal();
|
|
159
|
+
await fetchData();
|
|
160
|
+
await modal.message(
|
|
161
|
+
"Renamed",
|
|
162
|
+
`Profile renamed to "${newName.trim()}".`,
|
|
163
|
+
"success",
|
|
164
|
+
);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
modal.hideModal();
|
|
167
|
+
await modal.message("Error", `Failed to rename: ${error}`, "error");
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const handleDelete = async () => {
|
|
172
|
+
if (!selectedProfile) return;
|
|
173
|
+
|
|
174
|
+
const confirmed = await modal.confirm(
|
|
175
|
+
`Delete "${selectedProfile.name}"?`,
|
|
176
|
+
"This will permanently remove the profile.",
|
|
177
|
+
);
|
|
178
|
+
if (!confirmed) return;
|
|
179
|
+
|
|
180
|
+
modal.loading("Deleting...");
|
|
181
|
+
try {
|
|
182
|
+
await deleteProfile(
|
|
183
|
+
selectedProfile.id,
|
|
184
|
+
selectedProfile.scope,
|
|
185
|
+
state.projectPath,
|
|
186
|
+
);
|
|
187
|
+
modal.hideModal();
|
|
188
|
+
// Adjust selection if we deleted the last item
|
|
189
|
+
const newIndex = Math.max(
|
|
190
|
+
0,
|
|
191
|
+
Math.min(profilesState.selectedIndex, profileList.length - 2),
|
|
192
|
+
);
|
|
193
|
+
dispatch({ type: "PROFILES_SELECT", index: newIndex });
|
|
194
|
+
await fetchData();
|
|
195
|
+
await modal.message("Deleted", "Profile deleted.", "success");
|
|
196
|
+
} catch (error) {
|
|
197
|
+
modal.hideModal();
|
|
198
|
+
await modal.message("Error", `Failed to delete: ${error}`, "error");
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const handleCopy = async () => {
|
|
203
|
+
if (!selectedProfile) return;
|
|
204
|
+
|
|
205
|
+
modal.loading("Exporting...");
|
|
206
|
+
try {
|
|
207
|
+
const json = await exportProfileToJson(
|
|
208
|
+
selectedProfile.id,
|
|
209
|
+
selectedProfile.scope,
|
|
210
|
+
state.projectPath,
|
|
211
|
+
);
|
|
212
|
+
modal.hideModal();
|
|
213
|
+
try {
|
|
214
|
+
await writeClipboard(json);
|
|
215
|
+
await modal.message(
|
|
216
|
+
"Copied",
|
|
217
|
+
`Profile JSON copied to clipboard.\nShare it with teammates to import.`,
|
|
218
|
+
"success",
|
|
219
|
+
);
|
|
220
|
+
} catch (err) {
|
|
221
|
+
if (err instanceof ClipboardUnavailableError) {
|
|
222
|
+
// Fallback: show JSON in modal for manual copy
|
|
223
|
+
await modal.message("Profile JSON (copy manually)", json, "info");
|
|
224
|
+
} else {
|
|
225
|
+
throw err;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
} catch (error) {
|
|
229
|
+
modal.hideModal();
|
|
230
|
+
await modal.message("Error", `Failed to export: ${error}`, "error");
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const handleImport = async () => {
|
|
235
|
+
let json: string | null = null;
|
|
236
|
+
|
|
237
|
+
// Try to read from clipboard first
|
|
238
|
+
try {
|
|
239
|
+
json = await readClipboard();
|
|
240
|
+
} catch (err) {
|
|
241
|
+
if (err instanceof ClipboardUnavailableError) {
|
|
242
|
+
// Fallback: ask user to paste JSON manually
|
|
243
|
+
json = await modal.input("Import Profile", "Paste profile JSON:");
|
|
244
|
+
} else {
|
|
245
|
+
await modal.message("Error", `Clipboard error: ${err}`, "error");
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (json === null || !json.trim()) return;
|
|
251
|
+
|
|
252
|
+
const scopeChoice = await modal.select(
|
|
253
|
+
"Import Profile",
|
|
254
|
+
"Save imported profile to which scope?",
|
|
255
|
+
[
|
|
256
|
+
{ label: "User — ~/.claude/profiles.json (global)", value: "user" },
|
|
257
|
+
{
|
|
258
|
+
label: "Project — .claude/profiles.json (this project)",
|
|
259
|
+
value: "project",
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
);
|
|
263
|
+
if (scopeChoice === null) return;
|
|
264
|
+
|
|
265
|
+
const targetScope = scopeChoice as "user" | "project";
|
|
266
|
+
|
|
267
|
+
modal.loading("Importing...");
|
|
268
|
+
try {
|
|
269
|
+
const id = await importProfileFromJson(
|
|
270
|
+
json,
|
|
271
|
+
targetScope,
|
|
272
|
+
state.projectPath,
|
|
273
|
+
);
|
|
274
|
+
modal.hideModal();
|
|
275
|
+
await fetchData();
|
|
276
|
+
await modal.message(
|
|
277
|
+
"Imported",
|
|
278
|
+
`Profile imported successfully (id: ${id}).`,
|
|
279
|
+
"success",
|
|
280
|
+
);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
modal.hideModal();
|
|
283
|
+
await modal.message("Error", `Failed to import: ${error}`, "error");
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const formatDate = (iso: string): string => {
|
|
288
|
+
try {
|
|
289
|
+
const d = new Date(iso);
|
|
290
|
+
return d.toLocaleDateString("en-US", {
|
|
291
|
+
month: "short",
|
|
292
|
+
day: "numeric",
|
|
293
|
+
year: "numeric",
|
|
294
|
+
});
|
|
295
|
+
} catch {
|
|
296
|
+
return iso;
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const renderListItem = (
|
|
301
|
+
entry: ProfileEntry,
|
|
302
|
+
_idx: number,
|
|
303
|
+
isSelected: boolean,
|
|
304
|
+
) => {
|
|
305
|
+
const pluginCount = Object.keys(entry.plugins).length;
|
|
306
|
+
const dateStr = formatDate(entry.updatedAt);
|
|
307
|
+
const scopeColor = entry.scope === "user" ? "cyan" : "green";
|
|
308
|
+
const scopeLabel = entry.scope === "user" ? "[user]" : "[proj]";
|
|
309
|
+
|
|
310
|
+
if (isSelected) {
|
|
311
|
+
return (
|
|
312
|
+
<text bg="magenta" fg="white">
|
|
313
|
+
{" "}
|
|
314
|
+
{scopeLabel} {entry.name} — {pluginCount} plugin
|
|
315
|
+
{pluginCount !== 1 ? "s" : ""} · {dateStr}{" "}
|
|
316
|
+
</text>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return (
|
|
321
|
+
<text>
|
|
322
|
+
<span fg={scopeColor}>{scopeLabel}</span>
|
|
323
|
+
<span> </span>
|
|
324
|
+
<span fg="white">{entry.name}</span>
|
|
325
|
+
<span fg="gray">
|
|
326
|
+
{" "}
|
|
327
|
+
— {pluginCount} plugin{pluginCount !== 1 ? "s" : ""} · {dateStr}
|
|
328
|
+
</span>
|
|
329
|
+
</text>
|
|
330
|
+
);
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const renderDetail = () => {
|
|
334
|
+
if (profilesState.profiles.status === "loading") {
|
|
335
|
+
return <text fg="gray">Loading profiles...</text>;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (profilesState.profiles.status === "error") {
|
|
339
|
+
return (
|
|
340
|
+
<text fg="red">Error: {profilesState.profiles.error.message}</text>
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (profileList.length === 0) {
|
|
345
|
+
return (
|
|
346
|
+
<box flexDirection="column">
|
|
347
|
+
<text fg="gray">No profiles yet.</text>
|
|
348
|
+
<box marginTop={1}>
|
|
349
|
+
<text fg="green">
|
|
350
|
+
Press 's' in the Plugins screen to save the current selection as a
|
|
351
|
+
profile.
|
|
352
|
+
</text>
|
|
353
|
+
</box>
|
|
354
|
+
</box>
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (!selectedProfile) {
|
|
359
|
+
return <text fg="gray">Select a profile to see details</text>;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const plugins = Object.keys(selectedProfile.plugins);
|
|
363
|
+
const scopeColor = selectedProfile.scope === "user" ? "cyan" : "green";
|
|
364
|
+
const scopeLabel =
|
|
365
|
+
selectedProfile.scope === "user"
|
|
366
|
+
? "User (~/.claude/profiles.json)"
|
|
367
|
+
: "Project (.claude/profiles.json — committed to git)";
|
|
368
|
+
|
|
369
|
+
return (
|
|
370
|
+
<box flexDirection="column">
|
|
371
|
+
<text fg="cyan">
|
|
372
|
+
<strong>{selectedProfile.name}</strong>
|
|
373
|
+
</text>
|
|
374
|
+
<box marginTop={1}>
|
|
375
|
+
<text fg="gray">Scope: </text>
|
|
376
|
+
<text fg={scopeColor}>{scopeLabel}</text>
|
|
377
|
+
</box>
|
|
378
|
+
<box marginTop={1}>
|
|
379
|
+
<text fg="gray">
|
|
380
|
+
Created: {formatDate(selectedProfile.createdAt)} · Updated:{" "}
|
|
381
|
+
{formatDate(selectedProfile.updatedAt)}
|
|
382
|
+
</text>
|
|
383
|
+
</box>
|
|
384
|
+
<box marginTop={1} flexDirection="column">
|
|
385
|
+
<text fg="gray">
|
|
386
|
+
Plugins ({plugins.length}
|
|
387
|
+
{plugins.length === 0 ? " — applying will disable all plugins" : ""}
|
|
388
|
+
):
|
|
389
|
+
</text>
|
|
390
|
+
{plugins.length === 0 ? (
|
|
391
|
+
<text fg="yellow"> (none)</text>
|
|
392
|
+
) : (
|
|
393
|
+
plugins.map((p) => (
|
|
394
|
+
<box key={p}>
|
|
395
|
+
<text fg="white"> {p}</text>
|
|
396
|
+
</box>
|
|
397
|
+
))
|
|
398
|
+
)}
|
|
399
|
+
</box>
|
|
400
|
+
<box marginTop={2} flexDirection="column">
|
|
401
|
+
<box>
|
|
402
|
+
<text bg="magenta" fg="white">
|
|
403
|
+
{" "}
|
|
404
|
+
Enter/a{" "}
|
|
405
|
+
</text>
|
|
406
|
+
<text fg="gray"> Apply profile</text>
|
|
407
|
+
</box>
|
|
408
|
+
<box marginTop={1}>
|
|
409
|
+
<text bg="#333333" fg="white">
|
|
410
|
+
{" "}
|
|
411
|
+
r{" "}
|
|
412
|
+
</text>
|
|
413
|
+
<text fg="gray"> Rename</text>
|
|
414
|
+
</box>
|
|
415
|
+
<box marginTop={1}>
|
|
416
|
+
<text bg="red" fg="white">
|
|
417
|
+
{" "}
|
|
418
|
+
d{" "}
|
|
419
|
+
</text>
|
|
420
|
+
<text fg="gray"> Delete</text>
|
|
421
|
+
</box>
|
|
422
|
+
<box marginTop={1}>
|
|
423
|
+
<text bg="blue" fg="white">
|
|
424
|
+
{" "}
|
|
425
|
+
c{" "}
|
|
426
|
+
</text>
|
|
427
|
+
<text fg="gray"> Copy JSON to clipboard</text>
|
|
428
|
+
</box>
|
|
429
|
+
<box marginTop={1}>
|
|
430
|
+
<text bg="green" fg="white">
|
|
431
|
+
{" "}
|
|
432
|
+
i{" "}
|
|
433
|
+
</text>
|
|
434
|
+
<text fg="gray"> Import from clipboard</text>
|
|
435
|
+
</box>
|
|
436
|
+
</box>
|
|
437
|
+
</box>
|
|
438
|
+
);
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
const profileCount = profileList.length;
|
|
442
|
+
const userCount = profileList.filter((p) => p.scope === "user").length;
|
|
443
|
+
const projCount = profileList.filter((p) => p.scope === "project").length;
|
|
444
|
+
|
|
445
|
+
const statusContent = (
|
|
446
|
+
<text>
|
|
447
|
+
<span fg="gray">Profiles: </span>
|
|
448
|
+
<span fg="cyan">{userCount} user</span>
|
|
449
|
+
<span fg="gray"> + </span>
|
|
450
|
+
<span fg="green">{projCount} project</span>
|
|
451
|
+
<span fg="gray"> = </span>
|
|
452
|
+
<span fg="white">{profileCount} total</span>
|
|
453
|
+
</text>
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
return (
|
|
457
|
+
<ScreenLayout
|
|
458
|
+
title="claudeup Plugin Profiles"
|
|
459
|
+
currentScreen="profiles"
|
|
460
|
+
statusLine={statusContent}
|
|
461
|
+
footerHints="↑↓:nav │ Enter/a:apply │ r:rename │ d:delete │ c:copy │ i:import"
|
|
462
|
+
listPanel={
|
|
463
|
+
profileList.length === 0 &&
|
|
464
|
+
profilesState.profiles.status !== "loading" ? (
|
|
465
|
+
<box flexDirection="column">
|
|
466
|
+
<text fg="gray">No profiles yet.</text>
|
|
467
|
+
<box marginTop={1}>
|
|
468
|
+
<text fg="green">
|
|
469
|
+
Go to Plugins (1) and press 's' to save a profile.
|
|
470
|
+
</text>
|
|
471
|
+
</box>
|
|
472
|
+
</box>
|
|
473
|
+
) : (
|
|
474
|
+
<ScrollableList
|
|
475
|
+
items={profileList}
|
|
476
|
+
selectedIndex={profilesState.selectedIndex}
|
|
477
|
+
renderItem={renderListItem}
|
|
478
|
+
maxHeight={dimensions.listPanelHeight}
|
|
479
|
+
/>
|
|
480
|
+
)
|
|
481
|
+
}
|
|
482
|
+
detailPanel={renderDetail()}
|
|
483
|
+
/>
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export default ProfilesScreen;
|