@cueloop/client 0.1.0-alpha.65 → 0.1.0-alpha.66
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 +5 -5
- package/src/App.diff.test.tsx +8 -73
- package/src/App.plan-review.test.tsx +4 -185
- package/src/App.prototype.test.tsx +1 -8
- package/src/App.readonly.test.tsx +6 -28
- package/src/App.submit-confirm.test.tsx +19 -72
- package/src/App.test.tsx +70 -24
- package/src/App.tsx +177 -254
- package/src/App.walk.test.tsx +1 -1
- package/src/app-screens.tsx +40 -51
- package/src/app-view-model.ts +2 -4
- package/src/components/AnnotationCard.stories.tsx +1 -1
- package/src/components/AppShell.stories.tsx +84 -0
- package/src/components/AppShell.tsx +127 -0
- package/src/components/Breadcrumb.stories.tsx +1 -1
- package/src/components/ChangesColumn.stories.tsx +39 -0
- package/src/components/ChangesColumn.tsx +147 -0
- package/src/components/CodeBlock.stories.tsx +1 -1
- package/src/components/CompletionOverlay.stories.tsx +1 -1
- package/src/components/ConfirmCard.stories.tsx +1 -1
- package/src/components/ConfirmCard.tsx +3 -4
- package/src/components/ConfirmDialog.stories.tsx +1 -1
- package/src/components/DiffSheet.stories.tsx +1 -1
- package/src/components/InboxList.stories.tsx +23 -8
- package/src/components/InboxList.tsx +45 -19
- package/src/components/KeybindsDialog.stories.tsx +1 -1
- package/src/components/MarkerPopover.stories.tsx +1 -1
- package/src/components/MenuBar.stories.tsx +3 -12
- package/src/components/MenuBar.tsx +25 -56
- package/src/components/PromptDialog.stories.tsx +1 -1
- package/src/components/PrototypeSheet.stories.tsx +1 -1
- package/src/components/ReviewPanel.stories.tsx +1 -1
- package/src/components/ReviewRail.stories.tsx +1 -1
- package/src/components/SettingsDialog.stories.tsx +7 -1
- package/src/components/SettingsDialog.tsx +112 -66
- package/src/components/SettingsRows.stories.tsx +1 -1
- package/src/components/ShellHeader.stories.tsx +50 -0
- package/src/components/ShellHeader.tsx +137 -0
- package/src/components/ThreadFooter.stories.tsx +30 -0
- package/src/components/ThreadFooter.tsx +49 -0
- package/src/components/ThreadView.slash.test.tsx +113 -0
- package/src/components/ThreadView.stories.tsx +1 -1
- package/src/components/ThreadView.tsx +99 -11
- package/src/components/ThreadsSidebar.stories.tsx +36 -0
- package/src/components/ThreadsSidebar.tsx +57 -0
- package/src/components/Toast.stories.tsx +1 -1
- package/src/components/TreePane.stories.tsx +1 -1
- package/src/components/WalkWizard.stories.tsx +1 -1
- package/src/components/__snapshots__/stories.test.tsx.snap +348 -138
- package/src/components/agent-launcher.stories.tsx +1 -1
- package/src/components/file-tree.test.ts +45 -0
- package/src/components/file-tree.ts +57 -0
- package/src/components/pages/Workspace.stories.tsx +181 -0
- package/src/components/primitives/Button.stories.tsx +1 -1
- package/src/components/primitives/Card.stories.tsx +1 -1
- package/src/components/primitives/Dialog.stories.tsx +1 -1
- package/src/components/primitives/IconButton.stories.tsx +36 -0
- package/src/components/primitives/IconButton.tsx +43 -0
- package/src/components/primitives/StatusBar.stories.tsx +1 -1
- package/src/components/primitives/Tabs.stories.tsx +1 -1
- package/src/components/primitives/Toolbar.stories.tsx +1 -1
- package/src/components/primitives/Tree.stories.tsx +124 -0
- package/src/components/primitives/Tree.tsx +87 -0
- package/src/components/primitives/icons.ts +38 -0
- package/src/components/primitives/tree-model.test.ts +99 -0
- package/src/components/primitives/tree-model.ts +137 -0
- package/src/components/quick-actions-editor.stories.tsx +1 -1
- package/src/components/session-tree.test.ts +114 -0
- package/src/components/session-tree.ts +94 -0
- package/src/components/stories-app.tsx +142 -82
- package/src/components/story-tree.test.ts +100 -0
- package/src/components/story.ts +55 -0
- package/src/components/theme-context.stories.tsx +1 -1
- package/src/session-controller.ts +9 -4
- package/src/theme.ts +4 -4
- package/src/use-settings-dialog.tsx +9 -0
- package/src/App.review-panel.test.tsx +0 -140
- package/src/App.tree.test.tsx +0 -224
package/src/app-screens.tsx
CHANGED
|
@@ -12,11 +12,11 @@ import type { SettingsCategory, SettingsValues } from "./components/SettingsDial
|
|
|
12
12
|
import type { SettingsNav } from "./use-settings-dialog";
|
|
13
13
|
import { CLIENT_VERSION } from "./version";
|
|
14
14
|
import { ThemeProvider } from "./components/theme-context";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
15
|
+
import { NERD } from "./components/primitives/icons";
|
|
16
|
+
import type { InboxRow } from "./components/session-tree";
|
|
17
17
|
import { SettingsDialog } from "./components/SettingsDialog";
|
|
18
18
|
import { CompletionOverlay } from "./components/CompletionOverlay";
|
|
19
|
-
import {
|
|
19
|
+
import { ThreadsSidebar } from "./components/ThreadsSidebar";
|
|
20
20
|
import { ConfirmDialog } from "./components/ConfirmDialog";
|
|
21
21
|
import { PromptDialog } from "./components/PromptDialog";
|
|
22
22
|
import { WalkWizard } from "./components/WalkWizard";
|
|
@@ -39,12 +39,8 @@ export function ConnectingScreen({ theme }: { theme: Theme }): React.ReactNode {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export function MenuChrome(props: {
|
|
42
|
-
menuOpen: boolean;
|
|
43
42
|
menuDialog: "keybinds" | "settings" | null;
|
|
44
|
-
status: string;
|
|
45
43
|
theme: Theme;
|
|
46
|
-
setMenuOpen: Dispatch<SetStateAction<boolean>>;
|
|
47
|
-
setMenuDialog: Dispatch<SetStateAction<"keybinds" | "settings" | null>>;
|
|
48
44
|
keybindsSections: CheatsheetSection[];
|
|
49
45
|
settingsCategories: SettingsCategory[];
|
|
50
46
|
settingsValues: SettingsValues;
|
|
@@ -53,12 +49,8 @@ export function MenuChrome(props: {
|
|
|
53
49
|
cycleSetting: (rowKey: string) => void;
|
|
54
50
|
}): React.ReactNode {
|
|
55
51
|
const {
|
|
56
|
-
menuOpen,
|
|
57
52
|
menuDialog,
|
|
58
|
-
status,
|
|
59
53
|
theme,
|
|
60
|
-
setMenuOpen,
|
|
61
|
-
setMenuDialog,
|
|
62
54
|
keybindsSections,
|
|
63
55
|
settingsCategories,
|
|
64
56
|
settingsValues,
|
|
@@ -67,53 +59,37 @@ export function MenuChrome(props: {
|
|
|
67
59
|
cycleSetting,
|
|
68
60
|
} = props;
|
|
69
61
|
|
|
62
|
+
// the gear opens the settings dialog directly; Keybinds is a leaf in its tree nav
|
|
63
|
+
if (menuDialog !== "settings") return null;
|
|
64
|
+
|
|
70
65
|
return (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}}
|
|
85
|
-
theme={theme}
|
|
86
|
-
/>
|
|
87
|
-
{menuDialog === "keybinds" ? (
|
|
88
|
-
<KeybindsDialog sections={keybindsSections} theme={theme} />
|
|
89
|
-
) : null}
|
|
90
|
-
{menuDialog === "settings" ? (
|
|
91
|
-
<SettingsDialog
|
|
92
|
-
isOpen
|
|
93
|
-
categories={settingsCategories}
|
|
94
|
-
values={settingsValues}
|
|
95
|
-
activeCategoryId={settingsNav.categoryId}
|
|
96
|
-
activeRowIndex={settingsNav.rowIndex}
|
|
97
|
-
activeZone={settingsNav.zone}
|
|
98
|
-
onCategorySelect={onCategorySelect}
|
|
99
|
-
onRowActivate={(row) => cycleSetting(row.key)}
|
|
100
|
-
theme={theme}
|
|
101
|
-
/>
|
|
102
|
-
) : null}
|
|
103
|
-
</>
|
|
66
|
+
<SettingsDialog
|
|
67
|
+
isOpen
|
|
68
|
+
version={CLIENT_VERSION}
|
|
69
|
+
keybindsSections={keybindsSections}
|
|
70
|
+
categories={settingsCategories}
|
|
71
|
+
values={settingsValues}
|
|
72
|
+
activeCategoryId={settingsNav.categoryId}
|
|
73
|
+
activeRowIndex={settingsNav.rowIndex}
|
|
74
|
+
activeZone={settingsNav.zone}
|
|
75
|
+
onCategorySelect={onCategorySelect}
|
|
76
|
+
onRowActivate={(row) => cycleSetting(row.key)}
|
|
77
|
+
theme={theme}
|
|
78
|
+
/>
|
|
104
79
|
);
|
|
105
80
|
}
|
|
106
81
|
|
|
107
82
|
export function InboxScreen(props: {
|
|
108
|
-
|
|
83
|
+
rows: InboxRow[];
|
|
109
84
|
inboxCursor: number;
|
|
110
85
|
mode: Mode;
|
|
111
86
|
theme: Theme;
|
|
112
87
|
controller: ReviewController;
|
|
113
88
|
setMode: Dispatch<SetStateAction<Mode>>;
|
|
114
89
|
menuChrome: React.ReactNode;
|
|
90
|
+
onOpenMenu: () => void;
|
|
115
91
|
}): React.ReactNode {
|
|
116
|
-
const {
|
|
92
|
+
const { rows, inboxCursor, mode, theme, controller, setMode, menuChrome, onOpenMenu } = props;
|
|
117
93
|
const confirming = mode.type === "confirmDelete" ? mode : null;
|
|
118
94
|
|
|
119
95
|
return (
|
|
@@ -139,6 +115,9 @@ export function InboxScreen(props: {
|
|
|
139
115
|
flexDirection: "row",
|
|
140
116
|
}}
|
|
141
117
|
>
|
|
118
|
+
<box onMouseUp={onOpenMenu} style={{ paddingRight: 2 }}>
|
|
119
|
+
<text fg={theme.textMuted}>{NERD.settings}</text>
|
|
120
|
+
</box>
|
|
142
121
|
<text>
|
|
143
122
|
<span fg={theme.accent}>cueloop</span>
|
|
144
123
|
<span fg={theme.textDim}> · resume</span>
|
|
@@ -146,11 +125,21 @@ export function InboxScreen(props: {
|
|
|
146
125
|
</box>
|
|
147
126
|
<box style={{ flexGrow: 1 }} />
|
|
148
127
|
</box>
|
|
149
|
-
<
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
128
|
+
<box style={{ flexGrow: 1, flexDirection: "row" }}>
|
|
129
|
+
<ThreadsSidebar
|
|
130
|
+
open
|
|
131
|
+
rows={rows}
|
|
132
|
+
cursor={inboxCursor}
|
|
133
|
+
onSelect={(id) => controller.open(id)}
|
|
134
|
+
onRequestDelete={(id, title) =>
|
|
135
|
+
setMode({ type: "confirmDelete", sessionId: id, title })
|
|
136
|
+
}
|
|
137
|
+
theme={theme}
|
|
138
|
+
/>
|
|
139
|
+
<box style={{ flexGrow: 1, paddingLeft: 2, paddingTop: 1 }}>
|
|
140
|
+
<text fg={theme.textDim}>Select a thread</text>
|
|
141
|
+
</box>
|
|
142
|
+
</box>
|
|
154
143
|
{menuChrome}
|
|
155
144
|
<ConfirmDialog
|
|
156
145
|
isOpen={confirming !== null}
|
package/src/app-view-model.ts
CHANGED
|
@@ -112,17 +112,15 @@ export function buildRenderFlags(params: {
|
|
|
112
112
|
isDiff: boolean;
|
|
113
113
|
isPrototype: boolean;
|
|
114
114
|
resolved: boolean;
|
|
115
|
-
menuOpen: boolean;
|
|
116
115
|
menuDialog: "keybinds" | "settings" | null;
|
|
117
116
|
resolvedIds: Set<string>;
|
|
118
117
|
}) {
|
|
119
|
-
const { session, isOwner, isDiff, isPrototype, resolved,
|
|
120
|
-
params;
|
|
118
|
+
const { session, isOwner, isDiff, isPrototype, resolved, menuDialog, resolvedIds } = params;
|
|
121
119
|
|
|
122
120
|
return {
|
|
123
121
|
showOwnerActions: isOwner && !isDiff && !resolved,
|
|
124
122
|
prototypeCanComment: isOwner && !resolved,
|
|
125
|
-
chromeHidden:
|
|
123
|
+
chromeHidden: menuDialog !== null,
|
|
126
124
|
prototypePath: session.artifact.meta.prototypePath ?? "",
|
|
127
125
|
railResolvedIds: isDiff || isPrototype ? null : resolvedIds,
|
|
128
126
|
};
|
|
@@ -3,7 +3,7 @@ import { DARK } from "../theme";
|
|
|
3
3
|
import type { Story, StoryMeta } from "./story";
|
|
4
4
|
import { AnnotationCard } from "./AnnotationCard";
|
|
5
5
|
|
|
6
|
-
export const meta: StoryMeta = { title: "AnnotationCard" };
|
|
6
|
+
export const meta: StoryMeta = { title: "Cards/AnnotationCard" };
|
|
7
7
|
|
|
8
8
|
const draftCallbacks = { onInput: () => {}, onSave: () => {}, onCancel: () => {} };
|
|
9
9
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "./story";
|
|
4
|
+
import { AppShell } from "./AppShell";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "Layout/AppShell" };
|
|
7
|
+
|
|
8
|
+
function Sidebar(): React.ReactNode {
|
|
9
|
+
return (
|
|
10
|
+
<box style={{ flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
|
|
11
|
+
<text fg={DARK.textDim}>Projects</text>
|
|
12
|
+
<text fg={DARK.text}> cueloop</text>
|
|
13
|
+
<box style={{ backgroundColor: DARK.elevated }}>
|
|
14
|
+
<text fg={DARK.accent}> Read Cueloop Repository</text>
|
|
15
|
+
</box>
|
|
16
|
+
<text> </text>
|
|
17
|
+
<text fg={DARK.textDim}>Threads</text>
|
|
18
|
+
<text fg={DARK.textMuted}> Welcome to cueloop</text>
|
|
19
|
+
</box>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function Main(): React.ReactNode {
|
|
24
|
+
return (
|
|
25
|
+
<box style={{ flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
|
|
26
|
+
<text fg={DARK.text}>read the repo</text>
|
|
27
|
+
<text> </text>
|
|
28
|
+
<text fg={DARK.textMuted}>Repository context loaded. main is clean.</text>
|
|
29
|
+
<text> </text>
|
|
30
|
+
<text fg={DARK.text}>Core architecture</text>
|
|
31
|
+
<text fg={DARK.textMuted}> - packages/schema is the dependency root</text>
|
|
32
|
+
<text fg={DARK.textMuted}> - packages/daemon owns session state</text>
|
|
33
|
+
</box>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function Inspector(): React.ReactNode {
|
|
38
|
+
return (
|
|
39
|
+
<box style={{ flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
|
|
40
|
+
<text fg={DARK.text}>cueloop/main</text>
|
|
41
|
+
<text fg={DARK.textDim}> No changes</text>
|
|
42
|
+
</box>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function Header(): React.ReactNode {
|
|
47
|
+
return (
|
|
48
|
+
<box style={{ flexDirection: "row", paddingLeft: 1 }}>
|
|
49
|
+
<text fg={DARK.textMuted}>Read Cueloop Repository</text>
|
|
50
|
+
<box style={{ flexGrow: 1 }} />
|
|
51
|
+
<text fg={DARK.textDim}>markdown.test.ts</text>
|
|
52
|
+
</box>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function Footer(): React.ReactNode {
|
|
57
|
+
return (
|
|
58
|
+
<box style={{ flexDirection: "row", paddingLeft: 1 }}>
|
|
59
|
+
<text fg={DARK.textDim}>cueloop / main</text>
|
|
60
|
+
<box style={{ flexGrow: 1 }} />
|
|
61
|
+
<text fg={DARK.textDim}>Fable 5</text>
|
|
62
|
+
</box>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const ThreePane: Story = {
|
|
67
|
+
render: () => (
|
|
68
|
+
<AppShell
|
|
69
|
+
sidebar={<Sidebar />}
|
|
70
|
+
main={<Main />}
|
|
71
|
+
mainHeader={<Header />}
|
|
72
|
+
inspector={<Inspector />}
|
|
73
|
+
footer={<Footer />}
|
|
74
|
+
/>
|
|
75
|
+
),
|
|
76
|
+
expectedColors: [DARK.border, DARK.accent],
|
|
77
|
+
size: { width: 120, height: 28 },
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const SidebarAndMain: Story = {
|
|
81
|
+
render: () => <AppShell sidebar={<Sidebar />} main={<Main />} />,
|
|
82
|
+
expectedColors: [DARK.border],
|
|
83
|
+
size: { width: 90, height: 20 },
|
|
84
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// App shell: a grid of pane columns. Each column stacks a header cell over its body; the vertical
|
|
2
|
+
// rules run full height (through the header), and each header cell's bottom rule joins into one
|
|
3
|
+
// horizontal line across the columns. Rules only - no enclosing frames.
|
|
4
|
+
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { DARK, type Theme } from "../theme";
|
|
7
|
+
import type { BorderSides } from "@opentui/core";
|
|
8
|
+
|
|
9
|
+
export interface AppShellProps {
|
|
10
|
+
headerHeight?: number;
|
|
11
|
+
sidebar?: React.ReactNode;
|
|
12
|
+
sidebarHeader?: React.ReactNode;
|
|
13
|
+
sidebarWidth?: number;
|
|
14
|
+
main: React.ReactNode;
|
|
15
|
+
mainHeader?: React.ReactNode;
|
|
16
|
+
inspector?: React.ReactNode;
|
|
17
|
+
inspectorHeader?: React.ReactNode;
|
|
18
|
+
inspectorWidth?: number;
|
|
19
|
+
footer?: React.ReactNode;
|
|
20
|
+
theme?: Theme;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface PaneColumnProps {
|
|
24
|
+
width?: number;
|
|
25
|
+
grow?: boolean;
|
|
26
|
+
side?: BorderSides;
|
|
27
|
+
header: React.ReactNode;
|
|
28
|
+
body: React.ReactNode;
|
|
29
|
+
headerHeight: number;
|
|
30
|
+
theme: Theme;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function PaneColumn({
|
|
34
|
+
width,
|
|
35
|
+
grow,
|
|
36
|
+
side,
|
|
37
|
+
header,
|
|
38
|
+
body,
|
|
39
|
+
headerHeight,
|
|
40
|
+
theme,
|
|
41
|
+
}: PaneColumnProps): React.ReactNode {
|
|
42
|
+
return (
|
|
43
|
+
<box
|
|
44
|
+
style={{
|
|
45
|
+
flexDirection: "column",
|
|
46
|
+
...(grow ? { flexGrow: 1 } : { width }),
|
|
47
|
+
borderStyle: "single",
|
|
48
|
+
border: side ? [side] : [],
|
|
49
|
+
borderColor: theme.border,
|
|
50
|
+
}}
|
|
51
|
+
>
|
|
52
|
+
<box
|
|
53
|
+
style={{
|
|
54
|
+
height: headerHeight,
|
|
55
|
+
borderStyle: "single",
|
|
56
|
+
border: ["bottom"],
|
|
57
|
+
borderColor: theme.border,
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
{header}
|
|
61
|
+
</box>
|
|
62
|
+
<box style={{ flexGrow: 1 }}>{body}</box>
|
|
63
|
+
</box>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function AppShell({
|
|
68
|
+
headerHeight = 3,
|
|
69
|
+
sidebar,
|
|
70
|
+
sidebarHeader,
|
|
71
|
+
sidebarWidth = 28,
|
|
72
|
+
main,
|
|
73
|
+
mainHeader,
|
|
74
|
+
inspector,
|
|
75
|
+
inspectorHeader,
|
|
76
|
+
inspectorWidth = 40,
|
|
77
|
+
footer,
|
|
78
|
+
theme,
|
|
79
|
+
}: AppShellProps): React.ReactNode {
|
|
80
|
+
const tokens = theme ?? DARK;
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<box
|
|
84
|
+
style={{
|
|
85
|
+
flexDirection: "column",
|
|
86
|
+
width: "100%",
|
|
87
|
+
height: "100%",
|
|
88
|
+
backgroundColor: tokens.background,
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
<box style={{ flexDirection: "row", flexGrow: 1 }}>
|
|
92
|
+
{sidebar !== undefined ? (
|
|
93
|
+
<PaneColumn
|
|
94
|
+
width={sidebarWidth}
|
|
95
|
+
side="right"
|
|
96
|
+
header={sidebarHeader}
|
|
97
|
+
body={sidebar}
|
|
98
|
+
headerHeight={headerHeight}
|
|
99
|
+
theme={tokens}
|
|
100
|
+
/>
|
|
101
|
+
) : null}
|
|
102
|
+
<PaneColumn
|
|
103
|
+
grow
|
|
104
|
+
header={mainHeader}
|
|
105
|
+
body={main}
|
|
106
|
+
headerHeight={headerHeight}
|
|
107
|
+
theme={tokens}
|
|
108
|
+
/>
|
|
109
|
+
{inspector !== undefined ? (
|
|
110
|
+
<PaneColumn
|
|
111
|
+
width={inspectorWidth}
|
|
112
|
+
side="left"
|
|
113
|
+
header={inspectorHeader}
|
|
114
|
+
body={inspector}
|
|
115
|
+
headerHeight={headerHeight}
|
|
116
|
+
theme={tokens}
|
|
117
|
+
/>
|
|
118
|
+
) : null}
|
|
119
|
+
</box>
|
|
120
|
+
{footer !== undefined ? (
|
|
121
|
+
<box style={{ borderStyle: "single", border: ["top"], borderColor: tokens.border }}>
|
|
122
|
+
{footer}
|
|
123
|
+
</box>
|
|
124
|
+
) : null}
|
|
125
|
+
</box>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
@@ -3,7 +3,7 @@ import { DARK } from "../theme";
|
|
|
3
3
|
import type { Story, StoryMeta } from "./story";
|
|
4
4
|
import { Breadcrumb } from "./Breadcrumb";
|
|
5
5
|
|
|
6
|
-
export const meta: StoryMeta = { title: "Breadcrumb" };
|
|
6
|
+
export const meta: StoryMeta = { title: "Chrome/Breadcrumb" };
|
|
7
7
|
|
|
8
8
|
export const SessionHeader: Story = {
|
|
9
9
|
render: () => (
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { DiffFileContents } from "@cueloop/schema";
|
|
3
|
+
import { DARK } from "../theme";
|
|
4
|
+
import type { Story, StoryMeta } from "./story";
|
|
5
|
+
import { ChangesColumn } from "./ChangesColumn";
|
|
6
|
+
|
|
7
|
+
export const meta: StoryMeta = { title: "Surfaces/ChangesColumn" };
|
|
8
|
+
|
|
9
|
+
function Frame({ children }: { children: React.ReactNode }): React.ReactNode {
|
|
10
|
+
return <box style={{ width: "100%", height: "100%", flexDirection: "row" }}>{children}</box>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const files: DiffFileContents[] = [
|
|
14
|
+
{ path: "packages/client/src/App.tsx", oldContents: "a", newContents: "b", status: "modified" },
|
|
15
|
+
{
|
|
16
|
+
path: "packages/client/src/components/ChangesColumn.tsx",
|
|
17
|
+
oldContents: "",
|
|
18
|
+
newContents: "c",
|
|
19
|
+
status: "added",
|
|
20
|
+
},
|
|
21
|
+
{ path: "packages/daemon/src/review.ts", oldContents: "d", newContents: "", status: "deleted" },
|
|
22
|
+
{ path: "README.md", oldContents: "e", newContents: "f", status: "modified" },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export const Changed: Story = {
|
|
26
|
+
render: () => (
|
|
27
|
+
<Frame>
|
|
28
|
+
<ChangesColumn
|
|
29
|
+
open
|
|
30
|
+
files={files}
|
|
31
|
+
selectedPath="packages/client/src/App.tsx"
|
|
32
|
+
onSelectFile={() => {}}
|
|
33
|
+
theme={DARK}
|
|
34
|
+
/>
|
|
35
|
+
</Frame>
|
|
36
|
+
),
|
|
37
|
+
expectedColors: [DARK.accent, DARK.border],
|
|
38
|
+
size: { width: 60, height: 12 },
|
|
39
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// The right-hand Changes column: the diff's changed files as a directory tree.
|
|
2
|
+
// A click on a file scrolls the diff to that file. Renders nothing when collapsed
|
|
3
|
+
// so the diff reclaims the width. Mirrors the left Threads column on the far side.
|
|
4
|
+
// Co-located here: the state hook that opens it by context and the header toggle.
|
|
5
|
+
|
|
6
|
+
import React, { useMemo, useState } from "react";
|
|
7
|
+
import type { DiffFileContents, ReviewSession } from "@cueloop/schema";
|
|
8
|
+
import type { Theme } from "../theme";
|
|
9
|
+
import type { DiffRow } from "../view-diff";
|
|
10
|
+
import { Tree } from "./primitives/Tree";
|
|
11
|
+
import { allFolderIds } from "./primitives/tree-model";
|
|
12
|
+
import { IconButton } from "./primitives/IconButton";
|
|
13
|
+
import { NERD } from "./primitives/icons";
|
|
14
|
+
import { buildFileTree } from "./file-tree";
|
|
15
|
+
|
|
16
|
+
export interface DiffColumnsState {
|
|
17
|
+
changesOpen: boolean;
|
|
18
|
+
toggleChanges: () => void;
|
|
19
|
+
/** The path under the diff cursor, highlighted in the Changes tree. */
|
|
20
|
+
currentFilePath?: string;
|
|
21
|
+
scrollToFile: (path: string) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Owns the right Changes column for a diff: context opens it when you enter a
|
|
26
|
+
* diff thread, and a file click scrolls the sheet to that file's first row.
|
|
27
|
+
* Resets on session change via the guarded render-time pattern, no effect.
|
|
28
|
+
*/
|
|
29
|
+
export function useDiffColumns(params: {
|
|
30
|
+
session: ReviewSession | null;
|
|
31
|
+
rows: DiffRow[];
|
|
32
|
+
cursor: number;
|
|
33
|
+
setCursor: (index: number) => void;
|
|
34
|
+
}): DiffColumnsState {
|
|
35
|
+
const { session, rows, cursor, setCursor } = params;
|
|
36
|
+
const contextDefault = session?.artifact.type === "diff";
|
|
37
|
+
const [changesOpen, setChangesOpen] = useState(contextDefault);
|
|
38
|
+
const [seenSessionId, setSeenSessionId] = useState(session?.id);
|
|
39
|
+
|
|
40
|
+
// entering another thread lets context decide again; within a thread the manual toggle stays
|
|
41
|
+
if (session?.id !== seenSessionId) {
|
|
42
|
+
setSeenSessionId(session?.id);
|
|
43
|
+
setChangesOpen(contextDefault);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
changesOpen,
|
|
48
|
+
toggleChanges: () => setChangesOpen((open) => !open),
|
|
49
|
+
currentFilePath: rows[cursor]?.file,
|
|
50
|
+
scrollToFile: (path: string) => {
|
|
51
|
+
const fileRowIndex = rows.findIndex((row) => row.kind === "file" && row.file === path);
|
|
52
|
+
|
|
53
|
+
if (fileRowIndex >= 0) setCursor(fileRowIndex);
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The header toggle for the Changes column; shown only for a diff, where files exist to browse. */
|
|
59
|
+
export function DiffChangesToggle({
|
|
60
|
+
isDiff,
|
|
61
|
+
open,
|
|
62
|
+
onToggle,
|
|
63
|
+
theme,
|
|
64
|
+
}: {
|
|
65
|
+
isDiff: boolean;
|
|
66
|
+
open: boolean;
|
|
67
|
+
onToggle: () => void;
|
|
68
|
+
theme?: Theme;
|
|
69
|
+
}): React.ReactNode {
|
|
70
|
+
if (!isDiff) return null;
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<IconButton
|
|
74
|
+
glyph={open ? NERD.sidebarRight : NERD.sidebarRightOff}
|
|
75
|
+
onPress={onToggle}
|
|
76
|
+
marginLeft={2}
|
|
77
|
+
theme={theme}
|
|
78
|
+
/>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface ChangesColumnProps {
|
|
83
|
+
open: boolean;
|
|
84
|
+
files?: readonly DiffFileContents[];
|
|
85
|
+
/** The path whose diff the sheet is scrolled to, highlighted in the tree. */
|
|
86
|
+
selectedPath?: string;
|
|
87
|
+
onSelectFile: (path: string) => void;
|
|
88
|
+
width?: number;
|
|
89
|
+
theme?: Theme;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function ChangesColumn({
|
|
93
|
+
open,
|
|
94
|
+
files = [],
|
|
95
|
+
selectedPath,
|
|
96
|
+
onSelectFile,
|
|
97
|
+
width = 32,
|
|
98
|
+
theme,
|
|
99
|
+
}: ChangesColumnProps): React.ReactNode {
|
|
100
|
+
const nodes = useMemo(() => buildFileTree(files), [files]);
|
|
101
|
+
// every folder opens by default so each changed file is reachable; the user
|
|
102
|
+
// only ever names the folders they fold shut, so a new file set stays open
|
|
103
|
+
const [collapsedIds, setCollapsedIds] = useState<ReadonlySet<string>>(new Set());
|
|
104
|
+
const expandedIds = useMemo(() => {
|
|
105
|
+
const expanded = allFolderIds(nodes);
|
|
106
|
+
|
|
107
|
+
for (const id of collapsedIds) expanded.delete(id);
|
|
108
|
+
|
|
109
|
+
return expanded;
|
|
110
|
+
}, [nodes, collapsedIds]);
|
|
111
|
+
|
|
112
|
+
if (!open) return null;
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<box
|
|
116
|
+
style={{
|
|
117
|
+
width,
|
|
118
|
+
flexDirection: "column",
|
|
119
|
+
borderStyle: "single",
|
|
120
|
+
border: ["left"],
|
|
121
|
+
borderColor: theme?.border,
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
<scrollbox style={{ flexGrow: 1 }} focused={false}>
|
|
125
|
+
<Tree
|
|
126
|
+
nodes={nodes}
|
|
127
|
+
expandedIds={expandedIds}
|
|
128
|
+
selectedId={selectedPath}
|
|
129
|
+
flattenEmptyDirectories
|
|
130
|
+
showStatus
|
|
131
|
+
onSelect={onSelectFile}
|
|
132
|
+
onToggle={(id) =>
|
|
133
|
+
setCollapsedIds((current) => {
|
|
134
|
+
const next = new Set(current);
|
|
135
|
+
|
|
136
|
+
if (next.has(id)) next.delete(id);
|
|
137
|
+
else next.add(id);
|
|
138
|
+
|
|
139
|
+
return next;
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
theme={theme}
|
|
143
|
+
/>
|
|
144
|
+
</scrollbox>
|
|
145
|
+
</box>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
@@ -3,7 +3,7 @@ import { DARK } from "../theme";
|
|
|
3
3
|
import type { Story, StoryMeta } from "./story";
|
|
4
4
|
import { CodeBlock } from "./CodeBlock";
|
|
5
5
|
|
|
6
|
-
export const meta: StoryMeta = { title: "CodeBlock" };
|
|
6
|
+
export const meta: StoryMeta = { title: "Primitives/CodeBlock" };
|
|
7
7
|
|
|
8
8
|
const SNIPPET = `export function gate(full: number) {
|
|
9
9
|
// threshold
|
|
@@ -3,7 +3,7 @@ import { DARK } from "../theme";
|
|
|
3
3
|
import type { Story, StoryMeta } from "./story";
|
|
4
4
|
import { CompletionOverlay } from "./CompletionOverlay";
|
|
5
5
|
|
|
6
|
-
export const meta: StoryMeta = { title: "CompletionOverlay" };
|
|
6
|
+
export const meta: StoryMeta = { title: "Overlays/CompletionOverlay" };
|
|
7
7
|
|
|
8
8
|
export const ApprovedPrompt: Story = {
|
|
9
9
|
render: () => <CompletionOverlay verdict="approve" completion={{ phase: "prompt" }} status="" />,
|
|
@@ -3,7 +3,7 @@ import { DARK } from "../theme";
|
|
|
3
3
|
import type { Story, StoryMeta } from "./story";
|
|
4
4
|
import { ConfirmCard } from "./ConfirmCard";
|
|
5
5
|
|
|
6
|
-
export const meta: StoryMeta = { title: "ConfirmCard" };
|
|
6
|
+
export const meta: StoryMeta = { title: "Cards/ConfirmCard" };
|
|
7
7
|
|
|
8
8
|
const callbacks = {
|
|
9
9
|
onInput: () => {},
|
|
@@ -98,11 +98,10 @@ export function ConfirmCard({
|
|
|
98
98
|
|
|
99
99
|
return (
|
|
100
100
|
<Card
|
|
101
|
-
title="
|
|
101
|
+
title=" send message "
|
|
102
102
|
contentRows={CONFIRM_CONTENT_ROWS + (viewedSummary !== undefined ? 2 : 0)}
|
|
103
103
|
borderColor={tokens.text}
|
|
104
|
-
backgroundColor=
|
|
105
|
-
marginRight={1}
|
|
104
|
+
backgroundColor={tokens.elevated}
|
|
106
105
|
theme={theme}
|
|
107
106
|
>
|
|
108
107
|
{viewedSummary !== undefined ? <text fg={tokens.textDim}>{viewedSummary}</text> : null}
|
|
@@ -118,7 +117,7 @@ export function ConfirmCard({
|
|
|
118
117
|
<box style={{ height: 1 }} />
|
|
119
118
|
<Toolbar>
|
|
120
119
|
<Button variant="solid" marginRight={2} onPress={onSubmit} theme={theme}>
|
|
121
|
-
{"
|
|
120
|
+
{" send message "}
|
|
122
121
|
</Button>
|
|
123
122
|
<Button onPress={onCancel} theme={theme}>
|
|
124
123
|
{" Cancel "}
|
|
@@ -3,7 +3,7 @@ import { DARK } from "../theme";
|
|
|
3
3
|
import type { Story, StoryMeta } from "./story";
|
|
4
4
|
import { ConfirmDialog } from "./ConfirmDialog";
|
|
5
5
|
|
|
6
|
-
export const meta: StoryMeta = { title: "ConfirmDialog" };
|
|
6
|
+
export const meta: StoryMeta = { title: "Overlays/ConfirmDialog" };
|
|
7
7
|
|
|
8
8
|
const callbacks = { onConfirm: () => {}, onCancel: () => {} };
|
|
9
9
|
|
|
@@ -4,7 +4,7 @@ import type { Story, StoryMeta } from "./story";
|
|
|
4
4
|
import { DiffSheet } from "./DiffSheet";
|
|
5
5
|
import { FIXTURE_PATCH, fixtureDiffRows } from "./story-fixtures";
|
|
6
6
|
|
|
7
|
-
export const meta: StoryMeta = { title: "DiffSheet" };
|
|
7
|
+
export const meta: StoryMeta = { title: "Surfaces/DiffSheet" };
|
|
8
8
|
|
|
9
9
|
export const SignedRows: Story = {
|
|
10
10
|
render: () => <DiffSheet rows={fixtureDiffRows()} cursor={0} annotations={[]} />,
|