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
@@ -8,7 +8,9 @@ import { useKeyboard } from "../hooks/useKeyboard.js";
8
8
  import { ScreenLayout } from "../components/layout/index.js";
9
9
  import { ScrollableList } from "../components/ScrollableList.js";
10
10
  import { cliTools } from "../../data/cli-tools.js";
11
+ import { renderCliToolRow, renderCliToolDetail, } from "../renderers/cliToolRenderers.js";
11
12
  const execAsync = promisify(exec);
13
+ // ─── Version helpers ───────────────────────────────────────────────────────────
12
14
  // Session-level cache
13
15
  let cachedToolStatuses = null;
14
16
  let cacheInitialized = false;
@@ -53,10 +55,7 @@ async function getLatestNpmVersion(packageName) {
53
55
  }
54
56
  async function getLatestPipVersion(packageName) {
55
57
  try {
56
- const { stdout } = await execAsync(`pip index versions ${packageName} 2>/dev/null | head -1`, {
57
- timeout: 10000,
58
- shell: "/bin/bash",
59
- });
58
+ const { stdout } = await execAsync(`pip index versions ${packageName} 2>/dev/null | head -1`, { timeout: 10000, shell: "/bin/bash" });
60
59
  const match = stdout.trim().match(/\(([^)]+)\)/);
61
60
  return match ? match[1] : undefined;
62
61
  }
@@ -65,12 +64,9 @@ async function getLatestPipVersion(packageName) {
65
64
  }
66
65
  }
67
66
  async function getLatestVersion(tool) {
68
- if (tool.packageManager === "npm") {
69
- return getLatestNpmVersion(tool.packageName);
70
- }
71
- else {
72
- return getLatestPipVersion(tool.packageName);
73
- }
67
+ return tool.packageManager === "npm"
68
+ ? getLatestNpmVersion(tool.packageName)
69
+ : getLatestPipVersion(tool.packageName);
74
70
  }
75
71
  function compareVersions(v1, v2) {
76
72
  const parts1 = v1.split(/[-.]/).map((p) => parseInt(p, 10) || 0);
@@ -85,6 +81,7 @@ function compareVersions(v1, v2) {
85
81
  }
86
82
  return 0;
87
83
  }
84
+ // ─── Component ─────────────────────────────────────────────────────────────────
88
85
  export function CliToolsScreen() {
89
86
  const { state, dispatch } = useApp();
90
87
  const { cliTools: cliToolsState } = state;
@@ -101,7 +98,6 @@ export function CliToolsScreen() {
101
98
  });
102
99
  const statusesRef = useRef(toolStatuses);
103
100
  statusesRef.current = toolStatuses;
