claudeup 3.7.2 → 3.9.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/settings-catalog.js +612 -0
- package/src/data/settings-catalog.ts +689 -0
- package/src/data/skill-repos.js +86 -0
- package/src/data/skill-repos.ts +97 -0
- package/src/services/plugin-manager.js +2 -0
- package/src/services/plugin-manager.ts +3 -0
- package/src/services/profiles.js +161 -0
- package/src/services/profiles.ts +225 -0
- package/src/services/settings-manager.js +108 -0
- package/src/services/settings-manager.ts +140 -0
- package/src/services/skills-manager.js +239 -0
- package/src/services/skills-manager.ts +328 -0
- package/src/services/skillsmp-client.js +67 -0
- package/src/services/skillsmp-client.ts +89 -0
- package/src/types/index.ts +101 -1
- package/src/ui/App.js +23 -18
- package/src/ui/App.tsx +27 -23
- package/src/ui/components/TabBar.js +9 -8
- package/src/ui/components/TabBar.tsx +15 -19
- package/src/ui/components/layout/ScreenLayout.js +8 -14
- package/src/ui/components/layout/ScreenLayout.tsx +51 -58
- package/src/ui/components/modals/ModalContainer.js +43 -11
- package/src/ui/components/modals/ModalContainer.tsx +44 -12
- package/src/ui/components/modals/SelectModal.js +4 -18
- package/src/ui/components/modals/SelectModal.tsx +10 -21
- package/src/ui/screens/CliToolsScreen.js +2 -2
- package/src/ui/screens/CliToolsScreen.tsx +8 -8
- package/src/ui/screens/EnvVarsScreen.js +248 -116
- package/src/ui/screens/EnvVarsScreen.tsx +419 -184
- package/src/ui/screens/McpRegistryScreen.tsx +18 -6
- package/src/ui/screens/McpScreen.js +1 -1
- package/src/ui/screens/McpScreen.tsx +15 -5
- package/src/ui/screens/ModelSelectorScreen.js +3 -5
- package/src/ui/screens/ModelSelectorScreen.tsx +12 -16
- package/src/ui/screens/PluginsScreen.js +154 -66
- package/src/ui/screens/PluginsScreen.tsx +280 -97
- package/src/ui/screens/ProfilesScreen.js +255 -0
- package/src/ui/screens/ProfilesScreen.tsx +487 -0
- package/src/ui/screens/SkillsScreen.js +325 -0
- package/src/ui/screens/SkillsScreen.tsx +574 -0
- package/src/ui/screens/StatusLineScreen.js +2 -2
- package/src/ui/screens/StatusLineScreen.tsx +10 -12
- package/src/ui/screens/index.js +3 -2
- package/src/ui/screens/index.ts +3 -2
- package/src/ui/state/AppContext.js +2 -1
- package/src/ui/state/AppContext.tsx +2 -0
- package/src/ui/state/reducer.js +151 -19
- package/src/ui/state/reducer.ts +167 -19
- package/src/ui/state/types.ts +58 -14
- package/src/utils/clipboard.js +56 -0
- package/src/utils/clipboard.ts +58 -0
package/src/ui/App.js
CHANGED
|
@@ -5,7 +5,7 @@ import fs from "node:fs";
|
|
|
5
5
|
import { AppProvider, useApp, useNavigation, useModal, } from "./state/AppContext.js";
|
|
6
6
|
import { DimensionsProvider, useDimensions, } from "./state/DimensionsContext.js";
|
|
7
7
|
import { ModalContainer } from "./components/modals/index.js";
|
|
8
|
-
import { PluginsScreen, McpScreen, McpRegistryScreen,
|
|
8
|
+
import { PluginsScreen, McpScreen, McpRegistryScreen, SettingsScreen, CliToolsScreen, ModelSelectorScreen, ProfilesScreen, SkillsScreen, } from "./screens/index.js";
|
|
9
9
|
import { repairAllMarketplaces } from "../services/local-marketplace.js";
|
|
10
10
|
import { migrateMarketplaceRename } from "../services/claude-settings.js";
|
|
11
11
|
import { checkForUpdates, getCurrentVersion, } from "../services/version-check.js";
|
|
@@ -26,14 +26,16 @@ function Router() {
|
|
|
26
26
|
return _jsx(McpScreen, {});
|
|
27
27
|
case "mcp-registry":
|
|
28
28
|
return _jsx(McpRegistryScreen, {});
|
|
29
|
-
case "
|
|
30
|
-
return _jsx(
|
|
31
|
-
case "env-vars":
|
|
32
|
-
return _jsx(EnvVarsScreen, {});
|
|
29
|
+
case "settings":
|
|
30
|
+
return _jsx(SettingsScreen, {});
|
|
33
31
|
case "cli-tools":
|
|
34
32
|
return _jsx(CliToolsScreen, {});
|
|
35
33
|
case "model-selector":
|
|
36
34
|
return _jsx(ModelSelectorScreen, {});
|
|
35
|
+
case "profiles":
|
|
36
|
+
return _jsx(ProfilesScreen, {});
|
|
37
|
+
case "skills":
|
|
38
|
+
return _jsx(SkillsScreen, {});
|
|
37
39
|
default:
|
|
38
40
|
return _jsx(PluginsScreen, {});
|
|
39
41
|
}
|
|
@@ -80,10 +82,11 @@ function GlobalKeyHandler({ onDebugToggle, onExit, }) {
|
|
|
80
82
|
"plugins",
|
|
81
83
|
"mcp",
|
|
82
84
|
"mcp-registry",
|
|
83
|
-
"
|
|
84
|
-
"env-vars",
|
|
85
|
+
"settings",
|
|
85
86
|
"cli-tools",
|
|
86
87
|
"model-selector",
|
|
88
|
+
"profiles",
|
|
89
|
+
"skills",
|
|
87
90
|
].includes(state.currentRoute.screen);
|
|
88
91
|
if (isTopLevel) {
|
|
89
92
|
if (input === "1")
|
|
@@ -91,19 +94,22 @@ function GlobalKeyHandler({ onDebugToggle, onExit, }) {
|
|
|
91
94
|
else if (input === "2")
|
|
92
95
|
navigateToScreen("mcp");
|
|
93
96
|
else if (input === "3")
|
|
94
|
-
navigateToScreen("
|
|
97
|
+
navigateToScreen("settings");
|
|
95
98
|
else if (input === "4")
|
|
96
|
-
navigateToScreen("env-vars");
|
|
97
|
-
else if (input === "5")
|
|
98
99
|
navigateToScreen("cli-tools");
|
|
100
|
+
else if (input === "5")
|
|
101
|
+
navigateToScreen("profiles");
|
|
102
|
+
else if (input === "6")
|
|
103
|
+
navigateToScreen("skills");
|
|
99
104
|
// Tab navigation cycling
|
|
100
105
|
if (key.tab) {
|
|
101
106
|
const screens = [
|
|
102
107
|
"plugins",
|
|
103
108
|
"mcp",
|
|
104
|
-
"
|
|
105
|
-
"env-vars",
|
|
109
|
+
"settings",
|
|
106
110
|
"cli-tools",
|
|
111
|
+
"profiles",
|
|
112
|
+
"skills",
|
|
107
113
|
];
|
|
108
114
|
const currentIndex = screens.indexOf(state.currentRoute.screen);
|
|
109
115
|
if (currentIndex !== -1) {
|
|
@@ -138,9 +144,9 @@ function GlobalKeyHandler({ onDebugToggle, onExit, }) {
|
|
|
138
144
|
? This help
|
|
139
145
|
|
|
140
146
|
Quick Navigation
|
|
141
|
-
1 Plugins 4
|
|
142
|
-
2 MCP Servers 5
|
|
143
|
-
3
|
|
147
|
+
1 Plugins 4 CLI Tools
|
|
148
|
+
2 MCP Servers 5 Profiles
|
|
149
|
+
3 Settings 6 Skills
|
|
144
150
|
|
|
145
151
|
Plugin Actions
|
|
146
152
|
u Update d Uninstall
|
|
@@ -157,7 +163,7 @@ MCP Servers
|
|
|
157
163
|
* UpdateBanner Component
|
|
158
164
|
* Shows version update notification
|
|
159
165
|
*/
|
|
160
|
-
function UpdateBanner({ result
|
|
166
|
+
function UpdateBanner({ result }) {
|
|
161
167
|
if (!result.updateAvailable)
|
|
162
168
|
return null;
|
|
163
169
|
return (_jsxs("box", { paddingLeft: 1, paddingRight: 1, children: [_jsx("text", { bg: "yellow", fg: "black", children: _jsx("strong", { children: " UPDATE " }) }), _jsxs("text", { fg: "yellow", children: [" ", "v", result.currentVersion, " \u2192 v", result.latestVersion] }), _jsx("text", { fg: "gray", children: " Run: " }), _jsx("text", { fg: "cyan", children: "npm i -g claudeup" })] }));
|
|
@@ -183,8 +189,7 @@ function AppContentInner({ showDebug, onDebugToggle, updateInfo, onExit, }) {
|
|
|
183
189
|
state: { message: "Scanning marketplaces..." },
|
|
184
190
|
});
|
|
185
191
|
// Migrate old marketplace names → magus (idempotent), then repair plugin.json files
|
|
186
|
-
migrateMarketplaceRename()
|
|
187
|
-
.catch(() => { }); // non-blocking, best-effort
|
|
192
|
+
migrateMarketplaceRename().catch(() => { }); // non-blocking, best-effort
|
|
188
193
|
repairAllMarketplaces()
|
|
189
194
|
.then(async () => {
|
|
190
195
|
dispatch({ type: "HIDE_PROGRESS" });
|
package/src/ui/App.tsx
CHANGED
|
@@ -16,10 +16,11 @@ import {
|
|
|
16
16
|
PluginsScreen,
|
|
17
17
|
McpScreen,
|
|
18
18
|
McpRegistryScreen,
|
|
19
|
-
|
|
20
|
-
EnvVarsScreen,
|
|
19
|
+
SettingsScreen,
|
|
21
20
|
CliToolsScreen,
|
|
22
21
|
ModelSelectorScreen,
|
|
22
|
+
ProfilesScreen,
|
|
23
|
+
SkillsScreen,
|
|
23
24
|
} from "./screens/index.js";
|
|
24
25
|
import type { Screen } from "./state/types.js";
|
|
25
26
|
import { repairAllMarketplaces } from "../services/local-marketplace.js";
|
|
@@ -49,14 +50,16 @@ function Router() {
|
|
|
49
50
|
return <McpScreen />;
|
|
50
51
|
case "mcp-registry":
|
|
51
52
|
return <McpRegistryScreen />;
|
|
52
|
-
case "
|
|
53
|
-
return <
|
|
54
|
-
case "env-vars":
|
|
55
|
-
return <EnvVarsScreen />;
|
|
53
|
+
case "settings":
|
|
54
|
+
return <SettingsScreen />;
|
|
56
55
|
case "cli-tools":
|
|
57
56
|
return <CliToolsScreen />;
|
|
58
57
|
case "model-selector":
|
|
59
58
|
return <ModelSelectorScreen />;
|
|
59
|
+
case "profiles":
|
|
60
|
+
return <ProfilesScreen />;
|
|
61
|
+
case "skills":
|
|
62
|
+
return <SkillsScreen />;
|
|
60
63
|
default:
|
|
61
64
|
return <PluginsScreen />;
|
|
62
65
|
}
|
|
@@ -112,27 +115,30 @@ function GlobalKeyHandler({
|
|
|
112
115
|
"plugins",
|
|
113
116
|
"mcp",
|
|
114
117
|
"mcp-registry",
|
|
115
|
-
"
|
|
116
|
-
"env-vars",
|
|
118
|
+
"settings",
|
|
117
119
|
"cli-tools",
|
|
118
120
|
"model-selector",
|
|
121
|
+
"profiles",
|
|
122
|
+
"skills",
|
|
119
123
|
].includes(state.currentRoute.screen);
|
|
120
124
|
|
|
121
125
|
if (isTopLevel) {
|
|
122
126
|
if (input === "1") navigateToScreen("plugins");
|
|
123
127
|
else if (input === "2") navigateToScreen("mcp");
|
|
124
|
-
else if (input === "3") navigateToScreen("
|
|
125
|
-
else if (input === "4") navigateToScreen("
|
|
126
|
-
else if (input === "5") navigateToScreen("
|
|
128
|
+
else if (input === "3") navigateToScreen("settings");
|
|
129
|
+
else if (input === "4") navigateToScreen("cli-tools");
|
|
130
|
+
else if (input === "5") navigateToScreen("profiles");
|
|
131
|
+
else if (input === "6") navigateToScreen("skills");
|
|
127
132
|
|
|
128
133
|
// Tab navigation cycling
|
|
129
134
|
if (key.tab) {
|
|
130
135
|
const screens: Screen[] = [
|
|
131
136
|
"plugins",
|
|
132
137
|
"mcp",
|
|
133
|
-
"
|
|
134
|
-
"env-vars",
|
|
138
|
+
"settings",
|
|
135
139
|
"cli-tools",
|
|
140
|
+
"profiles",
|
|
141
|
+
"skills",
|
|
136
142
|
];
|
|
137
143
|
const currentIndex = screens.indexOf(
|
|
138
144
|
state.currentRoute.screen as Screen,
|
|
@@ -171,9 +177,9 @@ function GlobalKeyHandler({
|
|
|
171
177
|
? This help
|
|
172
178
|
|
|
173
179
|
Quick Navigation
|
|
174
|
-
1 Plugins 4
|
|
175
|
-
2 MCP Servers 5
|
|
176
|
-
3
|
|
180
|
+
1 Plugins 4 CLI Tools
|
|
181
|
+
2 MCP Servers 5 Profiles
|
|
182
|
+
3 Settings 6 Skills
|
|
177
183
|
|
|
178
184
|
Plugin Actions
|
|
179
185
|
u Update d Uninstall
|
|
@@ -194,13 +200,11 @@ MCP Servers
|
|
|
194
200
|
* UpdateBanner Component
|
|
195
201
|
* Shows version update notification
|
|
196
202
|
*/
|
|
197
|
-
function UpdateBanner({
|
|
198
|
-
result,
|
|
199
|
-
}: { result: VersionCheckResult }) {
|
|
203
|
+
function UpdateBanner({ result }: { result: VersionCheckResult }) {
|
|
200
204
|
if (!result.updateAvailable) return null;
|
|
201
205
|
|
|
202
206
|
return (
|
|
203
|
-
<box paddingLeft={1} paddingRight={1}
|
|
207
|
+
<box paddingLeft={1} paddingRight={1}>
|
|
204
208
|
<text bg="yellow" fg="black">
|
|
205
209
|
<strong> UPDATE </strong>
|
|
206
210
|
</text>
|
|
@@ -266,8 +270,7 @@ function AppContentInner({
|
|
|
266
270
|
});
|
|
267
271
|
|
|
268
272
|
// Migrate old marketplace names → magus (idempotent), then repair plugin.json files
|
|
269
|
-
migrateMarketplaceRename()
|
|
270
|
-
.catch(() => {}); // non-blocking, best-effort
|
|
273
|
+
migrateMarketplaceRename().catch(() => {}); // non-blocking, best-effort
|
|
271
274
|
|
|
272
275
|
repairAllMarketplaces()
|
|
273
276
|
.then(async () => {
|
|
@@ -295,7 +298,8 @@ function AppContentInner({
|
|
|
295
298
|
<box
|
|
296
299
|
flexDirection="column"
|
|
297
300
|
height={dimensions.contentHeight}
|
|
298
|
-
paddingLeft={1}
|
|
301
|
+
paddingLeft={1}
|
|
302
|
+
paddingRight={1}
|
|
299
303
|
>
|
|
300
304
|
<Router />
|
|
301
305
|
</box>
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
-
import { useKeyboardHandler } from
|
|
2
|
+
import { useKeyboardHandler } from "../hooks/useKeyboardHandler";
|
|
3
3
|
const TABS = [
|
|
4
|
-
{ key:
|
|
5
|
-
{ key:
|
|
6
|
-
{ key:
|
|
7
|
-
{ key:
|
|
8
|
-
{ key:
|
|
4
|
+
{ key: "1", label: "Plugins", screen: "plugins" },
|
|
5
|
+
{ key: "2", label: "MCP", screen: "mcp" },
|
|
6
|
+
{ key: "3", label: "Settings", screen: "settings" },
|
|
7
|
+
{ key: "4", label: "CLI", screen: "cli-tools" },
|
|
8
|
+
{ key: "5", label: "Profiles", screen: "profiles" },
|
|
9
|
+
{ key: "6", label: "Skills", screen: "skills" },
|
|
9
10
|
];
|
|
10
|
-
export function TabBar({ currentScreen, onTabChange
|
|
11
|
+
export function TabBar({ currentScreen, onTabChange }) {
|
|
11
12
|
// Handle number key shortcuts (1-5)
|
|
12
13
|
useKeyboardHandler((input, key) => {
|
|
13
14
|
if (!onTabChange)
|
|
@@ -32,7 +33,7 @@ export function TabBar({ currentScreen, onTabChange, }) {
|
|
|
32
33
|
return (_jsx("box", { flexDirection: "row", gap: 0, children: TABS.map((tab, index) => {
|
|
33
34
|
const isSelected = tab.screen === currentScreen;
|
|
34
35
|
const isLast = index === TABS.length - 1;
|
|
35
|
-
return (_jsxs("box", { flexDirection: "row", children: [isSelected ? (_jsx("box", { children: _jsx("text", { bg: "#7e57c2", fg: "white", children: _jsxs("strong", { children: [
|
|
36
|
+
return (_jsxs("box", { flexDirection: "row", children: [isSelected ? (_jsx("box", { children: _jsx("text", { bg: "#7e57c2", fg: "white", children: _jsxs("strong", { children: [" ", tab.key, ":", tab.label, " "] }) }) })) : (_jsx("box", { children: _jsxs("text", { fg: "gray", children: [" ", tab.key, ":", tab.label, " "] }) })), !isLast && _jsx("text", { fg: "#666666", children: "\u2502" })] }, tab.key));
|
|
36
37
|
}) }));
|
|
37
38
|
}
|
|
38
39
|
export default TabBar;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import React from
|
|
2
|
-
import { useKeyboardHandler } from
|
|
3
|
-
import type { Screen } from
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useKeyboardHandler } from "../hooks/useKeyboardHandler";
|
|
3
|
+
import type { Screen } from "../state/types.js";
|
|
4
4
|
|
|
5
5
|
interface Tab {
|
|
6
6
|
key: string;
|
|
@@ -9,11 +9,12 @@ interface Tab {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
const TABS: Tab[] = [
|
|
12
|
-
{ key:
|
|
13
|
-
{ key:
|
|
14
|
-
{ key:
|
|
15
|
-
{ key:
|
|
16
|
-
{ key:
|
|
12
|
+
{ key: "1", label: "Plugins", screen: "plugins" },
|
|
13
|
+
{ key: "2", label: "MCP", screen: "mcp" },
|
|
14
|
+
{ key: "3", label: "Settings", screen: "settings" },
|
|
15
|
+
{ key: "4", label: "CLI", screen: "cli-tools" },
|
|
16
|
+
{ key: "5", label: "Profiles", screen: "profiles" },
|
|
17
|
+
{ key: "6", label: "Skills", screen: "skills" },
|
|
17
18
|
];
|
|
18
19
|
|
|
19
20
|
interface TabBarProps {
|
|
@@ -21,10 +22,7 @@ interface TabBarProps {
|
|
|
21
22
|
onTabChange?: (screen: Screen) => void;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
export function TabBar({
|
|
25
|
-
currentScreen,
|
|
26
|
-
onTabChange,
|
|
27
|
-
}: TabBarProps) {
|
|
25
|
+
export function TabBar({ currentScreen, onTabChange }: TabBarProps) {
|
|
28
26
|
// Handle number key shortcuts (1-5)
|
|
29
27
|
useKeyboardHandler((input, key) => {
|
|
30
28
|
if (!onTabChange) return;
|
|
@@ -61,23 +59,21 @@ export function TabBar({
|
|
|
61
59
|
<box>
|
|
62
60
|
<text bg="#7e57c2" fg="white">
|
|
63
61
|
<strong>
|
|
64
|
-
{
|
|
65
|
-
{tab.key}:{tab.label}{
|
|
62
|
+
{" "}
|
|
63
|
+
{tab.key}:{tab.label}{" "}
|
|
66
64
|
</strong>
|
|
67
65
|
</text>
|
|
68
66
|
</box>
|
|
69
67
|
) : (
|
|
70
68
|
<box>
|
|
71
69
|
<text fg="gray">
|
|
72
|
-
{
|
|
73
|
-
{tab.key}:{tab.label}{
|
|
70
|
+
{" "}
|
|
71
|
+
{tab.key}:{tab.label}{" "}
|
|
74
72
|
</text>
|
|
75
73
|
</box>
|
|
76
74
|
)}
|
|
77
75
|
{/* Separator */}
|
|
78
|
-
{!isLast &&
|
|
79
|
-
<text fg="#666666">│</text>
|
|
80
|
-
)}
|
|
76
|
+
{!isLast && <text fg="#666666">│</text>}
|
|
81
77
|
</box>
|
|
82
78
|
);
|
|
83
79
|
})}
|
|
@@ -1,21 +1,15 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
2
|
import { useDimensions } from "../../state/DimensionsContext.js";
|
|
3
3
|
import { TabBar } from "../TabBar.js";
|
|
4
4
|
const HEADER_COLOR = "#7e57c2";
|
|
5
5
|
export function ScreenLayout({ title, subtitle, currentScreen, search, statusLine, footerHints, listPanel, detailPanel, }) {
|
|
6
6
|
const dimensions = useDimensions();
|
|
7
|
-
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
return (_jsxs("box", { flexDirection: "column", height: dimensions.contentHeight, children: [
|
|
14
|
-
// Search mode
|
|
15
|
-
_jsxs(_Fragment, { children: [_jsx("text", { fg: "green", children: "> " }), search.isActive ? (_jsxs(_Fragment, { children: [_jsx("text", { fg: "white", children: search.query }), _jsx("text", { bg: "white", fg: "black", children: " " })] })) : (_jsx("text", { fg: "gray", children: search.query || search.placeholder || "/" }))] })) : statusLine ? (
|
|
16
|
-
// Custom status line
|
|
17
|
-
statusLine) : (
|
|
18
|
-
// Default empty status
|
|
19
|
-
_jsx("text", { fg: "gray", children: "\u2500" })) })] }), _jsxs("box", { flexDirection: "row", height: panelHeight, children: [_jsx("box", { flexDirection: "column", width: "49%", height: panelHeight, paddingRight: 1, children: listPanel }), _jsx("box", { flexDirection: "column", width: 1, height: panelHeight, children: _jsx("text", { fg: "#444444", children: "│".repeat(panelHeight) }) }), _jsx("box", { flexDirection: "column", width: "50%", height: panelHeight, paddingLeft: 1, children: detailPanel })] }), _jsxs("box", { height: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsx("text", { fg: "gray", children: footerHints }), _jsx(TabBar, { currentScreen: currentScreen })] })] }));
|
|
7
|
+
const hasSearchBar = search && (search.isActive || search.query);
|
|
8
|
+
// Fixed chrome: top line + tabs + line + header + separator + footer = 6
|
|
9
|
+
// Search bar adds 1 when active
|
|
10
|
+
const fixedHeight = 6 + (hasSearchBar ? 1 : 0);
|
|
11
|
+
const panelHeight = Math.max(5, dimensions.contentHeight - fixedHeight);
|
|
12
|
+
const lineWidth = Math.max(10, dimensions.terminalWidth - 4);
|
|
13
|
+
return (_jsxs("box", { flexDirection: "column", height: dimensions.contentHeight, children: [_jsx("box", { height: 1, paddingLeft: 1, paddingRight: 1, children: _jsx("text", { fg: "#333333", children: "─".repeat(lineWidth) }) }), _jsx("box", { height: 1, paddingLeft: 1, paddingRight: 1, children: _jsx(TabBar, { currentScreen: currentScreen }) }), _jsx("box", { height: 1, paddingLeft: 1, paddingRight: 1, children: _jsx("text", { fg: "#333333", children: "─".repeat(lineWidth) }) }), _jsxs("box", { height: 1, paddingLeft: 1, paddingRight: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsx("text", { fg: HEADER_COLOR, children: _jsx("strong", { children: title }) }), subtitle && _jsx("text", { fg: "gray", children: subtitle }), !subtitle && statusLine ? statusLine : null] }), hasSearchBar && (_jsx("box", { height: 1, paddingLeft: 1, paddingRight: 1, children: search.isActive ? (_jsxs("text", { children: [_jsx("span", { fg: "green", children: "Filter: " }), _jsx("span", { fg: "white", children: search.query }), _jsx("span", { bg: "white", fg: "black", children: " " })] })) : (_jsxs("text", { children: [_jsx("span", { fg: "green", children: "Filter: " }), _jsx("span", { fg: "yellow", children: search.query }), _jsx("span", { fg: "gray", children: " (Esc to clear)" })] })) })), _jsx("box", { height: 1, paddingLeft: 1, paddingRight: 1, children: _jsx("text", { fg: "#444444", children: "─".repeat(lineWidth) }) }), _jsxs("box", { flexDirection: "row", height: panelHeight, children: [_jsx("box", { flexDirection: "column", width: "49%", height: panelHeight, paddingRight: 1, children: listPanel }), _jsx("box", { flexDirection: "column", width: 1, height: panelHeight, children: _jsx("text", { fg: "#444444", children: "│".repeat(panelHeight) }) }), _jsx("box", { flexDirection: "column", width: "50%", height: panelHeight, paddingLeft: 1, children: detailPanel })] }), _jsx("box", { height: 1, paddingLeft: 1, children: _jsx("text", { fg: "gray", children: footerHints }) })] }));
|
|
20
14
|
}
|
|
21
15
|
export default ScreenLayout;
|
|
@@ -43,60 +43,62 @@ export function ScreenLayout({
|
|
|
43
43
|
}: ScreenLayoutProps) {
|
|
44
44
|
const dimensions = useDimensions();
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
const panelHeight = Math.max(
|
|
52
|
-
|
|
53
|
-
dimensions.contentHeight - headerHeight - footerHeight,
|
|
54
|
-
);
|
|
46
|
+
const hasSearchBar = search && (search.isActive || search.query);
|
|
47
|
+
|
|
48
|
+
// Fixed chrome: top line + tabs + line + header + separator + footer = 6
|
|
49
|
+
// Search bar adds 1 when active
|
|
50
|
+
const fixedHeight = 6 + (hasSearchBar ? 1 : 0);
|
|
51
|
+
const panelHeight = Math.max(5, dimensions.contentHeight - fixedHeight);
|
|
52
|
+
const lineWidth = Math.max(10, dimensions.terminalWidth - 4);
|
|
55
53
|
|
|
56
54
|
return (
|
|
57
55
|
<box flexDirection="column" height={dimensions.contentHeight}>
|
|
58
|
-
{/*
|
|
59
|
-
<box
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
>
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
{subtitle && <text fg="gray">{subtitle}</text>}
|
|
73
|
-
</box>
|
|
56
|
+
{/* Line above tabs */}
|
|
57
|
+
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
58
|
+
<text fg="#333333">{"─".repeat(lineWidth)}</text>
|
|
59
|
+
</box>
|
|
60
|
+
|
|
61
|
+
{/* Tab bar */}
|
|
62
|
+
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
63
|
+
<TabBar currentScreen={currentScreen} />
|
|
64
|
+
</box>
|
|
65
|
+
|
|
66
|
+
{/* Line below tabs */}
|
|
67
|
+
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
68
|
+
<text fg="#333333">{"─".repeat(lineWidth)}</text>
|
|
69
|
+
</box>
|
|
74
70
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
// Custom status line
|
|
94
|
-
statusLine
|
|
71
|
+
{/* Header — title on left, subtitle/status on right */}
|
|
72
|
+
<box height={1} paddingLeft={1} paddingRight={1} flexDirection="row" justifyContent="space-between">
|
|
73
|
+
<text fg={HEADER_COLOR}>
|
|
74
|
+
<strong>{title}</strong>
|
|
75
|
+
</text>
|
|
76
|
+
{subtitle && <text fg="gray">{subtitle}</text>}
|
|
77
|
+
{!subtitle && statusLine ? statusLine : null}
|
|
78
|
+
</box>
|
|
79
|
+
|
|
80
|
+
{/* Search bar — own line, only when active */}
|
|
81
|
+
{hasSearchBar && (
|
|
82
|
+
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
83
|
+
{search.isActive ? (
|
|
84
|
+
<text>
|
|
85
|
+
<span fg="green">{"Filter: "}</span>
|
|
86
|
+
<span fg="white">{search.query}</span>
|
|
87
|
+
<span bg="white" fg="black"> </span>
|
|
88
|
+
</text>
|
|
95
89
|
) : (
|
|
96
|
-
|
|
97
|
-
|
|
90
|
+
<text>
|
|
91
|
+
<span fg="green">{"Filter: "}</span>
|
|
92
|
+
<span fg="yellow">{search.query}</span>
|
|
93
|
+
<span fg="gray"> (Esc to clear)</span>
|
|
94
|
+
</text>
|
|
98
95
|
)}
|
|
99
96
|
</box>
|
|
97
|
+
)}
|
|
98
|
+
|
|
99
|
+
{/* Separator below header */}
|
|
100
|
+
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
101
|
+
<text fg="#444444">{"─".repeat(lineWidth)}</text>
|
|
100
102
|
</box>
|
|
101
103
|
|
|
102
104
|
{/* Main content area */}
|
|
@@ -112,11 +114,7 @@ export function ScreenLayout({
|
|
|
112
114
|
</box>
|
|
113
115
|
|
|
114
116
|
{/* Vertical separator */}
|
|
115
|
-
<box
|
|
116
|
-
flexDirection="column"
|
|
117
|
-
width={1}
|
|
118
|
-
height={panelHeight}
|
|
119
|
-
>
|
|
117
|
+
<box flexDirection="column" width={1} height={panelHeight}>
|
|
120
118
|
<text fg="#444444">{"│".repeat(panelHeight)}</text>
|
|
121
119
|
</box>
|
|
122
120
|
|
|
@@ -132,13 +130,8 @@ export function ScreenLayout({
|
|
|
132
130
|
</box>
|
|
133
131
|
|
|
134
132
|
{/* Footer */}
|
|
135
|
-
<box
|
|
136
|
-
height={1}
|
|
137
|
-
flexDirection="row"
|
|
138
|
-
justifyContent="space-between"
|
|
139
|
-
>
|
|
133
|
+
<box height={1} paddingLeft={1}>
|
|
140
134
|
<text fg="gray">{footerHints}</text>
|
|
141
|
-
<TabBar currentScreen={currentScreen} />
|
|
142
135
|
</box>
|
|
143
136
|
</box>
|
|
144
137
|
);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import React, { useState } from "react";
|
|
2
3
|
import { useApp } from "../../state/AppContext.js";
|
|
3
4
|
import { useKeyboard } from "../../hooks/useKeyboard.js";
|
|
4
5
|
import { ConfirmModal } from "./ConfirmModal.js";
|
|
@@ -8,25 +9,56 @@ import { MessageModal } from "./MessageModal.js";
|
|
|
8
9
|
import { LoadingModal } from "./LoadingModal.js";
|
|
9
10
|
/**
|
|
10
11
|
* Container that renders the active modal as an overlay
|
|
11
|
-
* Handles
|
|
12
|
+
* Handles ALL keyboard events when a modal is open to avoid
|
|
13
|
+
* conflicts with multiple useKeyboard hooks in child components
|
|
12
14
|
*/
|
|
13
15
|
export function ModalContainer() {
|
|
14
16
|
const { state } = useApp();
|
|
15
17
|
const { modal } = state;
|
|
16
|
-
//
|
|
18
|
+
// Track select modal index here (lifted from SelectModal)
|
|
19
|
+
const [selectIndex, setSelectIndex] = useState(0);
|
|
20
|
+
// Reset select index when modal changes
|
|
21
|
+
const modalRef = React.useRef(modal);
|
|
22
|
+
if (modal !== modalRef.current) {
|
|
23
|
+
modalRef.current = modal;
|
|
24
|
+
if (modal?.type === "select") {
|
|
25
|
+
setSelectIndex(modal.defaultIndex ?? 0);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Handle ALL keyboard events for modals
|
|
17
29
|
useKeyboard((key) => {
|
|
18
|
-
if (
|
|
19
|
-
|
|
20
|
-
|
|
30
|
+
if (!modal)
|
|
31
|
+
return;
|
|
32
|
+
if (modal.type === "loading")
|
|
33
|
+
return;
|
|
34
|
+
// Escape — close any modal
|
|
35
|
+
if (key.name === "escape" || key.name === "q") {
|
|
36
|
+
if (modal.type === "confirm")
|
|
21
37
|
modal.onCancel();
|
|
22
|
-
|
|
23
|
-
else if (modal.type === "input") {
|
|
38
|
+
else if (modal.type === "input")
|
|
24
39
|
modal.onCancel();
|
|
25
|
-
|
|
26
|
-
else if (modal.type === "select") {
|
|
40
|
+
else if (modal.type === "select")
|
|
27
41
|
modal.onCancel();
|
|
42
|
+
else if (modal.type === "message")
|
|
43
|
+
modal.onDismiss();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
// Select modal — handle navigation and selection
|
|
47
|
+
if (modal.type === "select") {
|
|
48
|
+
if (key.name === "return" || key.name === "enter") {
|
|
49
|
+
modal.onSelect(modal.options[selectIndex].value);
|
|
50
|
+
}
|
|
51
|
+
else if (key.name === "up" || key.name === "k") {
|
|
52
|
+
setSelectIndex((prev) => Math.max(0, prev - 1));
|
|
28
53
|
}
|
|
29
|
-
else if (
|
|
54
|
+
else if (key.name === "down" || key.name === "j") {
|
|
55
|
+
setSelectIndex((prev) => Math.min(modal.options.length - 1, prev + 1));
|
|
56
|
+
}
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Message modal — Enter to dismiss
|
|
60
|
+
if (modal.type === "message") {
|
|
61
|
+
if (key.name === "return" || key.name === "enter") {
|
|
30
62
|
modal.onDismiss();
|
|
31
63
|
}
|
|
32
64
|
}
|
|
@@ -41,7 +73,7 @@ export function ModalContainer() {
|
|
|
41
73
|
case "input":
|
|
42
74
|
return (_jsx(InputModal, { title: modal.title, label: modal.label, defaultValue: modal.defaultValue, onSubmit: modal.onSubmit, onCancel: modal.onCancel }));
|
|
43
75
|
case "select":
|
|
44
|
-
return (_jsx(SelectModal, { title: modal.title, message: modal.message, options: modal.options, onSelect: modal.onSelect, onCancel: modal.onCancel }));
|
|
76
|
+
return (_jsx(SelectModal, { title: modal.title, message: modal.message, options: modal.options, defaultIndex: selectIndex, onSelect: modal.onSelect, onCancel: modal.onCancel }));
|
|
45
77
|
case "message":
|
|
46
78
|
return (_jsx(MessageModal, { title: modal.title, message: modal.message, variant: modal.variant, onDismiss: modal.onDismiss }));
|
|
47
79
|
case "loading":
|