@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,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The settings overlay, promoted from the design prototype: a centered
|
|
3
|
+
* Dialog with a left category nav and a right body of typed rows, all driven
|
|
4
|
+
* by one settings record. The component is controlled - the caller owns the
|
|
5
|
+
* open flag, the active category/row/zone, and value changes - so the app's
|
|
6
|
+
* keyboard grammar (and the stories catalog) can drive it without a second
|
|
7
|
+
* key handler inside. The modal sizes itself from the terminal dimensions.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React from "react";
|
|
11
|
+
import { useTerminalDimensions } from "@opentui/react";
|
|
12
|
+
import type { Theme } from "../theme";
|
|
13
|
+
import { useComponentTheme } from "./theme-context";
|
|
14
|
+
import { Dialog } from "./primitives/Dialog";
|
|
15
|
+
import { CycleRow, TextRow, ToggleRow, type SettingsRowDescriptor } from "./SettingsRows";
|
|
16
|
+
|
|
17
|
+
export interface SettingsCategory {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
description: string;
|
|
21
|
+
rows: SettingsRowDescriptor[];
|
|
22
|
+
/** A bespoke body (e.g. the quick-actions editor) rendered instead of typed rows. */
|
|
23
|
+
customBody?: React.ReactNode;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type SettingsValues = Record<string, string | boolean>;
|
|
27
|
+
|
|
28
|
+
export interface SettingsDialogProps {
|
|
29
|
+
isOpen: boolean;
|
|
30
|
+
categories: SettingsCategory[];
|
|
31
|
+
values: SettingsValues;
|
|
32
|
+
activeCategoryId: string;
|
|
33
|
+
activeRowIndex: number;
|
|
34
|
+
activeZone: "nav" | "body";
|
|
35
|
+
onCategorySelect: (categoryId: string) => void;
|
|
36
|
+
onRowActivate: (row: SettingsRowDescriptor) => void;
|
|
37
|
+
theme?: Theme;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function SettingsDialog({
|
|
41
|
+
isOpen,
|
|
42
|
+
categories,
|
|
43
|
+
values,
|
|
44
|
+
activeCategoryId,
|
|
45
|
+
activeRowIndex,
|
|
46
|
+
activeZone,
|
|
47
|
+
onCategorySelect,
|
|
48
|
+
onRowActivate,
|
|
49
|
+
theme,
|
|
50
|
+
}: SettingsDialogProps): React.ReactNode {
|
|
51
|
+
const tokens = useComponentTheme(theme);
|
|
52
|
+
const { width: terminalWidth, height: terminalHeight } = useTerminalDimensions();
|
|
53
|
+
|
|
54
|
+
if (!isOpen) return null;
|
|
55
|
+
const category =
|
|
56
|
+
categories.find((candidate) => candidate.id === activeCategoryId) ?? categories[0];
|
|
57
|
+
|
|
58
|
+
if (!category) return null;
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<Dialog
|
|
62
|
+
isOpen
|
|
63
|
+
title=" Settings "
|
|
64
|
+
width={Math.min(76, terminalWidth - 6)}
|
|
65
|
+
height={Math.min(22, terminalHeight - 4)}
|
|
66
|
+
background={tokens.elevated}
|
|
67
|
+
theme={theme}
|
|
68
|
+
>
|
|
69
|
+
<box style={{ flexDirection: "row", flexGrow: 1 }}>
|
|
70
|
+
<box style={{ flexDirection: "column", width: 18, paddingLeft: 1, paddingRight: 1 }}>
|
|
71
|
+
<text fg={tokens.textDim}>SETTINGS</text>
|
|
72
|
+
{categories.map((candidate) => {
|
|
73
|
+
const isActive = candidate.id === category.id;
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<box
|
|
77
|
+
key={candidate.id}
|
|
78
|
+
style={{
|
|
79
|
+
backgroundColor: isActive && activeZone === "nav" ? tokens.border : undefined,
|
|
80
|
+
}}
|
|
81
|
+
onMouseUp={() => onCategorySelect(candidate.id)}
|
|
82
|
+
>
|
|
83
|
+
<text
|
|
84
|
+
fg={isActive ? tokens.accent : tokens.textMuted}
|
|
85
|
+
>{`${isActive ? "› " : " "}${candidate.name}`}</text>
|
|
86
|
+
</box>
|
|
87
|
+
);
|
|
88
|
+
})}
|
|
89
|
+
</box>
|
|
90
|
+
<box style={{ width: 1, backgroundColor: tokens.border }} />
|
|
91
|
+
<box style={{ flexDirection: "column", flexGrow: 1, paddingLeft: 2, paddingRight: 1 }}>
|
|
92
|
+
<text fg={tokens.text}>{category.name}</text>
|
|
93
|
+
<text fg={tokens.textDim}>{category.description}</text>
|
|
94
|
+
<box style={{ height: 1 }} />
|
|
95
|
+
{category.customBody}
|
|
96
|
+
{category.rows.map((row, rowIndex) => {
|
|
97
|
+
const isActive = activeZone === "body" && rowIndex === activeRowIndex;
|
|
98
|
+
const value = values[row.key];
|
|
99
|
+
|
|
100
|
+
if (row.kind === "toggle") {
|
|
101
|
+
return (
|
|
102
|
+
<ToggleRow
|
|
103
|
+
key={row.key}
|
|
104
|
+
label={row.label}
|
|
105
|
+
value={value === true}
|
|
106
|
+
isActive={isActive}
|
|
107
|
+
onPress={() => onRowActivate(row)}
|
|
108
|
+
theme={theme}
|
|
109
|
+
/>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
if (row.kind === "cycle") {
|
|
113
|
+
return (
|
|
114
|
+
<CycleRow
|
|
115
|
+
key={row.key}
|
|
116
|
+
label={row.label}
|
|
117
|
+
value={String(value ?? "")}
|
|
118
|
+
isActive={isActive}
|
|
119
|
+
onPress={() => onRowActivate(row)}
|
|
120
|
+
theme={theme}
|
|
121
|
+
/>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<TextRow
|
|
127
|
+
key={row.key}
|
|
128
|
+
label={row.label}
|
|
129
|
+
value={String(value ?? "")}
|
|
130
|
+
isActive={isActive}
|
|
131
|
+
onPress={() => onRowActivate(row)}
|
|
132
|
+
theme={theme}
|
|
133
|
+
/>
|
|
134
|
+
);
|
|
135
|
+
})}
|
|
136
|
+
</box>
|
|
137
|
+
</box>
|
|
138
|
+
<box style={{ flexDirection: "row", height: 1, paddingLeft: 1 }}>
|
|
139
|
+
<text fg={tokens.textDim}>
|
|
140
|
+
{activeZone === "nav"
|
|
141
|
+
? "j/k category · l/enter into settings · esc close"
|
|
142
|
+
: "j/k row · l/space change · h back · esc close"}
|
|
143
|
+
</text>
|
|
144
|
+
</box>
|
|
145
|
+
</Dialog>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "./story";
|
|
4
|
+
import { CycleRow, TextRow, ToggleRow } from "./SettingsRows";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "SettingsRows" };
|
|
7
|
+
|
|
8
|
+
export const AllKinds: Story = {
|
|
9
|
+
render: () => (
|
|
10
|
+
<box style={{ flexDirection: "column", width: 50 }}>
|
|
11
|
+
<ToggleRow label="Line numbers" value={true} isActive={false} onPress={() => {}} />
|
|
12
|
+
<ToggleRow label="Hide whitespace" value={false} isActive={true} onPress={() => {}} />
|
|
13
|
+
<CycleRow label="Palette" value="dark" isActive={false} onPress={() => {}} />
|
|
14
|
+
<TextRow label="Vault path" value="~/vaults/notes" isActive={false} onPress={() => {}} />
|
|
15
|
+
</box>
|
|
16
|
+
),
|
|
17
|
+
expectedColors: [DARK.green, DARK.accent, DARK.border],
|
|
18
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed settings rows for the settings dialog: toggle (on/off), cycle
|
|
3
|
+
* (enumerated options), and text (freeform value). Every row is one line -
|
|
4
|
+
* label column left, value right - and presses go through `onPress` so mouse
|
|
5
|
+
* and keyboard activation share one path.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from "react";
|
|
9
|
+
import type { Theme } from "../theme";
|
|
10
|
+
import { useComponentTheme } from "./theme-context";
|
|
11
|
+
|
|
12
|
+
export interface SettingsRowDescriptor {
|
|
13
|
+
key: string;
|
|
14
|
+
label: string;
|
|
15
|
+
kind: "toggle" | "cycle" | "text";
|
|
16
|
+
options?: string[];
|
|
17
|
+
hint?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface RowShellProps {
|
|
21
|
+
label: string;
|
|
22
|
+
isActive: boolean;
|
|
23
|
+
onPress: () => void;
|
|
24
|
+
theme?: Theme;
|
|
25
|
+
children: React.ReactNode;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function RowShell({ label, isActive, onPress, theme, children }: RowShellProps): React.ReactNode {
|
|
29
|
+
const tokens = useComponentTheme(theme);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<box
|
|
33
|
+
style={{
|
|
34
|
+
flexDirection: "row",
|
|
35
|
+
height: 1,
|
|
36
|
+
backgroundColor: isActive ? tokens.border : undefined,
|
|
37
|
+
}}
|
|
38
|
+
onMouseUp={onPress}
|
|
39
|
+
>
|
|
40
|
+
<box style={{ width: 26, paddingLeft: 1 }}>
|
|
41
|
+
<text fg={isActive ? tokens.text : tokens.textMuted}>{label}</text>
|
|
42
|
+
</box>
|
|
43
|
+
{children}
|
|
44
|
+
</box>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ToggleRowProps {
|
|
49
|
+
label: string;
|
|
50
|
+
value: boolean;
|
|
51
|
+
isActive: boolean;
|
|
52
|
+
onPress: () => void;
|
|
53
|
+
theme?: Theme;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function ToggleRow({
|
|
57
|
+
label,
|
|
58
|
+
value,
|
|
59
|
+
isActive,
|
|
60
|
+
onPress,
|
|
61
|
+
theme,
|
|
62
|
+
}: ToggleRowProps): React.ReactNode {
|
|
63
|
+
const tokens = useComponentTheme(theme);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<RowShell label={label} isActive={isActive} onPress={onPress} theme={theme}>
|
|
67
|
+
<text fg={value ? tokens.green : tokens.accent}>{value ? "on" : "off"}</text>
|
|
68
|
+
</RowShell>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface CycleRowProps {
|
|
73
|
+
label: string;
|
|
74
|
+
value: string;
|
|
75
|
+
isActive: boolean;
|
|
76
|
+
onPress: () => void;
|
|
77
|
+
theme?: Theme;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function CycleRow({
|
|
81
|
+
label,
|
|
82
|
+
value,
|
|
83
|
+
isActive,
|
|
84
|
+
onPress,
|
|
85
|
+
theme,
|
|
86
|
+
}: CycleRowProps): React.ReactNode {
|
|
87
|
+
const tokens = useComponentTheme(theme);
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<RowShell label={label} isActive={isActive} onPress={onPress} theme={theme}>
|
|
91
|
+
<text fg={tokens.accent}>{value}</text>
|
|
92
|
+
</RowShell>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface TextRowProps {
|
|
97
|
+
label: string;
|
|
98
|
+
value: string;
|
|
99
|
+
isActive: boolean;
|
|
100
|
+
onPress: () => void;
|
|
101
|
+
theme?: Theme;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function TextRow({ label, value, isActive, onPress, theme }: TextRowProps): React.ReactNode {
|
|
105
|
+
const tokens = useComponentTheme(theme);
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<RowShell label={label} isActive={isActive} onPress={onPress} theme={theme}>
|
|
109
|
+
<text fg={tokens.accent}>{value}</text>
|
|
110
|
+
</RowShell>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "./story";
|
|
4
|
+
import { Toast } from "./Toast";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "Toast" };
|
|
7
|
+
|
|
8
|
+
export const ShareLink: Story = {
|
|
9
|
+
render: () => <Toast title="share link copied" body="ssh review-4f2a@cueloop.dev" />,
|
|
10
|
+
expectedColors: [DARK.accent],
|
|
11
|
+
size: { width: 60, height: 12 },
|
|
12
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useTerminalDimensions } from "@opentui/react";
|
|
3
|
+
import type { Theme } from "../theme";
|
|
4
|
+
import { useComponentTheme } from "./theme-context";
|
|
5
|
+
import { Dialog } from "./primitives/Dialog";
|
|
6
|
+
|
|
7
|
+
export interface ToastProps {
|
|
8
|
+
title?: string;
|
|
9
|
+
body: string;
|
|
10
|
+
theme?: Theme;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function Toast({ title, body, theme }: ToastProps): React.ReactNode {
|
|
14
|
+
const tokens = useComponentTheme(theme);
|
|
15
|
+
const { width: terminalWidth } = useTerminalDimensions();
|
|
16
|
+
const width = Math.min(terminalWidth - 6, Math.max(28, body.length + 4));
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<Dialog
|
|
20
|
+
isOpen
|
|
21
|
+
title={title ? ` ${title} ` : undefined}
|
|
22
|
+
width={width}
|
|
23
|
+
height={6}
|
|
24
|
+
background={tokens.elevated}
|
|
25
|
+
theme={theme}
|
|
26
|
+
>
|
|
27
|
+
<box
|
|
28
|
+
style={{
|
|
29
|
+
flexDirection: "column",
|
|
30
|
+
flexGrow: 1,
|
|
31
|
+
paddingLeft: 1,
|
|
32
|
+
paddingRight: 1,
|
|
33
|
+
paddingTop: 1,
|
|
34
|
+
}}
|
|
35
|
+
>
|
|
36
|
+
<text fg={tokens.text}>{body}</text>
|
|
37
|
+
<box style={{ flexGrow: 1 }} />
|
|
38
|
+
<box style={{ flexDirection: "row" }}>
|
|
39
|
+
<box style={{ flexGrow: 1 }} />
|
|
40
|
+
<text fg={tokens.textDim}>[esc]</text>
|
|
41
|
+
</box>
|
|
42
|
+
</box>
|
|
43
|
+
</Dialog>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../theme";
|
|
3
|
+
import type { WalkFile } from "../walk";
|
|
4
|
+
import type { Story, StoryMeta } from "./story";
|
|
5
|
+
import { WalkWizard } from "./WalkWizard";
|
|
6
|
+
|
|
7
|
+
export const meta: StoryMeta = { title: "WalkWizard" };
|
|
8
|
+
|
|
9
|
+
const FILES: WalkFile[] = [
|
|
10
|
+
{
|
|
11
|
+
path: "packages/daemon/src/store.ts",
|
|
12
|
+
added: 12,
|
|
13
|
+
removed: 3,
|
|
14
|
+
preview: [
|
|
15
|
+
{ sign: "+", text: "const viewed = new Set<string>();" },
|
|
16
|
+
{ sign: "+", text: "record.viewedPaths = [...viewed];" },
|
|
17
|
+
{ sign: "-", text: "// viewed tracking lives in the client only" },
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
path: "packages/client/src/walk.ts",
|
|
22
|
+
added: 48,
|
|
23
|
+
removed: 0,
|
|
24
|
+
preview: [{ sign: "+", text: "export function firstUnviewedIndex(files, viewed) {" }],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
path: "packages/schema/src/types.ts",
|
|
28
|
+
added: 3,
|
|
29
|
+
removed: 0,
|
|
30
|
+
preview: [{ sign: "+", text: "viewedPaths?: string[];" }],
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const noop = (): void => {};
|
|
35
|
+
|
|
36
|
+
export const FirstFile: Story = {
|
|
37
|
+
render: () => (
|
|
38
|
+
<WalkWizard
|
|
39
|
+
files={FILES}
|
|
40
|
+
index={0}
|
|
41
|
+
viewedPaths={new Set()}
|
|
42
|
+
terminalWidth={80}
|
|
43
|
+
onSubmitRequest={noop}
|
|
44
|
+
onBack={noop}
|
|
45
|
+
/>
|
|
46
|
+
),
|
|
47
|
+
// the main card wears the accent border; stats carry the +/- colors
|
|
48
|
+
expectedColors: [
|
|
49
|
+
DARK.accent,
|
|
50
|
+
DARK.green,
|
|
51
|
+
DARK.red,
|
|
52
|
+
DARK.insertedForeground,
|
|
53
|
+
DARK.deletedForeground,
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const WithAgentNote: Story = {
|
|
58
|
+
render: () => (
|
|
59
|
+
<WalkWizard
|
|
60
|
+
files={FILES}
|
|
61
|
+
index={1}
|
|
62
|
+
viewedPaths={new Set(["packages/daemon/src/store.ts"])}
|
|
63
|
+
note="New module: pure helpers that pick the next unviewed file. No IO, unit tested."
|
|
64
|
+
terminalWidth={80}
|
|
65
|
+
onSubmitRequest={noop}
|
|
66
|
+
onBack={noop}
|
|
67
|
+
/>
|
|
68
|
+
),
|
|
69
|
+
// the agent-note block wears the plain gray border token, not the accent
|
|
70
|
+
expectedColors: [DARK.accent, DARK.border, DARK.textMuted],
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const EndCard: Story = {
|
|
74
|
+
render: () => (
|
|
75
|
+
<WalkWizard
|
|
76
|
+
files={FILES}
|
|
77
|
+
index={3}
|
|
78
|
+
viewedPaths={new Set(FILES.map((file) => file.path))}
|
|
79
|
+
terminalWidth={80}
|
|
80
|
+
onSubmitRequest={noop}
|
|
81
|
+
onBack={noop}
|
|
82
|
+
/>
|
|
83
|
+
),
|
|
84
|
+
// the end card wears the green border and the solid submit button
|
|
85
|
+
expectedColors: [DARK.green, DARK.accent, DARK.accentInk],
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const EndCardPartialPass: Story = {
|
|
89
|
+
render: () => (
|
|
90
|
+
<WalkWizard
|
|
91
|
+
files={FILES}
|
|
92
|
+
index={3}
|
|
93
|
+
viewedPaths={new Set(["packages/daemon/src/store.ts"])}
|
|
94
|
+
terminalWidth={80}
|
|
95
|
+
onSubmitRequest={noop}
|
|
96
|
+
onBack={noop}
|
|
97
|
+
/>
|
|
98
|
+
),
|
|
99
|
+
expectedColors: [DARK.green],
|
|
100
|
+
};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The guided walk's focused card wizard: one card per changed file
|
|
3
|
+
* floating over the dimmed diff, with a plain step count in the title - no
|
|
4
|
+
* progress bar. Below the main card a separate gray-bordered block carries
|
|
5
|
+
* the submitting agent's note for the file, when one exists. Past the last
|
|
6
|
+
* file the end card offers the submit action directly with honest counts.
|
|
7
|
+
* The keys live in the keymap's walk overlay; this component only renders.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React from "react";
|
|
11
|
+
import type { Theme } from "../theme";
|
|
12
|
+
import { useComponentTheme } from "./theme-context";
|
|
13
|
+
import type { WalkFile } from "../walk";
|
|
14
|
+
import { viewedCount } from "../walk";
|
|
15
|
+
import { Card } from "./primitives/Card";
|
|
16
|
+
import { Button } from "./primitives/Button";
|
|
17
|
+
import { Toolbar } from "./primitives/Toolbar";
|
|
18
|
+
|
|
19
|
+
export interface WalkWizardProps {
|
|
20
|
+
files: WalkFile[];
|
|
21
|
+
/** The wizard step; index === files.length renders the end card. */
|
|
22
|
+
index: number;
|
|
23
|
+
viewedPaths: ReadonlySet<string>;
|
|
24
|
+
/** The agent's note for the current file; the gray block renders only when set. */
|
|
25
|
+
note?: string;
|
|
26
|
+
/** Terminal width bounds the card width. */
|
|
27
|
+
terminalWidth: number;
|
|
28
|
+
/** End-card submit: leaves the walk and opens the rail confirm. */
|
|
29
|
+
onSubmitRequest: () => void;
|
|
30
|
+
onBack: () => void;
|
|
31
|
+
theme?: Theme;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The card never crowds the terminal edge nor stretches past reading width. */
|
|
35
|
+
function wizardWidth(terminalWidth: number): number {
|
|
36
|
+
return Math.min(72, Math.max(24, terminalWidth - 8));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Note rows from a crude wrap estimate, capped so the block stays a block. */
|
|
40
|
+
function noteRows(note: string, innerWidth: number): number {
|
|
41
|
+
return Math.min(3, Math.max(1, Math.ceil(note.length / Math.max(1, innerWidth))));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function WalkWizard({
|
|
45
|
+
files,
|
|
46
|
+
index,
|
|
47
|
+
viewedPaths,
|
|
48
|
+
note,
|
|
49
|
+
terminalWidth,
|
|
50
|
+
onSubmitRequest,
|
|
51
|
+
onBack,
|
|
52
|
+
theme,
|
|
53
|
+
}: WalkWizardProps): React.ReactNode {
|
|
54
|
+
const tokens = useComponentTheme(theme);
|
|
55
|
+
const width = wizardWidth(terminalWidth);
|
|
56
|
+
const viewed = viewedCount(files, viewedPaths);
|
|
57
|
+
const atEnd = index >= files.length;
|
|
58
|
+
const currentFile = files[index];
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<box
|
|
62
|
+
style={{
|
|
63
|
+
position: "absolute",
|
|
64
|
+
left: 0,
|
|
65
|
+
top: 0,
|
|
66
|
+
width: "100%",
|
|
67
|
+
height: "100%",
|
|
68
|
+
alignItems: "center",
|
|
69
|
+
justifyContent: "center",
|
|
70
|
+
flexDirection: "column",
|
|
71
|
+
}}
|
|
72
|
+
>
|
|
73
|
+
<box style={{ width, flexDirection: "column" }}>
|
|
74
|
+
{atEnd || !currentFile ? (
|
|
75
|
+
<Card
|
|
76
|
+
title=" walk complete "
|
|
77
|
+
contentRows={3}
|
|
78
|
+
borderColor={tokens.green}
|
|
79
|
+
backgroundColor={tokens.elevated}
|
|
80
|
+
theme={theme}
|
|
81
|
+
>
|
|
82
|
+
<text fg={tokens.text}>
|
|
83
|
+
{viewed === files.length
|
|
84
|
+
? `every file viewed (${viewed}/${files.length})`
|
|
85
|
+
: `${viewed} of ${files.length} files viewed`}
|
|
86
|
+
</text>
|
|
87
|
+
<box style={{ height: 1 }} />
|
|
88
|
+
<Toolbar>
|
|
89
|
+
<Button variant="solid" marginRight={2} onPress={onSubmitRequest} theme={theme}>
|
|
90
|
+
{" Submit review "}
|
|
91
|
+
</Button>
|
|
92
|
+
<Button onPress={onBack} theme={theme}>
|
|
93
|
+
{" [ back "}
|
|
94
|
+
</Button>
|
|
95
|
+
</Toolbar>
|
|
96
|
+
</Card>
|
|
97
|
+
) : (
|
|
98
|
+
<>
|
|
99
|
+
<Card
|
|
100
|
+
title={` file ${index + 1} of ${files.length} · ${viewed} viewed `}
|
|
101
|
+
contentRows={3 + Math.max(1, currentFile.preview.length)}
|
|
102
|
+
borderColor={tokens.accent}
|
|
103
|
+
backgroundColor={tokens.elevated}
|
|
104
|
+
theme={theme}
|
|
105
|
+
>
|
|
106
|
+
<text fg={tokens.text}>{currentFile.path}</text>
|
|
107
|
+
<text>
|
|
108
|
+
<span fg={tokens.green}>{`+${currentFile.added}`}</span>
|
|
109
|
+
<span fg={tokens.textDim}> </span>
|
|
110
|
+
<span fg={tokens.red}>{`-${currentFile.removed}`}</span>
|
|
111
|
+
<span fg={tokens.textDim}>
|
|
112
|
+
{viewedPaths.has(currentFile.path) ? " · viewed" : ""}
|
|
113
|
+
</span>
|
|
114
|
+
</text>
|
|
115
|
+
<box style={{ height: 1 }} />
|
|
116
|
+
{currentFile.preview.length === 0 ? (
|
|
117
|
+
<text fg={tokens.textDim}>(no changed lines)</text>
|
|
118
|
+
) : (
|
|
119
|
+
currentFile.preview.map((previewLine, previewIndex) => (
|
|
120
|
+
<text
|
|
121
|
+
key={previewIndex}
|
|
122
|
+
style={{ wrapMode: "none" }}
|
|
123
|
+
fg={
|
|
124
|
+
previewLine.sign === "+"
|
|
125
|
+
? tokens.insertedForeground
|
|
126
|
+
: tokens.deletedForeground
|
|
127
|
+
}
|
|
128
|
+
>
|
|
129
|
+
{previewLine.sign + previewLine.text}
|
|
130
|
+
</text>
|
|
131
|
+
))
|
|
132
|
+
)}
|
|
133
|
+
</Card>
|
|
134
|
+
{note !== undefined ? (
|
|
135
|
+
<box style={{ marginTop: 1, flexDirection: "column" }}>
|
|
136
|
+
<Card
|
|
137
|
+
title=" agent note "
|
|
138
|
+
contentRows={noteRows(note, width - 4)}
|
|
139
|
+
borderColor={tokens.border}
|
|
140
|
+
backgroundColor={tokens.elevated}
|
|
141
|
+
theme={theme}
|
|
142
|
+
>
|
|
143
|
+
<text fg={tokens.textMuted}>{note}</text>
|
|
144
|
+
</Card>
|
|
145
|
+
</box>
|
|
146
|
+
) : null}
|
|
147
|
+
</>
|
|
148
|
+
)}
|
|
149
|
+
</box>
|
|
150
|
+
</box>
|
|
151
|
+
);
|
|
152
|
+
}
|