104
- // Update single tool status
105
101
  const updateToolStatus = useCallback((index, updates) => {
106
102
  setToolStatuses((prev) => {
107
103
  const newStatuses = [...prev];
@@ -110,9 +106,7 @@ export function CliToolsScreen() {
110
106
  return newStatuses;
111
107
  });
112
108
  }, []);
113
- // Fetch all version info
114
109
  const fetchVersionInfo = useCallback(async () => {
115
- // Check installed versions
116
110
  for (let i = 0; i < cliTools.length; i++) {
117
111
  const tool = cliTools[i];
118
112
  getInstalledVersion(tool).then((version) => {
@@ -140,7 +134,6 @@ export function CliToolsScreen() {
140
134
  fetchVersionInfo();
141
135
  }
142
136
  }, [fetchVersionInfo, toolStatuses]);
143
- // Keyboard handling
144
137
  useKeyboard((event) => {
145
138
  if (state.isSearching || state.modal)
146
139
  return;
@@ -178,24 +171,15 @@ export function CliToolsScreen() {
178
171
  if (!status)
179
172
  return;
180
173
  const { tool, installed, hasUpdate } = status;
181
- const action = !installed
182
- ? "Installing"
183
- : hasUpdate
184
- ? "Updating"
185
- : "Reinstalling";
174
+ const action = !installed ? "Installing" : hasUpdate ? "Updating" : "Reinstalling";
186
175
  const viaHomebrew = installed
187
176
  ? await isInstalledViaHomebrew(tool.name)
188
177
  : false;
189
- let command;
190
- if (!installed) {
191
- command = tool.installCommand;
192
- }
193
- else if (viaHomebrew) {
194
- command = `brew upgrade ${tool.name}`;
195
- }
196
- else {
197
- command = tool.installCommand;
198
- }
178
+ const command = !installed
179
+ ? tool.installCommand
180
+ : viaHomebrew
181
+ ? `brew upgrade ${tool.name}`
182
+ : tool.installCommand;
199
183
  modal.loading(`${action} ${tool.displayName}...`);
200
184
  try {
201
185
  execSync(command, {
@@ -237,38 +221,10 @@ export function CliToolsScreen() {
237
221
  modal.hideModal();
238
222
  handleRefresh();
239
223
  };
240
- // Get selected item
241
224
  const selectedStatus = toolStatuses[cliToolsState.selectedIndex];
242
- const renderDetail = () => {
243
- if (!selectedStatus) {
244
- return (_jsx("box", { flexDirection: "column", alignItems: "center", justifyContent: "center", flexGrow: 1, children: _jsx("text", { fg: "gray", children: "Select a tool to see details" }) }));
245
- }
246
- const { tool, installed, installedVersion, latestVersion, hasUpdate, checking, } = selectedStatus;
247
- return (_jsxs("box", { flexDirection: "column", children: [_jsxs("box", { marginBottom: 1, children: [_jsx("text", { fg: "cyan", children: _jsxs("strong", { children: ["\u2699 ", tool.displayName] }) }), hasUpdate && _jsx("text", { fg: "yellow", children: " \u2B06" })] }), _jsx("text", { fg: "gray", children: tool.description }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("box", { children: [_jsx("text", { fg: "gray", children: "Status " }), !installed ? (_jsx("text", { fg: "gray", children: "\u25CB Not installed" })) : checking ? (_jsx("text", { fg: "green", children: "\u25CF Checking..." })) : hasUpdate ? (_jsx("text", { fg: "yellow", children: "\u25CF Update available" })) : (_jsx("text", { fg: "green", children: "\u25CF Up to date" }))] }), installedVersion && (_jsxs("box", { children: [_jsx("text", { fg: "gray", children: "Installed " }), _jsxs("text", { fg: "green", children: ["v", installedVersion] })] })), latestVersion && (_jsxs("box", { children: [_jsx("text", { fg: "gray", children: "Latest " }), _jsxs("text", { fg: "white", children: ["v", latestVersion] })] })), _jsxs("box", { children: [_jsx("text", { fg: "gray", children: "Website " }), _jsx("text", { fg: "#5c9aff", children: tool.website })] })] }), _jsx("box", { marginTop: 2, children: !installed ? (_jsxs("box", { children: [_jsxs("text", { bg: "green", fg: "black", children: [" ", "Enter", " "] }), _jsx("text", { fg: "gray", children: " Install" })] })) : hasUpdate ? (_jsxs("box", { children: [_jsxs("text", { bg: "yellow", fg: "black", children: [" ", "Enter", " "] }), _jsxs("text", { fg: "gray", children: [" Update to v", latestVersion] })] })) : (_jsxs("box", { children: [_jsxs("text", { bg: "gray", fg: "white", children: [" ", "Enter", " "] }), _jsx("text", { fg: "gray", children: " Reinstall" })] })) })] }));
248
- };
249
- const renderListItem = (status, _idx, isSelected) => {
250
- const { tool, installed, installedVersion, hasUpdate, checking } = status;
251
- let icon;
252
- let iconColor;
253
- if (!installed) {
254
- icon = "○";
255
- iconColor = "gray";
256
- }
257
- else if (hasUpdate) {
258
- icon = "⬆";
259
- iconColor = "yellow";
260
- }
261
- else {
262
- icon = "●";
263
- iconColor = "green";
264
- }
265
- const versionText = installedVersion ? `v${installedVersion}` : "";
266
- return isSelected ? (_jsxs("text", { bg: "magenta", fg: "white", children: [" ", icon, " ", tool.displayName, " ", versionText, checking ? "..." : "", " "] })) : (_jsxs("text", { children: [_jsx("span", { fg: iconColor, children: icon }), _jsxs("span", { fg: "white", children: [" ", tool.displayName] }), versionText && _jsxs("span", { fg: "green", children: [" ", versionText] }), checking && _jsx("span", { fg: "gray", children: "..." })] }));
267
- };
268
- // Calculate stats for status line
269
225
  const installedCount = toolStatuses.filter((s) => s.installed).length;
270
226
  const updateCount = toolStatuses.filter((s) => s.hasUpdate).length;
271
227
  const statusContent = (_jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Installed: " }), _jsxs("span", { fg: "cyan", children: [installedCount, "/", toolStatuses.length] }), updateCount > 0 && (_jsxs(_Fragment, { children: [_jsx("span", { fg: "gray", children: " \u2502 Updates: " }), _jsx("span", { fg: "yellow", children: updateCount })] }))] }));
272
- return (_jsx(ScreenLayout, { title: "claudeup CLI Tools", currentScreen: "cli-tools", statusLine: statusContent, footerHints: "\u2191\u2193:nav \u2502 Enter:install \u2502 a:update all \u2502 r:refresh", listPanel: _jsx(ScrollableList, { items: toolStatuses, selectedIndex: cliToolsState.selectedIndex, renderItem: renderListItem, maxHeight: dimensions.listPanelHeight }), detailPanel: renderDetail() }));
228
+ return (_jsx(ScreenLayout, { title: "claudeup CLI Tools", currentScreen: "cli-tools", statusLine: statusContent, footerHints: "\u2191\u2193:nav \u2502 Enter:install \u2502 a:update all \u2502 r:refresh", listPanel: _jsx(ScrollableList, { items: toolStatuses, selectedIndex: cliToolsState.selectedIndex, renderItem: renderCliToolRow, maxHeight: dimensions.listPanelHeight }), detailPanel: renderCliToolDetail(selectedStatus) }));
273
229
  }
274
230
  export default CliToolsScreen;
@@ -6,21 +6,19 @@ import { useDimensions } from "../state/DimensionsContext.js";
6
6
  import { useKeyboard } from "../hooks/useKeyboard.js";
7
7
  import { ScreenLayout } from "../components/layout/index.js";
8
8
  import { ScrollableList } from "../components/ScrollableList.js";
9
- import { cliTools, type CliTool } from "../../data/cli-tools.js";
9
+ import { cliTools } from "../../data/cli-tools.js";
10
+ import {
11
+ renderCliToolRow,
12
+ renderCliToolDetail,
13
+ type CliToolStatus,
14
+ } from "../renderers/cliToolRenderers.js";
10
15
 
11
16
  const execAsync = promisify(exec);
12
17
 
13
- interface ToolStatus {
14
- tool: CliTool;
15
- installed: boolean;
16
- installedVersion?: string;
17
- latestVersion?: string;
18
- hasUpdate?: boolean;
19
- checking: boolean;
20
- }
18
+ // ─── Version helpers ───────────────────────────────────────────────────────────
21
19
 
22
20
  // Session-level cache
23
- let cachedToolStatuses: ToolStatus[] | null = null;
21
+ let cachedToolStatuses: CliToolStatus[] | null = null;
24
22
  let cacheInitialized = false;
25
23
 
26
24
  function clearCliToolsCache(): void {
@@ -46,7 +44,9 @@ async function isInstalledViaHomebrew(toolName: string): Promise<boolean> {
46
44
  }
47
45
  }
48
46
 
49
- async function getInstalledVersion(tool: CliTool): Promise<string | undefined> {
47
+ async function getInstalledVersion(
48
+ tool: import("../../data/cli-tools.js").CliTool,
49
+ ): Promise<string | undefined> {
50
50
  try {
51
51
  const { stdout } = await execAsync(tool.checkCommand, { timeout: 5000 });
52
52
  return parseVersion(stdout.trim());
@@ -55,9 +55,7 @@ async function getInstalledVersion(tool: CliTool): Promise<string | undefined> {
55
55
  }
56
56
  }
57
57
 
58
- async function getLatestNpmVersion(
59
- packageName: string,
60
- ): Promise<string | undefined> {
58
+ async function getLatestNpmVersion(packageName: string): Promise<string | undefined> {
61
59
  try {
62
60
  const { stdout } = await execAsync(
63
61
  `npm view ${packageName} version 2>/dev/null`,
@@ -69,16 +67,11 @@ async function getLatestNpmVersion(
69
67
  }
70
68
  }
71
69
 
72
- async function getLatestPipVersion(
73
- packageName: string,
74
- ): Promise<string | undefined> {
70
+ async function getLatestPipVersion(packageName: string): Promise<string | undefined> {
75
71
  try {
76
72
  const { stdout } = await execAsync(
77
73
  `pip index versions ${packageName} 2>/dev/null | head -1`,
78
- {
79
- timeout: 10000,
80
- shell: "/bin/bash",
81
- },
74
+ { timeout: 10000, shell: "/bin/bash" },
82
75
  );
83
76
  const match = stdout.trim().match(/\(([^)]+)\)/);
84
77
  return match ? match[1] : undefined;
@@ -87,18 +80,17 @@ async function getLatestPipVersion(
87
80
  }
88
81
  }
89
82
 
90
- async function getLatestVersion(tool: CliTool): Promise<string | undefined> {
91
- if (tool.packageManager === "npm") {
92
- return getLatestNpmVersion(tool.packageName);
93
- } else {
94
- return getLatestPipVersion(tool.packageName);
95
- }
83
+ async function getLatestVersion(
84
+ tool: import("../../data/cli-tools.js").CliTool,
85
+ ): Promise<string | undefined> {
86
+ return tool.packageManager === "npm"
87
+ ? getLatestNpmVersion(tool.packageName)
88
+ : getLatestPipVersion(tool.packageName);
96
89
  }
97
90
 
98
91
  function compareVersions(v1: string, v2: string): number {
99
92
  const parts1 = v1.split(/[-.]/).map((p) => parseInt(p, 10) || 0);
100
93
  const parts2 = v2.split(/[-.]/).map((p) => parseInt(p, 10) || 0);
101
-
102
94
  for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
103
95
  const p1 = parts1[i] || 0;
104
96
  const p2 = parts2[i] || 0;
@@ -108,13 +100,15 @@ function compareVersions(v1: string, v2: string): number {
108
100
  return 0;
109
101
  }
110
102
 
103
+ // ─── Component ─────────────────────────────────────────────────────────────────
104
+
111
105
  export function CliToolsScreen() {
112
106
  const { state, dispatch } = useApp();
113
107
  const { cliTools: cliToolsState } = state;
114
108
  const modal = useModal();
115
109
  const dimensions = useDimensions();
116
110
 
117
- const [toolStatuses, setToolStatuses] = useState<ToolStatus[]>(() => {
111
+ const [toolStatuses, setToolStatuses] = useState<CliToolStatus[]>(() => {
118
112
  return (
119
113
  cachedToolStatuses ||
120
114
  cliTools.map((tool) => ({
@@ -129,12 +123,11 @@ export function CliToolsScreen() {
129
123
  const statusesRef = useRef(toolStatuses);
130
124
  statusesRef.current = toolStatuses;
131
125
 
132
- // Update single tool status
133
126
  const updateToolStatus = useCallback(
134
- (index: number, updates: Partial<ToolStatus>) => {
127
+ (index: number, updates: Partial<CliToolStatus>) => {
135
128
  setToolStatuses((prev) => {
136
129
  const newStatuses = [...prev];
137
- newStatuses[index] = { ...newStatuses[index], ...updates };
130
+ newStatuses[index] = { ...newStatuses[index]!, ...updates };
138
131
  cachedToolStatuses = newStatuses;
139
132
  return newStatuses;
140
133
  });
@@ -142,11 +135,9 @@ export function CliToolsScreen() {
142
135
  [],
143
136
  );
144
137
 
145
- // Fetch all version info
146
138
  const fetchVersionInfo = useCallback(async () => {
147
- // Check installed versions
148
139
  for (let i = 0; i < cliTools.length; i++) {
149
- const tool = cliTools[i];
140
+ const tool = cliTools[i]!;
150
141
  getInstalledVersion(tool).then((version) => {
151
142
  updateToolStatus(i, {
152
143
  installedVersion: version,
@@ -155,7 +146,7 @@ export function CliToolsScreen() {
155
146
  });
156
147
 
157
148
  getLatestVersion(tool).then((latest) => {
158
- const current = statusesRef.current[i];
149
+ const current = statusesRef.current[i]!;
159
150
  updateToolStatus(i, {
160
151
  latestVersion: latest,
161
152
  checking: false,
@@ -176,7 +167,6 @@ export function CliToolsScreen() {
176
167
  }
177
168
  }, [fetchVersionInfo, toolStatuses]);
178
169
 
179
- // Keyboard handling
180
170
  useKeyboard((event) => {
181
171
  if (state.isSearching || state.modal) return;
182
172
 
@@ -217,27 +207,19 @@ export function CliToolsScreen() {
217
207
  if (!status) return;
218
208
 
219
209
  const { tool, installed, hasUpdate } = status;
220
- const action = !installed
221
- ? "Installing"
222
- : hasUpdate
223
- ? "Updating"
224
- : "Reinstalling";
210
+ const action = !installed ? "Installing" : hasUpdate ? "Updating" : "Reinstalling";
225
211
 
226
212
  const viaHomebrew = installed
227
213
  ? await isInstalledViaHomebrew(tool.name)
228
214
  : false;
229
215
 
230
- let command: string;
231
- if (!installed) {
232
- command = tool.installCommand;
233
- } else if (viaHomebrew) {
234
- command = `brew upgrade ${tool.name}`;
235
- } else {
236
- command = tool.installCommand;
237
- }
216
+ const command = !installed
217
+ ? tool.installCommand
218
+ : viaHomebrew
219
+ ? `brew upgrade ${tool.name}`
220
+ : tool.installCommand;
238
221
 
239
222
  modal.loading(`${action} ${tool.displayName}...`);
240
-
241
223
  try {
242
224
  execSync(command, {
243
225
  encoding: "utf-8",
@@ -259,11 +241,7 @@ export function CliToolsScreen() {
259
241
  const handleUpdateAll = async () => {
260
242
  const updatable = toolStatuses.filter((s) => s.hasUpdate);
261
243
  if (updatable.length === 0) {
262
- await modal.message(
263
- "Up to Date",
264
- "All tools are already up to date.",
265
- "info",
266
- );
244
+ await modal.message("Up to Date", "All tools are already up to date.", "info");
267
245
  return;
268
246
  }
269
247
 
@@ -274,7 +252,6 @@ export function CliToolsScreen() {
274
252
  const command = viaHomebrew
275
253
  ? `brew upgrade ${status.tool.name}`
276
254
  : status.tool.installCommand;
277
-
278
255
  try {
279
256
  execSync(command, {
280
257
  encoding: "utf-8",
@@ -290,145 +267,8 @@ export function CliToolsScreen() {
290
267
  handleRefresh();
291
268
  };
292
269
 
293
- // Get selected item
294
270
  const selectedStatus = toolStatuses[cliToolsState.selectedIndex];
295
271
 
296
- const renderDetail = () => {
297
- if (!selectedStatus) {
298
- return (
299
- <box
300
- flexDirection="column"
301
- alignItems="center"
302
- justifyContent="center"
303
- flexGrow={1}
304
- >
305
- <text fg="gray">Select a tool to see details</text>
306
- </box>
307
- );
308
- }
309
-
310
- const {
311
- tool,
312
- installed,
313
- installedVersion,
314
- latestVersion,
315
- hasUpdate,
316
- checking,
317
- } = selectedStatus;
318
-
319
- return (
320
- <box flexDirection="column">
321
- <box marginBottom={1}>
322
- <text fg="cyan">
323
- <strong>⚙ {tool.displayName}</strong>
324
- </text>
325
- {hasUpdate && <text fg="yellow"> ⬆</text>}
326
- </box>
327
-
328
- <text fg="gray">{tool.description}</text>
329
-
330
- <box marginTop={1} flexDirection="column">
331
- <box>
332
- <text fg="gray">Status </text>
333
- {!installed ? (
334
- <text fg="gray">○ Not installed</text>
335
- ) : checking ? (
336
- <text fg="green">● Checking...</text>
337
- ) : hasUpdate ? (
338
- <text fg="yellow">● Update available</text>
339
- ) : (
340
- <text fg="green">● Up to date</text>
341
- )}
342
- </box>
343
- {installedVersion && (
344
- <box>
345
- <text fg="gray">Installed </text>
346
- <text fg="green">v{installedVersion}</text>
347
- </box>
348
- )}
349
- {latestVersion && (
350
- <box>
351
- <text fg="gray">Latest </text>
352
- <text fg="white">v{latestVersion}</text>
353
- </box>
354
- )}
355
- <box>
356
- <text fg="gray">Website </text>
357
- <text fg="#5c9aff">{tool.website}</text>
358
- </box>
359
- </box>
360
-
361
- <box marginTop={2}>
362
- {!installed ? (
363
- <box>
364
- <text bg="green" fg="black">
365
- {" "}
366
- Enter{" "}
367
- </text>
368
- <text fg="gray"> Install</text>
369
- </box>
370
- ) : hasUpdate ? (
371
- <box>
372
- <text bg="yellow" fg="black">
373
- {" "}
374
- Enter{" "}
375
- </text>
376
- <text fg="gray"> Update to v{latestVersion}</text>
377
- </box>
378
- ) : (
379
- <box>
380
- <text bg="gray" fg="white">
381
- {" "}
382
- Enter{" "}
383
- </text>
384
- <text fg="gray"> Reinstall</text>
385
- </box>
386
- )}
387
- </box>
388
- </box>
389
- );
390
- };
391
-
392
- const renderListItem = (
393
- status: ToolStatus,
394
- _idx: number,
395
- isSelected: boolean,
396
- ) => {
397
- const { tool, installed, installedVersion, hasUpdate, checking } = status;
398
-
399
- let icon: string;
400
- let iconColor: string;
401
-
402
- if (!installed) {
403
- icon = "○";
404
- iconColor = "gray";
405
- } else if (hasUpdate) {
406
- icon = "⬆";
407
- iconColor = "yellow";
408
- } else {
409
- icon = "●";
410
- iconColor = "green";
411
- }
412
-
413
- const versionText = installedVersion ? `v${installedVersion}` : "";
414
-
415
- return isSelected ? (
416
- <text bg="magenta" fg="white">
417
- {" "}
418
- {icon} {tool.displayName} {versionText}
419
- {checking ? "..." : ""}{" "}
420
- </text>
421
- ) : (
422
- <text>
423
- <span fg={iconColor}>{icon}</span>
424
- <span fg="white"> {tool.displayName}</span>
425
- {versionText && <span fg="green"> {versionText}</span>}
426
- {checking && <span fg="gray">...</span>}
427
- </text>
428
- );
429
- };
430
-
431
- // Calculate stats for status line
432
272
  const installedCount = toolStatuses.filter((s) => s.installed).length;
433
273
  const updateCount = toolStatuses.filter((s) => s.hasUpdate).length;
434
274
  const statusContent = (
@@ -456,11 +296,11 @@ export function CliToolsScreen() {
456
296
  <ScrollableList
457
297
  items={toolStatuses}
458
298
  selectedIndex={cliToolsState.selectedIndex}
459
- renderItem={renderListItem}
299
+ renderItem={renderCliToolRow}
460
300
  maxHeight={dimensions.listPanelHeight}
461
301
  />
462
302
  }
463
- detailPanel={renderDetail()}
303
+ detailPanel={renderCliToolDetail(selectedStatus)}
464
304
  />
465
305
  );
466
306
  }