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