@cueloop/client 0.1.0-alpha.6 → 0.1.0-alpha.61
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/native/build-pty.sh +51 -0
- package/native/build.sh +82 -0
- package/native/darwin-arm64/libcuelooppty.dylib +0 -0
- package/native/darwin-arm64/libcueloopvt.dylib +0 -0
- package/native/src/pty.zig +193 -0
- package/native/src/shim.zig +114 -0
- package/package.json +37 -33
- package/src/App.appearance.test.tsx +51 -0
- package/src/App.autoclose.test.tsx +73 -47
- package/src/App.diff.test.tsx +169 -22
- package/src/App.export.test.tsx +49 -37
- package/src/App.plan-review.test.tsx +714 -0
- package/src/App.prototype.test.tsx +194 -0
- package/src/App.readonly.test.tsx +69 -35
- package/src/App.rendering.test.tsx +59 -31
- package/src/App.review-panel.test.tsx +143 -0
- package/src/App.submit-confirm.test.tsx +229 -0
- package/src/App.test.tsx +139 -68
- package/src/App.toast.test.tsx +77 -0
- package/src/App.tsx +0 -0
- package/src/App.walk.test.tsx +299 -0
- package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
- package/src/app-key-state.ts +61 -0
- package/src/app-screens.tsx +270 -0
- package/src/app-view-model.ts +307 -0
- package/src/attribution.test.ts +31 -0
- package/src/attribution.ts +42 -0
- package/src/clipboard.test.ts +23 -0
- package/src/clipboard.ts +33 -0
- package/src/components/AnnotationCard.compose-rows.test.ts +49 -0
- package/src/components/AnnotationCard.stories.tsx +94 -0
- package/src/components/AnnotationCard.tsx +250 -0
- package/src/components/Breadcrumb.stories.tsx +19 -0
- package/src/components/Breadcrumb.tsx +45 -0
- package/src/components/CodeBlock.stories.tsx +42 -0
- package/src/components/CodeBlock.tsx +84 -0
- package/src/components/CompletionOverlay.stories.tsx +23 -0
- package/src/components/CompletionOverlay.tsx +60 -0
- package/src/components/ConfirmCard.stories.tsx +35 -0
- package/src/components/ConfirmCard.tsx +129 -0
- package/src/components/ConfirmDialog.stories.tsx +21 -0
- package/src/components/ConfirmDialog.tsx +66 -0
- package/src/components/DiffSheet.scroll.test.tsx +135 -0
- package/src/components/DiffSheet.stories.tsx +78 -0
- package/src/components/DiffSheet.syntax.test.tsx +65 -0
- package/src/components/DiffSheet.tsx +374 -0
- package/src/components/InboxList.stories.tsx +31 -0
- package/src/components/InboxList.tsx +64 -0
- package/src/components/KeybindsDialog.stories.tsx +31 -0
- package/src/components/KeybindsDialog.tsx +59 -0
- package/src/components/MarkerPopover.stories.tsx +58 -0
- package/src/components/MarkerPopover.tsx +109 -0
- package/src/components/MenuBar.stories.tsx +37 -0
- package/src/components/MenuBar.tsx +75 -0
- package/src/components/PlanSheet.stories.tsx +113 -0
- package/src/components/PlanSheet.tsx +422 -0
- package/src/components/PromptDialog.stories.tsx +36 -0
- package/src/components/PromptDialog.tsx +80 -0
- package/src/components/PrototypeSheet.stories.tsx +20 -0
- package/src/components/PrototypeSheet.tsx +450 -0
- package/src/components/ReviewPanel.stories.tsx +86 -0
- package/src/components/ReviewPanel.tsx +103 -0
- package/src/components/ReviewRail.stories.tsx +218 -0
- package/src/components/ReviewRail.tsx +318 -0
- package/src/components/SettingsDialog.stories.tsx +111 -0
- package/src/components/SettingsDialog.tsx +147 -0
- package/src/components/SettingsRows.stories.tsx +18 -0
- package/src/components/SettingsRows.tsx +112 -0
- package/src/components/Toast.stories.tsx +12 -0
- package/src/components/Toast.tsx +45 -0
- package/src/components/WalkWizard.stories.tsx +100 -0
- package/src/components/WalkWizard.tsx +152 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +1857 -0
- package/src/components/agent-launcher.stories.tsx +26 -0
- package/src/components/agent-launcher.test.tsx +50 -0
- package/src/components/agent-launcher.tsx +149 -0
- package/src/components/diff-sheet-layout.test.ts +128 -0
- package/src/components/diff-sheet-layout.ts +184 -0
- package/src/components/plan-sheet-run-style.ts +90 -0
- package/src/components/primitives/Button.stories.tsx +32 -0
- package/src/components/primitives/Button.tsx +52 -0
- package/src/components/primitives/Card.stories.tsx +25 -0
- package/src/components/primitives/Card.tsx +60 -0
- package/src/components/primitives/Dialog.stories.tsx +24 -0
- package/src/components/primitives/Dialog.tsx +66 -0
- package/src/components/primitives/StatusBar.stories.tsx +11 -0
- package/src/components/primitives/StatusBar.tsx +23 -0
- package/src/components/primitives/Tabs.stories.tsx +29 -0
- package/src/components/primitives/Tabs.tsx +102 -0
- package/src/components/primitives/Toolbar.stories.tsx +17 -0
- package/src/components/primitives/Toolbar.tsx +14 -0
- package/src/components/primitives/frame.test.ts +12 -0
- package/src/components/primitives/frame.ts +14 -0
- package/src/components/quick-actions-editor.stories.tsx +54 -0
- package/src/components/quick-actions-editor.tsx +138 -0
- package/src/components/stories-app.tsx +124 -0
- package/src/components/stories.test.tsx +66 -0
- package/src/components/story-fixtures.ts +98 -0
- package/src/components/story.ts +75 -0
- package/src/components/syntax-highlight.ts +102 -0
- package/src/components/terminal-pane.test.tsx +40 -0
- package/src/components/terminal-pane.ts +193 -0
- package/src/components/theme-context.stories.tsx +46 -0
- package/src/components/theme-context.tsx +37 -0
- package/src/components/truncate-text.ts +7 -0
- package/src/config.test.ts +448 -31
- package/src/config.ts +339 -57
- package/src/diff-hunk-curate.test.ts +315 -0
- package/src/diff-hunk-curate.ts +253 -0
- package/src/diff-intraline.test.ts +165 -0
- package/src/diff-intraline.ts +146 -0
- package/src/diff-syntax.test.ts +96 -0
- package/src/diff-syntax.ts +176 -0
- package/src/editor.test.ts +149 -0
- package/src/editor.ts +135 -9
- package/src/ghostty-terminal.test.ts +84 -0
- package/src/ghostty-terminal.ts +176 -0
- package/src/herdr.test.ts +9 -34
- package/src/herdr.ts +17 -21
- package/src/index.ts +10 -0
- package/src/integrations.test.ts +41 -0
- package/src/integrations.ts +32 -0
- package/src/intent-dispatch.test.ts +499 -0
- package/src/intent-dispatch.ts +585 -0
- package/src/key-bindings.test.ts +156 -0
- package/src/key-bindings.ts +452 -0
- package/src/keymap.test.ts +635 -0
- package/src/keymap.ts +404 -0
- package/src/kitty-image.test.ts +66 -0
- package/src/kitty-image.ts +138 -0
- package/src/prototype-browser.test.ts +56 -0
- package/src/prototype-browser.ts +289 -0
- package/src/pty.test.ts +81 -0
- package/src/pty.ts +237 -0
- package/src/review-panel.test.ts +113 -0
- package/src/review-panel.ts +83 -0
- package/src/run.ts +16 -5
- package/src/serve.test.ts +25 -9
- package/src/serve.ts +19 -8
- package/src/session-controller.curate.test.ts +260 -0
- package/src/session-controller.share.test.ts +320 -0
- package/src/session-controller.ts +993 -0
- package/src/share.ts +120 -0
- package/src/test-support.test.ts +60 -0
- package/src/test-support.ts +197 -0
- package/src/theme-presets.test.ts +51 -0
- package/src/theme-presets.ts +187 -0
- package/src/theme.ts +64 -17
- package/src/use-settings-dialog.tsx +275 -0
- package/src/version.ts +5 -0
- package/src/view-diff.test.ts +22 -8
- package/src/view-diff.ts +39 -23
- package/src/view-plan.regression.test.ts +60 -0
- package/src/view-plan.test.ts +406 -0
- package/src/view-plan.ts +554 -0
- package/src/walk.test.ts +121 -0
- package/src/walk.ts +74 -0
- package/src/syntax.test.ts +0 -41
- package/src/syntax.ts +0 -88
- package/src/view.test.ts +0 -157
- package/src/view.ts +0 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Word-button: a pressable label. `onPress` unifies mouse and (future)
|
|
3
|
+
* keyboard activation; `isDisabled` swallows presses without changing the
|
|
4
|
+
* rendered label, so read-only affordances keep their place in the layout.
|
|
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 ButtonProps {
|
|
13
|
+
/** Unified activation: mouse release today, keyboard activation tomorrow. */
|
|
14
|
+
onPress: () => void;
|
|
15
|
+
isDisabled?: boolean;
|
|
16
|
+
/** solid = accent-filled call to action; plain = quiet word-button. */
|
|
17
|
+
variant?: "solid" | "plain" | "accent-text";
|
|
18
|
+
marginRight?: number;
|
|
19
|
+
theme?: Theme;
|
|
20
|
+
children: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function Button({
|
|
24
|
+
onPress,
|
|
25
|
+
isDisabled = false,
|
|
26
|
+
variant = "plain",
|
|
27
|
+
marginRight,
|
|
28
|
+
theme,
|
|
29
|
+
children,
|
|
30
|
+
}: ButtonProps): React.ReactNode {
|
|
31
|
+
const tokens = useComponentTheme(theme);
|
|
32
|
+
const backgroundColor = variant === "solid" ? tokens.accent : undefined;
|
|
33
|
+
const foreground =
|
|
34
|
+
variant === "solid"
|
|
35
|
+
? tokens.accentInk
|
|
36
|
+
: variant === "accent-text"
|
|
37
|
+
? tokens.accent
|
|
38
|
+
: tokens.textDim;
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<box
|
|
42
|
+
style={{ backgroundColor, marginRight }}
|
|
43
|
+
onMouseUp={(event: MouseEvent) => {
|
|
44
|
+
// Consume the press so it never bubbles to an ancestor's onMouseUp (e.g. a card).
|
|
45
|
+
event.stopPropagation();
|
|
46
|
+
if (!isDisabled) onPress();
|
|
47
|
+
}}
|
|
48
|
+
>
|
|
49
|
+
<text fg={foreground}>{children}</text>
|
|
50
|
+
</box>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "../story";
|
|
4
|
+
import { Card } from "./Card";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "primitives/Card" };
|
|
7
|
+
|
|
8
|
+
export const Titled: Story = {
|
|
9
|
+
render: () => (
|
|
10
|
+
<Card title=" a titled card " contentRows={2}>
|
|
11
|
+
<text>first content row</text>
|
|
12
|
+
<text>second content row</text>
|
|
13
|
+
</Card>
|
|
14
|
+
),
|
|
15
|
+
expectedColors: [DARK.border, DARK.elevated],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const AccentBorder: Story = {
|
|
19
|
+
render: () => (
|
|
20
|
+
<Card title=" accented " contentRows={1} borderColor={DARK.accent}>
|
|
21
|
+
<text>content</text>
|
|
22
|
+
</Card>
|
|
23
|
+
),
|
|
24
|
+
expectedColors: [DARK.accent],
|
|
25
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bordered rounded box with an optional title. The card owns the
|
|
3
|
+
* height-derivation rule: its height is always the declared content row
|
|
4
|
+
* count plus the two border rows, so layout math and the mounted box can
|
|
5
|
+
* never drift apart - border-collapse bugs die here.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from "react";
|
|
9
|
+
import type { Theme } from "../../theme";
|
|
10
|
+
import { useComponentTheme } from "../theme-context";
|
|
11
|
+
import { FRAME_BORDER_STYLE } from "./frame";
|
|
12
|
+
|
|
13
|
+
export interface CardProps {
|
|
14
|
+
title?: string;
|
|
15
|
+
/** Number of content rows inside the border; the box adds the two border rows. */
|
|
16
|
+
contentRows: number;
|
|
17
|
+
borderColor?: string;
|
|
18
|
+
backgroundColor?: string;
|
|
19
|
+
marginLeft?: number;
|
|
20
|
+
marginRight?: number;
|
|
21
|
+
theme?: Theme;
|
|
22
|
+
children: React.ReactNode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The one height rule: content rows plus the top and bottom border rows. */
|
|
26
|
+
export function cardHeight(contentRows: number): number {
|
|
27
|
+
return contentRows + 2;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function Card({
|
|
31
|
+
title,
|
|
32
|
+
contentRows,
|
|
33
|
+
borderColor,
|
|
34
|
+
backgroundColor,
|
|
35
|
+
marginLeft,
|
|
36
|
+
marginRight,
|
|
37
|
+
theme,
|
|
38
|
+
children,
|
|
39
|
+
}: CardProps): React.ReactNode {
|
|
40
|
+
const tokens = useComponentTheme(theme);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<box
|
|
44
|
+
style={{
|
|
45
|
+
height: cardHeight(contentRows),
|
|
46
|
+
marginLeft,
|
|
47
|
+
marginRight,
|
|
48
|
+
border: true,
|
|
49
|
+
borderStyle: FRAME_BORDER_STYLE,
|
|
50
|
+
borderColor: borderColor ?? tokens.border,
|
|
51
|
+
backgroundColor: backgroundColor ?? tokens.elevated,
|
|
52
|
+
flexDirection: "column",
|
|
53
|
+
paddingLeft: 1,
|
|
54
|
+
}}
|
|
55
|
+
title={title}
|
|
56
|
+
>
|
|
57
|
+
{children}
|
|
58
|
+
</box>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "../story";
|
|
4
|
+
import { Dialog } from "./Dialog";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "primitives/Dialog" };
|
|
7
|
+
|
|
8
|
+
export const Open: Story = {
|
|
9
|
+
render: () => (
|
|
10
|
+
<Dialog isOpen title=" a modal " width={40} height={8}>
|
|
11
|
+
<text>dialog body content</text>
|
|
12
|
+
</Dialog>
|
|
13
|
+
),
|
|
14
|
+
expectedColors: [DARK.accent],
|
|
15
|
+
size: { width: 60, height: 16 },
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const Closed: Story = {
|
|
19
|
+
render: () => (
|
|
20
|
+
<Dialog isOpen={false} title=" hidden " width={40} height={8}>
|
|
21
|
+
<text>never rendered</text>
|
|
22
|
+
</Dialog>
|
|
23
|
+
),
|
|
24
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centered modal: a full-screen layer (backdrop token, transparent by
|
|
3
|
+
* default) centers a bordered, unpainted panel over the session - the frame
|
|
4
|
+
* delineates the dialog, the content behind stays visible.
|
|
5
|
+
* `isOpen` keeps the mount decision with the caller-visible prop vocabulary.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from "react";
|
|
9
|
+
import type { Theme } from "../../theme";
|
|
10
|
+
import { useComponentTheme } from "../theme-context";
|
|
11
|
+
import { FRAME_BORDER_STYLE } from "./frame";
|
|
12
|
+
|
|
13
|
+
export interface DialogProps {
|
|
14
|
+
isOpen: boolean;
|
|
15
|
+
title?: string;
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
/** Solid panel fill for content-heavy dialogs; default transparent. */
|
|
19
|
+
background?: string;
|
|
20
|
+
theme?: Theme;
|
|
21
|
+
children: React.ReactNode;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function Dialog({
|
|
25
|
+
isOpen,
|
|
26
|
+
title,
|
|
27
|
+
width,
|
|
28
|
+
height,
|
|
29
|
+
background,
|
|
30
|
+
theme,
|
|
31
|
+
children,
|
|
32
|
+
}: DialogProps): React.ReactNode {
|
|
33
|
+
const tokens = useComponentTheme(theme);
|
|
34
|
+
|
|
35
|
+
if (!isOpen) return null;
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<box
|
|
39
|
+
style={{
|
|
40
|
+
position: "absolute",
|
|
41
|
+
left: 0,
|
|
42
|
+
top: 0,
|
|
43
|
+
width: "100%",
|
|
44
|
+
height: "100%",
|
|
45
|
+
backgroundColor: tokens.backdrop,
|
|
46
|
+
alignItems: "center",
|
|
47
|
+
justifyContent: "center",
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
<box
|
|
51
|
+
style={{
|
|
52
|
+
width,
|
|
53
|
+
height,
|
|
54
|
+
border: true,
|
|
55
|
+
borderStyle: FRAME_BORDER_STYLE,
|
|
56
|
+
borderColor: tokens.accent,
|
|
57
|
+
backgroundColor: background,
|
|
58
|
+
flexDirection: "column",
|
|
59
|
+
}}
|
|
60
|
+
title={title}
|
|
61
|
+
>
|
|
62
|
+
{children}
|
|
63
|
+
</box>
|
|
64
|
+
</box>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "../story";
|
|
4
|
+
import { StatusBar } from "./StatusBar";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "primitives/StatusBar" };
|
|
7
|
+
|
|
8
|
+
export const HintLine: Story = {
|
|
9
|
+
render: () => <StatusBar>j/k move · v span · c comment · q quit</StatusBar>,
|
|
10
|
+
expectedColors: [DARK.textDim],
|
|
11
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-row panel bar for the status/hint line. Content renders as dim text;
|
|
3
|
+
* callers pass richer spans as children when a segment needs another tone.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import type { Theme } from "../../theme";
|
|
8
|
+
import { useComponentTheme } from "../theme-context";
|
|
9
|
+
|
|
10
|
+
export interface StatusBarProps {
|
|
11
|
+
theme?: Theme;
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function StatusBar({ theme, children }: StatusBarProps): React.ReactNode {
|
|
16
|
+
const tokens = useComponentTheme(theme);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<box style={{ height: 1, backgroundColor: tokens.panel, paddingLeft: 1 }}>
|
|
20
|
+
<text fg={tokens.textDim}>{children}</text>
|
|
21
|
+
</box>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "../story";
|
|
4
|
+
import { Tab, TabList, Tabs } from "./Tabs";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "primitives/Tabs" };
|
|
7
|
+
|
|
8
|
+
export const TwoTabs: Story = {
|
|
9
|
+
render: () => (
|
|
10
|
+
<Tabs selectedKey="review" onSelectionChange={() => {}}>
|
|
11
|
+
<TabList>
|
|
12
|
+
<Tab id="review">Review (2)</Tab>
|
|
13
|
+
<Tab id="agent">Agent</Tab>
|
|
14
|
+
</TabList>
|
|
15
|
+
</Tabs>
|
|
16
|
+
),
|
|
17
|
+
expectedColors: [DARK.accent, DARK.textDim],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const SecondSelected: Story = {
|
|
21
|
+
render: () => (
|
|
22
|
+
<Tabs selectedKey="agent" onSelectionChange={() => {}}>
|
|
23
|
+
<TabList>
|
|
24
|
+
<Tab id="review">Review (0)</Tab>
|
|
25
|
+
<Tab id="agent">Agent</Tab>
|
|
26
|
+
</TabList>
|
|
27
|
+
</Tabs>
|
|
28
|
+
),
|
|
29
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tabs / TabList / Tab: a transparent, full-width rounded strip of clickable
|
|
3
|
+
* text tabs. The composition follows the ARIA tabs pattern: Tabs owns the
|
|
4
|
+
* controlled selection (`selectedKey` + `onSelectionChange`), TabList renders
|
|
5
|
+
* the strip, Tab declares one item. Selection stays controlled from outside so
|
|
6
|
+
* the keyboard grammar remains the key owner; each tab click sets the key.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React, { createContext, useContext } from "react";
|
|
10
|
+
import type { Theme } from "../../theme";
|
|
11
|
+
import { useComponentTheme } from "../theme-context";
|
|
12
|
+
import { FRAME_BORDER_STYLE } from "./frame";
|
|
13
|
+
|
|
14
|
+
interface TabsContextValue {
|
|
15
|
+
selectedKey: string;
|
|
16
|
+
onSelectionChange: (key: string) => void;
|
|
17
|
+
/** Text color of the selected tab; defaults to the accent token. */
|
|
18
|
+
selectedColor?: string;
|
|
19
|
+
theme?: Theme;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const TabsContext = createContext<TabsContextValue | null>(null);
|
|
23
|
+
|
|
24
|
+
export interface TabsProps {
|
|
25
|
+
selectedKey: string;
|
|
26
|
+
onSelectionChange: (key: string) => void;
|
|
27
|
+
selectedColor?: string;
|
|
28
|
+
theme?: Theme;
|
|
29
|
+
children: React.ReactNode;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function Tabs({
|
|
33
|
+
selectedKey,
|
|
34
|
+
onSelectionChange,
|
|
35
|
+
selectedColor,
|
|
36
|
+
theme,
|
|
37
|
+
children,
|
|
38
|
+
}: TabsProps): React.ReactNode {
|
|
39
|
+
return (
|
|
40
|
+
<TabsContext.Provider value={{ selectedKey, onSelectionChange, selectedColor, theme }}>
|
|
41
|
+
{children}
|
|
42
|
+
</TabsContext.Provider>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface TabProps {
|
|
47
|
+
id: string;
|
|
48
|
+
children: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Declarative item: TabList collects Tab children into the native strip. */
|
|
52
|
+
export function Tab(_props: TabProps): React.ReactNode {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface TabListProps {
|
|
57
|
+
children: React.ReactNode;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function TabList({ children }: TabListProps): React.ReactNode {
|
|
61
|
+
const tabs = useContext(TabsContext);
|
|
62
|
+
|
|
63
|
+
if (!tabs) throw new Error("TabList must render inside Tabs");
|
|
64
|
+
const tokens = useComponentTheme(tabs.theme);
|
|
65
|
+
const items: { id: string; label: string }[] = [];
|
|
66
|
+
|
|
67
|
+
React.Children.forEach(children, (child) => {
|
|
68
|
+
if (React.isValidElement<TabProps>(child) && typeof child.props.id === "string") {
|
|
69
|
+
items.push({ id: child.props.id, label: child.props.children });
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<box
|
|
75
|
+
style={{
|
|
76
|
+
width: "100%",
|
|
77
|
+
height: 3,
|
|
78
|
+
border: true,
|
|
79
|
+
borderStyle: FRAME_BORDER_STYLE,
|
|
80
|
+
borderColor: tokens.text,
|
|
81
|
+
flexDirection: "row",
|
|
82
|
+
paddingLeft: 1,
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
{items.map((item) => {
|
|
86
|
+
const selected = item.id === tabs.selectedKey;
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<box
|
|
90
|
+
key={item.id}
|
|
91
|
+
style={{ marginRight: 2 }}
|
|
92
|
+
onMouseUp={() => tabs.onSelectionChange(item.id)}
|
|
93
|
+
>
|
|
94
|
+
<text fg={selected ? (tabs.selectedColor ?? tokens.accent) : tokens.textDim}>
|
|
95
|
+
{item.label}
|
|
96
|
+
</text>
|
|
97
|
+
</box>
|
|
98
|
+
);
|
|
99
|
+
})}
|
|
100
|
+
</box>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Story, StoryMeta } from "../story";
|
|
3
|
+
import { Button } from "./Button";
|
|
4
|
+
import { Toolbar } from "./Toolbar";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "primitives/Toolbar" };
|
|
7
|
+
|
|
8
|
+
export const ActionRow: Story = {
|
|
9
|
+
render: () => (
|
|
10
|
+
<Toolbar>
|
|
11
|
+
<Button variant="solid" marginRight={2} onPress={() => {}}>
|
|
12
|
+
{" Submit "}
|
|
13
|
+
</Button>
|
|
14
|
+
<Button onPress={() => {}}>{" Cancel "}</Button>
|
|
15
|
+
</Toolbar>
|
|
16
|
+
),
|
|
17
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Horizontal action row: children flow left to right on one row. The classic
|
|
3
|
+
* home of Button groups (Save/Cancel, Submit/Cancel).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
|
|
8
|
+
export interface ToolbarProps {
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function Toolbar({ children }: ToolbarProps): React.ReactNode {
|
|
13
|
+
return <box style={{ flexDirection: "row", height: 1 }}>{children}</box>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locks the single frame border rule: every bordered frame reads this token,
|
|
3
|
+
* so a regression to square corners (or any other style) fails here first,
|
|
4
|
+
* before the slower VRT snapshots catch it. Decision: rounded (issue #116).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { expect, test } from "bun:test";
|
|
8
|
+
import { FRAME_BORDER_STYLE } from "./frame";
|
|
9
|
+
|
|
10
|
+
test("frames use the rounded border style", () => {
|
|
11
|
+
expect(FRAME_BORDER_STYLE).toBe("rounded");
|
|
12
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one frame border rule for the design system. Every bordered frame -
|
|
3
|
+
* cards, dialogs, and the stories gallery chrome - reads its corner style
|
|
4
|
+
* from FRAME_BORDER_STYLE, so the rounded-corner decision lives in a single
|
|
5
|
+
* place and can never drift between frames. Buttons stay text-first and
|
|
6
|
+
* borderless: the frame they sit in carries the border, not the button.
|
|
7
|
+
*
|
|
8
|
+
* Decision: rounded on the frames, not on the buttons (issue #116).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { BorderStyle } from "@opentui/core";
|
|
12
|
+
|
|
13
|
+
/** Corner style shared by every bordered frame in the app. */
|
|
14
|
+
export const FRAME_BORDER_STYLE: BorderStyle = "rounded";
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../theme";
|
|
3
|
+
import { DEFAULT_QUICK_ACTIONS } from "../config";
|
|
4
|
+
import type { Story, StoryMeta } from "./story";
|
|
5
|
+
import { QuickActionsEditor } from "./quick-actions-editor";
|
|
6
|
+
|
|
7
|
+
export const meta: StoryMeta = { title: "QuickActionsEditor" };
|
|
8
|
+
|
|
9
|
+
/** The editor renders inside the settings body column. */
|
|
10
|
+
function BodyFrame({ children }: { children: React.ReactNode }): React.ReactNode {
|
|
11
|
+
return (
|
|
12
|
+
<box style={{ width: 54, height: 22, paddingLeft: 2, backgroundColor: DARK.elevated }}>
|
|
13
|
+
{children}
|
|
14
|
+
</box>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const noop = () => {};
|
|
19
|
+
|
|
20
|
+
export const Collapsed: Story = {
|
|
21
|
+
render: () => (
|
|
22
|
+
<BodyFrame>
|
|
23
|
+
<QuickActionsEditor
|
|
24
|
+
actions={DEFAULT_QUICK_ACTIONS}
|
|
25
|
+
selectedIndex={0}
|
|
26
|
+
expandedIndex={null}
|
|
27
|
+
onToggleExpand={noop}
|
|
28
|
+
onEditMetadata={noop}
|
|
29
|
+
onReset={noop}
|
|
30
|
+
onAdd={noop}
|
|
31
|
+
theme={DARK}
|
|
32
|
+
/>
|
|
33
|
+
</BodyFrame>
|
|
34
|
+
),
|
|
35
|
+
size: { width: 54, height: 22 },
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const RowExpanded: Story = {
|
|
39
|
+
render: () => (
|
|
40
|
+
<BodyFrame>
|
|
41
|
+
<QuickActionsEditor
|
|
42
|
+
actions={DEFAULT_QUICK_ACTIONS}
|
|
43
|
+
selectedIndex={1}
|
|
44
|
+
expandedIndex={1}
|
|
45
|
+
onToggleExpand={noop}
|
|
46
|
+
onEditMetadata={noop}
|
|
47
|
+
onReset={noop}
|
|
48
|
+
onAdd={noop}
|
|
49
|
+
theme={DARK}
|
|
50
|
+
/>
|
|
51
|
+
</BodyFrame>
|
|
52
|
+
),
|
|
53
|
+
size: { width: 54, height: 22 },
|
|
54
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Settings "Actions" body: the quick-action vocabulary editor. Each row is a
|
|
3
|
+
* clickable prompt; the selected-and-expanded row reveals a focused input for the
|
|
4
|
+
* per-action system prompt (the extra guidance appended when the action is used).
|
|
5
|
+
* A reset-to-defaults control and an add-action row bracket the list. Controlled:
|
|
6
|
+
* the app owns the actions, the selection, and which row is expanded.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React, { useRef } from "react";
|
|
10
|
+
import type { KeyBinding, TextareaRenderable } from "@opentui/core";
|
|
11
|
+
import type { QuickAction } from "../config";
|
|
12
|
+
import type { Theme } from "../theme";
|
|
13
|
+
import { useComponentTheme } from "./theme-context";
|
|
14
|
+
|
|
15
|
+
// ⏎ never inserts a newline: a system prompt is one line, esc closes the editor.
|
|
16
|
+
const ACTION_INPUT_KEY_BINDINGS: KeyBinding[] = [{ name: "return", action: "submit" }];
|
|
17
|
+
|
|
18
|
+
export interface QuickActionsEditorProps {
|
|
19
|
+
actions: QuickAction[];
|
|
20
|
+
/** The highlighted row; equals the settings body row index. */
|
|
21
|
+
selectedIndex: number;
|
|
22
|
+
/** The row whose system-prompt input is open and focused, or null. */
|
|
23
|
+
expandedIndex: number | null;
|
|
24
|
+
/** Select and toggle a row's system-prompt input open/closed. */
|
|
25
|
+
onToggleExpand: (index: number) => void;
|
|
26
|
+
onEditMetadata: (index: number, metadata: string) => void;
|
|
27
|
+
onReset: () => void;
|
|
28
|
+
onAdd: () => void;
|
|
29
|
+
theme?: Theme;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function QuickActionsEditor({
|
|
33
|
+
actions,
|
|
34
|
+
selectedIndex,
|
|
35
|
+
expandedIndex,
|
|
36
|
+
onToggleExpand,
|
|
37
|
+
onEditMetadata,
|
|
38
|
+
onReset,
|
|
39
|
+
onAdd,
|
|
40
|
+
theme,
|
|
41
|
+
}: QuickActionsEditorProps): React.ReactNode {
|
|
42
|
+
const tokens = useComponentTheme(theme);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<box style={{ flexDirection: "column", flexGrow: 1 }}>
|
|
46
|
+
<box style={{ flexDirection: "row" }}>
|
|
47
|
+
<box style={{ flexGrow: 1 }}>
|
|
48
|
+
<text fg={tokens.textDim}>click a prompt to edit its system prompt</text>
|
|
49
|
+
</box>
|
|
50
|
+
<box onMouseUp={onReset}>
|
|
51
|
+
<text fg={tokens.textDim}>reset to defaults ▸</text>
|
|
52
|
+
</box>
|
|
53
|
+
</box>
|
|
54
|
+
<box style={{ height: 1 }} />
|
|
55
|
+
{actions.map((action, index) => (
|
|
56
|
+
<ActionRow
|
|
57
|
+
key={index}
|
|
58
|
+
action={action}
|
|
59
|
+
isSelected={index === selectedIndex}
|
|
60
|
+
isExpanded={index === expandedIndex}
|
|
61
|
+
onToggleExpand={() => onToggleExpand(index)}
|
|
62
|
+
onEditMetadata={(metadata) => onEditMetadata(index, metadata)}
|
|
63
|
+
theme={theme}
|
|
64
|
+
/>
|
|
65
|
+
))}
|
|
66
|
+
<box
|
|
67
|
+
style={{ backgroundColor: selectedIndex === actions.length ? tokens.border : undefined }}
|
|
68
|
+
onMouseUp={onAdd}
|
|
69
|
+
>
|
|
70
|
+
<text fg={tokens.textMuted}>+ Add action</text>
|
|
71
|
+
</box>
|
|
72
|
+
</box>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function ActionRow({
|
|
77
|
+
action,
|
|
78
|
+
isSelected,
|
|
79
|
+
isExpanded,
|
|
80
|
+
onToggleExpand,
|
|
81
|
+
onEditMetadata,
|
|
82
|
+
theme,
|
|
83
|
+
}: {
|
|
84
|
+
action: QuickAction;
|
|
85
|
+
isSelected: boolean;
|
|
86
|
+
isExpanded: boolean;
|
|
87
|
+
onToggleExpand: () => void;
|
|
88
|
+
onEditMetadata: (metadata: string) => void;
|
|
89
|
+
theme?: Theme;
|
|
90
|
+
}): React.ReactNode {
|
|
91
|
+
const tokens = useComponentTheme(theme);
|
|
92
|
+
const inputRef = useRef<TextareaRenderable | null>(null);
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<box style={{ flexDirection: "column" }}>
|
|
96
|
+
<box
|
|
97
|
+
style={{ backgroundColor: isSelected ? tokens.border : undefined }}
|
|
98
|
+
onMouseUp={onToggleExpand}
|
|
99
|
+
>
|
|
100
|
+
<text fg={isSelected ? tokens.text : tokens.textMuted}>
|
|
101
|
+
{`${isExpanded ? "▾ " : "▸ "}${action.prompt}`}
|
|
102
|
+
</text>
|
|
103
|
+
</box>
|
|
104
|
+
{isExpanded ? (
|
|
105
|
+
<box style={{ flexDirection: "row", paddingLeft: 2 }}>
|
|
106
|
+
<box style={{ width: 2, flexShrink: 0 }}>
|
|
107
|
+
<text fg={tokens.textDim}>{">"}</text>
|
|
108
|
+
</box>
|
|
109
|
+
<textarea
|
|
110
|
+
ref={inputRef}
|
|
111
|
+
focused
|
|
112
|
+
initialValue={action.metadata ?? ""}
|
|
113
|
+
placeholder="extra system prompt appended to this comment (optional)"
|
|
114
|
+
keyBindings={ACTION_INPUT_KEY_BINDINGS}
|
|
115
|
+
onContentChange={() => onEditMetadata(inputRef.current?.plainText ?? "")}
|
|
116
|
+
style={{
|
|
117
|
+
height: 1,
|
|
118
|
+
flexGrow: 1,
|
|
119
|
+
backgroundColor: tokens.elevated,
|
|
120
|
+
focusedBackgroundColor: tokens.elevated,
|
|
121
|
+
textColor: tokens.text,
|
|
122
|
+
focusedTextColor: tokens.text,
|
|
123
|
+
}}
|
|
124
|
+
/>
|
|
125
|
+
</box>
|
|
126
|
+
) : action.metadata ? (
|
|
127
|
+
<box style={{ paddingLeft: 2 }}>
|
|
128
|
+
<text fg={tokens.textDim}>{truncateMetadata(action.metadata)}</text>
|
|
129
|
+
</box>
|
|
130
|
+
) : null}
|
|
131
|
+
</box>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** A one-line preview of the system prompt under a collapsed row. */
|
|
136
|
+
function truncateMetadata(metadata: string): string {
|
|
137
|
+
return metadata.length > 44 ? `${metadata.slice(0, 43)}…` : metadata;
|
|
138
|
+
}
|