@cueloop/client 0.1.0-alpha.64 → 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.autoclose.test.tsx +2 -2
- package/src/App.diff.test.tsx +8 -73
- package/src/App.export.test.tsx +2 -2
- package/src/App.plan-review.test.tsx +60 -412
- package/src/App.prototype.test.tsx +1 -8
- package/src/App.readonly.test.tsx +85 -62
- package/src/App.rendering.test.tsx +5 -6
- package/src/App.submit-confirm.test.tsx +32 -70
- package/src/App.test.tsx +136 -66
- package/src/App.toast.test.tsx +17 -8
- package/src/App.tsx +243 -284
- package/src/App.walk.test.tsx +1 -1
- package/src/app-screens.tsx +67 -52
- package/src/app-view-model.ts +6 -100
- package/src/components/AnnotationCard.stories.tsx +1 -1
- package/src/components/AnnotationCard.tsx +2 -2
- 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/KeybindsDialog.tsx +61 -17
- 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 +15 -1
- package/src/components/ReviewRail.stories.tsx +15 -1
- package/src/components/ReviewRail.tsx +13 -6
- 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.selection.test.tsx +812 -0
- package/src/components/ThreadView.slash.test.tsx +113 -0
- package/src/components/ThreadView.stories.tsx +26 -0
- package/src/components/ThreadView.tsx +1708 -0
- 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 +106 -0
- package/src/components/TreePane.tsx +156 -0
- package/src/components/WalkWizard.stories.tsx +1 -1
- package/src/components/__snapshots__/stories.test.tsx.snap +463 -305
- 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/diff-hunk-curate.ts +4 -104
- package/src/index.ts +1 -0
- package/src/intent-dispatch.test.ts +136 -3
- package/src/intent-dispatch.ts +92 -24
- package/src/key-bindings.test.ts +1 -1
- package/src/keymap.test.ts +4 -4
- package/src/keymap.ts +14 -7
- package/src/session-controller.curate.test.ts +89 -2
- package/src/session-controller.share.test.ts +195 -55
- package/src/session-controller.tree.test.ts +228 -0
- package/src/session-controller.ts +331 -72
- package/src/share.ts +114 -16
- package/src/test-support.test.ts +193 -1
- package/src/test-support.ts +64 -0
- package/src/theme.ts +4 -4
- package/src/thread-chords.test.ts +154 -0
- package/src/thread-chords.ts +144 -0
- package/src/thread-selection.test.ts +210 -0
- package/src/thread-selection.ts +243 -0
- package/src/tree-view.test.ts +128 -0
- package/src/tree-view.ts +148 -0
- package/src/use-settings-dialog.tsx +9 -0
- package/src/view-plan.ts +29 -12
- package/src/App.review-panel.test.tsx +0 -143
- package/src/components/PlanSheet.stories.tsx +0 -113
- package/src/components/PlanSheet.tsx +0 -419
- package/src/components/plan-sheet-run-style.ts +0 -88
|
@@ -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={[]} />,
|
|
@@ -2,9 +2,10 @@ import React from "react";
|
|
|
2
2
|
import { DARK } from "../theme";
|
|
3
3
|
import type { Story, StoryMeta } from "./story";
|
|
4
4
|
import { InboxList } from "./InboxList";
|
|
5
|
+
import { groupInbox } from "./session-tree";
|
|
5
6
|
import { fixtureDiffSession, fixturePlanSession } from "./story-fixtures";
|
|
6
7
|
|
|
7
|
-
export const meta: StoryMeta = { title: "InboxList" };
|
|
8
|
+
export const meta: StoryMeta = { title: "Surfaces/InboxList" };
|
|
8
9
|
|
|
9
10
|
/** InboxList is the body under App's header/menu chrome; a frame gives it height. */
|
|
10
11
|
function BodyFrame({ children }: { children: React.ReactNode }): React.ReactNode {
|
|
@@ -12,19 +13,33 @@ function BodyFrame({ children }: { children: React.ReactNode }): React.ReactNode
|
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export const TwoPending: Story = {
|
|
15
|
-
render: () =>
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
render: () => {
|
|
17
|
+
const plan = fixturePlanSession();
|
|
18
|
+
// one repo-bound thread (a project) and one standalone thread
|
|
19
|
+
const projectPlan = {
|
|
20
|
+
...plan,
|
|
21
|
+
workspace: {
|
|
22
|
+
...plan.workspace,
|
|
23
|
+
rootCommit: "root-1",
|
|
24
|
+
remote: "git@github.com:acme/widget.git",
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
const { rows } = groupInbox([projectPlan, fixtureDiffSession()]);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<BodyFrame>
|
|
31
|
+
<InboxList rows={rows} cursor={0} />
|
|
32
|
+
</BodyFrame>
|
|
33
|
+
);
|
|
34
|
+
},
|
|
35
|
+
expectedColors: [DARK.accent],
|
|
21
36
|
size: { width: 80, height: 12 },
|
|
22
37
|
};
|
|
23
38
|
|
|
24
39
|
export const Empty: Story = {
|
|
25
40
|
render: () => (
|
|
26
41
|
<BodyFrame>
|
|
27
|
-
<InboxList
|
|
42
|
+
<InboxList rows={[]} cursor={0} />
|
|
28
43
|
</BodyFrame>
|
|
29
44
|
),
|
|
30
45
|
size: { width: 80, height: 8 },
|
|
@@ -1,28 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The pending-review inbox: a
|
|
3
|
-
* the keyboard grammar
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* The pending-review inbox: a Projects and Threads tree. Selection stays with
|
|
3
|
+
* the keyboard grammar - the cursor indexes the flat thread order and this
|
|
4
|
+
* component only renders the snapshot. The selected thread carries a [delete]
|
|
5
|
+
* word-button (click, or `d` in the grammar). App supplies the surrounding
|
|
6
|
+
* header and MenuBar chrome; this is the flex-growing body.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import React from "react";
|
|
10
|
-
import type { ReviewSession } from "@cueloop/schema";
|
|
11
10
|
import type { Theme } from "../theme";
|
|
12
11
|
import { useComponentTheme } from "./theme-context";
|
|
13
12
|
import { Button } from "./primitives/Button";
|
|
13
|
+
import { NERD } from "./primitives/icons";
|
|
14
|
+
import type { InboxRow } from "./session-tree";
|
|
14
15
|
|
|
15
16
|
export interface InboxListProps {
|
|
16
|
-
|
|
17
|
+
rows: InboxRow[];
|
|
17
18
|
cursor: number;
|
|
18
|
-
/**
|
|
19
|
+
/** The open thread's id; highlights it instead of the cursor (the left sidebar case). */
|
|
20
|
+
activeId?: string;
|
|
21
|
+
/** Open a thread by clicking its row (the left sidebar case). */
|
|
22
|
+
onSelect?: (sessionId: string) => void;
|
|
23
|
+
/** Ask to delete a thread (the selected row's [delete] button). */
|
|
19
24
|
onRequestDelete?: (id: string, title: string) => void;
|
|
20
25
|
theme?: Theme;
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
export function InboxList({
|
|
24
|
-
|
|
29
|
+
rows,
|
|
25
30
|
cursor,
|
|
31
|
+
activeId,
|
|
32
|
+
onSelect,
|
|
26
33
|
onRequestDelete,
|
|
27
34
|
theme,
|
|
28
35
|
}: InboxListProps): React.ReactNode {
|
|
@@ -30,28 +37,47 @@ export function InboxList({
|
|
|
30
37
|
|
|
31
38
|
return (
|
|
32
39
|
<box style={{ flexGrow: 1, flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
|
|
33
|
-
{
|
|
40
|
+
{rows.length === 0 ? (
|
|
34
41
|
<text fg={tokens.textDim}>nothing waiting for review</text>
|
|
35
42
|
) : (
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
rows.map((row) => {
|
|
44
|
+
if (row.kind === "section") {
|
|
45
|
+
return (
|
|
46
|
+
<text key={row.id} fg={tokens.textDim}>
|
|
47
|
+
{row.label}
|
|
48
|
+
</text>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (row.kind === "project") {
|
|
53
|
+
return (
|
|
54
|
+
<text key={row.id}>
|
|
55
|
+
<span fg={tokens.blue}>{` ${NERD.folderOpen} `}</span>
|
|
56
|
+
<span fg={tokens.textMuted}>{row.label}</span>
|
|
57
|
+
</text>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const selected =
|
|
62
|
+
activeId !== undefined ? activeId === row.session.id : row.selectionIndex === cursor;
|
|
63
|
+
const title = row.session.artifact.meta.title ?? row.session.id;
|
|
39
64
|
|
|
40
65
|
return (
|
|
41
66
|
<box
|
|
42
|
-
key={
|
|
67
|
+
key={row.id}
|
|
68
|
+
onMouseUp={onSelect ? () => onSelect(row.session.id) : undefined}
|
|
43
69
|
style={{
|
|
44
70
|
flexDirection: "row",
|
|
45
|
-
backgroundColor: selected ? tokens.
|
|
71
|
+
backgroundColor: selected ? tokens.elevated : undefined,
|
|
46
72
|
}}
|
|
47
73
|
>
|
|
48
|
-
<text fg={selected ? tokens.
|
|
49
|
-
{
|
|
50
|
-
{title}
|
|
74
|
+
<text fg={selected ? tokens.accent : tokens.textMuted}>
|
|
75
|
+
{" "}
|
|
76
|
+
{title}
|
|
51
77
|
</text>
|
|
52
78
|
<box style={{ flexGrow: 1 }} />
|
|
53
79
|
{selected && onRequestDelete ? (
|
|
54
|
-
<Button onPress={() => onRequestDelete(session.id, title)} theme={theme}>
|
|
80
|
+
<Button onPress={() => onRequestDelete(row.session.id, title)} theme={theme}>
|
|
55
81
|
{" [delete] "}
|
|
56
82
|
</Button>
|
|
57
83
|
) : null}
|
|
@@ -3,7 +3,7 @@ import { DARK } from "../theme";
|
|
|
3
3
|
import type { Story, StoryMeta } from "./story";
|
|
4
4
|
import { KeybindsDialog } from "./KeybindsDialog";
|
|
5
5
|
|
|
6
|
-
export const meta: StoryMeta = { title: "KeybindsDialog" };
|
|
6
|
+
export const meta: StoryMeta = { title: "Overlays/KeybindsDialog" };
|
|
7
7
|
|
|
8
8
|
const SECTIONS = [
|
|
9
9
|
{
|
|
@@ -10,13 +10,52 @@ export interface KeybindsDialogProps {
|
|
|
10
10
|
theme?: Theme;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
/** Rows a section takes: its title, its entries, and the blank line after it. */
|
|
14
|
+
function sectionRows(section: CheatsheetSection): number {
|
|
15
|
+
return 1 + section.entries.length + 1;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Split the sections over `count` columns, filling each column to the
|
|
20
|
+
* balanced share of rows before starting the next, so a long cheatsheet
|
|
21
|
+
* reads top to bottom, then left to right.
|
|
22
|
+
*/
|
|
23
|
+
function columnsOf(sections: CheatsheetSection[], count: number): CheatsheetSection[][] {
|
|
24
|
+
const total = sections.reduce((sum, section) => sum + sectionRows(section), 0);
|
|
25
|
+
const share = Math.ceil(total / count);
|
|
26
|
+
const columns: CheatsheetSection[][] = [[]];
|
|
27
|
+
let filled = 0;
|
|
28
|
+
|
|
29
|
+
for (const section of sections) {
|
|
30
|
+
const column = columns[columns.length - 1]!;
|
|
31
|
+
|
|
32
|
+
if (filled > 0 && filled + sectionRows(section) > share && columns.length < count) {
|
|
33
|
+
columns.push([section]);
|
|
34
|
+
filled = sectionRows(section);
|
|
35
|
+
} else {
|
|
36
|
+
column.push(section);
|
|
37
|
+
filled += sectionRows(section);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return columns;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const COLUMN_WIDTH = 48;
|
|
45
|
+
|
|
13
46
|
export function KeybindsDialog({ sections, theme }: KeybindsDialogProps): React.ReactNode {
|
|
14
47
|
const tokens = useComponentTheme(theme);
|
|
15
48
|
const { width: terminalWidth, height: terminalHeight } = useTerminalDimensions();
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
49
|
+
const totalRows = sections.reduce((sum, section) => sum + sectionRows(section), 0) + 1;
|
|
50
|
+
// a cheatsheet taller than the terminal folds into a second column
|
|
51
|
+
const columnCount =
|
|
52
|
+
totalRows + 3 > terminalHeight - 4 && terminalWidth >= COLUMN_WIDTH * 2 + 6 ? 2 : 1;
|
|
53
|
+
const columns = columnsOf(sections, columnCount);
|
|
54
|
+
const tallest = Math.max(
|
|
55
|
+
...columns.map((column) => column.reduce((sum, section) => sum + sectionRows(section), 0)),
|
|
56
|
+
);
|
|
57
|
+
const width = Math.min(COLUMN_WIDTH * columnCount + 4, terminalWidth - 6);
|
|
58
|
+
const height = Math.min(terminalHeight - 4, tallest + 1 + 3);
|
|
20
59
|
|
|
21
60
|
return (
|
|
22
61
|
<Dialog
|
|
@@ -36,19 +75,24 @@ export function KeybindsDialog({ sections, theme }: KeybindsDialogProps): React.
|
|
|
36
75
|
paddingTop: 1,
|
|
37
76
|
}}
|
|
38
77
|
>
|
|
39
|
-
{
|
|
40
|
-
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
78
|
+
<box style={{ flexDirection: "row", flexGrow: 1 }}>
|
|
79
|
+
{columns.map((column, columnIndex) => (
|
|
80
|
+
<box key={columnIndex} style={{ flexDirection: "column", width: COLUMN_WIDTH }}>
|
|
81
|
+
{column.map((section) => (
|
|
82
|
+
<box key={section.title} style={{ flexDirection: "column" }}>
|
|
83
|
+
<text fg={tokens.accent}>{section.title}</text>
|
|
84
|
+
{section.entries.map((entry, index) => (
|
|
85
|
+
<text key={index}>
|
|
86
|
+
<span fg={tokens.text}>{entry.keys.padEnd(12)}</span>
|
|
87
|
+
<span fg={tokens.textMuted}>{entry.label}</span>
|
|
88
|
+
</text>
|
|
89
|
+
))}
|
|
90
|
+
<text> </text>
|
|
91
|
+
</box>
|
|
92
|
+
))}
|
|
93
|
+
</box>
|
|
94
|
+
))}
|
|
95
|
+
</box>
|
|
52
96
|
<box style={{ flexDirection: "row" }}>
|
|
53
97
|
<box style={{ flexGrow: 1 }} />
|
|
54
98
|
<text fg={tokens.textDim}>[esc]</text>
|
|
@@ -4,7 +4,7 @@ import { DEFAULT_QUICK_ACTIONS } from "../config";
|
|
|
4
4
|
import type { Story, StoryMeta } from "./story";
|
|
5
5
|
import { MarkerPopover } from "./MarkerPopover";
|
|
6
6
|
|
|
7
|
-
export const meta: StoryMeta = { title: "MarkerPopover" };
|
|
7
|
+
export const meta: StoryMeta = { title: "Overlays/MarkerPopover" };
|
|
8
8
|
|
|
9
9
|
const callbacks = {
|
|
10
10
|
onComment: () => {},
|
|
@@ -3,9 +3,9 @@ import { DARK } from "../theme";
|
|
|
3
3
|
import type { Story, StoryMeta } from "./story";
|
|
4
4
|
import { MenuBar } from "./MenuBar";
|
|
5
5
|
|
|
6
|
-
export const meta: StoryMeta = { title: "MenuBar" };
|
|
6
|
+
export const meta: StoryMeta = { title: "Chrome/MenuBar" };
|
|
7
7
|
|
|
8
|
-
const callbacks = {
|
|
8
|
+
const callbacks = { onSettings: () => {}, onKeybinds: () => {} };
|
|
9
9
|
|
|
10
10
|
function BottomFrame({ children }: { children: React.ReactNode }): React.ReactNode {
|
|
11
11
|
return (
|
|
@@ -17,19 +17,10 @@ function BottomFrame({ children }: { children: React.ReactNode }): React.ReactNo
|
|
|
17
17
|
);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export const Closed: Story = {
|
|
21
|
-
render: () => (
|
|
22
|
-
<BottomFrame>
|
|
23
|
-
<MenuBar open={false} version="0.1.0-alpha.32" {...callbacks} />
|
|
24
|
-
</BottomFrame>
|
|
25
|
-
),
|
|
26
|
-
size: { width: 60, height: 8 },
|
|
27
|
-
};
|
|
28
|
-
|
|
29
20
|
export const Open: Story = {
|
|
30
21
|
render: () => (
|
|
31
22
|
<BottomFrame>
|
|
32
|
-
<MenuBar open
|
|
23
|
+
<MenuBar open {...callbacks} />
|
|
33
24
|
</BottomFrame>
|
|
34
25
|
),
|
|
35
26
|
expectedColors: [DARK.text],
|
|
@@ -1,75 +1,44 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { useTerminalDimensions } from "@opentui/react";
|
|
3
2
|
import type { Theme } from "../theme";
|
|
4
3
|
import { useComponentTheme } from "./theme-context";
|
|
5
4
|
import { FRAME_BORDER_STYLE } from "./primitives/frame";
|
|
6
5
|
|
|
7
6
|
export interface MenuBarProps {
|
|
8
7
|
open: boolean;
|
|
9
|
-
version: string;
|
|
10
|
-
/** The transient status line, shown between the menu and the version. */
|
|
11
|
-
status?: string;
|
|
12
|
-
onToggle: () => void;
|
|
13
8
|
onSettings: () => void;
|
|
14
9
|
onKeybinds: () => void;
|
|
15
10
|
theme?: Theme;
|
|
16
11
|
}
|
|
17
12
|
|
|
18
|
-
const
|
|
13
|
+
const DROPDOWN_OPTION_ROWS = 2;
|
|
19
14
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
version,
|
|
23
|
-
status,
|
|
24
|
-
onToggle,
|
|
25
|
-
onSettings,
|
|
26
|
-
onKeybinds,
|
|
27
|
-
theme,
|
|
28
|
-
}: MenuBarProps): React.ReactNode {
|
|
15
|
+
/** The settings gear's drop-down: Settings and Keybinds, below the top-left gear. */
|
|
16
|
+
export function MenuBar({ open, onSettings, onKeybinds, theme }: MenuBarProps): React.ReactNode {
|
|
29
17
|
const tokens = useComponentTheme(theme);
|
|
30
|
-
const
|
|
31
|
-
|
|
18
|
+
const dropdownHeight = DROPDOWN_OPTION_ROWS + 2;
|
|
19
|
+
|
|
20
|
+
if (!open) return null;
|
|
32
21
|
|
|
33
22
|
return (
|
|
34
|
-
|
|
35
|
-
{
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
<box onMouseUp={onKeybinds}>
|
|
53
|
-
<text fg={tokens.text}> Keybinds</text>
|
|
54
|
-
</box>
|
|
55
|
-
</box>
|
|
56
|
-
) : null}
|
|
57
|
-
<box
|
|
58
|
-
style={{
|
|
59
|
-
flexDirection: "row",
|
|
60
|
-
height: 1,
|
|
61
|
-
backgroundColor: tokens.panel,
|
|
62
|
-
paddingLeft: 1,
|
|
63
|
-
paddingRight: 1,
|
|
64
|
-
}}
|
|
65
|
-
>
|
|
66
|
-
<box onMouseUp={onToggle}>
|
|
67
|
-
<text fg={tokens.textDim}>menu</text>
|
|
68
|
-
</box>
|
|
69
|
-
{status ? <text fg={tokens.accent}>{` ${status}`}</text> : null}
|
|
70
|
-
<box style={{ flexGrow: 1 }} />
|
|
71
|
-
<text fg={tokens.textDim}>v{version}</text>
|
|
23
|
+
<box
|
|
24
|
+
style={{
|
|
25
|
+
position: "absolute",
|
|
26
|
+
left: 1,
|
|
27
|
+
top: 2,
|
|
28
|
+
width: 16,
|
|
29
|
+
height: dropdownHeight,
|
|
30
|
+
border: true,
|
|
31
|
+
borderStyle: FRAME_BORDER_STYLE,
|
|
32
|
+
borderColor: tokens.text,
|
|
33
|
+
flexDirection: "column",
|
|
34
|
+
}}
|
|
35
|
+
>
|
|
36
|
+
<box onMouseUp={onSettings}>
|
|
37
|
+
<text fg={tokens.text}> Settings</text>
|
|
38
|
+
</box>
|
|
39
|
+
<box onMouseUp={onKeybinds}>
|
|
40
|
+
<text fg={tokens.text}> Keybinds</text>
|
|
72
41
|
</box>
|
|
73
|
-
|
|
42
|
+
</box>
|
|
74
43
|
);
|
|
75
44
|
}
|
|
@@ -3,7 +3,7 @@ import { DARK } from "../theme";
|
|
|
3
3
|
import type { Story, StoryMeta } from "./story";
|
|
4
4
|
import { PromptDialog } from "./PromptDialog";
|
|
5
5
|
|
|
6
|
-
export const meta: StoryMeta = { title: "PromptDialog" };
|
|
6
|
+
export const meta: StoryMeta = { title: "Overlays/PromptDialog" };
|
|
7
7
|
|
|
8
8
|
export const RenameAuthor: Story = {
|
|
9
9
|
render: () => (
|
|
@@ -3,7 +3,7 @@ import { DARK } from "../theme";
|
|
|
3
3
|
import type { Story, StoryMeta } from "./story";
|
|
4
4
|
import { PrototypeSheet } from "./PrototypeSheet";
|
|
5
5
|
|
|
6
|
-
export const meta: StoryMeta = { title: "PrototypeSheet" };
|
|
6
|
+
export const meta: StoryMeta = { title: "Surfaces/PrototypeSheet" };
|
|
7
7
|
|
|
8
8
|
// The story renderer reports no kitty graphics, so the sheet shows its fallback.
|
|
9
9
|
export const Unsupported: Story = {
|