claudeup 4.17.0 → 4.18.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-parser.test.ts +317 -0
- package/src/__tests__/alias-shell-writer.test.ts +661 -0
- package/src/__tests__/alias-store.test.ts +86 -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__/useGitignoreModal.test.ts +2 -2
- package/src/data/alias-flags.js +196 -0
- package/src/data/alias-flags.ts +291 -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 +764 -0
- package/src/services/alias-shell-writer.ts +873 -0
- package/src/services/alias-store.js +77 -0
- package/src/services/alias-store.ts +112 -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/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 +10 -4
- package/src/ui/App.tsx +9 -3
- 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/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 +1008 -0
- package/src/ui/screens/AliasScreen.tsx +1402 -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 +13 -2
- package/src/ui/screens/PluginsScreen.tsx +13 -2
- 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/types.ts +4 -2
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
|
}
|
|
@@ -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,
|
|
@@ -199,8 +205,8 @@ function GlobalKeyHandler({
|
|
|
199
205
|
? This help
|
|
200
206
|
|
|
201
207
|
Quick Navigation
|
|
202
|
-
1 Plugins 4 Settings 7
|
|
203
|
-
2 Skills 5 Profiles
|
|
208
|
+
1 Plugins 4 Settings 7 Git State
|
|
209
|
+
2 Skills 5 Profiles 8 Alias
|
|
204
210
|
3 MCP Servers 6 CLI Tools
|
|
205
211
|
|
|
206
212
|
Plugin Actions
|
|
@@ -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";
|
|
@@ -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")
|
|
@@ -5,7 +5,7 @@ import type { ModalState } from "../state/types.js";
|
|
|
5
5
|
/**
|
|
6
6
|
* Startup gitignore-violation modal. Mirrors `useMismatchModal` so the
|
|
7
7
|
* two flows feel identical to the user — claudeup launches, detects
|
|
8
|
-
* a problem, asks "
|
|
8
|
+
* a problem, asks "add missing ignores / open the tab / dismiss".
|
|
9
9
|
*
|
|
10
10
|
* Pure builder is exported separately for tests.
|
|
11
11
|
*/
|
|
@@ -24,12 +24,12 @@ export function buildGitignoreModal(args: BuildModalArgs): ModalState {
|
|
|
24
24
|
{
|
|
25
25
|
label:
|
|
26
26
|
safeCount > 0
|
|
27
|
-
? `
|
|
28
|
-
: "(no
|
|
27
|
+
? `Add ${safeCount} missing .gitignore entr${safeCount === 1 ? "y" : "ies"}`
|
|
28
|
+
: "(no missing .gitignore entries)",
|
|
29
29
|
value: "fix-safe",
|
|
30
30
|
},
|
|
31
31
|
{
|
|
32
|
-
label: "Open
|
|
32
|
+
label: "Open Git State tab to review",
|
|
33
33
|
value: "open-tab",
|
|
34
34
|
},
|
|
35
35
|
{
|
|
@@ -41,11 +41,10 @@ export function buildGitignoreModal(args: BuildModalArgs): ModalState {
|
|
|
41
41
|
type: "select",
|
|
42
42
|
title: "Gitignore violations detected",
|
|
43
43
|
message:
|
|
44
|
-
`Found ${violationCount}
|
|
45
|
-
`
|
|
46
|
-
`
|
|
47
|
-
`
|
|
48
|
-
`a time from the Gitignore tab.`,
|
|
44
|
+
`Found ${violationCount} managed gitignore item(s) that do not match ` +
|
|
45
|
+
`the current repo state.\n\n` +
|
|
46
|
+
`Missing .gitignore entries can be added now. Items that change git ` +
|
|
47
|
+
`tracking are reviewed and fixed from the Gitignore tab.`,
|
|
49
48
|
options,
|
|
50
49
|
onSelect: (value: string) => {
|
|
51
50
|
if (value === "fix-safe") onFixSafe();
|
|
@@ -1,33 +1,46 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
2
|
+
// Two color channels that never collide:
|
|
3
|
+
// • Badge color = direction (where the item ends up): green = tracked by git,
|
|
4
|
+
// reddish = ignored/dropped from git. Same on clean AND violation rows.
|
|
5
|
+
// • Filename color = severity: dim gray when fine, red when it's a violation.
|
|
6
|
+
// No row background tints — red is reserved entirely for "needs attention".
|
|
7
|
+
const TRACK_FG = "#5fd787"; // green — item ends up tracked by git
|
|
8
|
+
const IGNORE_FG = "#d78787"; // reddish — item ends up ignored / dropped
|
|
9
|
+
const VIOLATION_PATH_FG = "#ff5f5f"; // bright red filename = look here
|
|
10
|
+
const CLEAN_PATH_FG = "gray";
|
|
11
|
+
const SELECTED_BG = "#2a2a3a"; // faint slate highlight for the cursor row
|
|
12
|
+
// Violation badges are solid chips so the required direction reads at a glance:
|
|
13
|
+
// green chip = will be tracked, red chip = will be ignored/dropped.
|
|
14
|
+
const TRACK_CHIP_BG = "#1f5132";
|
|
15
|
+
const IGNORE_CHIP_BG = "#5a2323";
|
|
16
|
+
const CHIP_FG = "white";
|
|
17
|
+
// The only marker is the cursor: ▶ on the selected row, blank otherwise.
|
|
18
|
+
// Action (track/ignore) is read from the badge word and, for clean items,
|
|
19
|
+
// from the group the row sits under — so no per-row dot is needed.
|
|
20
|
+
const MARKER_SELECTED = "▶";
|
|
21
|
+
const MARKER_NONE = " ";
|
|
22
|
+
// Layout: " M action path" — keep the prefix compact so the narrow terminal
|
|
23
|
+
// case still has room for the path without wrapping.
|
|
8
24
|
const MAX_PATH_LEN = 22;
|
|
9
25
|
function truncatePath(p) {
|
|
10
26
|
return p.length > MAX_PATH_LEN ? p.slice(0, MAX_PATH_LEN - 1) + "…" : p;
|
|
11
27
|
}
|
|
12
28
|
export function renderRuleRow(row, isSelected) {
|
|
13
|
-
const { rule,
|
|
29
|
+
const { rule, violations } = row;
|
|
14
30
|
const path = truncatePath(rule.pattern);
|
|
15
31
|
const key = `${rule.action}:${rule.pattern}`;
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
32
|
+
// Badge text padded to a fixed width so paths line up in a column.
|
|
33
|
+
const badge = rule.action === "ignore" ? "ignore" : "track ";
|
|
34
|
+
const badgeFg = rule.action === "ignore" ? IGNORE_FG : TRACK_FG;
|
|
35
|
+
const suffix = violations.length > 1 ? ` +${violations.length - 1}` : "";
|
|
36
|
+
const rowBg = isSelected ? SELECTED_BG : undefined;
|
|
37
|
+
const marker = isSelected ? MARKER_SELECTED : MARKER_NONE;
|
|
38
|
+
const isViolation = violations.length > 0;
|
|
39
|
+
if (isViolation) {
|
|
40
|
+
// Solid badge chip + red filename. The chip color is the fix direction.
|
|
41
|
+
const chipBg = rule.action === "ignore" ? IGNORE_CHIP_BG : TRACK_CHIP_BG;
|
|
42
|
+
return (_jsxs("text", { bg: rowBg, children: [_jsx("span", { fg: VIOLATION_PATH_FG, children: ` ${marker} ` }), _jsx("span", { bg: chipBg, fg: CHIP_FG, children: ` ${badge} ` }), _jsx("span", { fg: VIOLATION_PATH_FG, children: ` ${path}${suffix}` })] }, key));
|
|
26
43
|
}
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
if (isSelected) {
|
|
30
|
-
return (_jsx("text", { bg: SELECTION_BG, fg: SELECTION_FG, children: `▶ ! ${verb} ${path}` }, key));
|
|
31
|
-
}
|
|
32
|
-
return (_jsxs("text", { children: [_jsx("span", { fg: sevColor, children: ` ! ` }), _jsx("span", { fg: "white", children: `${verb} ${path}` })] }, key));
|
|
44
|
+
// Clean row: flat colored badge word, dim path, no chip.
|
|
45
|
+
return (_jsxs("text", { bg: rowBg, children: [_jsx("span", { fg: badgeFg, children: ` ${marker} ` }), _jsx("span", { fg: badgeFg, children: badge }), _jsx("span", { fg: CLEAN_PATH_FG, children: ` ${path}` })] }, key));
|
|
33
46
|
}
|
|
@@ -3,68 +3,77 @@ import type { ResolvedRule, Violation } from "../../types/gitignore.js";
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* A unified row for the Gitignore screen — represents one resolved rule,
|
|
6
|
-
*
|
|
6
|
+
* annotated with every violation it has against the working tree.
|
|
7
7
|
*/
|
|
8
8
|
export interface RuleRow {
|
|
9
9
|
rule: ResolvedRule;
|
|
10
|
-
|
|
10
|
+
violations: Violation[];
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
// Two color channels that never collide:
|
|
14
|
+
// • Badge color = direction (where the item ends up): green = tracked by git,
|
|
15
|
+
// reddish = ignored/dropped from git. Same on clean AND violation rows.
|
|
16
|
+
// • Filename color = severity: dim gray when fine, red when it's a violation.
|
|
17
|
+
// No row background tints — red is reserved entirely for "needs attention".
|
|
18
|
+
const TRACK_FG = "#5fd787"; // green — item ends up tracked by git
|
|
19
|
+
const IGNORE_FG = "#d78787"; // reddish — item ends up ignored / dropped
|
|
20
|
+
const VIOLATION_PATH_FG = "#ff5f5f"; // bright red filename = look here
|
|
21
|
+
const CLEAN_PATH_FG = "gray";
|
|
22
|
+
const SELECTED_BG = "#2a2a3a"; // faint slate highlight for the cursor row
|
|
15
23
|
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
// Violation badges are solid chips so the required direction reads at a glance:
|
|
25
|
+
// green chip = will be tracked, red chip = will be ignored/dropped.
|
|
26
|
+
const TRACK_CHIP_BG = "#1f5132";
|
|
27
|
+
const IGNORE_CHIP_BG = "#5a2323";
|
|
28
|
+
const CHIP_FG = "white";
|
|
29
|
+
|
|
30
|
+
// The only marker is the cursor: ▶ on the selected row, blank otherwise.
|
|
31
|
+
// Action (track/ignore) is read from the badge word and, for clean items,
|
|
32
|
+
// from the group the row sits under — so no per-row dot is needed.
|
|
33
|
+
const MARKER_SELECTED = "▶";
|
|
34
|
+
const MARKER_NONE = " ";
|
|
35
|
+
|
|
36
|
+
// Layout: " M action path" — keep the prefix compact so the narrow terminal
|
|
37
|
+
// case still has room for the path without wrapping.
|
|
20
38
|
const MAX_PATH_LEN = 22;
|
|
21
39
|
|
|
22
40
|
function truncatePath(p: string): string {
|
|
23
41
|
return p.length > MAX_PATH_LEN ? p.slice(0, MAX_PATH_LEN - 1) + "…" : p;
|
|
24
42
|
}
|
|
25
43
|
|
|
26
|
-
export function renderRuleRow(
|
|
27
|
-
|
|
44
|
+
export function renderRuleRow(
|
|
45
|
+
row: RuleRow,
|
|
46
|
+
isSelected: boolean,
|
|
47
|
+
): React.ReactNode {
|
|
48
|
+
const { rule, violations } = row;
|
|
28
49
|
const path = truncatePath(rule.pattern);
|
|
29
50
|
const key = `${rule.action}:${rule.pattern}`;
|
|
51
|
+
// Badge text padded to a fixed width so paths line up in a column.
|
|
52
|
+
const badge = rule.action === "ignore" ? "ignore" : "track ";
|
|
53
|
+
const badgeFg = rule.action === "ignore" ? IGNORE_FG : TRACK_FG;
|
|
54
|
+
const suffix = violations.length > 1 ? ` +${violations.length - 1}` : "";
|
|
55
|
+
const rowBg = isSelected ? SELECTED_BG : undefined;
|
|
56
|
+
const marker = isSelected ? MARKER_SELECTED : MARKER_NONE;
|
|
57
|
+
const isViolation = violations.length > 0;
|
|
30
58
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const verb = rule.action === "ignore" ? "Ignore " : "Track ";
|
|
36
|
-
|
|
37
|
-
if (!violation) {
|
|
38
|
-
// Clean rule — muted, just confirmation
|
|
39
|
-
if (isSelected) {
|
|
40
|
-
return (
|
|
41
|
-
<text key={key} bg={SELECTION_BG} fg={SELECTION_FG}>
|
|
42
|
-
{`▶ ✓ ${verb} ${path}`}
|
|
43
|
-
</text>
|
|
44
|
-
);
|
|
45
|
-
}
|
|
59
|
+
if (isViolation) {
|
|
60
|
+
// Solid badge chip + red filename. The chip color is the fix direction.
|
|
61
|
+
const chipBg = rule.action === "ignore" ? IGNORE_CHIP_BG : TRACK_CHIP_BG;
|
|
46
62
|
return (
|
|
47
|
-
<text key={key}>
|
|
48
|
-
<span fg=
|
|
49
|
-
<span fg=
|
|
63
|
+
<text key={key} bg={rowBg}>
|
|
64
|
+
<span fg={VIOLATION_PATH_FG}>{` ${marker} `}</span>
|
|
65
|
+
<span bg={chipBg} fg={CHIP_FG}>{` ${badge} `}</span>
|
|
66
|
+
<span fg={VIOLATION_PATH_FG}>{` ${path}${suffix}`}</span>
|
|
50
67
|
</text>
|
|
51
68
|
);
|
|
52
69
|
}
|
|
53
70
|
|
|
54
|
-
//
|
|
55
|
-
const sevColor = violation.severity === "safe" ? "yellow" : "red";
|
|
56
|
-
|
|
57
|
-
if (isSelected) {
|
|
58
|
-
return (
|
|
59
|
-
<text key={key} bg={SELECTION_BG} fg={SELECTION_FG}>
|
|
60
|
-
{`▶ ! ${verb} ${path}`}
|
|
61
|
-
</text>
|
|
62
|
-
);
|
|
63
|
-
}
|
|
71
|
+
// Clean row: flat colored badge word, dim path, no chip.
|
|
64
72
|
return (
|
|
65
|
-
<text key={key}>
|
|
66
|
-
<span fg={
|
|
67
|
-
<span fg=
|
|
73
|
+
<text key={key} bg={rowBg}>
|
|
74
|
+
<span fg={badgeFg}>{` ${marker} `}</span>
|
|
75
|
+
<span fg={badgeFg}>{badge}</span>
|
|
76
|
+
<span fg={CLEAN_PATH_FG}>{` ${path}`}</span>
|
|
68
77
|
</text>
|
|
69
78
|
);
|
|
70
79
|
}
|