claudeup 3.7.2 → 3.8.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/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/types/index.ts +34 -0
- package/src/ui/App.js +17 -18
- package/src/ui/App.tsx +21 -23
- package/src/ui/components/TabBar.js +8 -8
- package/src/ui/components/TabBar.tsx +14 -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/StatusLineScreen.js +2 -2
- package/src/ui/screens/StatusLineScreen.tsx +10 -12
- package/src/ui/screens/index.js +2 -2
- package/src/ui/screens/index.ts +2 -2
- package/src/ui/state/AppContext.js +2 -1
- package/src/ui/state/AppContext.tsx +2 -0
- package/src/ui/state/reducer.js +63 -19
- package/src/ui/state/reducer.ts +68 -19
- package/src/ui/state/types.ts +33 -14
- package/src/utils/clipboard.js +56 -0
- package/src/utils/clipboard.ts +58 -0
package/src/types/index.ts
CHANGED
|
@@ -73,6 +73,15 @@ export interface MarketplaceSource {
|
|
|
73
73
|
autoUpdate?: boolean; // Enable auto-update for this marketplace (default: true for new marketplaces)
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
export interface ClaudeHookEntry {
|
|
77
|
+
type: string;
|
|
78
|
+
command: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface ClaudeHookGroup {
|
|
82
|
+
hooks: ClaudeHookEntry[];
|
|
83
|
+
}
|
|
84
|
+
|
|
76
85
|
export interface ClaudeSettings {
|
|
77
86
|
enabledMcpServers?: Record<string, boolean>;
|
|
78
87
|
mcpServers?: Record<string, McpServerConfig>;
|
|
@@ -80,6 +89,7 @@ export interface ClaudeSettings {
|
|
|
80
89
|
extraKnownMarketplaces?: Record<string, MarketplaceSource>;
|
|
81
90
|
installedPluginVersions?: Record<string, string>;
|
|
82
91
|
statusLine?: string;
|
|
92
|
+
hooks?: Record<string, ClaudeHookGroup[]>;
|
|
83
93
|
}
|
|
84
94
|
|
|
85
95
|
export interface McpServerConfig {
|
|
@@ -139,3 +149,27 @@ export interface InstalledPluginsRegistry {
|
|
|
139
149
|
version: number;
|
|
140
150
|
plugins: Record<string, InstalledPluginEntry[]>;
|
|
141
151
|
}
|
|
152
|
+
|
|
153
|
+
// ─── Plugin Profile Types ──────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
export interface Profile {
|
|
156
|
+
name: string;
|
|
157
|
+
plugins: Record<string, boolean>;
|
|
158
|
+
createdAt: string;
|
|
159
|
+
updatedAt: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface ProfilesFile {
|
|
163
|
+
version: number;
|
|
164
|
+
profiles: Record<string, Profile>;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** A profile with its id and scope, as used in UI lists */
|
|
168
|
+
export interface ProfileEntry {
|
|
169
|
+
id: string;
|
|
170
|
+
name: string;
|
|
171
|
+
plugins: Record<string, boolean>;
|
|
172
|
+
createdAt: string;
|
|
173
|
+
updatedAt: string;
|
|
174
|
+
scope: "user" | "project";
|
|
175
|
+
}
|
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, } 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,14 @@ 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
37
|
default:
|
|
38
38
|
return _jsx(PluginsScreen, {});
|
|
39
39
|
}
|
|
@@ -80,10 +80,10 @@ function GlobalKeyHandler({ onDebugToggle, onExit, }) {
|
|
|
80
80
|
"plugins",
|
|
81
81
|
"mcp",
|
|
82
82
|
"mcp-registry",
|
|
83
|
-
"
|
|
84
|
-
"env-vars",
|
|
83
|
+
"settings",
|
|
85
84
|
"cli-tools",
|
|
86
85
|
"model-selector",
|
|
86
|
+
"profiles",
|
|
87
87
|
].includes(state.currentRoute.screen);
|
|
88
88
|
if (isTopLevel) {
|
|
89
89
|
if (input === "1")
|
|
@@ -91,19 +91,19 @@ function GlobalKeyHandler({ onDebugToggle, onExit, }) {
|
|
|
91
91
|
else if (input === "2")
|
|
92
92
|
navigateToScreen("mcp");
|
|
93
93
|
else if (input === "3")
|
|
94
|
-
navigateToScreen("
|
|
94
|
+
navigateToScreen("settings");
|
|
95
95
|
else if (input === "4")
|
|
96
|
-
navigateToScreen("env-vars");
|
|
97
|
-
else if (input === "5")
|
|
98
96
|
navigateToScreen("cli-tools");
|
|
97
|
+
else if (input === "5")
|
|
98
|
+
navigateToScreen("profiles");
|
|
99
99
|
// Tab navigation cycling
|
|
100
100
|
if (key.tab) {
|
|
101
101
|
const screens = [
|
|
102
102
|
"plugins",
|
|
103
103
|
"mcp",
|
|
104
|
-
"
|
|
105
|
-
"env-vars",
|
|
104
|
+
"settings",
|
|
106
105
|
"cli-tools",
|
|
106
|
+
"profiles",
|
|
107
107
|
];
|
|
108
108
|
const currentIndex = screens.indexOf(state.currentRoute.screen);
|
|
109
109
|
if (currentIndex !== -1) {
|
|
@@ -138,9 +138,9 @@ function GlobalKeyHandler({ onDebugToggle, onExit, }) {
|
|
|
138
138
|
? This help
|
|
139
139
|
|
|
140
140
|
Quick Navigation
|
|
141
|
-
1 Plugins 4
|
|
142
|
-
2 MCP Servers 5
|
|
143
|
-
3
|
|
141
|
+
1 Plugins 4 CLI Tools
|
|
142
|
+
2 MCP Servers 5 Profiles
|
|
143
|
+
3 Settings
|
|
144
144
|
|
|
145
145
|
Plugin Actions
|
|
146
146
|
u Update d Uninstall
|
|
@@ -157,7 +157,7 @@ MCP Servers
|
|
|
157
157
|
* UpdateBanner Component
|
|
158
158
|
* Shows version update notification
|
|
159
159
|
*/
|
|
160
|
-
function UpdateBanner({ result
|
|
160
|
+
function UpdateBanner({ result }) {
|
|
161
161
|
if (!result.updateAvailable)
|
|
162
162
|
return null;
|
|
163
163
|
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 +183,7 @@ function AppContentInner({ showDebug, onDebugToggle, updateInfo, onExit, }) {
|
|
|
183
183
|
state: { message: "Scanning marketplaces..." },
|
|
184
184
|
});
|
|
185
185
|
// Migrate old marketplace names → magus (idempotent), then repair plugin.json files
|
|
186
|
-
migrateMarketplaceRename()
|
|
187
|
-
.catch(() => { }); // non-blocking, best-effort
|
|
186
|
+
migrateMarketplaceRename().catch(() => { }); // non-blocking, best-effort
|
|
188
187
|
repairAllMarketplaces()
|
|
189
188
|
.then(async () => {
|
|
190
189
|
dispatch({ type: "HIDE_PROGRESS" });
|
package/src/ui/App.tsx
CHANGED
|
@@ -16,10 +16,10 @@ import {
|
|
|
16
16
|
PluginsScreen,
|
|
17
17
|
McpScreen,
|
|
18
18
|
McpRegistryScreen,
|
|
19
|
-
|
|
20
|
-
EnvVarsScreen,
|
|
19
|
+
SettingsScreen,
|
|
21
20
|
CliToolsScreen,
|
|
22
21
|
ModelSelectorScreen,
|
|
22
|
+
ProfilesScreen,
|
|
23
23
|
} from "./screens/index.js";
|
|
24
24
|
import type { Screen } from "./state/types.js";
|
|
25
25
|
import { repairAllMarketplaces } from "../services/local-marketplace.js";
|
|
@@ -49,14 +49,14 @@ function Router() {
|
|
|
49
49
|
return <McpScreen />;
|
|
50
50
|
case "mcp-registry":
|
|
51
51
|
return <McpRegistryScreen />;
|
|
52
|
-
case "
|
|
53
|
-
return <
|
|
54
|
-
case "env-vars":
|
|
55
|
-
return <EnvVarsScreen />;
|
|
52
|
+
case "settings":
|
|
53
|
+
return <SettingsScreen />;
|
|
56
54
|
case "cli-tools":
|
|
57
55
|
return <CliToolsScreen />;
|
|
58
56
|
case "model-selector":
|
|
59
57
|
return <ModelSelectorScreen />;
|
|
58
|
+
case "profiles":
|
|
59
|
+
return <ProfilesScreen />;
|
|
60
60
|
default:
|
|
61
61
|
return <PluginsScreen />;
|
|
62
62
|
}
|
|
@@ -112,27 +112,27 @@ function GlobalKeyHandler({
|
|
|
112
112
|
"plugins",
|
|
113
113
|
"mcp",
|
|
114
114
|
"mcp-registry",
|
|
115
|
-
"
|
|
116
|
-
"env-vars",
|
|
115
|
+
"settings",
|
|
117
116
|
"cli-tools",
|
|
118
117
|
"model-selector",
|
|
118
|
+
"profiles",
|
|
119
119
|
].includes(state.currentRoute.screen);
|
|
120
120
|
|
|
121
121
|
if (isTopLevel) {
|
|
122
122
|
if (input === "1") navigateToScreen("plugins");
|
|
123
123
|
else if (input === "2") navigateToScreen("mcp");
|
|
124
|
-
else if (input === "3") navigateToScreen("
|
|
125
|
-
else if (input === "4") navigateToScreen("
|
|
126
|
-
else if (input === "5") navigateToScreen("
|
|
124
|
+
else if (input === "3") navigateToScreen("settings");
|
|
125
|
+
else if (input === "4") navigateToScreen("cli-tools");
|
|
126
|
+
else if (input === "5") navigateToScreen("profiles");
|
|
127
127
|
|
|
128
128
|
// Tab navigation cycling
|
|
129
129
|
if (key.tab) {
|
|
130
130
|
const screens: Screen[] = [
|
|
131
131
|
"plugins",
|
|
132
132
|
"mcp",
|
|
133
|
-
"
|
|
134
|
-
"env-vars",
|
|
133
|
+
"settings",
|
|
135
134
|
"cli-tools",
|
|
135
|
+
"profiles",
|
|
136
136
|
];
|
|
137
137
|
const currentIndex = screens.indexOf(
|
|
138
138
|
state.currentRoute.screen as Screen,
|
|
@@ -171,9 +171,9 @@ function GlobalKeyHandler({
|
|
|
171
171
|
? This help
|
|
172
172
|
|
|
173
173
|
Quick Navigation
|
|
174
|
-
1 Plugins 4
|
|
175
|
-
2 MCP Servers 5
|
|
176
|
-
3
|
|
174
|
+
1 Plugins 4 CLI Tools
|
|
175
|
+
2 MCP Servers 5 Profiles
|
|
176
|
+
3 Settings
|
|
177
177
|
|
|
178
178
|
Plugin Actions
|
|
179
179
|
u Update d Uninstall
|
|
@@ -194,13 +194,11 @@ MCP Servers
|
|
|
194
194
|
* UpdateBanner Component
|
|
195
195
|
* Shows version update notification
|
|
196
196
|
*/
|
|
197
|
-
function UpdateBanner({
|
|
198
|
-
result,
|
|
199
|
-
}: { result: VersionCheckResult }) {
|
|
197
|
+
function UpdateBanner({ result }: { result: VersionCheckResult }) {
|
|
200
198
|
if (!result.updateAvailable) return null;
|
|
201
199
|
|
|
202
200
|
return (
|
|
203
|
-
<box paddingLeft={1} paddingRight={1}
|
|
201
|
+
<box paddingLeft={1} paddingRight={1}>
|
|
204
202
|
<text bg="yellow" fg="black">
|
|
205
203
|
<strong> UPDATE </strong>
|
|
206
204
|
</text>
|
|
@@ -266,8 +264,7 @@ function AppContentInner({
|
|
|
266
264
|
});
|
|
267
265
|
|
|
268
266
|
// Migrate old marketplace names → magus (idempotent), then repair plugin.json files
|
|
269
|
-
migrateMarketplaceRename()
|
|
270
|
-
.catch(() => {}); // non-blocking, best-effort
|
|
267
|
+
migrateMarketplaceRename().catch(() => {}); // non-blocking, best-effort
|
|
271
268
|
|
|
272
269
|
repairAllMarketplaces()
|
|
273
270
|
.then(async () => {
|
|
@@ -295,7 +292,8 @@ function AppContentInner({
|
|
|
295
292
|
<box
|
|
296
293
|
flexDirection="column"
|
|
297
294
|
height={dimensions.contentHeight}
|
|
298
|
-
paddingLeft={1}
|
|
295
|
+
paddingLeft={1}
|
|
296
|
+
paddingRight={1}
|
|
299
297
|
>
|
|
300
298
|
<Router />
|
|
301
299
|
</box>
|
|
@@ -1,13 +1,13 @@
|
|
|
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
9
|
];
|
|
10
|
-
export function TabBar({ currentScreen, onTabChange
|
|
10
|
+
export function TabBar({ currentScreen, onTabChange }) {
|
|
11
11
|
// Handle number key shortcuts (1-5)
|
|
12
12
|
useKeyboardHandler((input, key) => {
|
|
13
13
|
if (!onTabChange)
|
|
@@ -32,7 +32,7 @@ export function TabBar({ currentScreen, onTabChange, }) {
|
|
|
32
32
|
return (_jsx("box", { flexDirection: "row", gap: 0, children: TABS.map((tab, index) => {
|
|
33
33
|
const isSelected = tab.screen === currentScreen;
|
|
34
34
|
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: [
|
|
35
|
+
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
36
|
}) }));
|
|
37
37
|
}
|
|
38
38
|
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,11 @@ 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
17
|
];
|
|
18
18
|
|
|
19
19
|
interface TabBarProps {
|
|
@@ -21,10 +21,7 @@ interface TabBarProps {
|
|
|
21
21
|
onTabChange?: (screen: Screen) => void;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export function TabBar({
|
|
25
|
-
currentScreen,
|
|
26
|
-
onTabChange,
|
|
27
|
-
}: TabBarProps) {
|
|
24
|
+
export function TabBar({ currentScreen, onTabChange }: TabBarProps) {
|
|
28
25
|
// Handle number key shortcuts (1-5)
|
|
29
26
|
useKeyboardHandler((input, key) => {
|
|
30
27
|
if (!onTabChange) return;
|
|
@@ -61,23 +58,21 @@ export function TabBar({
|
|
|
61
58
|
<box>
|
|
62
59
|
<text bg="#7e57c2" fg="white">
|
|
63
60
|
<strong>
|
|
64
|
-
{
|
|
65
|
-
{tab.key}:{tab.label}{
|
|
61
|
+
{" "}
|
|
62
|
+
{tab.key}:{tab.label}{" "}
|
|
66
63
|
</strong>
|
|
67
64
|
</text>
|
|
68
65
|
</box>
|
|
69
66
|
) : (
|
|
70
67
|
<box>
|
|
71
68
|
<text fg="gray">
|
|
72
|
-
{
|
|
73
|
-
{tab.key}:{tab.label}{
|
|
69
|
+
{" "}
|
|
70
|
+
{tab.key}:{tab.label}{" "}
|
|
74
71
|
</text>
|
|
75
72
|
</box>
|
|
76
73
|
)}
|
|
77
74
|
{/* Separator */}
|
|
78
|
-
{!isLast &&
|
|
79
|
-
<text fg="#666666">│</text>
|
|
80
|
-
)}
|
|
75
|
+
{!isLast && <text fg="#666666">│</text>}
|
|
81
76
|
</box>
|
|
82
77
|
);
|
|
83
78
|
})}
|
|
@@ -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":
|