claudeup 3.17.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.
Files changed (43) hide show
  1. package/package.json +1 -1
  2. package/src/ui/adapters/pluginsAdapter.js +139 -0
  3. package/src/ui/adapters/pluginsAdapter.ts +202 -0
  4. package/src/ui/adapters/settingsAdapter.js +111 -0
  5. package/src/ui/adapters/settingsAdapter.ts +165 -0
  6. package/src/ui/components/ScrollableList.js +4 -4
  7. package/src/ui/components/ScrollableList.tsx +4 -4
  8. package/src/ui/components/SearchInput.js +2 -2
  9. package/src/ui/components/SearchInput.tsx +3 -3
  10. package/src/ui/components/StyledText.js +1 -1
  11. package/src/ui/components/StyledText.tsx +5 -1
  12. package/src/ui/components/layout/ProgressBar.js +1 -1
  13. package/src/ui/components/layout/ProgressBar.tsx +1 -5
  14. package/src/ui/components/modals/InputModal.tsx +1 -6
  15. package/src/ui/components/modals/LoadingModal.js +1 -1
  16. package/src/ui/components/modals/LoadingModal.tsx +1 -3
  17. package/src/ui/hooks/index.js +3 -3
  18. package/src/ui/hooks/index.ts +3 -3
  19. package/src/ui/hooks/useKeyboard.ts +1 -3
  20. package/src/ui/hooks/useKeyboardHandler.js +9 -9
  21. package/src/ui/hooks/useKeyboardHandler.ts +9 -9
  22. package/src/ui/renderers/cliToolRenderers.js +33 -0
  23. package/src/ui/renderers/cliToolRenderers.tsx +153 -0
  24. package/src/ui/renderers/mcpRenderers.js +26 -0
  25. package/src/ui/renderers/mcpRenderers.tsx +145 -0
  26. package/src/ui/renderers/pluginRenderers.js +124 -0
  27. package/src/ui/renderers/pluginRenderers.tsx +362 -0
  28. package/src/ui/renderers/profileRenderers.js +172 -0
  29. package/src/ui/renderers/profileRenderers.tsx +410 -0
  30. package/src/ui/renderers/settingsRenderers.js +69 -0
  31. package/src/ui/renderers/settingsRenderers.tsx +205 -0
  32. package/src/ui/screens/CliToolsScreen.js +14 -58
  33. package/src/ui/screens/CliToolsScreen.tsx +36 -196
  34. package/src/ui/screens/EnvVarsScreen.js +12 -168
  35. package/src/ui/screens/EnvVarsScreen.tsx +16 -327
  36. package/src/ui/screens/McpScreen.js +12 -62
  37. package/src/ui/screens/McpScreen.tsx +21 -190
  38. package/src/ui/screens/PluginsScreen.js +52 -425
  39. package/src/ui/screens/PluginsScreen.tsx +70 -758
  40. package/src/ui/screens/ProfilesScreen.js +32 -97
  41. package/src/ui/screens/ProfilesScreen.tsx +58 -328
  42. package/src/ui/screens/SkillsScreen.js +16 -16
  43. package/src/ui/screens/SkillsScreen.tsx +20 -23
