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.
- package/package.json +1 -1
- 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 +32 -97
- package/src/ui/screens/ProfilesScreen.tsx +58 -328
- package/src/ui/screens/SkillsScreen.js +16 -16
- package/src/ui/screens/SkillsScreen.tsx +20 -23
package/package.json
CHANGED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// Virtual marketplace name for the community sub-section of claude-plugins-official
|
|
2
|
+
export const COMMUNITY_VIRTUAL_MARKETPLACE = "claude-plugins-official:community";
|
|
3
|
+
// The marketplace that gets split into Anthropic Official + Community sections
|
|
4
|
+
export const SPLIT_MARKETPLACE = "claude-plugins-official";
|
|
5
|
+
/**
|
|
6
|
+
* Derives tone and badge for a category item based on marketplace identity.
|
|
7
|
+
*/
|
|
8
|
+
function categoryStyling(mp, isCommunitySection, marketplaceEnabled) {
|
|
9
|
+
if (!marketplaceEnabled) {
|
|
10
|
+
return { tone: "gray" };
|
|
11
|
+
}
|
|
12
|
+
if (isCommunitySection) {
|
|
13
|
+
return { tone: "gray", badge: "3rd Party" };
|
|
14
|
+
}
|
|
15
|
+
if (mp.name === "claude-plugins-official") {
|
|
16
|
+
return { tone: "yellow", badge: "★ Official" };
|
|
17
|
+
}
|
|
18
|
+
if (mp.name === "claude-code-plugins") {
|
|
19
|
+
return { tone: "red", badge: "⚠ Deprecated" };
|
|
20
|
+
}
|
|
21
|
+
if (mp.official) {
|
|
22
|
+
return { tone: "yellow", badge: "★ Official" };
|
|
23
|
+
}
|
|
24
|
+
return { tone: "green", badge: "✓ Added" };
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Builds the flat list of items for the PluginsScreen list panel.
|
|
28
|
+
* Extracted from PluginsScreen so it can be tested independently.
|
|
29
|
+
*/
|
|
30
|
+
export function buildPluginBrowserItems({ marketplaces, plugins, collapsedMarketplaces, }) {
|
|
31
|
+
const pluginsByMarketplace = new Map();
|
|
32
|
+
for (const plugin of plugins) {
|
|
33
|
+
const existing = pluginsByMarketplace.get(plugin.marketplace) || [];
|
|
34
|
+
existing.push(plugin);
|
|
35
|
+
pluginsByMarketplace.set(plugin.marketplace, existing);
|
|
36
|
+
}
|
|
37
|
+
// Sort marketplaces: deprecated ones go to the bottom
|
|
38
|
+
const sortedMarketplaces = [...marketplaces].sort((a, b) => {
|
|
39
|
+
const aDeprecated = a.name === "claude-code-plugins" ? 1 : 0;
|
|
40
|
+
const bDeprecated = b.name === "claude-code-plugins" ? 1 : 0;
|
|
41
|
+
return aDeprecated - bDeprecated;
|
|
42
|
+
});
|
|
43
|
+
const items = [];
|
|
44
|
+
for (const marketplace of sortedMarketplaces) {
|
|
45
|
+
const marketplacePlugins = pluginsByMarketplace.get(marketplace.name) || [];
|
|
46
|
+
const isCollapsed = collapsedMarketplaces.has(marketplace.name);
|
|
47
|
+
const isEnabled = marketplacePlugins.length > 0 || !!marketplace.official;
|
|
48
|
+
const hasPlugins = marketplacePlugins.length > 0;
|
|
49
|
+
// Special handling: split claude-plugins-official into two sub-sections
|
|
50
|
+
if (marketplace.name === SPLIT_MARKETPLACE && hasPlugins) {
|
|
51
|
+
const anthropicPlugins = marketplacePlugins.filter((p) => p.author?.name?.toLowerCase() === "anthropic");
|
|
52
|
+
const communityPlugins = marketplacePlugins.filter((p) => p.author?.name?.toLowerCase() !== "anthropic");
|
|
53
|
+
// Sub-section 1: Anthropic Official (plugins by Anthropic)
|
|
54
|
+
const anthropicCollapsed = collapsedMarketplaces.has(marketplace.name);
|
|
55
|
+
const anthropicHasPlugins = anthropicPlugins.length > 0;
|
|
56
|
+
const anthropicStyle = categoryStyling(marketplace, false, isEnabled);
|
|
57
|
+
items.push({
|
|
58
|
+
id: `mp:${marketplace.name}`,
|
|
59
|
+
kind: "category",
|
|
60
|
+
label: marketplace.displayName,
|
|
61
|
+
marketplace,
|
|
62
|
+
marketplaceEnabled: isEnabled,
|
|
63
|
+
pluginCount: anthropicPlugins.length,
|
|
64
|
+
isExpanded: !anthropicCollapsed && anthropicHasPlugins,
|
|
65
|
+
tone: anthropicStyle.tone,
|
|
66
|
+
badge: anthropicStyle.badge,
|
|
67
|
+
});
|
|
68
|
+
if (isEnabled && anthropicHasPlugins && !anthropicCollapsed) {
|
|
69
|
+
for (const plugin of anthropicPlugins) {
|
|
70
|
+
items.push({
|
|
71
|
+
id: `pl:${plugin.id}`,
|
|
72
|
+
kind: "plugin",
|
|
73
|
+
label: plugin.name,
|
|
74
|
+
plugin,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Sub-section 2: Community (third-party plugins in same marketplace)
|
|
79
|
+
if (communityPlugins.length > 0) {
|
|
80
|
+
const communityVirtualMp = {
|
|
81
|
+
name: COMMUNITY_VIRTUAL_MARKETPLACE,
|
|
82
|
+
displayName: "Anthropic Official — 3rd Party",
|
|
83
|
+
source: marketplace.source,
|
|
84
|
+
description: "Third-party plugins in the Anthropic Official marketplace",
|
|
85
|
+
};
|
|
86
|
+
const communityCollapsed = collapsedMarketplaces.has(COMMUNITY_VIRTUAL_MARKETPLACE);
|
|
87
|
+
const communityStyle = categoryStyling(communityVirtualMp, true, true);
|
|
88
|
+
items.push({
|
|
89
|
+
id: `mp:${COMMUNITY_VIRTUAL_MARKETPLACE}`,
|
|
90
|
+
kind: "category",
|
|
91
|
+
label: "Anthropic Official — 3rd Party",
|
|
92
|
+
marketplace: communityVirtualMp,
|
|
93
|
+
marketplaceEnabled: true,
|
|
94
|
+
pluginCount: communityPlugins.length,
|
|
95
|
+
isExpanded: !communityCollapsed,
|
|
96
|
+
isCommunitySection: true,
|
|
97
|
+
tone: communityStyle.tone,
|
|
98
|
+
badge: communityStyle.badge,
|
|
99
|
+
});
|
|
100
|
+
if (!communityCollapsed) {
|
|
101
|
+
for (const plugin of communityPlugins) {
|
|
102
|
+
items.push({
|
|
103
|
+
id: `pl:${plugin.id}`,
|
|
104
|
+
kind: "plugin",
|
|
105
|
+
label: plugin.name,
|
|
106
|
+
plugin,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
// Category header (marketplace)
|
|
114
|
+
const style = categoryStyling(marketplace, false, isEnabled);
|
|
115
|
+
items.push({
|
|
116
|
+
id: `mp:${marketplace.name}`,
|
|
117
|
+
kind: "category",
|
|
118
|
+
label: marketplace.displayName,
|
|
119
|
+
marketplace,
|
|
120
|
+
marketplaceEnabled: isEnabled,
|
|
121
|
+
pluginCount: marketplacePlugins.length,
|
|
122
|
+
isExpanded: !isCollapsed && hasPlugins,
|
|
123
|
+
tone: style.tone,
|
|
124
|
+
badge: style.badge,
|
|
125
|
+
});
|
|
126
|
+
// Plugins under this marketplace (if expanded)
|
|
127
|
+
if (isEnabled && hasPlugins && !isCollapsed) {
|
|
128
|
+
for (const plugin of marketplacePlugins) {
|
|
129
|
+
items.push({
|
|
130
|
+
id: `pl:${plugin.id}`,
|
|
131
|
+
kind: "plugin",
|
|
132
|
+
label: plugin.name,
|
|
133
|
+
plugin,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return items;
|
|
139
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type { Marketplace } from "../../types/index.js";
|
|
2
|
+
import type { PluginInfo } from "../../services/plugin-manager.js";
|
|
3
|
+
|
|
4
|
+
// Virtual marketplace name for the community sub-section of claude-plugins-official
|
|
5
|
+
export const COMMUNITY_VIRTUAL_MARKETPLACE = "claude-plugins-official:community";
|
|
6
|
+
// The marketplace that gets split into Anthropic Official + Community sections
|
|
7
|
+
export const SPLIT_MARKETPLACE = "claude-plugins-official";
|
|
8
|
+
|
|
9
|
+
// ─── Item types ───────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
export interface PluginCategoryItem {
|
|
12
|
+
id: string;
|
|
13
|
+
kind: "category";
|
|
14
|
+
label: string;
|
|
15
|
+
marketplace: Marketplace;
|
|
16
|
+
marketplaceEnabled: boolean;
|
|
17
|
+
pluginCount: number;
|
|
18
|
+
isExpanded: boolean;
|
|
19
|
+
isCommunitySection?: boolean;
|
|
20
|
+
/** Visual tone for the category row */
|
|
21
|
+
tone: "yellow" | "gray" | "green" | "red" | "purple" | "teal";
|
|
22
|
+
/** Badge text shown on category row (e.g. "★ Official") */
|
|
23
|
+
badge?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface PluginPluginItem {
|
|
27
|
+
id: string;
|
|
28
|
+
kind: "plugin";
|
|
29
|
+
label: string;
|
|
30
|
+
plugin: PluginInfo;
|
|
31
|
+
/** Fuzzy match highlight indices, if search is active */
|
|
32
|
+
matches?: number[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type PluginBrowserItem = PluginCategoryItem | PluginPluginItem;
|
|
36
|
+
|
|
37
|
+
// ─── Adapter ─────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
export interface BuildPluginBrowserItemsArgs {
|
|
40
|
+
marketplaces: Marketplace[];
|
|
41
|
+
plugins: PluginInfo[];
|
|
42
|
+
collapsedMarketplaces: Set<string>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Derives tone and badge for a category item based on marketplace identity.
|
|
47
|
+
*/
|
|
48
|
+
function categoryStyling(
|
|
49
|
+
mp: Marketplace,
|
|
50
|
+
isCommunitySection: boolean,
|
|
51
|
+
marketplaceEnabled: boolean,
|
|
52
|
+
): { tone: PluginCategoryItem["tone"]; badge?: string } {
|
|
53
|
+
if (!marketplaceEnabled) {
|
|
54
|
+
return { tone: "gray" };
|
|
55
|
+
}
|
|
56
|
+
if (isCommunitySection) {
|
|
57
|
+
return { tone: "gray", badge: "3rd Party" };
|
|
58
|
+
}
|
|
59
|
+
if (mp.name === "claude-plugins-official") {
|
|
60
|
+
return { tone: "yellow", badge: "★ Official" };
|
|
61
|
+
}
|
|
62
|
+
if (mp.name === "claude-code-plugins") {
|
|
63
|
+
return { tone: "red", badge: "⚠ Deprecated" };
|
|
64
|
+
}
|
|
65
|
+
if (mp.official) {
|
|
66
|
+
return { tone: "yellow", badge: "★ Official" };
|
|
67
|
+
}
|
|
68
|
+
return { tone: "green", badge: "✓ Added" };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Builds the flat list of items for the PluginsScreen list panel.
|
|
73
|
+
* Extracted from PluginsScreen so it can be tested independently.
|
|
74
|
+
*/
|
|
75
|
+
export function buildPluginBrowserItems({
|
|
76
|
+
marketplaces,
|
|
77
|
+
plugins,
|
|
78
|
+
collapsedMarketplaces,
|
|
79
|
+
}: BuildPluginBrowserItemsArgs): PluginBrowserItem[] {
|
|
80
|
+
const pluginsByMarketplace = new Map<string, PluginInfo[]>();
|
|
81
|
+
for (const plugin of plugins) {
|
|
82
|
+
const existing = pluginsByMarketplace.get(plugin.marketplace) || [];
|
|
83
|
+
existing.push(plugin);
|
|
84
|
+
pluginsByMarketplace.set(plugin.marketplace, existing);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Sort marketplaces: deprecated ones go to the bottom
|
|
88
|
+
const sortedMarketplaces = [...marketplaces].sort((a, b) => {
|
|
89
|
+
const aDeprecated = a.name === "claude-code-plugins" ? 1 : 0;
|
|
90
|
+
const bDeprecated = b.name === "claude-code-plugins" ? 1 : 0;
|
|
91
|
+
return aDeprecated - bDeprecated;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const items: PluginBrowserItem[] = [];
|
|
95
|
+
|
|
96
|
+
for (const marketplace of sortedMarketplaces) {
|
|
97
|
+
const marketplacePlugins = pluginsByMarketplace.get(marketplace.name) || [];
|
|
98
|
+
const isCollapsed = collapsedMarketplaces.has(marketplace.name);
|
|
99
|
+
const isEnabled = marketplacePlugins.length > 0 || !!marketplace.official;
|
|
100
|
+
const hasPlugins = marketplacePlugins.length > 0;
|
|
101
|
+
|
|
102
|
+
// Special handling: split claude-plugins-official into two sub-sections
|
|
103
|
+
if (marketplace.name === SPLIT_MARKETPLACE && hasPlugins) {
|
|
104
|
+
const anthropicPlugins = marketplacePlugins.filter(
|
|
105
|
+
(p) => p.author?.name?.toLowerCase() === "anthropic",
|
|
106
|
+
);
|
|
107
|
+
const communityPlugins = marketplacePlugins.filter(
|
|
108
|
+
(p) => p.author?.name?.toLowerCase() !== "anthropic",
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
// Sub-section 1: Anthropic Official (plugins by Anthropic)
|
|
112
|
+
const anthropicCollapsed = collapsedMarketplaces.has(marketplace.name);
|
|
113
|
+
const anthropicHasPlugins = anthropicPlugins.length > 0;
|
|
114
|
+
const anthropicStyle = categoryStyling(marketplace, false, isEnabled);
|
|
115
|
+
items.push({
|
|
116
|
+
id: `mp:${marketplace.name}`,
|
|
117
|
+
kind: "category",
|
|
118
|
+
label: marketplace.displayName,
|
|
119
|
+
marketplace,
|
|
120
|
+
marketplaceEnabled: isEnabled,
|
|
121
|
+
pluginCount: anthropicPlugins.length,
|
|
122
|
+
isExpanded: !anthropicCollapsed && anthropicHasPlugins,
|
|
123
|
+
tone: anthropicStyle.tone,
|
|
124
|
+
badge: anthropicStyle.badge,
|
|
125
|
+
});
|
|
126
|
+
if (isEnabled && anthropicHasPlugins && !anthropicCollapsed) {
|
|
127
|
+
for (const plugin of anthropicPlugins) {
|
|
128
|
+
items.push({
|
|
129
|
+
id: `pl:${plugin.id}`,
|
|
130
|
+
kind: "plugin",
|
|
131
|
+
label: plugin.name,
|
|
132
|
+
plugin,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Sub-section 2: Community (third-party plugins in same marketplace)
|
|
138
|
+
if (communityPlugins.length > 0) {
|
|
139
|
+
const communityVirtualMp: Marketplace = {
|
|
140
|
+
name: COMMUNITY_VIRTUAL_MARKETPLACE,
|
|
141
|
+
displayName: "Anthropic Official — 3rd Party",
|
|
142
|
+
source: marketplace.source,
|
|
143
|
+
description: "Third-party plugins in the Anthropic Official marketplace",
|
|
144
|
+
};
|
|
145
|
+
const communityCollapsed = collapsedMarketplaces.has(COMMUNITY_VIRTUAL_MARKETPLACE);
|
|
146
|
+
const communityStyle = categoryStyling(communityVirtualMp, true, true);
|
|
147
|
+
items.push({
|
|
148
|
+
id: `mp:${COMMUNITY_VIRTUAL_MARKETPLACE}`,
|
|
149
|
+
kind: "category",
|
|
150
|
+
label: "Anthropic Official — 3rd Party",
|
|
151
|
+
marketplace: communityVirtualMp,
|
|
152
|
+
marketplaceEnabled: true,
|
|
153
|
+
pluginCount: communityPlugins.length,
|
|
154
|
+
isExpanded: !communityCollapsed,
|
|
155
|
+
isCommunitySection: true,
|
|
156
|
+
tone: communityStyle.tone,
|
|
157
|
+
badge: communityStyle.badge,
|
|
158
|
+
});
|
|
159
|
+
if (!communityCollapsed) {
|
|
160
|
+
for (const plugin of communityPlugins) {
|
|
161
|
+
items.push({
|
|
162
|
+
id: `pl:${plugin.id}`,
|
|
163
|
+
kind: "plugin",
|
|
164
|
+
label: plugin.name,
|
|
165
|
+
plugin,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Category header (marketplace)
|
|
175
|
+
const style = categoryStyling(marketplace, false, isEnabled);
|
|
176
|
+
items.push({
|
|
177
|
+
id: `mp:${marketplace.name}`,
|
|
178
|
+
kind: "category",
|
|
179
|
+
label: marketplace.displayName,
|
|
180
|
+
marketplace,
|
|
181
|
+
marketplaceEnabled: isEnabled,
|
|
182
|
+
pluginCount: marketplacePlugins.length,
|
|
183
|
+
isExpanded: !isCollapsed && hasPlugins,
|
|
184
|
+
tone: style.tone,
|
|
185
|
+
badge: style.badge,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Plugins under this marketplace (if expanded)
|
|
189
|
+
if (isEnabled && hasPlugins && !isCollapsed) {
|
|
190
|
+
for (const plugin of marketplacePlugins) {
|
|
191
|
+
items.push({
|
|
192
|
+
id: `pl:${plugin.id}`,
|
|
193
|
+
kind: "plugin",
|
|
194
|
+
label: plugin.name,
|
|
195
|
+
plugin,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return items;
|
|
202
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { SETTINGS_CATALOG, } from "../../data/settings-catalog.js";
|
|
2
|
+
// ─── Constants ─────────────────────────────────────────────────────────────────
|
|
3
|
+
export const CATEGORY_LABELS = {
|
|
4
|
+
recommended: "Recommended",
|
|
5
|
+
agents: "Agents & Teams",
|
|
6
|
+
models: "Models & Thinking",
|
|
7
|
+
workflow: "Workflow",
|
|
8
|
+
terminal: "Terminal & UI",
|
|
9
|
+
performance: "Performance",
|
|
10
|
+
advanced: "Advanced",
|
|
11
|
+
};
|
|
12
|
+
export const CATEGORY_ORDER = [
|
|
13
|
+
"recommended",
|
|
14
|
+
"agents",
|
|
15
|
+
"models",
|
|
16
|
+
"workflow",
|
|
17
|
+
"terminal",
|
|
18
|
+
"performance",
|
|
19
|
+
"advanced",
|
|
20
|
+
];
|
|
21
|
+
export const CATEGORY_BG = {
|
|
22
|
+
recommended: "#2e7d32",
|
|
23
|
+
agents: "#00838f",
|
|
24
|
+
models: "#4527a0",
|
|
25
|
+
workflow: "#1565c0",
|
|
26
|
+
terminal: "#4e342e",
|
|
27
|
+
performance: "#6a1b9a",
|
|
28
|
+
advanced: "#e65100",
|
|
29
|
+
};
|
|
30
|
+
export const CATEGORY_COLOR = {
|
|
31
|
+
recommended: "green",
|
|
32
|
+
agents: "cyan",
|
|
33
|
+
models: "cyan",
|
|
34
|
+
workflow: "blue",
|
|
35
|
+
terminal: "blue",
|
|
36
|
+
performance: "magentaBright",
|
|
37
|
+
advanced: "yellow",
|
|
38
|
+
};
|
|
39
|
+
export const CATEGORY_DESCRIPTIONS = {
|
|
40
|
+
recommended: "Most impactful settings every user should know.",
|
|
41
|
+
agents: "Agent teams, task lists, and subagent configuration.",
|
|
42
|
+
models: "Model selection, extended thinking, and effort.",
|
|
43
|
+
workflow: "Git, plans, permissions, output style, and languages.",
|
|
44
|
+
terminal: "Shell, spinners, progress bars, voice, and UI behavior.",
|
|
45
|
+
performance: "Compaction, token limits, timeouts, and caching.",
|
|
46
|
+
advanced: "Telemetry, updates, debugging, and internal controls.",
|
|
47
|
+
};
|
|
48
|
+
// ─── Helpers ───────────────────────────────────────────────────────────────────
|
|
49
|
+
/** Get the effective value (project overrides user) */
|
|
50
|
+
export function getEffectiveValue(scoped) {
|
|
51
|
+
return scoped.project !== undefined ? scoped.project : scoped.user;
|
|
52
|
+
}
|
|
53
|
+
export function formatValue(setting, value) {
|
|
54
|
+
if (value === undefined || value === "") {
|
|
55
|
+
if (setting.defaultValue !== undefined) {
|
|
56
|
+
return setting.type === "boolean"
|
|
57
|
+
? setting.defaultValue === "true"
|
|
58
|
+
? "on"
|
|
59
|
+
: "off"
|
|
60
|
+
: setting.defaultValue || "default";
|
|
61
|
+
}
|
|
62
|
+
return "—";
|
|
63
|
+
}
|
|
64
|
+
if (setting.type === "boolean") {
|
|
65
|
+
return value === "true" || value === "1" ? "on" : "off";
|
|
66
|
+
}
|
|
67
|
+
if (setting.type === "select" && setting.options) {
|
|
68
|
+
const opt = setting.options.find((o) => o.value === value);
|
|
69
|
+
return opt ? opt.label : value;
|
|
70
|
+
}
|
|
71
|
+
if (value.length > 20) {
|
|
72
|
+
return value.slice(0, 20) + "...";
|
|
73
|
+
}
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
// ─── Adapter ───────────────────────────────────────────────────────────────────
|
|
77
|
+
/**
|
|
78
|
+
* Builds the flat list of items for the SettingsScreen list panel.
|
|
79
|
+
* Extracted from SettingsScreen so it can be tested independently.
|
|
80
|
+
*/
|
|
81
|
+
export function buildSettingsBrowserItems(values) {
|
|
82
|
+
const items = [];
|
|
83
|
+
for (const category of CATEGORY_ORDER) {
|
|
84
|
+
items.push({
|
|
85
|
+
id: `cat:${category}`,
|
|
86
|
+
kind: "category",
|
|
87
|
+
label: CATEGORY_LABELS[category],
|
|
88
|
+
category,
|
|
89
|
+
isDefault: true,
|
|
90
|
+
});
|
|
91
|
+
const categorySettings = SETTINGS_CATALOG.filter((s) => s.category === category);
|
|
92
|
+
for (const setting of categorySettings) {
|
|
93
|
+
const scoped = values.get(setting.id) || {
|
|
94
|
+
user: undefined,
|
|
95
|
+
project: undefined,
|
|
96
|
+
};
|
|
97
|
+
const effective = getEffectiveValue(scoped);
|
|
98
|
+
items.push({
|
|
99
|
+
id: `setting:${setting.id}`,
|
|
100
|
+
kind: "setting",
|
|
101
|
+
label: setting.name,
|
|
102
|
+
category,
|
|
103
|
+
setting,
|
|
104
|
+
scopedValues: scoped,
|
|
105
|
+
effectiveValue: effective,
|
|
106
|
+
isDefault: effective === undefined || effective === "",
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return items;
|
|
111
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SETTINGS_CATALOG,
|
|
3
|
+
type SettingCategory,
|
|
4
|
+
type SettingDefinition,
|
|
5
|
+
} from "../../data/settings-catalog.js";
|
|
6
|
+
import type { ScopedSettingValues } from "../../services/settings-manager.js";
|
|
7
|
+
|
|
8
|
+
// ─── Item types ────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export interface SettingsCategoryItem {
|
|
11
|
+
id: string;
|
|
12
|
+
kind: "category";
|
|
13
|
+
label: string;
|
|
14
|
+
category: SettingCategory;
|
|
15
|
+
isDefault: true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SettingsSettingItem {
|
|
19
|
+
id: string;
|
|
20
|
+
kind: "setting";
|
|
21
|
+
label: string;
|
|
22
|
+
category: SettingCategory;
|
|
23
|
+
setting: SettingDefinition;
|
|
24
|
+
scopedValues: ScopedSettingValues;
|
|
25
|
+
effectiveValue: string | undefined;
|
|
26
|
+
isDefault: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type SettingsBrowserItem = SettingsCategoryItem | SettingsSettingItem;
|
|
30
|
+
|
|
31
|
+
// ─── Constants ─────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
export const CATEGORY_LABELS: Record<SettingCategory, string> = {
|
|
34
|
+
recommended: "Recommended",
|
|
35
|
+
agents: "Agents & Teams",
|
|
36
|
+
models: "Models & Thinking",
|
|
37
|
+
workflow: "Workflow",
|
|
38
|
+
terminal: "Terminal & UI",
|
|
39
|
+
performance: "Performance",
|
|
40
|
+
advanced: "Advanced",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const CATEGORY_ORDER: SettingCategory[] = [
|
|
44
|
+
"recommended",
|
|
45
|
+
"agents",
|
|
46
|
+
"models",
|
|
47
|
+
"workflow",
|
|
48
|
+
"terminal",
|
|
49
|
+
"performance",
|
|
50
|
+
"advanced",
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
export const CATEGORY_BG: Record<SettingCategory, string> = {
|
|
54
|
+
recommended: "#2e7d32",
|
|
55
|
+
agents: "#00838f",
|
|
56
|
+
models: "#4527a0",
|
|
57
|
+
workflow: "#1565c0",
|
|
58
|
+
terminal: "#4e342e",
|
|
59
|
+
performance: "#6a1b9a",
|
|
60
|
+
advanced: "#e65100",
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const CATEGORY_COLOR: Record<SettingCategory, string> = {
|
|
64
|
+
recommended: "green",
|
|
65
|
+
agents: "cyan",
|
|
66
|
+
models: "cyan",
|
|
67
|
+
workflow: "blue",
|
|
68
|
+
terminal: "blue",
|
|
69
|
+
performance: "magentaBright",
|
|
70
|
+
advanced: "yellow",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const CATEGORY_DESCRIPTIONS: Record<SettingCategory, string> = {
|
|
74
|
+
recommended: "Most impactful settings every user should know.",
|
|
75
|
+
agents: "Agent teams, task lists, and subagent configuration.",
|
|
76
|
+
models: "Model selection, extended thinking, and effort.",
|
|
77
|
+
workflow: "Git, plans, permissions, output style, and languages.",
|
|
78
|
+
terminal: "Shell, spinners, progress bars, voice, and UI behavior.",
|
|
79
|
+
performance: "Compaction, token limits, timeouts, and caching.",
|
|
80
|
+
advanced: "Telemetry, updates, debugging, and internal controls.",
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// ─── Helpers ───────────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
/** Get the effective value (project overrides user) */
|
|
86
|
+
export function getEffectiveValue(
|
|
87
|
+
scoped: ScopedSettingValues,
|
|
88
|
+
): string | undefined {
|
|
89
|
+
return scoped.project !== undefined ? scoped.project : scoped.user;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function formatValue(
|
|
93
|
+
setting: SettingDefinition,
|
|
94
|
+
value: string | undefined,
|
|
95
|
+
): string {
|
|
96
|
+
if (value === undefined || value === "") {
|
|
97
|
+
if (setting.defaultValue !== undefined) {
|
|
98
|
+
return setting.type === "boolean"
|
|
99
|
+
? setting.defaultValue === "true"
|
|
100
|
+
? "on"
|
|
101
|
+
: "off"
|
|
102
|
+
: setting.defaultValue || "default";
|
|
103
|
+
}
|
|
104
|
+
return "—";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (setting.type === "boolean") {
|
|
108
|
+
return value === "true" || value === "1" ? "on" : "off";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (setting.type === "select" && setting.options) {
|
|
112
|
+
const opt = setting.options.find((o) => o.value === value);
|
|
113
|
+
return opt ? opt.label : value;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (value.length > 20) {
|
|
117
|
+
return value.slice(0, 20) + "...";
|
|
118
|
+
}
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ─── Adapter ───────────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Builds the flat list of items for the SettingsScreen list panel.
|
|
126
|
+
* Extracted from SettingsScreen so it can be tested independently.
|
|
127
|
+
*/
|
|
128
|
+
export function buildSettingsBrowserItems(
|
|
129
|
+
values: Map<string, ScopedSettingValues>,
|
|
130
|
+
): SettingsBrowserItem[] {
|
|
131
|
+
const items: SettingsBrowserItem[] = [];
|
|
132
|
+
|
|
133
|
+
for (const category of CATEGORY_ORDER) {
|
|
134
|
+
items.push({
|
|
135
|
+
id: `cat:${category}`,
|
|
136
|
+
kind: "category",
|
|
137
|
+
label: CATEGORY_LABELS[category],
|
|
138
|
+
category,
|
|
139
|
+
isDefault: true,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const categorySettings = SETTINGS_CATALOG.filter(
|
|
143
|
+
(s) => s.category === category,
|
|
144
|
+
);
|
|
145
|
+
for (const setting of categorySettings) {
|
|
146
|
+
const scoped = values.get(setting.id) || {
|
|
147
|
+
user: undefined,
|
|
148
|
+
project: undefined,
|
|
149
|
+
};
|
|
150
|
+
const effective = getEffectiveValue(scoped);
|
|
151
|
+
items.push({
|
|
152
|
+
id: `setting:${setting.id}`,
|
|
153
|
+
kind: "setting",
|
|
154
|
+
label: setting.name,
|
|
155
|
+
category,
|
|
156
|
+
setting,
|
|
157
|
+
scopedValues: scoped,
|
|
158
|
+
effectiveValue: effective,
|
|
159
|
+
isDefault: effective === undefined || effective === "",
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return items;
|
|
165
|
+
}
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
-
import { useState, useEffect, useMemo } from
|
|
3
|
-
import { useKeyboardHandler } from
|
|
2
|
+
import { useState, useEffect, useMemo } from "react";
|
|
3
|
+
import { useKeyboardHandler } from "../hooks/useKeyboardHandler";
|
|
4
4
|
export function ScrollableList({ items, selectedIndex, renderItem, maxHeight, showScrollIndicators = true, onSelect, focused = false, }) {
|
|
5
5
|
const [scrollOffset, setScrollOffset] = useState(0);
|
|
6
6
|
// Handle keyboard navigation
|
|
7
7
|
useKeyboardHandler((input, key) => {
|
|
8
8
|
if (!focused || !onSelect)
|
|
9
9
|
return;
|
|
10
|
-
if (key.upArrow || input ===
|
|
10
|
+
if (key.upArrow || input === "k") {
|
|
11
11
|
const newIndex = Math.max(0, selectedIndex - 1);
|
|
12
12
|
onSelect(newIndex);
|
|
13
13
|
}
|
|
14
|
-
else if (key.downArrow || input ===
|
|
14
|
+
else if (key.downArrow || input === "j") {
|
|
15
15
|
const newIndex = Math.min(items.length - 1, selectedIndex + 1);
|
|
16
16
|
onSelect(newIndex);
|
|
17
17
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React, { useState, useEffect, useMemo } from
|
|
2
|
-
import { useKeyboardHandler } from
|
|
1
|
+
import React, { useState, useEffect, useMemo } from "react";
|
|
2
|
+
import { useKeyboardHandler } from "../hooks/useKeyboardHandler";
|
|
3
3
|
|
|
4
4
|
interface ScrollableListProps<T> {
|
|
5
5
|
/** Array of items to display */
|
|
@@ -33,10 +33,10 @@ export function ScrollableList<T>({
|
|
|
33
33
|
useKeyboardHandler((input, key) => {
|
|
34
34
|
if (!focused || !onSelect) return;
|
|
35
35
|
|
|
36
|
-
if (key.upArrow || input ===
|
|
36
|
+
if (key.upArrow || input === "k") {
|
|
37
37
|
const newIndex = Math.max(0, selectedIndex - 1);
|
|
38
38
|
onSelect(newIndex);
|
|
39
|
-
} else if (key.downArrow || input ===
|
|
39
|
+
} else if (key.downArrow || input === "j") {
|
|
40
40
|
const newIndex = Math.min(items.length - 1, selectedIndex + 1);
|
|
41
41
|
onSelect(newIndex);
|
|
42
42
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
-
import { useKeyboardHandler } from
|
|
3
|
-
export function SearchInput({ value, onChange, placeholder =
|
|
2
|
+
import { useKeyboardHandler } from "../hooks/useKeyboardHandler.js";
|
|
3
|
+
export function SearchInput({ value, onChange, placeholder = "Search...", isActive, onExit, onSubmit, }) {
|
|
4
4
|
// Handle keyboard shortcuts when active
|
|
5
5
|
useKeyboardHandler((_input, key) => {
|
|
6
6
|
if (!isActive)
|