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