claudeup 4.17.0 → 4.19.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/__tests__/alias-adopt.test.ts +364 -0
- package/src/__tests__/alias-parser.test.ts +409 -0
- package/src/__tests__/alias-shell-writer.test.ts +668 -0
- package/src/__tests__/alias-store.test.ts +163 -0
- package/src/__tests__/gitignore-fixer.test.ts +64 -1
- package/src/__tests__/gitignore-prerun.test.ts +2 -2
- package/src/__tests__/gitignore-service.test.ts +42 -0
- package/src/__tests__/marketplaces.test.ts +40 -0
- package/src/__tests__/plugin-manager-fallback.test.ts +120 -0
- package/src/__tests__/plugin-setup.test.ts +111 -0
- package/src/__tests__/useGitignoreModal.test.ts +2 -2
- package/src/data/alias-flags.js +205 -0
- package/src/data/alias-flags.ts +301 -0
- package/src/data/gitignore-reasons.js +97 -0
- package/src/data/gitignore-reasons.ts +103 -0
- package/src/data/marketplaces.js +5 -3
- package/src/data/marketplaces.ts +5 -4
- package/src/services/alias-settings.js +51 -0
- package/src/services/alias-settings.ts +63 -0
- package/src/services/alias-shell-writer.js +1018 -0
- package/src/services/alias-shell-writer.ts +1247 -0
- package/src/services/alias-store.js +129 -0
- package/src/services/alias-store.ts +172 -0
- package/src/services/gitignore-fixer.js +70 -10
- package/src/services/gitignore-fixer.ts +76 -9
- package/src/services/gitignore-prerun.js +3 -3
- package/src/services/gitignore-prerun.ts +3 -3
- package/src/services/gitignore-service.js +20 -2
- package/src/services/gitignore-service.ts +23 -1
- package/src/services/marketplace-fetcher.js +96 -0
- package/src/services/marketplace-fetcher.ts +137 -0
- package/src/services/plugin-manager.js +6 -59
- package/src/services/plugin-manager.ts +16 -91
- package/src/services/plugin-setup.js +59 -4
- package/src/services/plugin-setup.ts +61 -4
- package/src/services/skillsmp-client.js +29 -9
- package/src/services/skillsmp-client.ts +38 -8
- package/src/types/gitignore.ts +1 -1
- package/src/ui/App.js +26 -11
- package/src/ui/App.tsx +25 -10
- package/src/ui/components/FlagDetailEditor.js +0 -0
- package/src/ui/components/FlagDetailEditor.tsx +0 -0
- package/src/ui/components/TabBar.js +2 -1
- package/src/ui/components/TabBar.tsx +2 -1
- package/src/ui/components/layout/FooterHints.js +29 -0
- package/src/ui/components/layout/FooterHints.tsx +52 -0
- package/src/ui/components/layout/ScreenLayout.js +2 -1
- package/src/ui/components/layout/ScreenLayout.tsx +12 -3
- package/src/ui/components/layout/index.js +1 -0
- package/src/ui/components/layout/index.ts +5 -0
- package/src/ui/components/modals/ConfirmModal.js +1 -1
- package/src/ui/components/modals/ConfirmModal.tsx +1 -1
- package/src/ui/components/modals/SelectModal.js +8 -1
- package/src/ui/components/modals/SelectModal.tsx +12 -1
- package/src/ui/hooks/useGitignoreModal.js +7 -8
- package/src/ui/hooks/useGitignoreModal.ts +8 -9
- package/src/ui/renderers/gitignoreRenderers.js +36 -23
- package/src/ui/renderers/gitignoreRenderers.tsx +50 -41
- package/src/ui/screens/AliasScreen.js +1111 -0
- package/src/ui/screens/AliasScreen.tsx +1534 -0
- package/src/ui/screens/CliToolsScreen.js +6 -1
- package/src/ui/screens/CliToolsScreen.tsx +6 -1
- package/src/ui/screens/EnvVarsScreen.js +6 -1
- package/src/ui/screens/EnvVarsScreen.tsx +6 -1
- package/src/ui/screens/GitignoreScreen.js +189 -88
- package/src/ui/screens/GitignoreScreen.tsx +312 -132
- package/src/ui/screens/McpRegistryScreen.js +13 -2
- package/src/ui/screens/McpRegistryScreen.tsx +13 -2
- package/src/ui/screens/McpScreen.js +6 -1
- package/src/ui/screens/McpScreen.tsx +6 -1
- package/src/ui/screens/ModelSelectorScreen.js +8 -2
- package/src/ui/screens/ModelSelectorScreen.tsx +8 -2
- package/src/ui/screens/PluginsScreen.js +17 -3
- package/src/ui/screens/PluginsScreen.tsx +16 -3
- package/src/ui/screens/ProfilesScreen.js +8 -1
- package/src/ui/screens/ProfilesScreen.tsx +8 -1
- package/src/ui/screens/SkillsScreen.js +21 -4
- package/src/ui/screens/SkillsScreen.tsx +39 -5
- package/src/ui/screens/StatusLineScreen.js +7 -1
- package/src/ui/screens/StatusLineScreen.tsx +7 -1
- package/src/ui/screens/index.js +1 -0
- package/src/ui/screens/index.ts +1 -0
- package/src/ui/state/reducer.js +5 -1
- package/src/ui/state/reducer.ts +6 -1
- package/src/ui/state/types.ts +12 -2
|
@@ -7,10 +7,28 @@
|
|
|
7
7
|
* - GitHub Tree API with ETag caching
|
|
8
8
|
* - Recommended skills list
|
|
9
9
|
*/
|
|
10
|
-
//
|
|
10
|
+
// Deployed `skills` Gen2 Cloud Function (project claudish-6da10, us-central1).
|
|
11
|
+
// Firebase keeps this cloudfunctions.net alias stable for the function.
|
|
11
12
|
const SKILLS_API_BASE = process.env.SKILLS_API_URL || "https://us-central1-claudish-6da10.cloudfunctions.net/skills";
|
|
12
13
|
/**
|
|
13
|
-
*
|
|
14
|
+
* Thrown when the skills API is unreachable or returns a non-OK status.
|
|
15
|
+
* Lets callers distinguish "search service is down" from "no skills matched",
|
|
16
|
+
* which a bare `[]` return cannot express.
|
|
17
|
+
*/
|
|
18
|
+
export class SkillsApiError extends Error {
|
|
19
|
+
status;
|
|
20
|
+
constructor(message, status) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.status = status;
|
|
23
|
+
this.name = "SkillsApiError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Search skills across all sources (SkillsMP keyword + AI search).
|
|
28
|
+
*
|
|
29
|
+
* Throws {@link SkillsApiError} on transport/HTTP failure so the caller can
|
|
30
|
+
* surface a distinct "search unavailable" state. An empty array means the
|
|
31
|
+
* query genuinely matched nothing.
|
|
14
32
|
*/
|
|
15
33
|
export async function searchSkills(query, options) {
|
|
16
34
|
const params = new URLSearchParams({ q: query });
|
|
@@ -18,18 +36,20 @@ export async function searchSkills(query, options) {
|
|
|
18
36
|
params.set("page", String(options.page));
|
|
19
37
|
if (options?.limit)
|
|
20
38
|
params.set("limit", String(options.limit));
|
|
39
|
+
let res;
|
|
21
40
|
try {
|
|
22
|
-
|
|
41
|
+
res = await fetch(`${SKILLS_API_BASE}/search?${params}`, {
|
|
23
42
|
signal: AbortSignal.timeout(12000),
|
|
24
43
|
});
|
|
25
|
-
if (!res.ok)
|
|
26
|
-
return [];
|
|
27
|
-
const data = (await res.json());
|
|
28
|
-
return data.skills || [];
|
|
29
44
|
}
|
|
30
|
-
catch {
|
|
31
|
-
|
|
45
|
+
catch (cause) {
|
|
46
|
+
throw new SkillsApiError(`Skills search request failed: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
47
|
+
}
|
|
48
|
+
if (!res.ok) {
|
|
49
|
+
throw new SkillsApiError(`Skills search returned HTTP ${res.status}`, res.status);
|
|
32
50
|
}
|
|
51
|
+
const data = (await res.json());
|
|
52
|
+
return data.skills || [];
|
|
33
53
|
}
|
|
34
54
|
/**
|
|
35
55
|
* Fetch skills from a specific GitHub repo (via our proxy)
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* - Recommended skills list
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
//
|
|
11
|
+
// Deployed `skills` Gen2 Cloud Function (project claudish-6da10, us-central1).
|
|
12
|
+
// Firebase keeps this cloudfunctions.net alias stable for the function.
|
|
12
13
|
const SKILLS_API_BASE =
|
|
13
14
|
process.env.SKILLS_API_URL || "https://us-central1-claudish-6da10.cloudfunctions.net/skills";
|
|
14
15
|
|
|
@@ -31,7 +32,26 @@ export interface RepoSkillResult {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
|
-
*
|
|
35
|
+
* Thrown when the skills API is unreachable or returns a non-OK status.
|
|
36
|
+
* Lets callers distinguish "search service is down" from "no skills matched",
|
|
37
|
+
* which a bare `[]` return cannot express.
|
|
38
|
+
*/
|
|
39
|
+
export class SkillsApiError extends Error {
|
|
40
|
+
constructor(
|
|
41
|
+
message: string,
|
|
42
|
+
readonly status?: number,
|
|
43
|
+
) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.name = "SkillsApiError";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Search skills across all sources (SkillsMP keyword + AI search).
|
|
51
|
+
*
|
|
52
|
+
* Throws {@link SkillsApiError} on transport/HTTP failure so the caller can
|
|
53
|
+
* surface a distinct "search unavailable" state. An empty array means the
|
|
54
|
+
* query genuinely matched nothing.
|
|
35
55
|
*/
|
|
36
56
|
export async function searchSkills(
|
|
37
57
|
query: string,
|
|
@@ -41,16 +61,26 @@ export async function searchSkills(
|
|
|
41
61
|
if (options?.page) params.set("page", String(options.page));
|
|
42
62
|
if (options?.limit) params.set("limit", String(options.limit));
|
|
43
63
|
|
|
64
|
+
let res: Response;
|
|
44
65
|
try {
|
|
45
|
-
|
|
66
|
+
res = await fetch(`${SKILLS_API_BASE}/search?${params}`, {
|
|
46
67
|
signal: AbortSignal.timeout(12000),
|
|
47
68
|
});
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return [];
|
|
69
|
+
} catch (cause) {
|
|
70
|
+
throw new SkillsApiError(
|
|
71
|
+
`Skills search request failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
72
|
+
);
|
|
53
73
|
}
|
|
74
|
+
|
|
75
|
+
if (!res.ok) {
|
|
76
|
+
throw new SkillsApiError(
|
|
77
|
+
`Skills search returned HTTP ${res.status}`,
|
|
78
|
+
res.status,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const data = (await res.json()) as { skills?: SkillSearchResult[] };
|
|
83
|
+
return data.skills || [];
|
|
54
84
|
}
|
|
55
85
|
|
|
56
86
|
/**
|
package/src/types/gitignore.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
export type RuleAction = "ignore" | "track";
|
|
8
8
|
|
|
9
|
-
export type Tier = "builtin" | "global" | "project";
|
|
9
|
+
export type Tier = "builtin" | "global" | "project" | "profile";
|
|
10
10
|
|
|
11
11
|
/** A user-authored manifest, deserialized from JSON. */
|
|
12
12
|
export interface GitignoreManifest {
|
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, SettingsScreen, CliToolsScreen, ModelSelectorScreen, ProfilesScreen, SkillsScreen, GitignoreScreen, } from "./screens/index.js";
|
|
8
|
+
import { PluginsScreen, McpScreen, McpRegistryScreen, SettingsScreen, CliToolsScreen, ModelSelectorScreen, ProfilesScreen, SkillsScreen, GitignoreScreen, AliasScreen, } from "./screens/index.js";
|
|
9
9
|
import { checkGitignore } from "../services/gitignore-prerun.js";
|
|
10
10
|
import { loadGitignoreState, applyAllSafeFixes, } from "../services/gitignore-service.js";
|
|
11
11
|
import { useGitignoreModal } from "./hooks/useGitignoreModal.js";
|
|
@@ -45,13 +45,15 @@ function Router() {
|
|
|
45
45
|
return _jsx(SkillsScreen, {});
|
|
46
46
|
case "gitignore":
|
|
47
47
|
return _jsx(GitignoreScreen, {});
|
|
48
|
+
case "alias":
|
|
49
|
+
return _jsx(AliasScreen, {});
|
|
48
50
|
default:
|
|
49
51
|
return _jsx(PluginsScreen, {});
|
|
50
52
|
}
|
|
51
53
|
}
|
|
52
54
|
/**
|
|
53
55
|
* GlobalKeyHandler Component
|
|
54
|
-
* Handles global keyboard shortcuts (1-
|
|
56
|
+
* Handles global keyboard shortcuts (1-8, Tab, Escape, q, Ctrl+C, ?, Shift+D)
|
|
55
57
|
* Does not render anything (returns null)
|
|
56
58
|
*/
|
|
57
59
|
function GlobalKeyHandler({ onDebugToggle, onExit, }) {
|
|
@@ -86,7 +88,7 @@ function GlobalKeyHandler({ onDebugToggle, onExit, }) {
|
|
|
86
88
|
// Don't handle keys when modal is open or searching
|
|
87
89
|
if (state.modal || state.isSearching)
|
|
88
90
|
return;
|
|
89
|
-
// Global navigation shortcuts (1-
|
|
91
|
+
// Global navigation shortcuts (1-8) - include mcp-registry as it's a sub-screen of mcp
|
|
90
92
|
const isTopLevel = [
|
|
91
93
|
"plugins",
|
|
92
94
|
"mcp",
|
|
@@ -97,6 +99,7 @@ function GlobalKeyHandler({ onDebugToggle, onExit, }) {
|
|
|
97
99
|
"profiles",
|
|
98
100
|
"skills",
|
|
99
101
|
"gitignore",
|
|
102
|
+
"alias",
|
|
100
103
|
].includes(state.currentRoute.screen);
|
|
101
104
|
if (isTopLevel) {
|
|
102
105
|
if (input === "1")
|
|
@@ -113,6 +116,8 @@ function GlobalKeyHandler({ onDebugToggle, onExit, }) {
|
|
|
113
116
|
navigateToScreen("cli-tools");
|
|
114
117
|
else if (input === "7")
|
|
115
118
|
navigateToScreen("gitignore");
|
|
119
|
+
else if (input === "8")
|
|
120
|
+
navigateToScreen("alias");
|
|
116
121
|
// Tab navigation cycling
|
|
117
122
|
if (key.tab) {
|
|
118
123
|
const screens = [
|
|
@@ -123,6 +128,7 @@ function GlobalKeyHandler({ onDebugToggle, onExit, }) {
|
|
|
123
128
|
"profiles",
|
|
124
129
|
"cli-tools",
|
|
125
130
|
"gitignore",
|
|
131
|
+
"alias",
|
|
126
132
|
];
|
|
127
133
|
const currentIndex = screens.indexOf(state.currentRoute.screen);
|
|
128
134
|
if (currentIndex !== -1) {
|
|
@@ -133,18 +139,26 @@ function GlobalKeyHandler({ onDebugToggle, onExit, }) {
|
|
|
133
139
|
}
|
|
134
140
|
}
|
|
135
141
|
}
|
|
136
|
-
//
|
|
137
|
-
if (key.
|
|
142
|
+
// q or Ctrl+C quits from anywhere — no need to bounce to home first.
|
|
143
|
+
if (input === "q" || (key.ctrl && input === "c")) {
|
|
144
|
+
onExit();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
// Escape goes back: registry → mcp, any other tab → plugins home,
|
|
148
|
+
// already-home → exit. But when a screen has a focused sub-view open
|
|
149
|
+
// (e.g. the Alias flag-detail editor), that sub-view owns Escape and
|
|
150
|
+
// backs out to itself — don't let the global handler jump to home.
|
|
151
|
+
// Scoped to the Escape branch ONLY so q/Ctrl+C still quit from anywhere.
|
|
152
|
+
if (key.escape) {
|
|
153
|
+
if (state.detailActive)
|
|
154
|
+
return;
|
|
138
155
|
if (state.currentRoute.screen === "plugins") {
|
|
139
|
-
// On home screen, exit immediately
|
|
140
156
|
onExit();
|
|
141
157
|
}
|
|
142
158
|
else if (state.currentRoute.screen === "mcp-registry") {
|
|
143
|
-
// Go back to MCP from registry
|
|
144
159
|
navigateToScreen("mcp");
|
|
145
160
|
}
|
|
146
161
|
else {
|
|
147
|
-
// Go back to plugins (home)
|
|
148
162
|
navigateToScreen("plugins");
|
|
149
163
|
}
|
|
150
164
|
}
|
|
@@ -153,12 +167,13 @@ function GlobalKeyHandler({ onDebugToggle, onExit, }) {
|
|
|
153
167
|
modal.message("claudeup Help", `Navigation
|
|
154
168
|
↑/↓ or j/k Move selection
|
|
155
169
|
Enter Select / Toggle
|
|
156
|
-
Escape
|
|
170
|
+
Escape Back (to home)
|
|
171
|
+
q / Ctrl+C Quit (from anywhere)
|
|
157
172
|
? This help
|
|
158
173
|
|
|
159
174
|
Quick Navigation
|
|
160
|
-
1 Plugins 4 Settings 7
|
|
161
|
-
2 Skills 5 Profiles
|
|
175
|
+
1 Plugins 4 Settings 7 Git State
|
|
176
|
+
2 Skills 5 Profiles 8 Alias
|
|
162
177
|
3 MCP Servers 6 CLI Tools
|
|
163
178
|
|
|
164
179
|
Plugin Actions
|
package/src/ui/App.tsx
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
ProfilesScreen,
|
|
23
23
|
SkillsScreen,
|
|
24
24
|
GitignoreScreen,
|
|
25
|
+
AliasScreen,
|
|
25
26
|
} from "./screens/index.js";
|
|
26
27
|
import type { Screen } from "./state/types.js";
|
|
27
28
|
import { checkGitignore } from "../services/gitignore-prerun.js";
|
|
@@ -79,6 +80,8 @@ function Router() {
|
|
|
79
80
|
return <SkillsScreen />;
|
|
80
81
|
case "gitignore":
|
|
81
82
|
return <GitignoreScreen />;
|
|
83
|
+
case "alias":
|
|
84
|
+
return <AliasScreen />;
|
|
82
85
|
default:
|
|
83
86
|
return <PluginsScreen />;
|
|
84
87
|
}
|
|
@@ -86,7 +89,7 @@ function Router() {
|
|
|
86
89
|
|
|
87
90
|
/**
|
|
88
91
|
* GlobalKeyHandler Component
|
|
89
|
-
* Handles global keyboard shortcuts (1-
|
|
92
|
+
* Handles global keyboard shortcuts (1-8, Tab, Escape, q, Ctrl+C, ?, Shift+D)
|
|
90
93
|
* Does not render anything (returns null)
|
|
91
94
|
*/
|
|
92
95
|
function GlobalKeyHandler({
|
|
@@ -129,7 +132,7 @@ function GlobalKeyHandler({
|
|
|
129
132
|
// Don't handle keys when modal is open or searching
|
|
130
133
|
if (state.modal || state.isSearching) return;
|
|
131
134
|
|
|
132
|
-
// Global navigation shortcuts (1-
|
|
135
|
+
// Global navigation shortcuts (1-8) - include mcp-registry as it's a sub-screen of mcp
|
|
133
136
|
const isTopLevel = [
|
|
134
137
|
"plugins",
|
|
135
138
|
"mcp",
|
|
@@ -140,6 +143,7 @@ function GlobalKeyHandler({
|
|
|
140
143
|
"profiles",
|
|
141
144
|
"skills",
|
|
142
145
|
"gitignore",
|
|
146
|
+
"alias",
|
|
143
147
|
].includes(state.currentRoute.screen);
|
|
144
148
|
|
|
145
149
|
if (isTopLevel) {
|
|
@@ -150,6 +154,7 @@ function GlobalKeyHandler({
|
|
|
150
154
|
else if (input === "5") navigateToScreen("profiles");
|
|
151
155
|
else if (input === "6") navigateToScreen("cli-tools");
|
|
152
156
|
else if (input === "7") navigateToScreen("gitignore");
|
|
157
|
+
else if (input === "8") navigateToScreen("alias");
|
|
153
158
|
|
|
154
159
|
// Tab navigation cycling
|
|
155
160
|
if (key.tab) {
|
|
@@ -161,6 +166,7 @@ function GlobalKeyHandler({
|
|
|
161
166
|
"profiles",
|
|
162
167
|
"cli-tools",
|
|
163
168
|
"gitignore",
|
|
169
|
+
"alias",
|
|
164
170
|
];
|
|
165
171
|
const currentIndex = screens.indexOf(
|
|
166
172
|
state.currentRoute.screen as Screen,
|
|
@@ -174,16 +180,24 @@ function GlobalKeyHandler({
|
|
|
174
180
|
}
|
|
175
181
|
}
|
|
176
182
|
|
|
177
|
-
//
|
|
178
|
-
if (key.
|
|
183
|
+
// q or Ctrl+C quits from anywhere — no need to bounce to home first.
|
|
184
|
+
if (input === "q" || (key.ctrl && input === "c")) {
|
|
185
|
+
onExit();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Escape goes back: registry → mcp, any other tab → plugins home,
|
|
190
|
+
// already-home → exit. But when a screen has a focused sub-view open
|
|
191
|
+
// (e.g. the Alias flag-detail editor), that sub-view owns Escape and
|
|
192
|
+
// backs out to itself — don't let the global handler jump to home.
|
|
193
|
+
// Scoped to the Escape branch ONLY so q/Ctrl+C still quit from anywhere.
|
|
194
|
+
if (key.escape) {
|
|
195
|
+
if (state.detailActive) return;
|
|
179
196
|
if (state.currentRoute.screen === "plugins") {
|
|
180
|
-
// On home screen, exit immediately
|
|
181
197
|
onExit();
|
|
182
198
|
} else if (state.currentRoute.screen === "mcp-registry") {
|
|
183
|
-
// Go back to MCP from registry
|
|
184
199
|
navigateToScreen("mcp");
|
|
185
200
|
} else {
|
|
186
|
-
// Go back to plugins (home)
|
|
187
201
|
navigateToScreen("plugins");
|
|
188
202
|
}
|
|
189
203
|
}
|
|
@@ -195,12 +209,13 @@ function GlobalKeyHandler({
|
|
|
195
209
|
`Navigation
|
|
196
210
|
↑/↓ or j/k Move selection
|
|
197
211
|
Enter Select / Toggle
|
|
198
|
-
Escape
|
|
212
|
+
Escape Back (to home)
|
|
213
|
+
q / Ctrl+C Quit (from anywhere)
|
|
199
214
|
? This help
|
|
200
215
|
|
|
201
216
|
Quick Navigation
|
|
202
|
-
1 Plugins 4 Settings 7
|
|
203
|
-
2 Skills 5 Profiles
|
|
217
|
+
1 Plugins 4 Settings 7 Git State
|
|
218
|
+
2 Skills 5 Profiles 8 Alias
|
|
204
219
|
3 MCP Servers 6 CLI Tools
|
|
205
220
|
|
|
206
221
|
Plugin Actions
|
|
Binary file
|
|
Binary file
|
|
@@ -6,7 +6,8 @@ const TABS = [
|
|
|
6
6
|
{ key: "4", label: "Settings", screen: "settings" },
|
|
7
7
|
{ key: "5", label: "Profiles", screen: "profiles" },
|
|
8
8
|
{ key: "6", label: "CLI", screen: "cli-tools" },
|
|
9
|
-
{ key: "7", label: "
|
|
9
|
+
{ key: "7", label: "Git State", screen: "gitignore" },
|
|
10
|
+
{ key: "8", label: "Alias", screen: "alias" },
|
|
10
11
|
];
|
|
11
12
|
export function TabBar({ currentScreen }) {
|
|
12
13
|
return (_jsx("box", { flexDirection: "row", gap: 0, children: TABS.map((tab, index) => {
|
|
@@ -14,7 +14,8 @@ const TABS: Tab[] = [
|
|
|
14
14
|
{ key: "4", label: "Settings", screen: "settings" },
|
|
15
15
|
{ key: "5", label: "Profiles", screen: "profiles" },
|
|
16
16
|
{ key: "6", label: "CLI", screen: "cli-tools" },
|
|
17
|
-
{ key: "7", label: "
|
|
17
|
+
{ key: "7", label: "Git State", screen: "gitignore" },
|
|
18
|
+
{ key: "8", label: "Alias", screen: "alias" },
|
|
18
19
|
];
|
|
19
20
|
|
|
20
21
|
interface TabBarProps {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
+
const KEY_BG = "#3f3f46";
|
|
3
|
+
const KEY_FG = "#fafafa";
|
|
4
|
+
const LABEL_FG = "gray";
|
|
5
|
+
const GAP = " ";
|
|
6
|
+
/**
|
|
7
|
+
* Render a row of footer hints as key badges + labels:
|
|
8
|
+
*
|
|
9
|
+
* [↑↓] move [U] update [/] search
|
|
10
|
+
*
|
|
11
|
+
* Each key is shown in a chip so it reads as a pressable key, not prose.
|
|
12
|
+
* Shared across screens so every footer looks the same.
|
|
13
|
+
*/
|
|
14
|
+
export function FooterHints({ hints }) {
|
|
15
|
+
const nodes = [];
|
|
16
|
+
hints.forEach((hint, i) => {
|
|
17
|
+
if (i > 0) {
|
|
18
|
+
nodes.push(_jsx("span", { fg: LABEL_FG, children: GAP }, `gap-${i}`));
|
|
19
|
+
}
|
|
20
|
+
nodes.push(_jsx("span", { bg: KEY_BG, fg: KEY_FG, children: ` ${hint.keys.join("")} ` }, `k-${i}`));
|
|
21
|
+
nodes.push(_jsx("span", { fg: LABEL_FG, children: ` ${hint.label}` }, `l-${i}`));
|
|
22
|
+
});
|
|
23
|
+
return _jsx("text", { children: nodes });
|
|
24
|
+
}
|
|
25
|
+
/** Convenience for callers that already have a node: same as <FooterHints>. */
|
|
26
|
+
export function renderFooterHints(hints) {
|
|
27
|
+
return _jsx(FooterHints, { hints: hints });
|
|
28
|
+
}
|
|
29
|
+
export default FooterHints;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
/** One footer hint: a key (or key group) and the action it performs. */
|
|
4
|
+
export interface FooterHint {
|
|
5
|
+
/** Key(s) for this action, e.g. ["↑", "↓"] or ["U"]. Joined for display. */
|
|
6
|
+
keys: string[];
|
|
7
|
+
label: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const KEY_BG = "#3f3f46";
|
|
11
|
+
const KEY_FG = "#fafafa";
|
|
12
|
+
const LABEL_FG = "gray";
|
|
13
|
+
const GAP = " ";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Render a row of footer hints as key badges + labels:
|
|
17
|
+
*
|
|
18
|
+
* [↑↓] move [U] update [/] search
|
|
19
|
+
*
|
|
20
|
+
* Each key is shown in a chip so it reads as a pressable key, not prose.
|
|
21
|
+
* Shared across screens so every footer looks the same.
|
|
22
|
+
*/
|
|
23
|
+
export function FooterHints({ hints }: { hints: FooterHint[] }): React.ReactNode {
|
|
24
|
+
const nodes: React.ReactNode[] = [];
|
|
25
|
+
hints.forEach((hint, i) => {
|
|
26
|
+
if (i > 0) {
|
|
27
|
+
nodes.push(
|
|
28
|
+
<span key={`gap-${i}`} fg={LABEL_FG}>
|
|
29
|
+
{GAP}
|
|
30
|
+
</span>,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
nodes.push(
|
|
34
|
+
<span key={`k-${i}`} bg={KEY_BG} fg={KEY_FG}>
|
|
35
|
+
{` ${hint.keys.join("")} `}
|
|
36
|
+
</span>,
|
|
37
|
+
);
|
|
38
|
+
nodes.push(
|
|
39
|
+
<span key={`l-${i}`} fg={LABEL_FG}>
|
|
40
|
+
{` ${hint.label}`}
|
|
41
|
+
</span>,
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
return <text>{nodes}</text>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Convenience for callers that already have a node: same as <FooterHints>. */
|
|
48
|
+
export function renderFooterHints(hints: FooterHint[]): React.ReactNode {
|
|
49
|
+
return <FooterHints hints={hints} />;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default FooterHints;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
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
|
+
import { FooterHints } from "./FooterHints.js";
|
|
4
5
|
const HEADER_COLOR = "#7e57c2";
|
|
5
6
|
export function ScreenLayout({ title, subtitle, currentScreen, search, statusLine, footerHints, listPanel, detailPanel, }) {
|
|
6
7
|
const dimensions = useDimensions();
|
|
@@ -10,6 +11,6 @@ export function ScreenLayout({ title, subtitle, currentScreen, search, statusLin
|
|
|
10
11
|
const fixedHeight = 6 + (hasSearchBar ? 1 : 0);
|
|
11
12
|
const panelHeight = Math.max(5, dimensions.contentHeight - fixedHeight);
|
|
12
13
|
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", { width: "50%", height: panelHeight, paddingLeft: 1, children: _jsx("scrollbox", { height: panelHeight, scrollY: true, scrollX: false, children: _jsx("box", { flexDirection: "column", children: detailPanel }) }) })] }), _jsx("box", { height: 1, paddingLeft: 1, children: _jsx("text", { fg: "gray", children: footerHints }) })] }));
|
|
14
|
+
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", { width: "50%", height: panelHeight, paddingLeft: 1, children: _jsx("scrollbox", { height: panelHeight, scrollY: true, scrollX: false, children: _jsx("box", { flexDirection: "column", children: detailPanel }) }) })] }), _jsx("box", { height: 1, paddingLeft: 1, children: Array.isArray(footerHints) ? (_jsx(FooterHints, { hints: footerHints })) : typeof footerHints === "string" ? (_jsx("text", { fg: "gray", children: footerHints })) : (footerHints) })] }));
|
|
14
15
|
}
|
|
15
16
|
export default ScreenLayout;
|
|
@@ -2,6 +2,7 @@ import React from "react";
|
|
|
2
2
|
import { useDimensions } from "../../state/DimensionsContext.js";
|
|
3
3
|
import { TabBar } from "../TabBar.js";
|
|
4
4
|
import type { Screen } from "../../state/types.js";
|
|
5
|
+
import { FooterHints, type FooterHint } from "./FooterHints.js";
|
|
5
6
|
|
|
6
7
|
interface ScreenLayoutProps {
|
|
7
8
|
/** Screen title (e.g., "claudeup Plugins") */
|
|
@@ -21,8 +22,10 @@ interface ScreenLayoutProps {
|
|
|
21
22
|
};
|
|
22
23
|
/** Status line content (for screens without search) - shown in second row */
|
|
23
24
|
statusLine?: React.ReactNode;
|
|
24
|
-
/** Footer hints (left side)
|
|
25
|
-
|
|
25
|
+
/** Footer hints (left side). Pass a FooterHint[] to render key badges
|
|
26
|
+
* (the standard look). A string or ReactNode is still accepted for
|
|
27
|
+
* free-form footers. */
|
|
28
|
+
footerHints: FooterHint[] | string | React.ReactNode;
|
|
26
29
|
/** Left panel content */
|
|
27
30
|
listPanel: React.ReactNode;
|
|
28
31
|
/** Right panel content (detail view) */
|
|
@@ -134,7 +137,13 @@ export function ScreenLayout({
|
|
|
134
137
|
|
|
135
138
|
{/* Footer */}
|
|
136
139
|
<box height={1} paddingLeft={1}>
|
|
137
|
-
|
|
140
|
+
{Array.isArray(footerHints) ? (
|
|
141
|
+
<FooterHints hints={footerHints as FooterHint[]} />
|
|
142
|
+
) : typeof footerHints === "string" ? (
|
|
143
|
+
<text fg="gray">{footerHints}</text>
|
|
144
|
+
) : (
|
|
145
|
+
footerHints
|
|
146
|
+
)}
|
|
138
147
|
</box>
|
|
139
148
|
</box>
|
|
140
149
|
);
|
|
@@ -2,3 +2,8 @@ export { Panel } from "./Panel.js";
|
|
|
2
2
|
export { ScopeTabs } from "./ScopeTabs.js";
|
|
3
3
|
export { ProgressBar } from "./ProgressBar.js";
|
|
4
4
|
export { ScreenLayout } from "./ScreenLayout.js";
|
|
5
|
+
export {
|
|
6
|
+
FooterHints,
|
|
7
|
+
renderFooterHints,
|
|
8
|
+
type FooterHint,
|
|
9
|
+
} from "./FooterHints.js";
|
|
@@ -9,6 +9,6 @@ export function ConfirmModal({ title, message, onConfirm, onCancel, }) {
|
|
|
9
9
|
onCancel();
|
|
10
10
|
}
|
|
11
11
|
});
|
|
12
|
-
return (_jsxs("box", { flexDirection: "column", border: true, borderStyle: "rounded", borderColor: "#525252", backgroundColor: "#1C1C1E", paddingLeft: 3, paddingRight: 3, paddingTop: 1, paddingBottom: 1, width: 60, children: [_jsx("box", { marginBottom: 1, children: _jsx("text", { fg: "#EDEDED", children: _jsx("strong", { children: title }) }) }), _jsx("box", { marginBottom: 1, children: _jsx("text", { fg: "#A1A1AA", children: message }) }), _jsxs("box", { marginTop: 1, children: [_jsx("text", { fg: "#71717A", children: "Press " }), _jsx("text", { fg: "#EDEDED", children: "Y" }), _jsx("text", { fg: "#71717A", children: " to confirm \u2022 " }), _jsx("text", { fg: "#EDEDED", children: "N" }), _jsx("text", { fg: "#71717A", children: " to cancel" })] })] }));
|
|
12
|
+
return (_jsxs("box", { flexDirection: "column", border: true, borderStyle: "rounded", borderColor: "#525252", backgroundColor: "#1C1C1E", paddingLeft: 3, paddingRight: 3, paddingTop: 1, paddingBottom: 1, width: 60, children: [_jsx("box", { marginBottom: 1, children: _jsx("text", { fg: "#EDEDED", children: _jsx("strong", { children: title }) }) }), _jsx("box", { marginBottom: 1, children: _jsx("text", { fg: "#A1A1AA", children: message }) }), _jsxs("box", { marginTop: 1, flexDirection: "row", children: [_jsx("text", { fg: "#71717A", children: "Press " }), _jsx("text", { fg: "#EDEDED", children: "Y" }), _jsx("text", { fg: "#71717A", children: " to confirm \u2022 " }), _jsx("text", { fg: "#EDEDED", children: "N" }), _jsx("text", { fg: "#71717A", children: " to cancel" })] })] }));
|
|
13
13
|
}
|
|
14
14
|
export default ConfirmModal;
|
|
@@ -47,7 +47,7 @@ export function ConfirmModal({
|
|
|
47
47
|
<box marginBottom={1}>
|
|
48
48
|
<text fg="#A1A1AA">{message}</text>
|
|
49
49
|
</box>
|
|
50
|
-
<box marginTop={1}>
|
|
50
|
+
<box marginTop={1} flexDirection="row">
|
|
51
51
|
<text fg="#71717A">Press </text>
|
|
52
52
|
<text fg="#EDEDED">Y</text>
|
|
53
53
|
<text fg="#71717A"> to confirm • </text>
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { useDimensions } from "../../state/DimensionsContext.js";
|
|
2
3
|
export function SelectModal({ title, message, options, defaultIndex, onSelect: _onSelect, onCancel: _onCancel, }) {
|
|
3
4
|
// Keyboard handling is done by ModalContainer
|
|
4
5
|
// defaultIndex is the live selectedIndex from ModalContainer state
|
|
5
6
|
const selectedIndex = defaultIndex ?? 0;
|
|
6
|
-
|
|
7
|
+
// Size to terminal width: 60% of available, clamped to [50, 90].
|
|
8
|
+
// Previously hardcoded at 50, which forced descriptions and long item
|
|
9
|
+
// labels (e.g. plugin specs) to wrap mid-sentence and produce orphan
|
|
10
|
+
// punctuation lines.
|
|
11
|
+
const dimensions = useDimensions();
|
|
12
|
+
const width = Math.max(50, Math.min(90, Math.floor(dimensions.terminalWidth * 0.6)));
|
|
13
|
+
return (_jsxs("box", { flexDirection: "column", border: true, borderStyle: "rounded", borderColor: "#525252", backgroundColor: "#1C1C1E", paddingLeft: 3, paddingRight: 3, paddingTop: 1, paddingBottom: 1, width: width, children: [_jsx("box", { marginBottom: 1, children: _jsx("text", { fg: "#EDEDED", children: _jsx("strong", { children: title }) }) }), _jsx("box", { marginBottom: 1, children: _jsx("text", { fg: "#A1A1AA", children: message }) }), _jsx("box", { flexDirection: "column", paddingLeft: 1, children: options.map((option, idx) => {
|
|
7
14
|
const isSelected = idx === selectedIndex;
|
|
8
15
|
return (_jsxs("text", { fg: isSelected ? "#F4F4F5" : "#A1A1AA", children: [_jsx("span", { fg: isSelected ? "#F4F4F5" : "#71717A", children: isSelected ? "❯ " : " " }), isSelected ? _jsx("strong", { children: option.label }) : option.label] }, option.value));
|
|
9
16
|
}) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "#71717A", children: "\u2191\u2193 Select \u2022 \u21B5 Confirm \u2022 Esc Cancel" }) })] }));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { SelectOption } from "../../state/types.js";
|
|
3
|
+
import { useDimensions } from "../../state/DimensionsContext.js";
|
|
3
4
|
|
|
4
5
|
interface SelectModalProps {
|
|
5
6
|
/** Modal title */
|
|
@@ -28,6 +29,16 @@ export function SelectModal({
|
|
|
28
29
|
// defaultIndex is the live selectedIndex from ModalContainer state
|
|
29
30
|
const selectedIndex = defaultIndex ?? 0;
|
|
30
31
|
|
|
32
|
+
// Size to terminal width: 60% of available, clamped to [50, 90].
|
|
33
|
+
// Previously hardcoded at 50, which forced descriptions and long item
|
|
34
|
+
// labels (e.g. plugin specs) to wrap mid-sentence and produce orphan
|
|
35
|
+
// punctuation lines.
|
|
36
|
+
const dimensions = useDimensions();
|
|
37
|
+
const width = Math.max(
|
|
38
|
+
50,
|
|
39
|
+
Math.min(90, Math.floor(dimensions.terminalWidth * 0.6)),
|
|
40
|
+
);
|
|
41
|
+
|
|
31
42
|
return (
|
|
32
43
|
<box
|
|
33
44
|
flexDirection="column"
|
|
@@ -39,7 +50,7 @@ export function SelectModal({
|
|
|
39
50
|
paddingRight={3}
|
|
40
51
|
paddingTop={1}
|
|
41
52
|
paddingBottom={1}
|
|
42
|
-
width={
|
|
53
|
+
width={width}
|
|
43
54
|
>
|
|
44
55
|
<box marginBottom={1}>
|
|
45
56
|
<text fg="#EDEDED">
|
|
@@ -5,12 +5,12 @@ export function buildGitignoreModal(args) {
|
|
|
5
5
|
const options = [
|
|
6
6
|
{
|
|
7
7
|
label: safeCount > 0
|
|
8
|
-
? `
|
|
9
|
-
: "(no
|
|
8
|
+
? `Add ${safeCount} missing .gitignore entr${safeCount === 1 ? "y" : "ies"}`
|
|
9
|
+
: "(no missing .gitignore entries)",
|
|
10
10
|
value: "fix-safe",
|
|
11
11
|
},
|
|
12
12
|
{
|
|
13
|
-
label: "Open
|
|
13
|
+
label: "Open Git State tab to review",
|
|
14
14
|
value: "open-tab",
|
|
15
15
|
},
|
|
16
16
|
{
|
|
@@ -21,11 +21,10 @@ export function buildGitignoreModal(args) {
|
|
|
21
21
|
return {
|
|
22
22
|
type: "select",
|
|
23
23
|
title: "Gitignore violations detected",
|
|
24
|
-
message: `Found ${violationCount}
|
|
25
|
-
`
|
|
26
|
-
`
|
|
27
|
-
`
|
|
28
|
-
`a time from the Gitignore tab.`,
|
|
24
|
+
message: `Found ${violationCount} managed gitignore item(s) that do not match ` +
|
|
25
|
+
`the current repo state.\n\n` +
|
|
26
|
+
`Missing .gitignore entries can be added now. Items that change git ` +
|
|
27
|
+
`tracking are reviewed and fixed from the Gitignore tab.`,
|
|
29
28
|
options,
|
|
30
29
|
onSelect: (value) => {
|
|
31
30
|
if (value === "fix-safe")
|