claudeup 3.16.0 → 4.0.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/predefined-profiles.js +191 -0
- package/src/data/predefined-profiles.ts +205 -0
- package/src/ui/adapters/pluginsAdapter.js +139 -0
- package/src/ui/adapters/pluginsAdapter.ts +202 -0
- package/src/ui/adapters/settingsAdapter.js +111 -0
- package/src/ui/adapters/settingsAdapter.ts +165 -0
- package/src/ui/components/ScrollableList.js +4 -4
- package/src/ui/components/ScrollableList.tsx +4 -4
- package/src/ui/components/SearchInput.js +2 -2
- package/src/ui/components/SearchInput.tsx +3 -3
- package/src/ui/components/StyledText.js +1 -1
- package/src/ui/components/StyledText.tsx +5 -1
- package/src/ui/components/layout/ProgressBar.js +1 -1
- package/src/ui/components/layout/ProgressBar.tsx +1 -5
- package/src/ui/components/modals/InputModal.tsx +1 -6
- package/src/ui/components/modals/LoadingModal.js +1 -1
- package/src/ui/components/modals/LoadingModal.tsx +1 -3
- package/src/ui/hooks/index.js +3 -3
- package/src/ui/hooks/index.ts +3 -3
- package/src/ui/hooks/useKeyboard.ts +1 -3
- package/src/ui/hooks/useKeyboardHandler.js +9 -9
- package/src/ui/hooks/useKeyboardHandler.ts +9 -9
- package/src/ui/renderers/cliToolRenderers.js +33 -0
- package/src/ui/renderers/cliToolRenderers.tsx +153 -0
- package/src/ui/renderers/mcpRenderers.js +26 -0
- package/src/ui/renderers/mcpRenderers.tsx +145 -0
- package/src/ui/renderers/pluginRenderers.js +124 -0
- package/src/ui/renderers/pluginRenderers.tsx +362 -0
- package/src/ui/renderers/profileRenderers.js +172 -0
- package/src/ui/renderers/profileRenderers.tsx +410 -0
- package/src/ui/renderers/settingsRenderers.js +69 -0
- package/src/ui/renderers/settingsRenderers.tsx +205 -0
- package/src/ui/screens/CliToolsScreen.js +14 -58
- package/src/ui/screens/CliToolsScreen.tsx +36 -196
- package/src/ui/screens/EnvVarsScreen.js +12 -168
- package/src/ui/screens/EnvVarsScreen.tsx +16 -327
- package/src/ui/screens/McpScreen.js +12 -62
- package/src/ui/screens/McpScreen.tsx +21 -190
- package/src/ui/screens/PluginsScreen.js +52 -425
- package/src/ui/screens/PluginsScreen.tsx +70 -758
- package/src/ui/screens/ProfilesScreen.js +104 -68
- package/src/ui/screens/ProfilesScreen.tsx +147 -221
- package/src/ui/screens/SkillsScreen.js +16 -16
- package/src/ui/screens/SkillsScreen.tsx +20 -23
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type {
|
|
3
|
+
PluginBrowserItem,
|
|
4
|
+
PluginCategoryItem,
|
|
5
|
+
PluginPluginItem,
|
|
6
|
+
} from "../adapters/pluginsAdapter.js";
|
|
7
|
+
import { CategoryHeader } from "../components/CategoryHeader.js";
|
|
8
|
+
import {
|
|
9
|
+
SelectableRow,
|
|
10
|
+
ScopeSquares,
|
|
11
|
+
ActionHints,
|
|
12
|
+
MetaText,
|
|
13
|
+
KeyValueLine,
|
|
14
|
+
DetailSection,
|
|
15
|
+
} from "../components/primitives/index.js";
|
|
16
|
+
import { theme } from "../theme.js";
|
|
17
|
+
import { highlightMatches } from "../../utils/fuzzy-search.js";
|
|
18
|
+
|
|
19
|
+
// ─── Category renderers ───────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
function categoryRow(item: PluginCategoryItem, isSelected: boolean): React.ReactNode {
|
|
22
|
+
const mp = item.marketplace;
|
|
23
|
+
|
|
24
|
+
if (isSelected) {
|
|
25
|
+
const arrow = item.isExpanded ? "▼" : "▶";
|
|
26
|
+
const count = item.pluginCount > 0 ? ` (${item.pluginCount})` : "";
|
|
27
|
+
return (
|
|
28
|
+
<SelectableRow selected={true}>
|
|
29
|
+
<strong>
|
|
30
|
+
{" "}
|
|
31
|
+
{arrow} {mp.displayName}
|
|
32
|
+
{count}{" "}
|
|
33
|
+
</strong>
|
|
34
|
+
</SelectableRow>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Map tone to a statusColor for CategoryHeader (legacy component)
|
|
39
|
+
const statusColorMap: Record<PluginCategoryItem["tone"], string> = {
|
|
40
|
+
yellow: "yellow",
|
|
41
|
+
gray: "gray",
|
|
42
|
+
green: "green",
|
|
43
|
+
red: "red",
|
|
44
|
+
purple: theme.colors.accent,
|
|
45
|
+
teal: "cyan",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<CategoryHeader
|
|
50
|
+
title={mp.displayName}
|
|
51
|
+
expanded={item.isExpanded}
|
|
52
|
+
count={item.pluginCount}
|
|
53
|
+
status={item.badge}
|
|
54
|
+
statusColor={statusColorMap[item.tone]}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function categoryDetail(
|
|
60
|
+
item: PluginCategoryItem,
|
|
61
|
+
collapsedMarketplaces: Set<string>,
|
|
62
|
+
): React.ReactNode {
|
|
63
|
+
const mp = item.marketplace;
|
|
64
|
+
const isEnabled = item.marketplaceEnabled;
|
|
65
|
+
const isCollapsed = collapsedMarketplaces.has(mp.name);
|
|
66
|
+
const hasPlugins = item.pluginCount > 0;
|
|
67
|
+
|
|
68
|
+
let actionHint = "Add";
|
|
69
|
+
if (isEnabled) {
|
|
70
|
+
if (isCollapsed) {
|
|
71
|
+
actionHint = "Expand";
|
|
72
|
+
} else if (hasPlugins) {
|
|
73
|
+
actionHint = "Collapse";
|
|
74
|
+
} else {
|
|
75
|
+
actionHint = "Remove";
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<box flexDirection="column">
|
|
81
|
+
<text fg={theme.colors.info}>
|
|
82
|
+
<strong>
|
|
83
|
+
{mp.displayName}
|
|
84
|
+
{item.badge ? ` ${item.badge}` : ""}
|
|
85
|
+
</strong>
|
|
86
|
+
</text>
|
|
87
|
+
<text fg={theme.colors.muted}>{mp.description || "No description"}</text>
|
|
88
|
+
<text fg={isEnabled ? theme.colors.success : theme.colors.muted}>
|
|
89
|
+
{isEnabled ? "● Added" : "○ Not added"}
|
|
90
|
+
</text>
|
|
91
|
+
<text fg={theme.colors.link}>github.com/{mp.source.repo}</text>
|
|
92
|
+
<text>Plugins: {item.pluginCount}</text>
|
|
93
|
+
<ActionHints
|
|
94
|
+
hints={[
|
|
95
|
+
{ key: "Enter", label: actionHint, tone: isEnabled ? "primary" : "default" },
|
|
96
|
+
]}
|
|
97
|
+
/>
|
|
98
|
+
{isEnabled ? (
|
|
99
|
+
<box>
|
|
100
|
+
<text fg={theme.colors.muted}>← → to expand/collapse</text>
|
|
101
|
+
</box>
|
|
102
|
+
) : null}
|
|
103
|
+
</box>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ─── Plugin renderers ─────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
function pluginRow(item: PluginPluginItem, isSelected: boolean): React.ReactNode {
|
|
110
|
+
const { plugin } = item;
|
|
111
|
+
const hasUser = !!plugin.userScope?.enabled;
|
|
112
|
+
const hasProject = !!plugin.projectScope?.enabled;
|
|
113
|
+
const hasLocal = !!plugin.localScope?.enabled;
|
|
114
|
+
const hasAnyScope = hasUser || hasProject || hasLocal;
|
|
115
|
+
|
|
116
|
+
// Build version string
|
|
117
|
+
let versionStr = "";
|
|
118
|
+
if (plugin.isOrphaned) {
|
|
119
|
+
versionStr = " deprecated";
|
|
120
|
+
} else if (plugin.installedVersion && plugin.installedVersion !== "0.0.0") {
|
|
121
|
+
versionStr = ` v${plugin.installedVersion}`;
|
|
122
|
+
if (plugin.hasUpdate && plugin.version) {
|
|
123
|
+
versionStr += ` → v${plugin.version}`;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (isSelected) {
|
|
128
|
+
return (
|
|
129
|
+
<text bg={theme.selection.bg} fg={theme.selection.fg}>
|
|
130
|
+
{" "}
|
|
131
|
+
<ScopeSquares user={hasUser} project={hasProject} local={hasLocal} selected={true} />
|
|
132
|
+
{" "}{plugin.name}{versionStr}{" "}
|
|
133
|
+
</text>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Fuzzy highlight
|
|
138
|
+
const segments = item.matches ? highlightMatches(plugin.name, item.matches) : null;
|
|
139
|
+
const displayName = segments ? segments.map((seg) => seg.text).join("") : plugin.name;
|
|
140
|
+
|
|
141
|
+
if (plugin.isOrphaned) {
|
|
142
|
+
const ver =
|
|
143
|
+
plugin.installedVersion && plugin.installedVersion !== "0.0.0"
|
|
144
|
+
? ` v${plugin.installedVersion}`
|
|
145
|
+
: "";
|
|
146
|
+
return (
|
|
147
|
+
<text>
|
|
148
|
+
<span fg={theme.colors.danger}> ■■■ </span>
|
|
149
|
+
<span fg={theme.colors.muted}>{displayName}</span>
|
|
150
|
+
{ver ? <span fg={theme.colors.warning}>{ver}</span> : null}
|
|
151
|
+
<span fg={theme.colors.danger}> deprecated</span>
|
|
152
|
+
</text>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<text>
|
|
158
|
+
<span> </span>
|
|
159
|
+
<ScopeSquares user={hasUser} project={hasProject} local={hasLocal} />
|
|
160
|
+
<span> </span>
|
|
161
|
+
<span fg={hasAnyScope ? theme.colors.text : theme.colors.muted}>{displayName}</span>
|
|
162
|
+
<MetaText text={versionStr} tone={plugin.hasUpdate ? "warning" : "muted"} />
|
|
163
|
+
</text>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function pluginDetail(item: PluginPluginItem): React.ReactNode {
|
|
168
|
+
const { plugin } = item;
|
|
169
|
+
const isInstalled =
|
|
170
|
+
plugin.userScope?.enabled ||
|
|
171
|
+
plugin.projectScope?.enabled ||
|
|
172
|
+
plugin.localScope?.enabled;
|
|
173
|
+
|
|
174
|
+
// Orphaned/deprecated plugin
|
|
175
|
+
if (plugin.isOrphaned) {
|
|
176
|
+
return (
|
|
177
|
+
<box flexDirection="column">
|
|
178
|
+
<box justifyContent="center">
|
|
179
|
+
<text bg={theme.colors.warning} fg="black">
|
|
180
|
+
<strong> {plugin.name} — DEPRECATED </strong>
|
|
181
|
+
</text>
|
|
182
|
+
</box>
|
|
183
|
+
<box marginTop={1}>
|
|
184
|
+
<text fg={theme.colors.warning}>
|
|
185
|
+
This plugin is no longer in the marketplace.
|
|
186
|
+
</text>
|
|
187
|
+
</box>
|
|
188
|
+
<box marginTop={1}>
|
|
189
|
+
<text fg={theme.colors.muted}>
|
|
190
|
+
It was removed from the marketplace but still referenced in your settings. Press d to
|
|
191
|
+
uninstall and clean up.
|
|
192
|
+
</text>
|
|
193
|
+
</box>
|
|
194
|
+
{isInstalled ? (
|
|
195
|
+
<ActionHints
|
|
196
|
+
hints={[{ key: "d", label: "Uninstall (recommended)", tone: "danger" }]}
|
|
197
|
+
/>
|
|
198
|
+
) : null}
|
|
199
|
+
</box>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Build component counts
|
|
204
|
+
const components: string[] = [];
|
|
205
|
+
if (plugin.agents?.length) components.push(`${plugin.agents.length} agents`);
|
|
206
|
+
if (plugin.commands?.length) components.push(`${plugin.commands.length} commands`);
|
|
207
|
+
if (plugin.skills?.length) components.push(`${plugin.skills.length} skills`);
|
|
208
|
+
if (plugin.mcpServers?.length) components.push(`${plugin.mcpServers.length} MCP`);
|
|
209
|
+
if (plugin.lspServers && Object.keys(plugin.lspServers).length) {
|
|
210
|
+
components.push(`${Object.keys(plugin.lspServers).length} LSP`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const showVersion = plugin.version && plugin.version !== "0.0.0";
|
|
214
|
+
const showInstalledVersion =
|
|
215
|
+
plugin.installedVersion && plugin.installedVersion !== "0.0.0";
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<box flexDirection="column">
|
|
219
|
+
{/* Plugin name header */}
|
|
220
|
+
<box justifyContent="center">
|
|
221
|
+
<text bg={theme.selection.bg} fg={theme.selection.fg}>
|
|
222
|
+
<strong>
|
|
223
|
+
{" "}
|
|
224
|
+
{plugin.name}
|
|
225
|
+
{plugin.hasUpdate ? " ⬆" : ""}{" "}
|
|
226
|
+
</strong>
|
|
227
|
+
</text>
|
|
228
|
+
</box>
|
|
229
|
+
|
|
230
|
+
{/* Status line */}
|
|
231
|
+
<box marginTop={1}>
|
|
232
|
+
<text fg={isInstalled ? theme.colors.success : theme.colors.muted}>
|
|
233
|
+
{isInstalled ? "● Installed" : "○ Not installed"}
|
|
234
|
+
</text>
|
|
235
|
+
</box>
|
|
236
|
+
|
|
237
|
+
{/* Description */}
|
|
238
|
+
<box marginTop={1} marginBottom={1}>
|
|
239
|
+
<text fg={theme.colors.text}>{plugin.description}</text>
|
|
240
|
+
</box>
|
|
241
|
+
|
|
242
|
+
{/* Metadata */}
|
|
243
|
+
{showVersion ? (
|
|
244
|
+
<KeyValueLine
|
|
245
|
+
label="Version"
|
|
246
|
+
value={
|
|
247
|
+
<span>
|
|
248
|
+
<span fg={theme.colors.link}>v{plugin.version}</span>
|
|
249
|
+
{showInstalledVersion && plugin.installedVersion !== plugin.version ? (
|
|
250
|
+
<span> (v{plugin.installedVersion} installed)</span>
|
|
251
|
+
) : null}
|
|
252
|
+
</span>
|
|
253
|
+
}
|
|
254
|
+
/>
|
|
255
|
+
) : null}
|
|
256
|
+
{plugin.category ? (
|
|
257
|
+
<KeyValueLine
|
|
258
|
+
label="Category"
|
|
259
|
+
value={<span fg={theme.colors.accent}>{plugin.category}</span>}
|
|
260
|
+
/>
|
|
261
|
+
) : null}
|
|
262
|
+
{plugin.author ? (
|
|
263
|
+
<KeyValueLine label="Author" value={<span>{plugin.author.name}</span>} />
|
|
264
|
+
) : null}
|
|
265
|
+
{components.length > 0 ? (
|
|
266
|
+
<KeyValueLine
|
|
267
|
+
label="Contains"
|
|
268
|
+
value={<span fg={theme.colors.warning}>{components.join(" · ")}</span>}
|
|
269
|
+
/>
|
|
270
|
+
) : null}
|
|
271
|
+
|
|
272
|
+
{/* Scope Status */}
|
|
273
|
+
<DetailSection>
|
|
274
|
+
<text>{"─".repeat(24)}</text>
|
|
275
|
+
<text>
|
|
276
|
+
<strong>Scopes:</strong>
|
|
277
|
+
</text>
|
|
278
|
+
<box marginTop={1} flexDirection="column">
|
|
279
|
+
<text>
|
|
280
|
+
<span bg={theme.scopes.user} fg="black">
|
|
281
|
+
{" "}
|
|
282
|
+
u{" "}
|
|
283
|
+
</span>
|
|
284
|
+
<span fg={plugin.userScope?.enabled ? theme.scopes.user : theme.colors.muted}>
|
|
285
|
+
{plugin.userScope?.enabled ? " ● " : " ○ "}
|
|
286
|
+
</span>
|
|
287
|
+
<span fg={theme.scopes.user}>User</span>
|
|
288
|
+
<span> global</span>
|
|
289
|
+
{plugin.userScope?.version ? (
|
|
290
|
+
<span fg={theme.scopes.user}> v{plugin.userScope.version}</span>
|
|
291
|
+
) : null}
|
|
292
|
+
</text>
|
|
293
|
+
<text>
|
|
294
|
+
<span bg={theme.scopes.project} fg="black">
|
|
295
|
+
{" "}
|
|
296
|
+
p{" "}
|
|
297
|
+
</span>
|
|
298
|
+
<span fg={plugin.projectScope?.enabled ? theme.scopes.project : theme.colors.muted}>
|
|
299
|
+
{plugin.projectScope?.enabled ? " ● " : " ○ "}
|
|
300
|
+
</span>
|
|
301
|
+
<span fg={theme.scopes.project}>Project</span>
|
|
302
|
+
<span> team</span>
|
|
303
|
+
{plugin.projectScope?.version ? (
|
|
304
|
+
<span fg={theme.scopes.project}> v{plugin.projectScope.version}</span>
|
|
305
|
+
) : null}
|
|
306
|
+
</text>
|
|
307
|
+
<text>
|
|
308
|
+
<span bg={theme.scopes.local} fg="black">
|
|
309
|
+
{" "}
|
|
310
|
+
l{" "}
|
|
311
|
+
</span>
|
|
312
|
+
<span fg={plugin.localScope?.enabled ? theme.scopes.local : theme.colors.muted}>
|
|
313
|
+
{plugin.localScope?.enabled ? " ● " : " ○ "}
|
|
314
|
+
</span>
|
|
315
|
+
<span fg={theme.scopes.local}>Local</span>
|
|
316
|
+
<span> private</span>
|
|
317
|
+
{plugin.localScope?.version ? (
|
|
318
|
+
<span fg={theme.scopes.local}> v{plugin.localScope.version}</span>
|
|
319
|
+
) : null}
|
|
320
|
+
</text>
|
|
321
|
+
</box>
|
|
322
|
+
</DetailSection>
|
|
323
|
+
|
|
324
|
+
{/* Update action */}
|
|
325
|
+
{isInstalled && plugin.hasUpdate ? (
|
|
326
|
+
<ActionHints
|
|
327
|
+
hints={[{ key: "U", label: `Update to v${plugin.version}`, tone: "primary" }]}
|
|
328
|
+
/>
|
|
329
|
+
) : null}
|
|
330
|
+
</box>
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ─── Public dispatch functions ────────────────────────────────────────────────
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Render a plugin browser list row by item kind.
|
|
338
|
+
*/
|
|
339
|
+
export function renderPluginRow(
|
|
340
|
+
item: PluginBrowserItem,
|
|
341
|
+
_index: number,
|
|
342
|
+
isSelected: boolean,
|
|
343
|
+
): React.ReactNode {
|
|
344
|
+
if (item.kind === "category") {
|
|
345
|
+
return categoryRow(item, isSelected);
|
|
346
|
+
}
|
|
347
|
+
return pluginRow(item, isSelected);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Render the detail panel for a plugin browser item.
|
|
352
|
+
*/
|
|
353
|
+
export function renderPluginDetail(
|
|
354
|
+
item: PluginBrowserItem | undefined,
|
|
355
|
+
collapsedMarketplaces: Set<string>,
|
|
356
|
+
): React.ReactNode {
|
|
357
|
+
if (!item) return <text fg={theme.colors.muted}>Select an item</text>;
|
|
358
|
+
if (item.kind === "category") {
|
|
359
|
+
return categoryDetail(item, collapsedMarketplaces);
|
|
360
|
+
}
|
|
361
|
+
return pluginDetail(item);
|
|
362
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { theme } from "../theme.js";
|
|
3
|
+
// ─── Helpers ───────────────────────────────────────────────────────────────────
|
|
4
|
+
function truncate(s, maxLen) {
|
|
5
|
+
if (s.length <= maxLen)
|
|
6
|
+
return s;
|
|
7
|
+
return s.slice(0, maxLen - 1) + "\u2026";
|
|
8
|
+
}
|
|
9
|
+
export function humanizeValue(_key, value) {
|
|
10
|
+
if (typeof value === "boolean")
|
|
11
|
+
return value ? "on" : "off";
|
|
12
|
+
if (typeof value !== "string")
|
|
13
|
+
return String(value);
|
|
14
|
+
if (value === "claude-sonnet-4-6")
|
|
15
|
+
return "Sonnet";
|
|
16
|
+
if (value === "claude-opus-4-6")
|
|
17
|
+
return "Opus";
|
|
18
|
+
if (value === "claude-haiku-4-6")
|
|
19
|
+
return "Haiku";
|
|
20
|
+
if (value.startsWith("claude-sonnet"))
|
|
21
|
+
return "Sonnet";
|
|
22
|
+
if (value.startsWith("claude-opus"))
|
|
23
|
+
return "Opus";
|
|
24
|
+
if (value.startsWith("claude-haiku"))
|
|
25
|
+
return "Haiku";
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
export function humanizeKey(key) {
|
|
29
|
+
const labels = {
|
|
30
|
+
effortLevel: "Effort",
|
|
31
|
+
model: "Model",
|
|
32
|
+
outputStyle: "Output",
|
|
33
|
+
alwaysThinkingEnabled: "Thinking",
|
|
34
|
+
};
|
|
35
|
+
return labels[key] ?? key;
|
|
36
|
+
}
|
|
37
|
+
export function stripSuffix(pluginName) {
|
|
38
|
+
return pluginName
|
|
39
|
+
.replace(/@magus$/, "")
|
|
40
|
+
.replace(/@claude-plugins-official$/, "");
|
|
41
|
+
}
|
|
42
|
+
export function wrapNames(names, lineMax = 40) {
|
|
43
|
+
const lines = [];
|
|
44
|
+
let current = "";
|
|
45
|
+
for (const name of names) {
|
|
46
|
+
const add = current ? `, ${name}` : name;
|
|
47
|
+
if (current && current.length + add.length > lineMax) {
|
|
48
|
+
lines.push(current);
|
|
49
|
+
current = name;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
current += current ? `, ${name}` : name;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (current)
|
|
56
|
+
lines.push(current);
|
|
57
|
+
return lines;
|
|
58
|
+
}
|
|
59
|
+
export function formatDate(iso) {
|
|
60
|
+
try {
|
|
61
|
+
const d = new Date(iso);
|
|
62
|
+
return d.toLocaleDateString("en-US", {
|
|
63
|
+
month: "short",
|
|
64
|
+
day: "numeric",
|
|
65
|
+
year: "numeric",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return iso;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// ─── Header renderer ───────────────────────────────────────────────────────────
|
|
73
|
+
const headerRenderer = {
|
|
74
|
+
renderRow: ({ item }) => (_jsx("text", { bg: item.color, fg: "white", children: _jsxs("strong", { children: [" ", item.label, " (", item.count, ")", " "] }) })),
|
|
75
|
+
renderDetail: () => (_jsx("text", { fg: theme.colors.muted, children: "Select a profile to see details" })),
|
|
76
|
+
};
|
|
77
|
+
// ─── Predefined renderer ───────────────────────────────────────────────────────
|
|
78
|
+
const DIVIDER = "────────────────────────";
|
|
79
|
+
const predefinedRenderer = {
|
|
80
|
+
renderRow: ({ item, isSelected }) => {
|
|
81
|
+
const { profile } = item;
|
|
82
|
+
const pluginCount = profile.magusPlugins.length + profile.anthropicPlugins.length;
|
|
83
|
+
const skillCount = profile.skills.length;
|
|
84
|
+
const label = truncate(`${profile.name} — ${pluginCount} plugins · ${skillCount} skill${skillCount !== 1 ? "s" : ""}`, 45);
|
|
85
|
+
if (isSelected) {
|
|
86
|
+
return (_jsxs("text", { bg: "blue", fg: "white", children: [" ", label, " "] }));
|
|
87
|
+
}
|
|
88
|
+
return (_jsxs("text", { children: [_jsx("span", { fg: theme.colors.muted, children: "- " }), _jsx("span", { fg: theme.colors.text, children: label })] }));
|
|
89
|
+
},
|
|
90
|
+
renderDetail: ({ item }) => {
|
|
91
|
+
const { profile } = item;
|
|
92
|
+
const magusNames = profile.magusPlugins;
|
|
93
|
+
const anthropicNames = profile.anthropicPlugins;
|
|
94
|
+
const magusLines = wrapNames(magusNames);
|
|
95
|
+
const anthropicLines = wrapNames(anthropicNames);
|
|
96
|
+
const skillLines = wrapNames(profile.skills);
|
|
97
|
+
const settingEntries = Object.entries(profile.settings).filter(([k]) => k !== "env");
|
|
98
|
+
const envMap = profile.settings["env"] ?? {};
|
|
99
|
+
const tasksOn = envMap["CLAUDE_CODE_ENABLE_TASKS"] === "true";
|
|
100
|
+
const teamsOn = envMap["CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS"] === "true";
|
|
101
|
+
return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: theme.colors.text, children: _jsx("strong", { children: profile.name }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: theme.colors.muted, children: profile.description }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: theme.colors.dim, children: DIVIDER }) }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { fg: theme.colors.muted, children: ["Magus plugins (", magusNames.length, "):"] }), magusLines.map((line, i) => (_jsxs("text", { fg: theme.colors.info, children: [" ", line] }, i)))] }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { fg: theme.colors.muted, children: ["Anthropic plugins (", anthropicNames.length, "):"] }), anthropicLines.map((line, i) => (_jsxs("text", { fg: theme.colors.warning, children: [" ", line] }, i)))] }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { fg: theme.colors.muted, children: ["Skills (", profile.skills.length, "):"] }), skillLines.map((line, i) => (_jsxs("text", { fg: theme.colors.text, children: [" ", line] }, i)))] }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: theme.colors.dim, children: DIVIDER }) }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsx("text", { fg: theme.colors.muted, children: "Settings:" }), settingEntries.map(([k, v]) => (_jsxs("text", { children: [_jsxs("span", { fg: theme.colors.muted, children: [" ", humanizeKey(k).padEnd(14)] }), _jsx("span", { fg: theme.colors.text, children: humanizeValue(k, v) })] }, k))), tasksOn && (_jsxs("text", { children: [_jsxs("span", { fg: theme.colors.muted, children: [" ", "Tasks".padEnd(14)] }), _jsx("span", { fg: theme.colors.text, children: "on" })] })), teamsOn && (_jsxs("text", { children: [_jsxs("span", { fg: theme.colors.muted, children: [" ", "Agent Teams".padEnd(14)] }), _jsx("span", { fg: theme.colors.text, children: "on" })] }))] }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: theme.colors.dim, children: DIVIDER }) }), _jsxs("box", { marginTop: 1, children: [_jsxs("text", { bg: "blue", fg: "white", children: [" ", "Enter/a", " "] }), _jsx("text", { fg: theme.colors.muted, children: " Apply to project" })] })] }));
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
// ─── Saved renderer ────────────────────────────────────────────────────────────
|
|
105
|
+
const savedRenderer = {
|
|
106
|
+
renderRow: ({ item, isSelected }) => {
|
|
107
|
+
const { entry } = item;
|
|
108
|
+
const pluginCount = Object.keys(entry.plugins).length;
|
|
109
|
+
const dateStr = formatDate(entry.updatedAt);
|
|
110
|
+
const label = truncate(`${entry.name} — ${pluginCount} plugin${pluginCount !== 1 ? "s" : ""} · ${dateStr}`, 45);
|
|
111
|
+
if (isSelected) {
|
|
112
|
+
return (_jsxs("text", { bg: theme.selection.bg, fg: theme.selection.fg, children: [" ", label, " "] }));
|
|
113
|
+
}
|
|
114
|
+
const scopeColor = entry.scope === "user" ? theme.scopes.user : theme.scopes.project;
|
|
115
|
+
const scopeLabel = entry.scope === "user" ? "[user]" : "[proj]";
|
|
116
|
+
return (_jsxs("text", { children: [_jsxs("span", { fg: scopeColor, children: [scopeLabel, " "] }), _jsx("span", { fg: theme.colors.text, children: label })] }));
|
|
117
|
+
},
|
|
118
|
+
renderDetail: ({ item }) => {
|
|
119
|
+
const { entry: selectedProfile } = item;
|
|
120
|
+
const plugins = Object.keys(selectedProfile.plugins);
|
|
121
|
+
const cleanPlugins = plugins.map(stripSuffix);
|
|
122
|
+
const scopeColor = selectedProfile.scope === "user" ? theme.scopes.user : theme.scopes.project;
|
|
123
|
+
const scopeLabel = selectedProfile.scope === "user"
|
|
124
|
+
? "User (~/.claude/profiles.json)"
|
|
125
|
+
: "Project (.claude/profiles.json — committed to git)";
|
|
126
|
+
return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: theme.colors.info, children: _jsx("strong", { children: selectedProfile.name }) }), _jsxs("box", { marginTop: 1, children: [_jsx("text", { fg: theme.colors.muted, children: "Scope: " }), _jsx("text", { fg: scopeColor, children: scopeLabel })] }), _jsx("box", { marginTop: 1, children: _jsxs("text", { fg: theme.colors.muted, children: ["Created: ", formatDate(selectedProfile.createdAt), " \u00B7 Updated:", " ", formatDate(selectedProfile.updatedAt)] }) }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { fg: theme.colors.muted, children: ["Plugins (", plugins.length, plugins.length === 0 ? " — applying will disable all plugins" : "", "):"] }), cleanPlugins.length === 0 ? (_jsx("text", { fg: theme.colors.warning, children: " (none)" })) : (wrapNames(cleanPlugins).map((line, i) => (_jsxs("text", { fg: theme.colors.text, children: [" ", line] }, i))))] }), _jsxs("box", { marginTop: 2, flexDirection: "column", children: [_jsxs("box", { children: [_jsxs("text", { bg: theme.selection.bg, fg: theme.selection.fg, children: [" ", "Enter/a", " "] }), _jsx("text", { fg: theme.colors.muted, children: " Apply profile" })] }), _jsxs("box", { marginTop: 1, children: [_jsxs("text", { bg: theme.colors.dim, fg: "white", children: [" ", "r", " "] }), _jsx("text", { fg: theme.colors.muted, children: " Rename" })] }), _jsxs("box", { marginTop: 1, children: [_jsxs("text", { bg: theme.colors.danger, fg: "white", children: [" ", "d", " "] }), _jsx("text", { fg: theme.colors.muted, children: " Delete" })] }), _jsxs("box", { marginTop: 1, children: [_jsxs("text", { bg: "blue", fg: "white", children: [" ", "c", " "] }), _jsx("text", { fg: theme.colors.muted, children: " Copy JSON to clipboard" })] }), _jsxs("box", { marginTop: 1, children: [_jsxs("text", { bg: theme.colors.success, fg: "white", children: [" ", "i", " "] }), _jsx("text", { fg: theme.colors.muted, children: " Import from clipboard" })] })] })] }));
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
// ─── Dispatch helpers ──────────────────────────────────────────────────────────
|
|
130
|
+
export function renderProfileRow(item, _index, isSelected) {
|
|
131
|
+
if (item.kind === "header") {
|
|
132
|
+
return headerRenderer.renderRow({ item, isSelected });
|
|
133
|
+
}
|
|
134
|
+
if (item.kind === "predefined") {
|
|
135
|
+
return predefinedRenderer.renderRow({ item, isSelected });
|
|
136
|
+
}
|
|
137
|
+
return savedRenderer.renderRow({ item, isSelected });
|
|
138
|
+
}
|
|
139
|
+
export function renderProfileDetail(item, loading, error) {
|
|
140
|
+
if (loading)
|
|
141
|
+
return _jsx("text", { fg: theme.colors.muted, children: "Loading profiles..." });
|
|
142
|
+
if (error)
|
|
143
|
+
return _jsxs("text", { fg: theme.colors.danger, children: ["Error: ", error] });
|
|
144
|
+
if (!item || item.kind === "header")
|
|
145
|
+
return _jsx("text", { fg: theme.colors.muted, children: "Select a profile to see details" });
|
|
146
|
+
if (item.kind === "predefined") {
|
|
147
|
+
return predefinedRenderer.renderDetail({ item });
|
|
148
|
+
}
|
|
149
|
+
return savedRenderer.renderDetail({ item });
|
|
150
|
+
}
|
|
151
|
+
export function buildProfileListItems(profileList, PREDEFINED_PROFILES) {
|
|
152
|
+
const items = [];
|
|
153
|
+
items.push({
|
|
154
|
+
kind: "header",
|
|
155
|
+
label: "Presets",
|
|
156
|
+
color: "#4527a0",
|
|
157
|
+
count: PREDEFINED_PROFILES.length,
|
|
158
|
+
});
|
|
159
|
+
for (const p of PREDEFINED_PROFILES) {
|
|
160
|
+
items.push({ kind: "predefined", profile: p });
|
|
161
|
+
}
|
|
162
|
+
items.push({
|
|
163
|
+
kind: "header",
|
|
164
|
+
label: "Your Profiles",
|
|
165
|
+
color: "#00695c",
|
|
166
|
+
count: profileList.length,
|
|
167
|
+
});
|
|
168
|
+
for (const e of profileList) {
|
|
169
|
+
items.push({ kind: "saved", entry: e });
|
|
170
|
+
}
|
|
171
|
+
return items;
|
|
172
|
+
}
|