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
@@ -1,5 +1,5 @@
1
- import React from 'react';
2
- import { useKeyboardHandler } from '../hooks/useKeyboardHandler.js';
1
+ import React from "react";
2
+ import { useKeyboardHandler } from "../hooks/useKeyboardHandler.js";
3
3
 
4
4
  interface SearchInputProps {
5
5
  /** Current search value */
@@ -19,7 +19,7 @@ interface SearchInputProps {
19
19
  export function SearchInput({
20
20
  value,
21
21
  onChange,
22
- placeholder = 'Search...',
22
+ placeholder = "Search...",
23
23
  isActive,
24
24
  onExit,
25
25
  onSubmit,
@@ -26,7 +26,7 @@ export function StyledText({ children, bold = false, color, bgColor, dim = false
26
26
  }
27
27
  if (inverse) {
28
28
  // Inverse effect approximated with contrasting colors
29
- content = _jsx("span", { fg: "black", bg: "white", children: content });
29
+ content = (_jsx("span", { fg: "black", bg: "white", children: content }));
30
30
  }
31
31
  // Apply colors (outer layer)
32
32
  const textProps = {};
@@ -56,7 +56,11 @@ export function StyledText({
56
56
  }
57
57
  if (inverse) {
58
58
  // Inverse effect approximated with contrasting colors
59
- content = <span fg="black" bg="white">{content}</span>;
59
+ content = (
60
+ <span fg="black" bg="white">
61
+ {content}
62
+ </span>
63
+ );
60
64
  }
61
65
 
62
66
  // Apply colors (outer layer)
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
- export function ProgressBar({ message, current, total, }) {
2
+ export function ProgressBar({ message, current, total }) {
3
3
  const isDeterminate = current !== undefined && total !== undefined && total > 0;
4
4
  if (isDeterminate) {
5
5
  const barWidth = 20;
@@ -9,11 +9,7 @@ interface ProgressBarProps {
9
9
  total?: number;
10
10
  }
11
11
 
12
- export function ProgressBar({
13
- message,
14
- current,
15
- total,
16
- }: ProgressBarProps) {
12
+ export function ProgressBar({ message, current, total }: ProgressBarProps) {
17
13
  const isDeterminate =
18
14
  current !== undefined && total !== undefined && total > 0;
19
15
 
@@ -52,12 +52,7 @@ export function InputModal({
52
52
  <text>{label}</text>
53
53
  </box>
54
54
 
55
- <input
56
- value={value}
57
- onChange={setValue}
58
- focused
59
- width={54}
60
- />
55
+ <input value={value} onChange={setValue} focused width={54} />
61
56
 
62
57
  <box marginTop={1}>
63
58
  <text fg="#666666">Enter to confirm • Escape to cancel</text>
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
2
  import { useState, useEffect } from "react";
3
3
  const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
4
- export function LoadingModal({ message, }) {
4
+ export function LoadingModal({ message }) {
5
5
  const [frame, setFrame] = useState(0);
6
6
  useEffect(() => {
7
7
  const interval = setInterval(() => {
@@ -7,9 +7,7 @@ interface LoadingModalProps {
7
7
 
8
8
  const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
9
9
 
10
- export function LoadingModal({
11
- message,
12
- }: LoadingModalProps) {
10
+ export function LoadingModal({ message }: LoadingModalProps) {
13
11
  const [frame, setFrame] = useState(0);
14
12
 
15
13
  useEffect(() => {
@@ -1,3 +1,3 @@
1
- export { useAsyncData, useDebouncedAsyncData } from './useAsyncData.js';
2
- export { useKeyboardHandler } from './useKeyboardHandler.js';
3
- export { useKeyboard } from './useKeyboard.js';
1
+ export { useAsyncData, useDebouncedAsyncData } from "./useAsyncData.js";
2
+ export { useKeyboardHandler } from "./useKeyboardHandler.js";
3
+ export { useKeyboard } from "./useKeyboard.js";
@@ -1,3 +1,3 @@
1
- export { useAsyncData, useDebouncedAsyncData } from './useAsyncData.js';
2
- export { useKeyboardHandler } from './useKeyboardHandler.js';
3
- export { useKeyboard } from './useKeyboard.js';
1
+ export { useAsyncData, useDebouncedAsyncData } from "./useAsyncData.js";
2
+ export { useKeyboardHandler } from "./useKeyboardHandler.js";
3
+ export { useKeyboard } from "./useKeyboard.js";
@@ -19,8 +19,6 @@ export interface KeyEvent {
19
19
  * - Arrow keys: "up", "down", "left", "right"
20
20
  * - Special: "enter", "escape", "tab", "backspace", "delete"
21
21
  */
22
- export function useKeyboard(
23
- handler: (event: KeyEvent) => void,
24
- ): void {
22
+ export function useKeyboard(handler: (event: KeyEvent) => void): void {
25
23
  useOpenTUIKeyboard(handler);
26
24
  }
@@ -1,4 +1,4 @@
1
- import { useKeyboard as useOpenTUIKeyboard } from '@opentui/react';
1
+ import { useKeyboard as useOpenTUIKeyboard } from "@opentui/react";
2
2
  /**
3
3
  * useKeyboardHandler hook
4
4
  * Provides Ink-like API for keyboard events
@@ -23,17 +23,17 @@ export function useKeyboardHandler(handler) {
23
23
  ctrl: event.ctrl || false,
24
24
  shift: event.shift || false,
25
25
  meta: event.meta || false,
26
- return: event.name === 'enter' || event.name === 'return',
27
- upArrow: event.name === 'up',
28
- downArrow: event.name === 'down',
29
- leftArrow: event.name === 'left',
30
- rightArrow: event.name === 'right',
31
- escape: event.name === 'escape',
32
- tab: event.name === 'tab',
26
+ return: event.name === "enter" || event.name === "return",
27
+ upArrow: event.name === "up",
28
+ downArrow: event.name === "down",
29
+ leftArrow: event.name === "left",
30
+ rightArrow: event.name === "right",
31
+ escape: event.name === "escape",
32
+ tab: event.name === "tab",
33
33
  };
34
34
  // For printable characters, pass the character as input
35
35
  // For special keys, pass empty string
36
- const input = event.name.length === 1 ? event.name : '';
36
+ const input = event.name.length === 1 ? event.name : "";
37
37
  handler(input, key);
38
38
  });
39
39
  }
@@ -1,4 +1,4 @@
1
- import { useKeyboard as useOpenTUIKeyboard } from '@opentui/react';
1
+ import { useKeyboard as useOpenTUIKeyboard } from "@opentui/react";
2
2
 
3
3
  /**
4
4
  * Wrapper hook for OpenTUI's useKeyboard that provides an Ink-like API
@@ -45,18 +45,18 @@ export function useKeyboardHandler(handler: KeyHandler): void {
45
45
  ctrl: event.ctrl || false,
46
46
  shift: event.shift || false,
47
47
  meta: event.meta || false,
48
- return: event.name === 'enter' || event.name === 'return',
49
- upArrow: event.name === 'up',
50
- downArrow: event.name === 'down',
51
- leftArrow: event.name === 'left',
52
- rightArrow: event.name === 'right',
53
- escape: event.name === 'escape',
54
- tab: event.name === 'tab',
48
+ return: event.name === "enter" || event.name === "return",
49
+ upArrow: event.name === "up",
50
+ downArrow: event.name === "down",
51
+ leftArrow: event.name === "left",
52
+ rightArrow: event.name === "right",
53
+ escape: event.name === "escape",
54
+ tab: event.name === "tab",
55
55
  };
56
56
 
57
57
  // For printable characters, pass the character as input
58
58
  // For special keys, pass empty string
59
- const input = event.name.length === 1 ? event.name : '';
59
+ const input = event.name.length === 1 ? event.name : "";
60
60
 
61
61
  handler(input, key);
62
62
  });
@@ -0,0 +1,33 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
2
+ import { theme } from "../theme.js";
3
+ // ─── Row renderer ──────────────────────────────────────────────────────────────
4
+ export function renderCliToolRow(status, _index, isSelected) {
5
+ const { tool, installed, installedVersion, hasUpdate, checking } = status;
6
+ let icon;
7
+ let iconColor;
8
+ if (!installed) {
9
+ icon = "○";
10
+ iconColor = theme.colors.muted;
11
+ }
12
+ else if (hasUpdate) {
13
+ icon = "⬆";
14
+ iconColor = theme.colors.warning;
15
+ }
16
+ else {
17
+ icon = "●";
18
+ iconColor = theme.colors.success;
19
+ }
20
+ const versionText = installedVersion ? `v${installedVersion}` : "";
21
+ if (isSelected) {
22
+ return (_jsxs("text", { bg: theme.selection.bg, fg: theme.selection.fg, children: [" ", icon, " ", tool.displayName, " ", versionText, checking ? "..." : "", " "] }));
23
+ }
24
+ return (_jsxs("text", { children: [_jsx("span", { fg: iconColor, children: icon }), _jsxs("span", { fg: theme.colors.text, children: [" ", tool.displayName] }), versionText ? _jsxs("span", { fg: theme.colors.success, children: [" ", versionText] }) : null, checking ? _jsx("span", { fg: theme.colors.muted, children: "..." }) : null] }));
25
+ }
26
+ // ─── Detail renderer ───────────────────────────────────────────────────────────
27
+ export function renderCliToolDetail(status) {
28
+ if (!status) {
29
+ return (_jsx("box", { flexDirection: "column", alignItems: "center", justifyContent: "center", flexGrow: 1, children: _jsx("text", { fg: theme.colors.muted, children: "Select a tool to see details" }) }));
30
+ }
31
+ const { tool, installed, installedVersion, latestVersion, hasUpdate, checking } = status;
32
+ return (_jsxs("box", { flexDirection: "column", children: [_jsxs("box", { marginBottom: 1, children: [_jsx("text", { fg: theme.colors.info, children: _jsxs("strong", { children: ["⚙ ", tool.displayName] }) }), hasUpdate ? _jsx("text", { fg: theme.colors.warning, children: " \u2B06" }) : null] }), _jsx("text", { fg: theme.colors.muted, children: tool.description }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("box", { children: [_jsx("text", { fg: theme.colors.muted, children: "Status " }), !installed ? (_jsx("text", { fg: theme.colors.muted, children: "○ Not installed" })) : checking ? (_jsx("text", { fg: theme.colors.success, children: "● Checking..." })) : hasUpdate ? (_jsx("text", { fg: theme.colors.warning, children: "● Update available" })) : (_jsx("text", { fg: theme.colors.success, children: "● Up to date" }))] }), installedVersion ? (_jsxs("box", { children: [_jsx("text", { fg: theme.colors.muted, children: "Installed " }), _jsxs("text", { fg: theme.colors.success, children: ["v", installedVersion] })] })) : null, latestVersion ? (_jsxs("box", { children: [_jsx("text", { fg: theme.colors.muted, children: "Latest " }), _jsxs("text", { fg: theme.colors.text, children: ["v", latestVersion] })] })) : null, _jsxs("box", { children: [_jsx("text", { fg: theme.colors.muted, children: "Website " }), _jsx("text", { fg: theme.colors.link, children: tool.website })] })] }), _jsx("box", { marginTop: 2, children: !installed ? (_jsxs("box", { children: [_jsxs("text", { bg: theme.colors.success, fg: "black", children: [" ", "Enter", " "] }), _jsx("text", { fg: theme.colors.muted, children: " Install" })] })) : hasUpdate ? (_jsxs("box", { children: [_jsxs("text", { bg: theme.colors.warning, fg: "black", children: [" ", "Enter", " "] }), _jsxs("text", { fg: theme.colors.muted, children: [" Update to v", latestVersion] })] })) : (_jsxs("box", { children: [_jsxs("text", { bg: theme.colors.muted, fg: "white", children: [" ", "Enter", " "] }), _jsx("text", { fg: theme.colors.muted, children: " Reinstall" })] })) })] }));
33
+ }
@@ -0,0 +1,153 @@
1
+ import React from "react";
2
+ import type { CliTool } from "../../data/cli-tools.js";
3
+ import { theme } from "../theme.js";
4
+
5
+ // ─── Status type ───────────────────────────────────────────────────────────────
6
+
7
+ export interface CliToolStatus {
8
+ tool: CliTool;
9
+ installed: boolean;
10
+ installedVersion?: string;
11
+ latestVersion?: string;
12
+ hasUpdate?: boolean;
13
+ checking: boolean;
14
+ }
15
+
16
+ // ─── Row renderer ──────────────────────────────────────────────────────────────
17
+
18
+ export function renderCliToolRow(
19
+ status: CliToolStatus,
20
+ _index: number,
21
+ isSelected: boolean,
22
+ ): React.ReactNode {
23
+ const { tool, installed, installedVersion, hasUpdate, checking } = status;
24
+
25
+ let icon: string;
26
+ let iconColor: string;
27
+
28
+ if (!installed) {
29
+ icon = "○";
30
+ iconColor = theme.colors.muted;
31
+ } else if (hasUpdate) {
32
+ icon = "⬆";
33
+ iconColor = theme.colors.warning;
34
+ } else {
35
+ icon = "●";
36
+ iconColor = theme.colors.success;
37
+ }
38
+
39
+ const versionText = installedVersion ? `v${installedVersion}` : "";
40
+
41
+ if (isSelected) {
42
+ return (
43
+ <text bg={theme.selection.bg} fg={theme.selection.fg}>
44
+ {" "}
45
+ {icon} {tool.displayName} {versionText}
46
+ {checking ? "..." : ""}{" "}
47
+ </text>
48
+ );
49
+ }
50
+
51
+ return (
52
+ <text>
53
+ <span fg={iconColor}>{icon}</span>
54
+ <span fg={theme.colors.text}> {tool.displayName}</span>
55
+ {versionText ? <span fg={theme.colors.success}> {versionText}</span> : null}
56
+ {checking ? <span fg={theme.colors.muted}>{"..."}</span> : null}
57
+ </text>
58
+ );
59
+ }
60
+
61
+ // ─── Detail renderer ───────────────────────────────────────────────────────────
62
+
63
+ export function renderCliToolDetail(
64
+ status: CliToolStatus | undefined,
65
+ ): React.ReactNode {
66
+ if (!status) {
67
+ return (
68
+ <box
69
+ flexDirection="column"
70
+ alignItems="center"
71
+ justifyContent="center"
72
+ flexGrow={1}
73
+ >
74
+ <text fg={theme.colors.muted}>Select a tool to see details</text>
75
+ </box>
76
+ );
77
+ }
78
+
79
+ const { tool, installed, installedVersion, latestVersion, hasUpdate, checking } =
80
+ status;
81
+
82
+ return (
83
+ <box flexDirection="column">
84
+ <box marginBottom={1}>
85
+ <text fg={theme.colors.info}>
86
+ <strong>{"⚙ "}{tool.displayName}</strong>
87
+ </text>
88
+ {hasUpdate ? <text fg={theme.colors.warning}> ⬆</text> : null}
89
+ </box>
90
+
91
+ <text fg={theme.colors.muted}>{tool.description}</text>
92
+
93
+ <box marginTop={1} flexDirection="column">
94
+ <box>
95
+ <text fg={theme.colors.muted}>{"Status "}</text>
96
+ {!installed ? (
97
+ <text fg={theme.colors.muted}>{"○ Not installed"}</text>
98
+ ) : checking ? (
99
+ <text fg={theme.colors.success}>{"● Checking..."}</text>
100
+ ) : hasUpdate ? (
101
+ <text fg={theme.colors.warning}>{"● Update available"}</text>
102
+ ) : (
103
+ <text fg={theme.colors.success}>{"● Up to date"}</text>
104
+ )}
105
+ </box>
106
+ {installedVersion ? (
107
+ <box>
108
+ <text fg={theme.colors.muted}>{"Installed "}</text>
109
+ <text fg={theme.colors.success}>v{installedVersion}</text>
110
+ </box>
111
+ ) : null}
112
+ {latestVersion ? (
113
+ <box>
114
+ <text fg={theme.colors.muted}>{"Latest "}</text>
115
+ <text fg={theme.colors.text}>v{latestVersion}</text>
116
+ </box>
117
+ ) : null}
118
+ <box>
119
+ <text fg={theme.colors.muted}>{"Website "}</text>
120
+ <text fg={theme.colors.link}>{tool.website}</text>
121
+ </box>
122
+ </box>
123
+
124
+ <box marginTop={2}>
125
+ {!installed ? (
126
+ <box>
127
+ <text bg={theme.colors.success} fg="black">
128
+ {" "}
129
+ Enter{" "}
130
+ </text>
131
+ <text fg={theme.colors.muted}> Install</text>
132
+ </box>
133
+ ) : hasUpdate ? (
134
+ <box>
135
+ <text bg={theme.colors.warning} fg="black">
136
+ {" "}
137
+ Enter{" "}
138
+ </text>
139
+ <text fg={theme.colors.muted}> Update to v{latestVersion}</text>
140
+ </box>
141
+ ) : (
142
+ <box>
143
+ <text bg={theme.colors.muted} fg="white">
144
+ {" "}
145
+ Enter{" "}
146
+ </text>
147
+ <text fg={theme.colors.muted}> Reinstall</text>
148
+ </box>
149
+ )}
150
+ </box>
151
+ </box>
152
+ );
153
+ }
@@ -0,0 +1,26 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
2
+ import { theme } from "../theme.js";
3
+ // ─── Row renderers ─────────────────────────────────────────────────────────────
4
+ export function renderMcpRow(item, _index, isSelected) {
5
+ if (item.kind === "category") {
6
+ return (_jsx("text", { fg: theme.selection.bg, children: _jsxs("strong", { children: ["▸ ", item.label] }) }));
7
+ }
8
+ const { server, isInstalled } = item;
9
+ const icon = isInstalled ? "●" : "○";
10
+ const iconColor = isInstalled ? theme.colors.success : theme.colors.muted;
11
+ if (isSelected) {
12
+ return (_jsxs("text", { bg: theme.selection.bg, fg: theme.selection.fg, children: [" ", icon, " ", server.name, " "] }));
13
+ }
14
+ return (_jsxs("text", { children: [_jsx("span", { fg: iconColor, children: icon }), _jsxs("span", { fg: theme.colors.text, children: [" ", server.name] }), server.requiresConfig ? (_jsx("span", { fg: theme.colors.warning, children: " \u2699" })) : null] }));
15
+ }
16
+ // ─── Detail renderer ───────────────────────────────────────────────────────────
17
+ export function renderMcpDetail(item, loading) {
18
+ if (loading) {
19
+ return _jsx("text", { fg: theme.colors.muted, children: "Loading MCP servers..." });
20
+ }
21
+ if (!item || item.kind === "category") {
22
+ return (_jsx("box", { flexDirection: "column", alignItems: "center", justifyContent: "center", flexGrow: 1, children: _jsx("text", { fg: theme.colors.muted, children: "Select a server to see details" }) }));
23
+ }
24
+ const { server, isInstalled } = item;
25
+ return (_jsxs("box", { flexDirection: "column", children: [_jsxs("box", { marginBottom: 1, children: [_jsx("text", { fg: theme.colors.info, children: _jsxs("strong", { children: ["⚡ ", server.name] }) }), server.requiresConfig ? (_jsx("text", { fg: theme.colors.warning, children: " \u2699" })) : null] }), _jsx("text", { fg: theme.colors.muted, children: server.description }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("box", { children: [_jsx("text", { fg: theme.colors.muted, children: "Status " }), isInstalled ? (_jsx("text", { fg: theme.colors.success, children: "● Installed" })) : (_jsx("text", { fg: theme.colors.muted, children: "○ Not installed" }))] }), _jsxs("box", { children: [_jsx("text", { fg: theme.colors.muted, children: "Type " }), _jsx("text", { fg: theme.colors.text, children: server.type === "http" ? "HTTP" : "Command" })] }), server.type === "http" ? (_jsxs("box", { children: [_jsx("text", { fg: theme.colors.muted, children: "URL " }), _jsx("text", { fg: theme.colors.link, children: server.url })] })) : (_jsxs("box", { children: [_jsx("text", { fg: theme.colors.muted, children: "Command " }), _jsx("text", { fg: theme.colors.info, children: server.command })] })), server.requiresConfig ? (_jsxs("box", { children: [_jsx("text", { fg: theme.colors.muted, children: "Config " }), _jsxs("text", { fg: theme.colors.warning, children: [server.configFields?.length ?? 0, " fields required"] })] })) : null] }), _jsx("box", { marginTop: 2, children: isInstalled ? (_jsxs("box", { children: [_jsxs("text", { bg: theme.colors.danger, fg: "white", children: [" ", "Enter", " "] }), _jsx("text", { fg: theme.colors.muted, children: " Remove server" })] })) : (_jsxs("box", { children: [_jsxs("text", { bg: theme.colors.success, fg: "black", children: [" ", "Enter", " "] }), _jsx("text", { fg: theme.colors.muted, children: " Install server" })] })) })] }));
26
+ }
@@ -0,0 +1,145 @@
1
+ import React from "react";
2
+ import type { McpServer } from "../../types/index.js";
3
+ import { theme } from "../theme.js";
4
+
5
+ // ─── List item types ───────────────────────────────────────────────────────────
6
+
7
+ export type McpListItem =
8
+ | { kind: "category"; label: string }
9
+ | { kind: "server"; server: McpServer; isInstalled: boolean };
10
+
11
+ // ─── Row renderers ─────────────────────────────────────────────────────────────
12
+
13
+ export function renderMcpRow(
14
+ item: McpListItem,
15
+ _index: number,
16
+ isSelected: boolean,
17
+ ): React.ReactNode {
18
+ if (item.kind === "category") {
19
+ return (
20
+ <text fg={theme.selection.bg}>
21
+ <strong>{"▸ "}{item.label}</strong>
22
+ </text>
23
+ );
24
+ }
25
+
26
+ const { server, isInstalled } = item;
27
+ const icon = isInstalled ? "●" : "○";
28
+ const iconColor = isInstalled ? theme.colors.success : theme.colors.muted;
29
+
30
+ if (isSelected) {
31
+ return (
32
+ <text bg={theme.selection.bg} fg={theme.selection.fg}>
33
+ {" "}
34
+ {icon} {server.name}{" "}
35
+ </text>
36
+ );
37
+ }
38
+
39
+ return (
40
+ <text>
41
+ <span fg={iconColor}>{icon}</span>
42
+ <span fg={theme.colors.text}> {server.name}</span>
43
+ {server.requiresConfig ? (
44
+ <span fg={theme.colors.warning}> ⚙</span>
45
+ ) : null}
46
+ </text>
47
+ );
48
+ }
49
+
50
+ // ─── Detail renderer ───────────────────────────────────────────────────────────
51
+
52
+ export function renderMcpDetail(
53
+ item: McpListItem | undefined,
54
+ loading: boolean,
55
+ ): React.ReactNode {
56
+ if (loading) {
57
+ return <text fg={theme.colors.muted}>Loading MCP servers...</text>;
58
+ }
59
+
60
+ if (!item || item.kind === "category") {
61
+ return (
62
+ <box
63
+ flexDirection="column"
64
+ alignItems="center"
65
+ justifyContent="center"
66
+ flexGrow={1}
67
+ >
68
+ <text fg={theme.colors.muted}>Select a server to see details</text>
69
+ </box>
70
+ );
71
+ }
72
+
73
+ const { server, isInstalled } = item;
74
+
75
+ return (
76
+ <box flexDirection="column">
77
+ <box marginBottom={1}>
78
+ <text fg={theme.colors.info}>
79
+ <strong>{"⚡ "}{server.name}</strong>
80
+ </text>
81
+ {server.requiresConfig ? (
82
+ <text fg={theme.colors.warning}> ⚙</text>
83
+ ) : null}
84
+ </box>
85
+
86
+ <text fg={theme.colors.muted}>{server.description}</text>
87
+
88
+ <box marginTop={1} flexDirection="column">
89
+ <box>
90
+ <text fg={theme.colors.muted}>{"Status "}</text>
91
+ {isInstalled ? (
92
+ <text fg={theme.colors.success}>{"● Installed"}</text>
93
+ ) : (
94
+ <text fg={theme.colors.muted}>{"○ Not installed"}</text>
95
+ )}
96
+ </box>
97
+ <box>
98
+ <text fg={theme.colors.muted}>{"Type "}</text>
99
+ <text fg={theme.colors.text}>
100
+ {server.type === "http" ? "HTTP" : "Command"}
101
+ </text>
102
+ </box>
103
+ {server.type === "http" ? (
104
+ <box>
105
+ <text fg={theme.colors.muted}>{"URL "}</text>
106
+ <text fg={theme.colors.link}>{server.url}</text>
107
+ </box>
108
+ ) : (
109
+ <box>
110
+ <text fg={theme.colors.muted}>{"Command "}</text>
111
+ <text fg={theme.colors.info}>{server.command}</text>
112
+ </box>
113
+ )}
114
+ {server.requiresConfig ? (
115
+ <box>
116
+ <text fg={theme.colors.muted}>{"Config "}</text>
117
+ <text fg={theme.colors.warning}>
118
+ {server.configFields?.length ?? 0} fields required
119
+ </text>
120
+ </box>
121
+ ) : null}
122
+ </box>
123
+
124
+ <box marginTop={2}>
125
+ {isInstalled ? (
126
+ <box>
127
+ <text bg={theme.colors.danger} fg="white">
128
+ {" "}
129
+ Enter{" "}
130
+ </text>
131
+ <text fg={theme.colors.muted}> Remove server</text>
132
+ </box>
133
+ ) : (
134
+ <box>
135
+ <text bg={theme.colors.success} fg="black">
136
+ {" "}
137
+ Enter{" "}
138
+ </text>
139
+ <text fg={theme.colors.muted}> Install server</text>
140
+ </box>
141
+ )}
142
+ </box>
143
+ </box>
144
+ );
145
+ }