@@ -0,0 +1,124 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
2
+ import { CategoryHeader } from "../components/CategoryHeader.js";
3
+ import { SelectableRow, ScopeSquares, ActionHints, MetaText, KeyValueLine, DetailSection, } from "../components/primitives/index.js";
4
+ import { theme } from "../theme.js";
5
+ import { highlightMatches } from "../../utils/fuzzy-search.js";
6
+ // ─── Category renderers ───────────────────────────────────────────────────────
7
+ function categoryRow(item, isSelected) {
8
+ const mp = item.marketplace;
9
+ if (isSelected) {
10
+ const arrow = item.isExpanded ? "▼" : "▶";
11
+ const count = item.pluginCount > 0 ? ` (${item.pluginCount})` : "";
12
+ return (_jsx(SelectableRow, { selected: true, children: _jsxs("strong", { children: [" ", arrow, " ", mp.displayName, count, " "] }) }));
13
+ }
14
+ // Map tone to a statusColor for CategoryHeader (legacy component)
15
+ const statusColorMap = {
16
+ yellow: "yellow",
17
+ gray: "gray",
18
+ green: "green",
19
+ red: "red",
20
+ purple: theme.colors.accent,
21
+ teal: "cyan",
22
+ };
23
+ return (_jsx(CategoryHeader, { title: mp.displayName, expanded: item.isExpanded, count: item.pluginCount, status: item.badge, statusColor: statusColorMap[item.tone] }));
24
+ }
25
+ function categoryDetail(item, collapsedMarketplaces) {
26
+ const mp = item.marketplace;
27
+ const isEnabled = item.marketplaceEnabled;
28
+ const isCollapsed = collapsedMarketplaces.has(mp.name);
29
+ const hasPlugins = item.pluginCount > 0;
30
+ let actionHint = "Add";
31
+ if (isEnabled) {
32
+ if (isCollapsed) {
33
+ actionHint = "Expand";
34
+ }
35
+ else if (hasPlugins) {
36
+ actionHint = "Collapse";
37
+ }
38
+ else {
39
+ actionHint = "Remove";
40
+ }
41
+ }
42
+ return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: theme.colors.info, children: _jsxs("strong", { children: [mp.displayName, item.badge ? ` ${item.badge}` : ""] }) }), _jsx("text", { fg: theme.colors.muted, children: mp.description || "No description" }), _jsx("text", { fg: isEnabled ? theme.colors.success : theme.colors.muted, children: isEnabled ? "● Added" : "○ Not added" }), _jsxs("text", { fg: theme.colors.link, children: ["github.com/", mp.source.repo] }), _jsxs("text", { children: ["Plugins: ", item.pluginCount] }), _jsx(ActionHints, { hints: [
43
+ { key: "Enter", label: actionHint, tone: isEnabled ? "primary" : "default" },
44
+ ] }), isEnabled ? (_jsx("box", { children: _jsx("text", { fg: theme.colors.muted, children: "\u2190 \u2192 to expand/collapse" }) })) : null] }));
45
+ }
46
+ // ─── Plugin renderers ─────────────────────────────────────────────────────────
47
+ function pluginRow(item, isSelected) {
48
+ const { plugin } = item;
49
+ const hasUser = !!plugin.userScope?.enabled;
50
+ const hasProject = !!plugin.projectScope?.enabled;
51
+ const hasLocal = !!plugin.localScope?.enabled;
52
+ const hasAnyScope = hasUser || hasProject || hasLocal;
53
+ // Build version string
54
+ let versionStr = "";
55
+ if (plugin.isOrphaned) {
56
+ versionStr = " deprecated";
57
+ }
58
+ else if (plugin.installedVersion && plugin.installedVersion !== "0.0.0") {
59
+ versionStr = ` v${plugin.installedVersion}`;
60
+ if (plugin.hasUpdate && plugin.version) {
61
+ versionStr += ` → v${plugin.version}`;
62
+ }
63
+ }
64
+ if (isSelected) {
65
+ return (_jsxs("text", { bg: theme.selection.bg, fg: theme.selection.fg, children: [" ", _jsx(ScopeSquares, { user: hasUser, project: hasProject, local: hasLocal, selected: true }), " ", plugin.name, versionStr, " "] }));
66
+ }
67
+ // Fuzzy highlight
68
+ const segments = item.matches ? highlightMatches(plugin.name, item.matches) : null;
69
+ const displayName = segments ? segments.map((seg) => seg.text).join("") : plugin.name;
70
+ if (plugin.isOrphaned) {
71
+ const ver = plugin.installedVersion && plugin.installedVersion !== "0.0.0"
72
+ ? ` v${plugin.installedVersion}`
73
+ : "";
74
+ return (_jsxs("text", { children: [_jsx("span", { fg: theme.colors.danger, children: " \u25A0\u25A0\u25A0 " }), _jsx("span", { fg: theme.colors.muted, children: displayName }), ver ? _jsx("span", { fg: theme.colors.warning, children: ver }) : null, _jsx("span", { fg: theme.colors.danger, children: " deprecated" })] }));
75
+ }
76
+ return (_jsxs("text", { children: [_jsx("span", { children: " " }), _jsx(ScopeSquares, { user: hasUser, project: hasProject, local: hasLocal }), _jsx("span", { children: " " }), _jsx("span", { fg: hasAnyScope ? theme.colors.text : theme.colors.muted, children: displayName }), _jsx(MetaText, { text: versionStr, tone: plugin.hasUpdate ? "warning" : "muted" })] }));
77
+ }
78
+ function pluginDetail(item) {
79
+ const { plugin } = item;
80
+ const isInstalled = plugin.userScope?.enabled ||
81
+ plugin.projectScope?.enabled ||
82
+ plugin.localScope?.enabled;
83
+ // Orphaned/deprecated plugin
84
+ if (plugin.isOrphaned) {
85
+ return (_jsxs("box", { flexDirection: "column", children: [_jsx("box", { justifyContent: "center", children: _jsx("text", { bg: theme.colors.warning, fg: "black", children: _jsxs("strong", { children: [" ", plugin.name, " \u2014 DEPRECATED "] }) }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: theme.colors.warning, children: "This plugin is no longer in the marketplace." }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: theme.colors.muted, children: "It was removed from the marketplace but still referenced in your settings. Press d to uninstall and clean up." }) }), isInstalled ? (_jsx(ActionHints, { hints: [{ key: "d", label: "Uninstall (recommended)", tone: "danger" }] })) : null] }));
86
+ }
87
+ // Build component counts
88
+ const components = [];
89
+ if (plugin.agents?.length)
90
+ components.push(`${plugin.agents.length} agents`);
91
+ if (plugin.commands?.length)
92
+ components.push(`${plugin.commands.length} commands`);
93
+ if (plugin.skills?.length)
94
+ components.push(`${plugin.skills.length} skills`);
95
+ if (plugin.mcpServers?.length)
96
+ components.push(`${plugin.mcpServers.length} MCP`);
97
+ if (plugin.lspServers && Object.keys(plugin.lspServers).length) {
98
+ components.push(`${Object.keys(plugin.lspServers).length} LSP`);
99
+ }
100
+ const showVersion = plugin.version && plugin.version !== "0.0.0";
101
+ const showInstalledVersion = plugin.installedVersion && plugin.installedVersion !== "0.0.0";
102
+ return (_jsxs("box", { flexDirection: "column", children: [_jsx("box", { justifyContent: "center", children: _jsx("text", { bg: theme.selection.bg, fg: theme.selection.fg, children: _jsxs("strong", { children: [" ", plugin.name, plugin.hasUpdate ? " ⬆" : "", " "] }) }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: isInstalled ? theme.colors.success : theme.colors.muted, children: isInstalled ? "● Installed" : "○ Not installed" }) }), _jsx("box", { marginTop: 1, marginBottom: 1, children: _jsx("text", { fg: theme.colors.text, children: plugin.description }) }), showVersion ? (_jsx(KeyValueLine, { label: "Version", value: _jsxs("span", { children: [_jsxs("span", { fg: theme.colors.link, children: ["v", plugin.version] }), showInstalledVersion && plugin.installedVersion !== plugin.version ? (_jsxs("span", { children: [" (v", plugin.installedVersion, " installed)"] })) : null] }) })) : null, plugin.category ? (_jsx(KeyValueLine, { label: "Category", value: _jsx("span", { fg: theme.colors.accent, children: plugin.category }) })) : null, plugin.author ? (_jsx(KeyValueLine, { label: "Author", value: _jsx("span", { children: plugin.author.name }) })) : null, components.length > 0 ? (_jsx(KeyValueLine, { label: "Contains", value: _jsx("span", { fg: theme.colors.warning, children: components.join(" · ") }) })) : null, _jsxs(DetailSection, { children: [_jsx("text", { children: "─".repeat(24) }), _jsx("text", { children: _jsx("strong", { children: "Scopes:" }) }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { children: [_jsxs("span", { bg: theme.scopes.user, fg: "black", children: [" ", "u", " "] }), _jsx("span", { fg: plugin.userScope?.enabled ? theme.scopes.user : theme.colors.muted, children: plugin.userScope?.enabled ? " ● " : " ○ " }), _jsx("span", { fg: theme.scopes.user, children: "User" }), _jsx("span", { children: " global" }), plugin.userScope?.version ? (_jsxs("span", { fg: theme.scopes.user, children: [" v", plugin.userScope.version] })) : null] }), _jsxs("text", { children: [_jsxs("span", { bg: theme.scopes.project, fg: "black", children: [" ", "p", " "] }), _jsx("span", { fg: plugin.projectScope?.enabled ? theme.scopes.project : theme.colors.muted, children: plugin.projectScope?.enabled ? " ● " : " ○ " }), _jsx("span", { fg: theme.scopes.project, children: "Project" }), _jsx("span", { children: " team" }), plugin.projectScope?.version ? (_jsxs("span", { fg: theme.scopes.project, children: [" v", plugin.projectScope.version] })) : null] }), _jsxs("text", { children: [_jsxs("span", { bg: theme.scopes.local, fg: "black", children: [" ", "l", " "] }), _jsx("span", { fg: plugin.localScope?.enabled ? theme.scopes.local : theme.colors.muted, children: plugin.localScope?.enabled ? " ● " : " ○ " }), _jsx("span", { fg: theme.scopes.local, children: "Local" }), _jsx("span", { children: " private" }), plugin.localScope?.version ? (_jsxs("span", { fg: theme.scopes.local, children: [" v", plugin.localScope.version] })) : null] })] })] }), isInstalled && plugin.hasUpdate ? (_jsx(ActionHints, { hints: [{ key: "U", label: `Update to v${plugin.version}`, tone: "primary" }] })) : null] }));
103
+ }
104
+ // ─── Public dispatch functions ────────────────────────────────────────────────
105
+ /**
106
+ * Render a plugin browser list row by item kind.
107
+ */
108
+ export function renderPluginRow(item, _index, isSelected) {
109
+ if (item.kind === "category") {
110
+ return categoryRow(item, isSelected);
111
+ }
112
+ return pluginRow(item, isSelected);
113
+ }
114
+ /**
115
+ * Render the detail panel for a plugin browser item.
116
+ */
117
+ export function renderPluginDetail(item, collapsedMarketplaces) {
118
+ if (!item)
119
+ return _jsx("text", { fg: theme.colors.muted, children: "Select an item" });
120
+ if (item.kind === "category") {
121
+ return categoryDetail(item, collapsedMarketplaces);
122
+ }
123
+ return pluginDetail(item);
124
+ }
@@ -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
+ }