claudeup 4.35.0 → 4.36.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 +4 -4
- package/src/__tests__/git-worktree.test.ts +108 -0
- package/src/__tests__/scope-squares.test.tsx +165 -0
- package/src/__tests__/theme-adaptive-colors.test.ts +307 -0
- package/src/__tests__/uppercase-keybindings.test.ts +101 -0
- package/src/__tests__/worktree-registry-resolution.test.ts +107 -0
- package/src/main.tsx +21 -5
- package/src/opentui.d.ts +21 -12
- package/src/services/claude-settings.ts +37 -7
- package/src/services/git-worktree.ts +129 -0
- package/src/services/plugin-manager.ts +10 -1
- package/src/ui/App.tsx +19 -12
- package/src/ui/adapters/pluginsAdapter.ts +174 -168
- package/src/ui/adapters/settingsAdapter.ts +119 -116
- package/src/ui/adapters/skillsAdapter.ts +203 -196
- package/src/ui/components/CategoryHeader.tsx +9 -8
- package/src/ui/components/EmptyFilterState.tsx +10 -5
- package/src/ui/components/FlagDetailEditor.tsx +0 -0
- package/src/ui/components/ScopeIndicator.tsx +10 -6
- package/src/ui/components/ScrollableList.tsx +3 -2
- package/src/ui/components/SearchInput.tsx +2 -1
- package/src/ui/components/StyledText.tsx +5 -4
- package/src/ui/components/TabBar.tsx +4 -3
- package/src/ui/components/layout/FooterHints.tsx +37 -30
- package/src/ui/components/layout/Panel.tsx +6 -5
- package/src/ui/components/layout/ProgressBar.tsx +7 -6
- package/src/ui/components/layout/ScopeTabs.tsx +6 -5
- package/src/ui/components/layout/ScreenLayout.tsx +27 -24
- package/src/ui/components/layout/index.ts +3 -3
- package/src/ui/components/modals/ConfirmModal.tsx +12 -11
- package/src/ui/components/modals/InputModal.tsx +14 -6
- package/src/ui/components/modals/LoadingModal.tsx +6 -5
- package/src/ui/components/modals/MessageModal.tsx +9 -8
- package/src/ui/components/modals/SelectModal.tsx +11 -7
- package/src/ui/components/modals/VersionMismatchModal.tsx +14 -16
- package/src/ui/components/primitives/ActionHints.tsx +26 -26
- package/src/ui/components/primitives/DetailSection.tsx +13 -12
- package/src/ui/components/primitives/KeyValueLine.tsx +9 -8
- package/src/ui/components/primitives/ListCategoryRow.tsx +25 -27
- package/src/ui/components/primitives/MetaText.tsx +3 -3
- package/src/ui/components/primitives/ScopeDetail.tsx +48 -48
- package/src/ui/components/primitives/ScopeSquares.tsx +47 -22
- package/src/ui/components/primitives/SelectableRow.tsx +22 -16
- package/src/ui/hooks/useGitignoreModal.ts +78 -74
- package/src/ui/registry.ts +11 -11
- package/src/ui/renderers/cliToolRenderers.tsx +260 -203
- package/src/ui/renderers/gitignoreRenderers.tsx +43 -42
- package/src/ui/renderers/mcpRenderers.tsx +121 -117
- package/src/ui/renderers/pluginRenderers.tsx +528 -471
- package/src/ui/renderers/profileRenderers.tsx +346 -300
- package/src/ui/renderers/settingsRenderers.tsx +183 -176
- package/src/ui/renderers/skillRenderers.tsx +410 -326
- package/src/ui/screens/AliasScreen.tsx +1336 -1309
- package/src/ui/screens/CliToolsScreen.tsx +92 -40
- package/src/ui/screens/EnvVarsScreen.tsx +19 -13
- package/src/ui/screens/GitignoreScreen.tsx +510 -493
- package/src/ui/screens/McpRegistryScreen.tsx +28 -21
- package/src/ui/screens/McpScreen.tsx +12 -3
- package/src/ui/screens/PluginsScreen.tsx +17 -7
- package/src/ui/screens/ProfilesScreen.tsx +39 -23
- package/src/ui/screens/SkillsScreen.tsx +832 -688
- package/src/ui/theme-mode.ts +73 -0
- package/src/ui/theme.ts +147 -53
|
@@ -2,13 +2,13 @@ import React from "react";
|
|
|
2
2
|
import { theme } from "../../theme.js";
|
|
3
3
|
|
|
4
4
|
export interface Hint {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
key: string;
|
|
6
|
+
label: string;
|
|
7
|
+
tone?: "default" | "primary" | "danger";
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
interface ActionHintsProps {
|
|
11
|
-
|
|
11
|
+
hints: Hint[];
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -16,26 +16,26 @@ interface ActionHintsProps {
|
|
|
16
16
|
* Renders: [bg] key [/bg] label
|
|
17
17
|
*/
|
|
18
18
|
export function ActionHints({ hints }: ActionHintsProps) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
19
|
+
return (
|
|
20
|
+
<box flexDirection="column" marginTop={1}>
|
|
21
|
+
{hints.map((hint) => (
|
|
22
|
+
<box key={`${hint.key}:${hint.label}`}>
|
|
23
|
+
<text
|
|
24
|
+
bg={
|
|
25
|
+
hint.tone === "danger"
|
|
26
|
+
? theme.hints.dangerBg
|
|
27
|
+
: hint.tone === "primary"
|
|
28
|
+
? theme.hints.primaryBg
|
|
29
|
+
: theme.hints.defaultBg
|
|
30
|
+
}
|
|
31
|
+
fg={theme.hints.fg}
|
|
32
|
+
>
|
|
33
|
+
{" "}
|
|
34
|
+
{hint.key}{" "}
|
|
35
|
+
</text>
|
|
36
|
+
<text fg={theme.colors.muted}> {hint.label}</text>
|
|
37
|
+
</box>
|
|
38
|
+
))}
|
|
39
|
+
</box>
|
|
40
|
+
);
|
|
41
41
|
}
|
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import { theme } from "../../theme.js";
|
|
2
3
|
|
|
3
4
|
interface DetailSectionProps {
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
title?: string;
|
|
6
|
+
children: React.ReactNode;
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* A labeled section in a detail panel with optional title header.
|
|
10
11
|
*/
|
|
11
12
|
export function DetailSection({ title, children }: DetailSectionProps) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
13
|
+
return (
|
|
14
|
+
<box flexDirection="column" marginTop={1}>
|
|
15
|
+
{title ? (
|
|
16
|
+
<text fg={theme.colors.info}>
|
|
17
|
+
<strong>{title}</strong>
|
|
18
|
+
</text>
|
|
19
|
+
) : null}
|
|
20
|
+
{children}
|
|
21
|
+
</box>
|
|
22
|
+
);
|
|
22
23
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import { theme } from "../../theme.js";
|
|
2
3
|
|
|
3
4
|
interface KeyValueLineProps {
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
label: string;
|
|
6
|
+
value: React.ReactNode;
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -10,10 +11,10 @@ interface KeyValueLineProps {
|
|
|
10
11
|
* Label is padded to 10 chars for consistent alignment.
|
|
11
12
|
*/
|
|
12
13
|
export function KeyValueLine({ label, value }: KeyValueLineProps) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
return (
|
|
15
|
+
<text fg={theme.colors.text}>
|
|
16
|
+
<span fg={theme.colors.muted}>{label.padEnd(10)}</span>
|
|
17
|
+
{value}
|
|
18
|
+
</text>
|
|
19
|
+
);
|
|
19
20
|
}
|
|
@@ -3,36 +3,34 @@ import { theme } from "../../theme.js";
|
|
|
3
3
|
import { SelectableRow } from "./SelectableRow.js";
|
|
4
4
|
|
|
5
5
|
interface ListCategoryRowProps {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
title: string;
|
|
7
|
+
expanded?: boolean;
|
|
8
|
+
count?: number;
|
|
9
|
+
badge?: string;
|
|
10
|
+
tone?: keyof typeof theme.category;
|
|
11
|
+
selected: boolean;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export function ListCategoryRow({
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
title,
|
|
16
|
+
expanded = false,
|
|
17
|
+
count,
|
|
18
|
+
badge,
|
|
19
|
+
tone = "gray",
|
|
20
|
+
selected,
|
|
21
21
|
}: ListCategoryRowProps) {
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
const colors = theme.category[tone];
|
|
23
|
+
const label = `${expanded ? "▼" : "▶"} ${title}${count !== undefined ? ` (${count})` : ""}`;
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
</SelectableRow>
|
|
37
|
-
);
|
|
25
|
+
return (
|
|
26
|
+
<SelectableRow selected={selected}>
|
|
27
|
+
{!selected && (
|
|
28
|
+
<span bg={colors.bg} fg={colors.fg}>
|
|
29
|
+
<strong> {label} </strong>
|
|
30
|
+
</span>
|
|
31
|
+
)}
|
|
32
|
+
{selected && <strong> {label} </strong>}
|
|
33
|
+
{!selected && badge ? <span fg={colors.badgeFg}> {badge}</span> : null}
|
|
34
|
+
</SelectableRow>
|
|
35
|
+
);
|
|
38
36
|
}
|
|
@@ -2,13 +2,13 @@ import React from "react";
|
|
|
2
2
|
import { theme } from "../../theme.js";
|
|
3
3
|
|
|
4
4
|
interface MetaTextProps {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
text: string;
|
|
6
|
+
tone?: "muted" | "warning" | "success" | "danger";
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Subdued text for versions, stars, status indicators.
|
|
11
11
|
*/
|
|
12
12
|
export function MetaText({ text, tone = "muted" }: MetaTextProps) {
|
|
13
|
-
|
|
13
|
+
return <span fg={theme.meta[tone]}>{text}</span>;
|
|
14
14
|
}
|
|
@@ -2,20 +2,20 @@ import React from "react";
|
|
|
2
2
|
import { theme } from "../../theme.js";
|
|
3
3
|
|
|
4
4
|
interface ScopeDetailScope {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
user?: boolean;
|
|
6
|
+
project?: boolean;
|
|
7
|
+
local?: boolean;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
interface ScopeDetailPaths {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
user?: string;
|
|
12
|
+
project?: string;
|
|
13
|
+
local?: string;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
interface ScopeDetailProps {
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
scopes: ScopeDetailScope;
|
|
18
|
+
paths?: ScopeDetailPaths;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/**
|
|
@@ -23,45 +23,45 @@ interface ScopeDetailProps {
|
|
|
23
23
|
* Renders each scope as: [bg=color] key [/bg] ● or ○ Label path
|
|
24
24
|
*/
|
|
25
25
|
export function ScopeDetail({ scopes, paths }: ScopeDetailProps) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
26
|
+
const items = [
|
|
27
|
+
{
|
|
28
|
+
key: "u",
|
|
29
|
+
label: "User",
|
|
30
|
+
color: theme.scopes.user,
|
|
31
|
+
active: scopes.user,
|
|
32
|
+
path: paths?.user,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
key: "p",
|
|
36
|
+
label: "Project",
|
|
37
|
+
color: theme.scopes.project,
|
|
38
|
+
active: scopes.project,
|
|
39
|
+
path: paths?.project,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
key: "l",
|
|
43
|
+
label: "Local",
|
|
44
|
+
color: theme.scopes.local,
|
|
45
|
+
active: scopes.local,
|
|
46
|
+
path: paths?.local,
|
|
47
|
+
},
|
|
48
|
+
].filter((i) => i.path !== undefined || i.active !== undefined);
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
50
|
+
return (
|
|
51
|
+
<box flexDirection="column">
|
|
52
|
+
{items.map((item) => (
|
|
53
|
+
<text key={item.key}>
|
|
54
|
+
<span bg={item.color} fg={theme.hints.fg}>
|
|
55
|
+
{" "}
|
|
56
|
+
{item.key}{" "}
|
|
57
|
+
</span>
|
|
58
|
+
<span fg={item.active ? item.color : theme.colors.muted}>
|
|
59
|
+
{item.active ? " ● " : " ○ "}
|
|
60
|
+
</span>
|
|
61
|
+
<span fg={item.color}>{item.label}</span>
|
|
62
|
+
{item.path && <span fg={theme.colors.muted}> {item.path}</span>}
|
|
63
|
+
</text>
|
|
64
|
+
))}
|
|
65
|
+
</box>
|
|
66
|
+
);
|
|
67
67
|
}
|
|
@@ -1,33 +1,58 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import type React from "react";
|
|
2
|
+
import { scopeOffFill } from "../../theme-mode.js";
|
|
2
3
|
import { theme } from "../../theme.js";
|
|
3
4
|
|
|
4
5
|
interface ScopeSquaresProps {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
user: boolean;
|
|
7
|
+
project: boolean;
|
|
8
|
+
local?: boolean;
|
|
9
|
+
selected?: boolean;
|
|
9
10
|
}
|
|
10
11
|
|
|
12
|
+
/** Every segment is a filled block; only the fill colour says lit or unlit. */
|
|
13
|
+
const SEGMENT = "■";
|
|
14
|
+
|
|
11
15
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
16
|
+
* Scope bar: three positional segments, user / project / local.
|
|
17
|
+
*
|
|
18
|
+
* Reads as a segmented indicator — each segment lit in its scope colour, or
|
|
19
|
+
* unlit in a fill that recedes toward the page. Position identifies the scope;
|
|
20
|
+
* the first segment is always user, the second project, the third local.
|
|
21
|
+
*
|
|
22
|
+
* The unlit fill used to be `theme.colors.muted` (#6B7280), the same mid-dark
|
|
23
|
+
* grey as de-emphasised body text. Against a light page that reads as a *dark*
|
|
24
|
+
* block, giving an unlit segment the same visual weight as a lit one — "non
|
|
25
|
+
* installed state is dark as well - confusing". The unlit fill now comes from
|
|
26
|
+
* `scopeOffFill()`, which steps toward whichever page the terminal actually has;
|
|
27
|
+
* see theme-mode.ts for why that one value cannot be fixed.
|
|
14
28
|
*/
|
|
15
29
|
export function ScopeSquares({
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
30
|
+
user,
|
|
31
|
+
project,
|
|
32
|
+
local,
|
|
33
|
+
selected = false,
|
|
20
34
|
}: ScopeSquaresProps) {
|
|
21
|
-
|
|
35
|
+
// On a selected row the background is our own accent, so an unlit segment
|
|
36
|
+
// recedes toward that block rather than toward the page.
|
|
37
|
+
const off = selected ? theme.selection.bg : scopeOffFill();
|
|
38
|
+
const lit = (on: boolean, color: string) =>
|
|
39
|
+
on ? (selected ? theme.selection.fg : color) : off;
|
|
40
|
+
|
|
41
|
+
const segments = [
|
|
42
|
+
lit(user, theme.scopes.user),
|
|
43
|
+
lit(project, theme.scopes.project),
|
|
44
|
+
...(local !== undefined ? [lit(local, theme.scopes.local)] : []),
|
|
45
|
+
];
|
|
22
46
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
47
|
+
// Fragments of <span> — safe to embed inside a coloured <text> parent.
|
|
48
|
+
return (
|
|
49
|
+
<>
|
|
50
|
+
{segments.map((fg, i) => (
|
|
51
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: fixed positional segments
|
|
52
|
+
<span key={i} fg={fg}>
|
|
53
|
+
{SEGMENT}
|
|
54
|
+
</span>
|
|
55
|
+
))}
|
|
56
|
+
</>
|
|
57
|
+
);
|
|
33
58
|
}
|
|
@@ -1,24 +1,30 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import type React from "react";
|
|
2
2
|
import { theme } from "../../theme.js";
|
|
3
3
|
|
|
4
4
|
interface SelectableRowProps {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
selected: boolean;
|
|
6
|
+
indent?: number;
|
|
7
|
+
children: React.ReactNode;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export function SelectableRow({
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
selected,
|
|
12
|
+
indent = 0,
|
|
13
|
+
children,
|
|
14
14
|
}: SelectableRowProps) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
return (
|
|
16
|
+
<text
|
|
17
|
+
// Unselected: no background, so the row sits on the terminal's own page.
|
|
18
|
+
bg={selected ? theme.selection.bg : undefined}
|
|
19
|
+
// Unselected must still NAME a colour. `undefined` does not mean "the
|
|
20
|
+
// terminal's foreground" — OpenTUI's fallback for an unstyled cell is
|
|
21
|
+
// truecolor white, so every unselected row here rendered white-on-cream
|
|
22
|
+
// and vanished on a light theme. Same root cause as the original bug,
|
|
23
|
+
// reached by omission rather than by a wrong literal.
|
|
24
|
+
fg={selected ? theme.selection.fg : theme.colors.text}
|
|
25
|
+
>
|
|
26
|
+
{" ".repeat(indent)}
|
|
27
|
+
{children}
|
|
28
|
+
</text>
|
|
29
|
+
);
|
|
24
30
|
}
|
|
@@ -11,86 +11,90 @@ import type { ModalState } from "../state/types.js";
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
export interface BuildModalArgs {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
violationCount: number;
|
|
15
|
+
safeCount: number;
|
|
16
|
+
onFixSafe: () => void;
|
|
17
|
+
onOpenTab: () => void;
|
|
18
|
+
onDismiss: () => void;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export function buildGitignoreModal(args: BuildModalArgs): ModalState {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
22
|
+
const { violationCount, safeCount, onFixSafe, onOpenTab, onDismiss } = args;
|
|
23
|
+
const options = [
|
|
24
|
+
{
|
|
25
|
+
label:
|
|
26
|
+
safeCount > 0
|
|
27
|
+
? `Add ${safeCount} missing .gitignore entr${safeCount === 1 ? "y" : "ies"}`
|
|
28
|
+
: "(no missing .gitignore entries)",
|
|
29
|
+
value: "fix-safe",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
label: "Open Git State tab to review",
|
|
33
|
+
value: "open-tab",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
label: "Dismiss",
|
|
37
|
+
value: "dismiss",
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
return {
|
|
41
|
+
type: "select",
|
|
42
|
+
title: "Gitignore violations detected",
|
|
43
|
+
message:
|
|
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.`,
|
|
48
|
+
options,
|
|
49
|
+
onSelect: (value: string) => {
|
|
50
|
+
if (value === "fix-safe") onFixSafe();
|
|
51
|
+
else if (value === "open-tab") onOpenTab();
|
|
52
|
+
else onDismiss();
|
|
53
|
+
},
|
|
54
|
+
onCancel: onDismiss,
|
|
55
|
+
};
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
export function useGitignoreModal() {
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
const { dispatch } = useApp();
|
|
60
|
+
const { navigateToScreen } = useNavigation();
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
62
|
+
const show = useCallback(
|
|
63
|
+
(args: {
|
|
64
|
+
violationCount: number;
|
|
65
|
+
safeCount: number;
|
|
66
|
+
onFixSafe: () => Promise<void> | void;
|
|
67
|
+
}) => {
|
|
68
|
+
if (args.violationCount === 0) return;
|
|
69
|
+
const dismiss = () => dispatch({ type: "HIDE_MODAL" });
|
|
70
|
+
const modal = buildGitignoreModal({
|
|
71
|
+
violationCount: args.violationCount,
|
|
72
|
+
safeCount: args.safeCount,
|
|
73
|
+
onFixSafe: () => {
|
|
74
|
+
dispatch({ type: "HIDE_MODAL" });
|
|
75
|
+
void Promise.resolve(args.onFixSafe()).catch((err) => {
|
|
76
|
+
dispatch({
|
|
77
|
+
type: "SHOW_MODAL",
|
|
78
|
+
modal: {
|
|
79
|
+
type: "message",
|
|
80
|
+
title: "Safe fixes failed",
|
|
81
|
+
message: err instanceof Error ? err.message : String(err),
|
|
82
|
+
variant: "error",
|
|
83
|
+
onDismiss: dismiss,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
onOpenTab: () => {
|
|
89
|
+
dispatch({ type: "HIDE_MODAL" });
|
|
90
|
+
navigateToScreen("gitignore");
|
|
91
|
+
},
|
|
92
|
+
onDismiss: dismiss,
|
|
93
|
+
});
|
|
94
|
+
dispatch({ type: "SHOW_MODAL", modal });
|
|
95
|
+
},
|
|
96
|
+
[dispatch, navigateToScreen],
|
|
97
|
+
);
|
|
94
98
|
|
|
95
|
-
|
|
99
|
+
return { show };
|
|
96
100
|
}
|
package/src/ui/registry.ts
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
import type React from "react";
|
|
2
2
|
|
|
3
3
|
export interface RowRenderProps<T> {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
item: T;
|
|
5
|
+
isSelected: boolean;
|
|
6
|
+
width?: number;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export interface DetailRenderProps<T> {
|
|
10
|
-
|
|
10
|
+
item: T;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export interface Hint {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
key: string;
|
|
15
|
+
label: string;
|
|
16
|
+
tone?: "default" | "primary" | "danger";
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export interface ItemRenderer<T> {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
renderRow: (props: RowRenderProps<T>) => React.ReactNode;
|
|
21
|
+
renderDetail: (props: DetailRenderProps<T>) => React.ReactNode;
|
|
22
|
+
getActionHints?: (item: T) => Hint[];
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export type RendererRegistry<TItem extends { kind: string }> = {
|
|
26
|
-
|
|
26
|
+
[K in TItem["kind"]]: ItemRenderer<Extract<TItem, { kind: K }>>;
|
|
27
27
|
};
|