@cueloop/client 0.1.0-alpha.77 → 0.1.0-alpha.79
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 +14 -13
- package/src/App.autoclose.test.tsx +3 -3
- package/src/App.diff.test.tsx +22 -5
- package/src/App.export.test.tsx +1 -2
- package/src/App.focus.test.ts +1 -59
- package/src/App.identity.test.tsx +3 -3
- package/src/App.plan-render.test.tsx +209 -0
- package/src/App.plan-review.test.tsx +91 -26
- package/src/App.readonly.test.tsx +6 -5
- package/src/App.share-dialog.test.tsx +172 -0
- package/src/App.submit-confirm.test.tsx +39 -8
- package/src/App.test.tsx +33 -30
- package/src/App.toast.test.tsx +11 -4
- package/src/App.tsx +263 -274
- package/src/App.walk.test.tsx +11 -10
- package/src/App.workbench.test.tsx +86 -0
- package/src/__snapshots__/view-plan.regression.test.ts.snap +1 -1
- package/src/annotation-palette.ts +1 -0
- package/src/ansi-screen.test.ts +28 -0
- package/src/ansi-screen.ts +72 -0
- package/src/app-screens.tsx +19 -9
- package/src/app-view-model.ts +3 -1
- package/src/components/AnnotatableFileView.tsx +1 -1
- package/src/components/AnnotationCards.tsx +18 -2
- package/src/components/AnsiScreen.stories.tsx +12 -0
- package/src/components/AnsiScreen.tsx +28 -0
- package/src/components/CompletionOverlay.stories.tsx +15 -1
- package/src/components/CompletionOverlay.tsx +22 -5
- package/src/components/ConfirmCard.stories.tsx +2 -0
- package/src/components/ConfirmCard.tsx +45 -18
- package/src/components/ConfirmDialog.stories.tsx +3 -3
- package/src/components/ConfirmDialog.tsx +11 -12
- package/src/components/DiffContentView.tsx +18 -11
- package/src/components/DiscussionMarkerRail.tsx +9 -4
- package/src/components/FileContentsView.tsx +1 -1
- package/src/components/GridTabContent.tsx +8 -2
- package/src/components/HandleChipsInput.stories.tsx +24 -0
- package/src/components/HandleChipsInput.test.tsx +35 -0
- package/src/components/HandleChipsInput.tsx +145 -0
- package/src/components/MarkdownGridBlock.stories.tsx +33 -0
- package/src/components/MarkdownGridBlock.tsx +72 -0
- package/src/components/MarkdownThreadEditor.stories.tsx +15 -0
- package/src/components/MarkdownThreadEditor.test.tsx +98 -0
- package/src/components/MarkdownThreadEditor.tsx +162 -0
- package/src/components/NavModeHint.stories.tsx +40 -0
- package/src/components/NavModeHint.tsx +31 -0
- package/src/components/PromptDialog.test.tsx +2 -2
- package/src/components/PromptDialog.tsx +9 -10
- package/src/components/SettingsDialog.tsx +1 -0
- package/src/components/ShareDialog.stories.tsx +121 -0
- package/src/components/ShareDialog.tsx +552 -0
- package/src/components/SlashComposer.stories.tsx +37 -0
- package/src/components/SlashComposer.tsx +87 -0
- package/src/components/ThreadFooter.tsx +4 -1
- package/src/components/ThreadTree.tsx +12 -7
- package/src/components/ThreadView.selection.test.tsx +88 -1
- package/src/components/ThreadView.slash.test.tsx +0 -36
- package/src/components/ThreadView.tsx +200 -67
- package/src/components/Toast.stories.tsx +7 -0
- package/src/components/Toast.tsx +15 -8
- package/src/components/WalkWizard.stories.tsx +2 -2
- package/src/components/WalkWizard.tsx +10 -12
- package/src/components/__snapshots__/stories.test.tsx.snap +436 -182
- package/src/components/primitives/Button.tsx +6 -2
- package/src/components/primitives/Card.tsx +5 -2
- package/src/components/primitives/Dialog.tsx +2 -2
- package/src/components/primitives/DialogActions.stories.tsx +20 -0
- package/src/components/primitives/DialogActions.tsx +39 -0
- package/src/components/primitives/Switch.stories.tsx +17 -0
- package/src/components/primitives/Switch.tsx +32 -0
- package/src/components/primitives/Tree.tsx +5 -1
- package/src/components/primitives/frame.ts +5 -0
- package/src/components/session-tree.ts +7 -6
- package/src/components/share-dialog-store.ts +58 -0
- package/src/components/stories-app.tsx +13 -4
- package/src/components/stories.test.tsx +12 -4
- package/src/components/story-tree.test.ts +32 -24
- package/src/components/story.ts +32 -15
- package/src/config.test.ts +18 -0
- package/src/config.ts +8 -1
- package/src/diff-fold.ts +1 -1
- package/src/diff-intraline.test.ts +75 -2
- package/src/diff-intraline.ts +95 -26
- package/src/intent-dispatch.test.ts +12 -3
- package/src/intent-dispatch.ts +10 -25
- package/src/key-bindings.ts +0 -6
- package/src/keymap.test.ts +2 -2
- package/src/keymap.ts +0 -2
- package/src/markdown-editor-syntax-style.ts +50 -0
- package/src/markdown-grid.test.ts +101 -0
- package/src/markdown-grid.ts +203 -0
- package/src/markdown-highlight.test.ts +61 -0
- package/src/markdown-highlight.ts +122 -0
- package/src/pasted-image.test.ts +67 -0
- package/src/pasted-image.ts +60 -0
- package/src/prototype-browser.test.ts +9 -1
- package/src/prototype-browser.ts +22 -0
- package/src/run.ts +39 -25
- package/src/serve.ts +1 -1
- package/src/share.ts +16 -2
- package/src/slash-palette.test.ts +17 -0
- package/src/slash-palette.ts +11 -0
- package/src/test-support.ts +14 -0
- package/src/thread-chords.test.ts +55 -178
- package/src/thread-chords.ts +78 -169
- package/src/thread-controller.curate.test.ts +1 -0
- package/src/thread-controller.derivation.test.ts +62 -0
- package/src/thread-controller.live-diff.test.ts +3 -0
- package/src/thread-controller.optimistic.test.ts +69 -0
- package/src/thread-controller.share.test.ts +60 -5
- package/src/thread-controller.switch-cache.test.ts +176 -0
- package/src/thread-controller.tree.test.ts +6 -1
- package/src/thread-controller.ts +386 -78
- package/src/use-annotation-surface.tsx +88 -63
- package/src/use-settings-dialog.tsx +14 -3
- package/src/use-thread-body-editing.tsx +58 -0
- package/src/view-plan.test.ts +56 -6
- package/src/view-plan.ts +154 -5
- package/src/App.share-choice.test.tsx +0 -105
- package/src/components/ManageAccessDialog.stories.tsx +0 -36
- package/src/components/ManageAccessDialog.test.tsx +0 -94
- package/src/components/ManageAccessDialog.tsx +0 -120
- package/src/components/ShareChoiceDialog.stories.tsx +0 -34
- package/src/components/ShareChoiceDialog.test.tsx +0 -103
- package/src/components/ShareChoiceDialog.tsx +0 -82
- package/src/components/ThreadsSidebar.stories.tsx +0 -36
- package/src/components/ThreadsSidebar.tsx +0 -71
- package/src/components/agent-launcher.stories.tsx +0 -26
- package/src/components/agent-launcher.test.tsx +0 -50
- package/src/components/agent-launcher.tsx +0 -149
|
@@ -29,10 +29,14 @@ export function Button({
|
|
|
29
29
|
children,
|
|
30
30
|
}: ButtonProps): React.ReactNode {
|
|
31
31
|
const tokens = useComponentTheme(theme);
|
|
32
|
-
|
|
32
|
+
// a disabled solid button reads as muted, so it never looks like a live call to action
|
|
33
|
+
const backgroundColor =
|
|
34
|
+
variant === "solid" ? (isDisabled ? tokens.border : tokens.accent) : undefined;
|
|
33
35
|
const foreground =
|
|
34
36
|
variant === "solid"
|
|
35
|
-
?
|
|
37
|
+
? isDisabled
|
|
38
|
+
? tokens.textMuted
|
|
39
|
+
: tokens.accentInk
|
|
36
40
|
: variant === "accent-text"
|
|
37
41
|
? tokens.accent
|
|
38
42
|
: tokens.textDim;
|
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
import React from "react";
|
|
9
9
|
import type { Theme } from "../../theme";
|
|
10
10
|
import { useComponentTheme } from "../theme-context";
|
|
11
|
-
import { FRAME_BORDER_STYLE } from "./frame";
|
|
11
|
+
import { FRAME_BORDER_STYLE, frameTitle } from "./frame";
|
|
12
12
|
|
|
13
13
|
export interface CardProps {
|
|
14
14
|
title?: string;
|
|
15
15
|
/** Number of content rows inside the border; the box adds the two border rows. */
|
|
16
16
|
contentRows: number;
|
|
17
|
+
width?: number;
|
|
17
18
|
borderColor?: string;
|
|
18
19
|
backgroundColor?: string;
|
|
19
20
|
marginLeft?: number;
|
|
@@ -30,6 +31,7 @@ export function cardHeight(contentRows: number): number {
|
|
|
30
31
|
export function Card({
|
|
31
32
|
title,
|
|
32
33
|
contentRows,
|
|
34
|
+
width,
|
|
33
35
|
borderColor,
|
|
34
36
|
backgroundColor,
|
|
35
37
|
marginLeft,
|
|
@@ -43,6 +45,7 @@ export function Card({
|
|
|
43
45
|
<box
|
|
44
46
|
style={{
|
|
45
47
|
height: cardHeight(contentRows),
|
|
48
|
+
width,
|
|
46
49
|
marginLeft,
|
|
47
50
|
marginRight,
|
|
48
51
|
border: true,
|
|
@@ -52,7 +55,7 @@ export function Card({
|
|
|
52
55
|
flexDirection: "column",
|
|
53
56
|
paddingLeft: 1,
|
|
54
57
|
}}
|
|
55
|
-
title={title}
|
|
58
|
+
title={frameTitle(title)}
|
|
56
59
|
>
|
|
57
60
|
{children}
|
|
58
61
|
</box>
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import React from "react";
|
|
9
9
|
import type { Theme } from "../../theme";
|
|
10
10
|
import { useComponentTheme } from "../theme-context";
|
|
11
|
-
import { FRAME_BORDER_STYLE } from "./frame";
|
|
11
|
+
import { FRAME_BORDER_STYLE, frameTitle } from "./frame";
|
|
12
12
|
|
|
13
13
|
export interface DialogProps {
|
|
14
14
|
isOpen: boolean;
|
|
@@ -62,7 +62,7 @@ export function Dialog({
|
|
|
62
62
|
backgroundColor: background,
|
|
63
63
|
flexDirection: "column",
|
|
64
64
|
}}
|
|
65
|
-
title={title}
|
|
65
|
+
title={frameTitle(title)}
|
|
66
66
|
>
|
|
67
67
|
{children}
|
|
68
68
|
</box>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "../story";
|
|
4
|
+
import { DialogActions } from "./DialogActions";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "Primitives/DialogActions" };
|
|
7
|
+
|
|
8
|
+
const noop = () => {};
|
|
9
|
+
|
|
10
|
+
export const SaveCancel: Story = {
|
|
11
|
+
render: () => <DialogActions confirmLabel="save" onConfirm={noop} onCancel={noop} />,
|
|
12
|
+
expectedColors: [DARK.accent],
|
|
13
|
+
size: { width: 40, height: 3 },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const DeleteCancel: Story = {
|
|
17
|
+
render: () => <DialogActions confirmLabel="delete" onConfirm={noop} onCancel={noop} />,
|
|
18
|
+
expectedColors: [DARK.accent],
|
|
19
|
+
size: { width: 40, height: 3 },
|
|
20
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** The one action row every overlay uses: a solid confirm word-button and a plain cancel, lowercased. */
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import type { Theme } from "../../theme";
|
|
5
|
+
import { Toolbar } from "./Toolbar";
|
|
6
|
+
import { Button } from "./Button";
|
|
7
|
+
|
|
8
|
+
export interface DialogActionsProps {
|
|
9
|
+
confirmLabel: string;
|
|
10
|
+
onConfirm: () => void;
|
|
11
|
+
onCancel: () => void;
|
|
12
|
+
cancelLabel?: string;
|
|
13
|
+
confirmDisabled?: boolean;
|
|
14
|
+
theme?: Theme;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function DialogActions({
|
|
18
|
+
confirmLabel,
|
|
19
|
+
onConfirm,
|
|
20
|
+
onCancel,
|
|
21
|
+
cancelLabel = "cancel",
|
|
22
|
+
confirmDisabled = false,
|
|
23
|
+
theme,
|
|
24
|
+
}: DialogActionsProps): React.ReactNode {
|
|
25
|
+
return (
|
|
26
|
+
<Toolbar>
|
|
27
|
+
<Button
|
|
28
|
+
variant="solid"
|
|
29
|
+
marginRight={2}
|
|
30
|
+
onPress={onConfirm}
|
|
31
|
+
isDisabled={confirmDisabled}
|
|
32
|
+
theme={theme}
|
|
33
|
+
>
|
|
34
|
+
{` ${confirmLabel} `}
|
|
35
|
+
</Button>
|
|
36
|
+
<Button onPress={onCancel} theme={theme}>{` ${cancelLabel} `}</Button>
|
|
37
|
+
</Toolbar>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "../story";
|
|
4
|
+
import { Switch } from "./Switch";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "Primitives/Switch" };
|
|
7
|
+
|
|
8
|
+
export const On: Story = {
|
|
9
|
+
render: () => <Switch on onToggle={() => {}} theme={DARK} />,
|
|
10
|
+
expectedColors: [DARK.accent],
|
|
11
|
+
size: { width: 8, height: 1 },
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const Off: Story = {
|
|
15
|
+
render: () => <Switch on={false} onToggle={() => {}} theme={DARK} />,
|
|
16
|
+
size: { width: 8, height: 1 },
|
|
17
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* On/off switch: a pill with a knob that sits right (accent) when on and left
|
|
3
|
+
* (dim) when off. `onToggle` unifies a mouse click and keyboard activation, so
|
|
4
|
+
* the dialog grammar and a pointer both flip it.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import type { MouseEvent } from "@opentui/core";
|
|
9
|
+
import type { Theme } from "../../theme";
|
|
10
|
+
import { useComponentTheme } from "../theme-context";
|
|
11
|
+
|
|
12
|
+
export interface SwitchProps {
|
|
13
|
+
on: boolean;
|
|
14
|
+
onToggle: () => void;
|
|
15
|
+
theme?: Theme;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function Switch({ on, onToggle, theme }: SwitchProps): React.ReactNode {
|
|
19
|
+
const tokens = useComponentTheme(theme);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<box
|
|
23
|
+
onMouseUp={(event: MouseEvent) => {
|
|
24
|
+
event.stopPropagation();
|
|
25
|
+
onToggle();
|
|
26
|
+
}}
|
|
27
|
+
style={{ flexShrink: 0 }}
|
|
28
|
+
>
|
|
29
|
+
<text fg={on ? tokens.accent : tokens.textDim}>{on ? "( ●)" : "(● )"}</text>
|
|
30
|
+
</box>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -13,6 +13,8 @@ export interface TreeProps {
|
|
|
13
13
|
showStatus?: boolean;
|
|
14
14
|
/** Drop the folder/file glyph prefix, for a plain text tree (e.g. the settings nav). */
|
|
15
15
|
hideIcons?: boolean;
|
|
16
|
+
/** The selected row's backdrop; defaults to `elevated` (invisible on an elevated dialog, use `border` there). */
|
|
17
|
+
selectedBackground?: string;
|
|
16
18
|
indentWidth?: number;
|
|
17
19
|
icons?: TreeIcons;
|
|
18
20
|
onSelect?: (id: string) => void;
|
|
@@ -41,6 +43,7 @@ export function Tree({
|
|
|
41
43
|
flattenEmptyDirectories,
|
|
42
44
|
showStatus,
|
|
43
45
|
hideIcons,
|
|
46
|
+
selectedBackground,
|
|
44
47
|
indentWidth = 2,
|
|
45
48
|
icons = NERD_TREE_ICONS,
|
|
46
49
|
onSelect,
|
|
@@ -48,6 +51,7 @@ export function Tree({
|
|
|
48
51
|
theme,
|
|
49
52
|
}: TreeProps): React.ReactNode {
|
|
50
53
|
const tokens = theme ?? DARK;
|
|
54
|
+
const selectedBackdrop = selectedBackground ?? tokens.elevated;
|
|
51
55
|
const rows = flattenTree(nodes, { expandedIds, flattenEmptyDirectories });
|
|
52
56
|
|
|
53
57
|
return (
|
|
@@ -71,7 +75,7 @@ export function Tree({
|
|
|
71
75
|
flexDirection: "row",
|
|
72
76
|
paddingLeft: 1 + row.depth * indentWidth,
|
|
73
77
|
paddingRight: 1,
|
|
74
|
-
backgroundColor: selected ?
|
|
78
|
+
backgroundColor: selected ? selectedBackdrop : undefined,
|
|
75
79
|
}}
|
|
76
80
|
onMouseUp={() => (row.isFolder ? onToggle?.(row.id) : onSelect?.(row.id))}
|
|
77
81
|
>
|
|
@@ -10,3 +10,8 @@ import type { BorderStyle } from "@opentui/core";
|
|
|
10
10
|
|
|
11
11
|
/** Corner style shared by every bordered frame in the app: square, not rounded. */
|
|
12
12
|
export const FRAME_BORDER_STYLE: BorderStyle = "single";
|
|
13
|
+
|
|
14
|
+
/** Every frame's border title renders lowercase, so the convention holds without each caller minding case. */
|
|
15
|
+
export function frameTitle(title: string | undefined): string | undefined {
|
|
16
|
+
return title?.toLowerCase();
|
|
17
|
+
}
|
|
@@ -33,7 +33,7 @@ export function projectName(workspace: WorkspaceKey): string {
|
|
|
33
33
|
export type InboxRow =
|
|
34
34
|
| { kind: "section"; id: string; label: string }
|
|
35
35
|
| { kind: "project"; id: string; label: string }
|
|
36
|
-
| { kind: "thread"; id: string; session: Thread; selectionIndex: number };
|
|
36
|
+
| { kind: "thread"; id: string; session: Thread; selectionIndex: number; depth: number };
|
|
37
37
|
|
|
38
38
|
export interface GroupedInbox {
|
|
39
39
|
rows: InboxRow[];
|
|
@@ -78,14 +78,14 @@ export function groupInbox(
|
|
|
78
78
|
|
|
79
79
|
const rows: InboxRow[] = [];
|
|
80
80
|
const ordered: Thread[] = [];
|
|
81
|
-
const pushThread = (session: Thread): void => {
|
|
82
|
-
rows.push({ kind: "thread", id: session.id, session, selectionIndex: ordered.length });
|
|
81
|
+
const pushThread = (session: Thread, depth: number): void => {
|
|
82
|
+
rows.push({ kind: "thread", id: session.id, session, selectionIndex: ordered.length, depth });
|
|
83
83
|
ordered.push(session);
|
|
84
84
|
};
|
|
85
85
|
|
|
86
86
|
if (pinned.length > 0) {
|
|
87
87
|
rows.push({ kind: "section", id: "section:pinned", label: "Starred" });
|
|
88
|
-
for (const session of pinned) pushThread(session);
|
|
88
|
+
for (const session of pinned) pushThread(session, 1);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
const projectEntries = [...projects.entries()].sort((a, b) => a[1].name.localeCompare(b[1].name));
|
|
@@ -93,13 +93,14 @@ export function groupInbox(
|
|
|
93
93
|
rows.push({ kind: "section", id: "section:projects", label: "Projects" });
|
|
94
94
|
for (const [key, project] of projectEntries) {
|
|
95
95
|
rows.push({ kind: "project", id: `project:${key}`, label: project.name });
|
|
96
|
-
|
|
96
|
+
// a project's threads cascade one level under its name
|
|
97
|
+
for (const session of project.sessions) pushThread(session, 2);
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
if (standalone.length > 0) {
|
|
101
102
|
rows.push({ kind: "section", id: "section:threads", label: "Threads" });
|
|
102
|
-
for (const session of standalone) pushThread(session);
|
|
103
|
+
for (const session of standalone) pushThread(session, 1);
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
return { rows, ordered };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI state for the share dialog, held in a zustand store rather than juggled
|
|
3
|
+
* across component state: which category is open (share externally / export),
|
|
4
|
+
* whether the link wizard is showing, and the wizard's draft (name, auth,
|
|
5
|
+
* allowlist). The links themselves live on the thread; this is only the
|
|
6
|
+
* ephemeral surface state.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createStore } from "zustand/vanilla";
|
|
10
|
+
import { useStore } from "zustand";
|
|
11
|
+
|
|
12
|
+
export type ShareCategory = "external" | "export";
|
|
13
|
+
|
|
14
|
+
export interface ShareWizardDraft {
|
|
15
|
+
/** The link being edited, or null when creating a new one. */
|
|
16
|
+
editingId: string | null;
|
|
17
|
+
name: string;
|
|
18
|
+
requireAuth: boolean;
|
|
19
|
+
allowlist: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ShareDialogState {
|
|
23
|
+
category: ShareCategory;
|
|
24
|
+
/** Null = the links list; a draft = the new/edit wizard. */
|
|
25
|
+
wizard: ShareWizardDraft | null;
|
|
26
|
+
setCategory: (category: ShareCategory) => void;
|
|
27
|
+
openWizard: (draft: ShareWizardDraft) => void;
|
|
28
|
+
closeWizard: () => void;
|
|
29
|
+
setName: (name: string) => void;
|
|
30
|
+
setRequireAuth: (requireAuth: boolean) => void;
|
|
31
|
+
setAllowlist: (allowlist: string[]) => void;
|
|
32
|
+
/** Reset to the opening state (links list, external tab) when the dialog opens. */
|
|
33
|
+
reset: () => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A fresh wizard draft, prefilled with the thread name for a new link. */
|
|
37
|
+
export function newWizardDraft(threadName: string): ShareWizardDraft {
|
|
38
|
+
return { editingId: null, name: threadName, requireAuth: false, allowlist: [] };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const shareDialogStore = createStore<ShareDialogState>((set) => ({
|
|
42
|
+
category: "external",
|
|
43
|
+
wizard: null,
|
|
44
|
+
setCategory: (category) => set({ category }),
|
|
45
|
+
openWizard: (wizard) => set({ wizard }),
|
|
46
|
+
closeWizard: () => set({ wizard: null }),
|
|
47
|
+
setName: (name) => set((state) => (state.wizard ? { wizard: { ...state.wizard, name } } : {})),
|
|
48
|
+
setRequireAuth: (requireAuth) =>
|
|
49
|
+
set((state) => (state.wizard ? { wizard: { ...state.wizard, requireAuth } } : {})),
|
|
50
|
+
setAllowlist: (allowlist) =>
|
|
51
|
+
set((state) => (state.wizard ? { wizard: { ...state.wizard, allowlist } } : {})),
|
|
52
|
+
reset: () => set({ category: "external", wizard: null }),
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
/** Subscribe a component to the share dialog store. */
|
|
56
|
+
export function useShareDialog<T>(selector: (state: ShareDialogState) => T): T {
|
|
57
|
+
return useStore(shareDialogStore, selector);
|
|
58
|
+
}
|
|
@@ -13,9 +13,10 @@ import { ThemeProvider } from "./theme-context";
|
|
|
13
13
|
import { buildStoryTree, loadStories, type LoadedStory } from "./story";
|
|
14
14
|
import { PaneGrid } from "./PaneGrid";
|
|
15
15
|
import { ShellHeader } from "./ShellHeader";
|
|
16
|
+
import { AnsiScreen } from "./AnsiScreen";
|
|
16
17
|
import { Tree } from "./primitives/Tree";
|
|
17
18
|
import { NERD } from "./primitives/icons";
|
|
18
|
-
import {
|
|
19
|
+
import { flattenTree } from "./primitives/tree-model";
|
|
19
20
|
|
|
20
21
|
interface StoriesAppProps {
|
|
21
22
|
stories: LoadedStory[];
|
|
@@ -30,8 +31,8 @@ function StoriesApp({ stories, onExit }: StoriesAppProps): React.ReactNode {
|
|
|
30
31
|
);
|
|
31
32
|
const firstStoryId = `${stories[0]!.moduleTitle}/${stories[0]!.storyName}`;
|
|
32
33
|
|
|
33
|
-
const [expandedIds, setExpandedIds] = useState<ReadonlySet<string>>(
|
|
34
|
-
|
|
34
|
+
const [expandedIds, setExpandedIds] = useState<ReadonlySet<string>>(
|
|
35
|
+
() => new Set(treeNodes.filter((node) => node.children !== undefined).map((node) => node.id)),
|
|
35
36
|
);
|
|
36
37
|
const [selectedId, setSelectedId] = useState(firstStoryId);
|
|
37
38
|
const [openedId, setOpenedId] = useState(firstStoryId);
|
|
@@ -126,7 +127,15 @@ function StoriesApp({ stories, onExit }: StoriesAppProps): React.ReactNode {
|
|
|
126
127
|
<scrollbox style={{ flexGrow: 1, paddingLeft: 1, paddingTop: 1 }} focused={false}>
|
|
127
128
|
{/* remount per story so component-local state never leaks across */}
|
|
128
129
|
<box key={openedId} style={{ flexDirection: "column" }}>
|
|
129
|
-
{opened.story.
|
|
130
|
+
{opened.story.ansi ? (
|
|
131
|
+
<AnsiScreen
|
|
132
|
+
ansi={opened.story.ansi()}
|
|
133
|
+
cols={opened.story.size?.width ?? 80}
|
|
134
|
+
rows={opened.story.size?.height ?? 24}
|
|
135
|
+
/>
|
|
136
|
+
) : (
|
|
137
|
+
opened.story.render?.()
|
|
138
|
+
)}
|
|
130
139
|
</box>
|
|
131
140
|
</scrollbox>
|
|
132
141
|
) : (
|
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
* stories file fails the harness, so the safety net cannot silently thin.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import React from "react";
|
|
8
9
|
import { describe, expect, test } from "bun:test";
|
|
9
10
|
import { testRender } from "@opentui/react/test-utils";
|
|
10
11
|
import { RGBA } from "@opentui/core";
|
|
11
12
|
import { componentFilesMissingStories, loadStories } from "./story";
|
|
12
|
-
import {
|
|
13
|
+
import { AnsiScreen } from "./AnsiScreen";
|
|
14
|
+
import { settle } from "../test-support";
|
|
13
15
|
|
|
14
16
|
const stories = await loadStories();
|
|
15
17
|
|
|
@@ -34,12 +36,18 @@ describe("stories catalog", () => {
|
|
|
34
36
|
test(`${moduleTitle}/${storyName} renders, snapshots, and carries its colors`, async () => {
|
|
35
37
|
// Arrange
|
|
36
38
|
const size = story.size ?? { width: 80, height: 24 };
|
|
39
|
+
const node = story.ansi ? (
|
|
40
|
+
<AnsiScreen ansi={story.ansi()} cols={size.width} rows={size.height} />
|
|
41
|
+
) : (
|
|
42
|
+
story.render!()
|
|
43
|
+
);
|
|
37
44
|
|
|
38
45
|
// Act
|
|
39
|
-
const setup = await testRender(
|
|
46
|
+
const setup = await testRender(node, size);
|
|
40
47
|
|
|
41
|
-
|
|
42
|
-
|
|
48
|
+
// settle, not just visual-idle: async stories (a file read, a virtualizer's
|
|
49
|
+
// first measure) commit after React's own scheduler, invisible to idle
|
|
50
|
+
await settle(setup);
|
|
43
51
|
const frame = setup.captureCharFrame();
|
|
44
52
|
|
|
45
53
|
// Assert
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { buildStoryTree, type LoadedStory } from "./story";
|
|
2
|
+
import { buildStoryTree, type LoadedStory, type StorySection } from "./story";
|
|
3
3
|
import type { TreeNode } from "./primitives/tree-model";
|
|
4
4
|
|
|
5
5
|
/** A story object whose render is never invoked in these structural tests. */
|
|
6
6
|
const STUB = { render: () => null };
|
|
7
7
|
|
|
8
|
-
function story(moduleTitle: string, storyName: string): LoadedStory {
|
|
9
|
-
return { moduleTitle, storyName, story: STUB };
|
|
8
|
+
function story(moduleTitle: string, storyName: string, section: StorySection = "App"): LoadedStory {
|
|
9
|
+
return { section, moduleTitle, storyName, story: STUB };
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
function childLabels(node: TreeNode): string[] {
|
|
@@ -14,19 +14,31 @@ function childLabels(node: TreeNode): string[] {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
describe("buildStoryTree", () => {
|
|
17
|
-
test("
|
|
17
|
+
test("groups the top level by section", () => {
|
|
18
|
+
const tree = buildStoryTree([
|
|
19
|
+
story("Primitives/Tree", "Default"),
|
|
20
|
+
story("Collaborator Join", "JoinSplash", "SSH"),
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
expect(tree.map((node) => [node.id, node.label])).toEqual([
|
|
24
|
+
["App", "App"],
|
|
25
|
+
["SSH", "SSH"],
|
|
26
|
+
]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("splits moduleTitle on / into nested folders under the section", () => {
|
|
18
30
|
// Arrange
|
|
19
31
|
const stories = [story("Primitives/Tree", "Default"), story("Layout/AppShell", "Default")];
|
|
20
32
|
|
|
21
33
|
// Act
|
|
22
|
-
const
|
|
34
|
+
const app = buildStoryTree(stories)[0]!;
|
|
23
35
|
|
|
24
36
|
// Assert
|
|
25
|
-
expect(
|
|
26
|
-
["Primitives", "Primitives"],
|
|
27
|
-
["Layout", "Layout"],
|
|
37
|
+
expect(app.children!.map((node) => [node.id, node.label])).toEqual([
|
|
38
|
+
["App/Primitives", "Primitives"],
|
|
39
|
+
["App/Layout", "Layout"],
|
|
28
40
|
]);
|
|
29
|
-
expect(
|
|
41
|
+
expect(app.children![0]!.children!.map((node) => node.id)).toEqual(["App/Primitives/Tree"]);
|
|
30
42
|
});
|
|
31
43
|
|
|
32
44
|
test("groups sibling components under one shared parent folder", () => {
|
|
@@ -34,11 +46,11 @@ describe("buildStoryTree", () => {
|
|
|
34
46
|
const stories = [story("Primitives/Tree", "Default"), story("Primitives/Card", "Default")];
|
|
35
47
|
|
|
36
48
|
// Act
|
|
37
|
-
const
|
|
49
|
+
const app = buildStoryTree(stories)[0]!;
|
|
38
50
|
|
|
39
51
|
// Assert
|
|
40
|
-
expect(
|
|
41
|
-
expect(childLabels(
|
|
52
|
+
expect(app.children).toHaveLength(1);
|
|
53
|
+
expect(childLabels(app.children![0]!)).toEqual(["Tree", "Card"]);
|
|
42
54
|
});
|
|
43
55
|
|
|
44
56
|
test("nests to the full depth of the moduleTitle path", () => {
|
|
@@ -46,13 +58,12 @@ describe("buildStoryTree", () => {
|
|
|
46
58
|
const stories = [story("A/B/C", "Only")];
|
|
47
59
|
|
|
48
60
|
// Act
|
|
49
|
-
const
|
|
50
|
-
const a = tree[0]!;
|
|
61
|
+
const a = buildStoryTree(stories)[0]!.children![0]!;
|
|
51
62
|
const b = a.children![0]!;
|
|
52
63
|
|
|
53
64
|
// Assert
|
|
54
|
-
expect([a.id, b.id]).toEqual(["A", "A/B"]);
|
|
55
|
-
expect(b.children![0]!.id).toBe("A/B/C");
|
|
65
|
+
expect([a.id, b.id]).toEqual(["App/A", "App/A/B"]);
|
|
66
|
+
expect(b.children![0]!.id).toBe("App/A/B/C");
|
|
56
67
|
});
|
|
57
68
|
|
|
58
69
|
test("adds each story as a leaf keyed `${moduleTitle}/${storyName}` under its component", () => {
|
|
@@ -60,11 +71,10 @@ describe("buildStoryTree", () => {
|
|
|
60
71
|
const stories = [story("Primitives/Tree", "Empty"), story("Primitives/Tree", "Deep")];
|
|
61
72
|
|
|
62
73
|
// Act
|
|
63
|
-
const
|
|
64
|
-
const component = tree[0]!.children![0]!;
|
|
74
|
+
const component = buildStoryTree(stories)[0]!.children![0]!.children![0]!;
|
|
65
75
|
|
|
66
76
|
// Assert
|
|
67
|
-
expect(component.id).toBe("Primitives/Tree");
|
|
77
|
+
expect(component.id).toBe("App/Primitives/Tree");
|
|
68
78
|
expect(component.children!.map((leaf) => [leaf.id, leaf.label])).toEqual([
|
|
69
79
|
["Primitives/Tree/Empty", "Empty"],
|
|
70
80
|
["Primitives/Tree/Deep", "Deep"],
|
|
@@ -76,8 +86,7 @@ describe("buildStoryTree", () => {
|
|
|
76
86
|
const stories = [story("Primitives/Card", "Card")];
|
|
77
87
|
|
|
78
88
|
// Act
|
|
79
|
-
const
|
|
80
|
-
const hoisted = tree[0]!.children![0]!;
|
|
89
|
+
const hoisted = buildStoryTree(stories)[0]!.children![0]!.children![0]!;
|
|
81
90
|
|
|
82
91
|
// Assert
|
|
83
92
|
expect(hoisted.children).toBeUndefined();
|
|
@@ -90,11 +99,10 @@ describe("buildStoryTree", () => {
|
|
|
90
99
|
const stories = [story("Primitives/Card", "Default")];
|
|
91
100
|
|
|
92
101
|
// Act
|
|
93
|
-
const
|
|
94
|
-
const component = tree[0]!.children![0]!;
|
|
102
|
+
const component = buildStoryTree(stories)[0]!.children![0]!.children![0]!;
|
|
95
103
|
|
|
96
104
|
// Assert
|
|
97
|
-
expect(component.id).toBe("Primitives/Card");
|
|
105
|
+
expect(component.id).toBe("App/Primitives/Card");
|
|
98
106
|
expect(component.children!.map((leaf) => leaf.id)).toEqual(["Primitives/Card/Default"]);
|
|
99
107
|
});
|
|
100
108
|
});
|
package/src/components/story.ts
CHANGED
|
@@ -15,7 +15,9 @@ export interface StoryMeta {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export interface Story {
|
|
18
|
-
render
|
|
18
|
+
render?: () => React.ReactNode;
|
|
19
|
+
/** Raw ANSI to paint through AnsiScreen instead of a React tree; exactly one of render or ansi is set. */
|
|
20
|
+
ansi?: () => string;
|
|
19
21
|
/**
|
|
20
22
|
* Colors (hex tokens) that must appear among the rendered frame's styled
|
|
21
23
|
* spans - the color regression net for color-bearing stories.
|
|
@@ -25,18 +27,21 @@ export interface Story {
|
|
|
25
27
|
size?: { width: number; height: number };
|
|
26
28
|
}
|
|
27
29
|
|
|
30
|
+
export type StorySection = "App" | "SSH";
|
|
31
|
+
|
|
28
32
|
export interface LoadedStory {
|
|
33
|
+
section: StorySection;
|
|
29
34
|
moduleTitle: string;
|
|
30
35
|
storyName: string;
|
|
31
36
|
story: Story;
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
export function isStory(value: unknown): value is Story {
|
|
40
|
+
if (typeof value !== "object" || value === null) return false;
|
|
41
|
+
|
|
35
42
|
return (
|
|
36
|
-
typeof value === "
|
|
37
|
-
value
|
|
38
|
-
"render" in value &&
|
|
39
|
-
typeof value.render === "function"
|
|
43
|
+
("render" in value && typeof value.render === "function") ||
|
|
44
|
+
("ansi" in value && typeof value.ansi === "function")
|
|
40
45
|
);
|
|
41
46
|
}
|
|
42
47
|
|
|
@@ -49,22 +54,34 @@ function isStoryMeta(value: unknown): value is StoryMeta {
|
|
|
49
54
|
);
|
|
50
55
|
}
|
|
51
56
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
const GATEWAY_SRC_DIR = `${import.meta.dir}/../../../gateway/src`;
|
|
58
|
+
|
|
59
|
+
async function collectStories(
|
|
60
|
+
dir: string,
|
|
61
|
+
pattern: string,
|
|
62
|
+
section: StorySection,
|
|
63
|
+
loaded: LoadedStory[],
|
|
64
|
+
): Promise<void> {
|
|
65
|
+
const glob = new Bun.Glob(pattern);
|
|
66
|
+
const files = [...glob.scanSync({ cwd: dir })].sort();
|
|
57
67
|
|
|
58
68
|
for (const file of files) {
|
|
59
|
-
const moduleExports = await import(`${
|
|
69
|
+
const moduleExports = await import(`${dir}/${file}`);
|
|
60
70
|
const meta = moduleExports["meta"];
|
|
61
|
-
const moduleTitle = isStoryMeta(meta) ? meta.title : file.replace(/\.stories\.tsx
|
|
71
|
+
const moduleTitle = isStoryMeta(meta) ? meta.title : file.replace(/\.stories\.tsx?$/, "");
|
|
62
72
|
|
|
63
73
|
for (const [exportName, exported] of Object.entries(moduleExports)) {
|
|
64
74
|
if (exportName === "meta" || !isStory(exported)) continue;
|
|
65
|
-
loaded.push({ moduleTitle, storyName: exportName, story: exported });
|
|
75
|
+
loaded.push({ section, moduleTitle, storyName: exportName, story: exported });
|
|
66
76
|
}
|
|
67
77
|
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function loadStories(): Promise<LoadedStory[]> {
|
|
81
|
+
const loaded: LoadedStory[] = [];
|
|
82
|
+
|
|
83
|
+
await collectStories(import.meta.dir, "**/*.stories.tsx", "App", loaded);
|
|
84
|
+
await collectStories(GATEWAY_SRC_DIR, "**/*.stories.ts", "SSH", loaded);
|
|
68
85
|
|
|
69
86
|
return loaded;
|
|
70
87
|
}
|
|
@@ -102,11 +119,11 @@ export function buildStoryTree(stories: LoadedStory[]): TreeNode[] {
|
|
|
102
119
|
const roots: TreeNode[] = [];
|
|
103
120
|
const foldersByPath = new Map<string, StoryTreeFolder>();
|
|
104
121
|
|
|
105
|
-
for (const { moduleTitle, storyName } of stories) {
|
|
122
|
+
for (const { section, moduleTitle, storyName } of stories) {
|
|
106
123
|
let siblings = roots;
|
|
107
124
|
let path = "";
|
|
108
125
|
|
|
109
|
-
for (const segment of moduleTitle.split("/")) {
|
|
126
|
+
for (const segment of [section, ...moduleTitle.split("/")]) {
|
|
110
127
|
path = path === "" ? segment : `${path}/${segment}`;
|
|
111
128
|
let folder = foldersByPath.get(path);
|
|
112
129
|
if (folder === undefined) {
|
package/src/config.test.ts
CHANGED
|
@@ -143,6 +143,24 @@ describe("loadConfig", () => {
|
|
|
143
143
|
}
|
|
144
144
|
});
|
|
145
145
|
|
|
146
|
+
test("[ui] default_verdict defaults to approve and parses request_changes", () => {
|
|
147
|
+
// Arrange
|
|
148
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-verdict-"));
|
|
149
|
+
const path = join(dir, "config.toml");
|
|
150
|
+
|
|
151
|
+
writeFileSync(path, `[ui]\ndefault_verdict = "request_changes"\n`);
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
// Assert
|
|
155
|
+
expect(loadConfig({ userConfigPath: "/nonexistent/config.toml" }).ui.defaultVerdict).toBe(
|
|
156
|
+
"approve",
|
|
157
|
+
);
|
|
158
|
+
expect(loadConfig({ userConfigPath: path }).ui.defaultVerdict).toBe("request_changes");
|
|
159
|
+
} finally {
|
|
160
|
+
rmSync(dir, { recursive: true, force: true });
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
146
164
|
test("[skills] path overrides the default and a malformed value never discards the config", () => {
|
|
147
165
|
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-skills-"));
|
|
148
166
|
const path = join(dir, "config.toml");
|