@cueloop/client 0.1.0-alpha.9 → 0.1.0-alpha.90
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 +42 -34
- package/src/App.appearance.test.tsx +53 -0
- package/src/App.autoclose.test.tsx +77 -51
- package/src/App.diff.test.tsx +259 -45
- package/src/App.export.test.tsx +58 -41
- package/src/App.focus.test.ts +89 -0
- package/src/App.identity.test.tsx +78 -0
- package/src/App.plan-render.test.tsx +209 -0
- package/src/App.plan-review.test.tsx +330 -161
- package/src/App.prototype-markdown.test.tsx +128 -0
- package/src/App.prototype.test.tsx +176 -0
- package/src/App.readonly.test.tsx +129 -67
- package/src/App.regression.test.tsx +94 -0
- package/src/App.rendering.test.tsx +61 -35
- package/src/App.share-dialog.test.tsx +172 -0
- package/src/App.submit-confirm.test.tsx +264 -0
- package/src/App.test.tsx +402 -126
- package/src/App.toast.test.tsx +98 -0
- package/src/App.tsx +1402 -1060
- package/src/App.visual.test.tsx +57 -0
- package/src/App.walk.test.tsx +299 -0
- package/src/App.workbench.test.tsx +774 -0
- package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
- package/src/annotation-palette.ts +62 -0
- package/src/ansi-screen.test.ts +28 -0
- package/src/ansi-screen.ts +72 -0
- package/src/app-key-state.ts +61 -0
- package/src/app-screens.tsx +577 -0
- package/src/app-view-model.ts +168 -0
- package/src/attribution.test.ts +31 -0
- package/src/attribution.ts +42 -0
- package/src/client-extension-registry.test.tsx +61 -0
- package/src/client-extension-registry.ts +159 -0
- package/src/clipboard.test.ts +23 -0
- package/src/clipboard.ts +33 -0
- package/src/components/AnnotatableFileView.stories.tsx +42 -0
- package/src/components/AnnotatableFileView.test.tsx +102 -0
- package/src/components/AnnotatableFileView.tsx +134 -0
- package/src/components/AnnotationCards.stories.tsx +86 -0
- package/src/components/AnnotationCards.tsx +341 -0
- package/src/components/AnsiScreen.stories.tsx +12 -0
- package/src/components/AnsiScreen.tsx +28 -0
- package/src/components/AppHeader.stories.tsx +50 -0
- package/src/components/AppHeader.tsx +83 -0
- package/src/components/AppShell.brand.test.tsx +58 -0
- package/src/components/AppShell.extensions.test.tsx +191 -0
- package/src/components/AppShell.stories.tsx +89 -0
- package/src/components/AppShell.tsx +362 -0
- package/src/components/AppShell.zoom.test.tsx +135 -0
- package/src/components/BareWorkbenchFileView.stories.tsx +27 -0
- package/src/components/BareWorkbenchFileView.tsx +64 -0
- package/src/components/Breadcrumb.stories.tsx +19 -0
- package/src/components/Breadcrumb.tsx +45 -0
- package/src/components/ChangesColumn.stories.tsx +44 -0
- package/src/components/ChangesColumn.tsx +199 -0
- package/src/components/ChangesFileTree.test.tsx +61 -0
- package/src/components/CodeBlock.stories.tsx +42 -0
- package/src/components/CodeBlock.tsx +84 -0
- package/src/components/CompletionOverlay.stories.tsx +37 -0
- package/src/components/CompletionOverlay.tsx +77 -0
- package/src/components/ConfirmCard.stories.tsx +37 -0
- package/src/components/ConfirmCard.tsx +150 -0
- package/src/components/ConfirmDialog.stories.tsx +21 -0
- package/src/components/ConfirmDialog.tsx +72 -0
- package/src/components/DiffContentView.scroll.test.tsx +149 -0
- package/src/components/DiffContentView.split.test.tsx +140 -0
- package/src/components/DiffContentView.stories.tsx +80 -0
- package/src/components/DiffContentView.syntax.test.tsx +82 -0
- package/src/components/DiffContentView.tsx +906 -0
- package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
- package/src/components/DiscussionMarkerRail.tsx +186 -0
- package/src/components/EditorGrid.stories.tsx +59 -0
- package/src/components/EditorGrid.tabs.test.tsx +118 -0
- package/src/components/EditorGrid.tsx +426 -0
- package/src/components/EditorGrid.zoom.test.tsx +68 -0
- package/src/components/ExtensionZone.stories.tsx +28 -0
- package/src/components/ExtensionZone.tsx +145 -0
- package/src/components/FileContentsView.stories.tsx +16 -0
- package/src/components/FileContentsView.test.tsx +29 -0
- package/src/components/FileContentsView.tsx +98 -0
- package/src/components/GridTabContent.stories.tsx +59 -0
- package/src/components/GridTabContent.tsx +182 -0
- 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/KeybindsDialog.stories.tsx +31 -0
- package/src/components/KeybindsDialog.tsx +103 -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/MenuBar.stories.tsx +28 -0
- package/src/components/MenuBar.tsx +44 -0
- package/src/components/NavModeHint.stories.tsx +40 -0
- package/src/components/NavModeHint.tsx +31 -0
- package/src/components/OverlayScrollbar.stories.tsx +33 -0
- package/src/components/OverlayScrollbar.test.tsx +139 -0
- package/src/components/OverlayScrollbar.tsx +129 -0
- package/src/components/PaneGrid.stories.tsx +84 -0
- package/src/components/PaneGrid.tsx +128 -0
- package/src/components/PanelColumn.stories.tsx +25 -0
- package/src/components/PanelColumn.tsx +98 -0
- package/src/components/ProjectTreeView.stories.tsx +25 -0
- package/src/components/ProjectTreeView.test.tsx +75 -0
- package/src/components/ProjectTreeView.tsx +125 -0
- package/src/components/PromptDialog.stories.tsx +36 -0
- package/src/components/PromptDialog.test.tsx +61 -0
- package/src/components/PromptDialog.tsx +99 -0
- package/src/components/PrototypeContentView.stories.tsx +19 -0
- package/src/components/PrototypeContentView.tsx +446 -0
- package/src/components/RootOverlay.stories.tsx +45 -0
- package/src/components/RootOverlay.tsx +48 -0
- package/src/components/ScrollArea.stories.tsx +20 -0
- package/src/components/ScrollArea.tsx +44 -0
- package/src/components/SettingsDialog.stories.tsx +117 -0
- package/src/components/SettingsDialog.tsx +186 -0
- package/src/components/SettingsRows.stories.tsx +18 -0
- package/src/components/SettingsRows.tsx +112 -0
- package/src/components/ShareDialog.stories.tsx +121 -0
- package/src/components/ShareDialog.tsx +552 -0
- package/src/components/ShellHeader.stories.tsx +50 -0
- package/src/components/ShellHeader.tsx +137 -0
- package/src/components/SlashComposer.stories.tsx +37 -0
- package/src/components/SlashComposer.tsx +87 -0
- package/src/components/ThreadFooter.stories.tsx +30 -0
- package/src/components/ThreadFooter.test.tsx +47 -0
- package/src/components/ThreadFooter.tsx +76 -0
- package/src/components/ThreadTree.stories.tsx +46 -0
- package/src/components/ThreadTree.tsx +255 -0
- package/src/components/ThreadView.selection.test.tsx +951 -0
- package/src/components/ThreadView.slash.test.tsx +146 -0
- package/src/components/ThreadView.stories.tsx +26 -0
- package/src/components/ThreadView.tsx +525 -0
- package/src/components/Toast.stories.tsx +19 -0
- package/src/components/Toast.tsx +52 -0
- package/src/components/Tooltip.stories.tsx +26 -0
- package/src/components/Tooltip.tsx +72 -0
- package/src/components/TreePane.stories.tsx +106 -0
- package/src/components/TreePane.tsx +157 -0
- package/src/components/WalkWizard.stories.tsx +100 -0
- package/src/components/WalkWizard.tsx +150 -0
- package/src/components/WelcomePlayground.stories.tsx +12 -0
- package/src/components/WelcomePlayground.test.tsx +49 -0
- package/src/components/WelcomePlayground.tsx +139 -0
- package/src/components/WorkspacePanels.stories.tsx +36 -0
- package/src/components/WorkspacePanels.tsx +116 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +2264 -0
- package/src/components/diff-content-view-layout.test.ts +198 -0
- package/src/components/diff-content-view-layout.ts +84 -0
- package/src/components/editor-grid.test.ts +86 -0
- package/src/components/editor-grid.ts +138 -0
- package/src/components/file-tree.test.ts +60 -0
- package/src/components/file-tree.ts +112 -0
- package/src/components/menu-control.stories.tsx +35 -0
- package/src/components/menu-control.tsx +43 -0
- package/src/components/pages/Workspace.stories.tsx +181 -0
- package/src/components/primitives/Button.stories.tsx +32 -0
- package/src/components/primitives/Button.tsx +59 -0
- package/src/components/primitives/Card.stories.tsx +25 -0
- package/src/components/primitives/Card.tsx +63 -0
- package/src/components/primitives/Dialog.stories.tsx +24 -0
- package/src/components/primitives/Dialog.test.tsx +37 -0
- package/src/components/primitives/Dialog.tsx +71 -0
- package/src/components/primitives/DialogActions.stories.tsx +20 -0
- package/src/components/primitives/DialogActions.tsx +39 -0
- package/src/components/primitives/IconButton.stories.tsx +36 -0
- package/src/components/primitives/IconButton.tsx +62 -0
- package/src/components/primitives/StatusBar.stories.tsx +11 -0
- package/src/components/primitives/StatusBar.tsx +23 -0
- package/src/components/primitives/Switch.stories.tsx +17 -0
- package/src/components/primitives/Switch.tsx +32 -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/Tree.stories.tsx +124 -0
- package/src/components/primitives/Tree.tsx +100 -0
- package/src/components/primitives/frame.test.ts +12 -0
- package/src/components/primitives/frame.ts +17 -0
- package/src/components/primitives/icons.ts +61 -0
- package/src/components/primitives/tree-model.test.ts +99 -0
- package/src/components/primitives/tree-model.ts +137 -0
- package/src/components/quick-actions-editor.stories.tsx +58 -0
- package/src/components/quick-actions-editor.test.tsx +117 -0
- package/src/components/quick-actions-editor.tsx +226 -0
- package/src/components/share-dialog-store.ts +58 -0
- package/src/components/stories-app.tsx +193 -0
- package/src/components/stories.test.tsx +74 -0
- package/src/components/story-fixtures.ts +102 -0
- package/src/components/story-tree.test.ts +108 -0
- package/src/components/story.ts +159 -0
- package/src/components/syntax-highlight.ts +133 -0
- package/src/components/terminal-pane.test.tsx +40 -0
- package/src/components/terminal-pane.ts +199 -0
- package/src/components/theme-context.stories.tsx +46 -0
- package/src/components/theme-context.tsx +37 -0
- package/src/components/thread-tree.test.ts +134 -0
- package/src/components/thread-tree.ts +109 -0
- package/src/components/truncate-text.ts +7 -0
- package/src/components/truncate-title.test.ts +31 -0
- package/src/components/truncate-title.ts +15 -0
- package/src/config-identity.test.ts +78 -0
- package/src/config.test.ts +579 -34
- package/src/config.ts +533 -57
- package/src/diff-fold.test.ts +78 -0
- package/src/diff-fold.ts +126 -0
- package/src/diff-hunk-curate.test.ts +315 -0
- package/src/diff-hunk-curate.ts +153 -0
- package/src/diff-intraline.test.ts +238 -0
- package/src/diff-intraline.ts +236 -0
- package/src/diff-syntax.test.ts +112 -0
- package/src/diff-syntax.ts +179 -0
- package/src/discussions.ts +79 -0
- package/src/editor.test.ts +149 -0
- package/src/editor.ts +139 -9
- package/src/ghostty-terminal.test.ts +85 -0
- package/src/ghostty-terminal.ts +199 -0
- package/src/github-identity.test.ts +58 -0
- package/src/github-identity.ts +41 -0
- package/src/herdr.test.ts +9 -34
- package/src/herdr.ts +25 -21
- package/src/index.ts +28 -0
- package/src/integrations.test.ts +41 -0
- package/src/integrations.ts +32 -0
- package/src/intent-dispatch.test.ts +724 -0
- package/src/intent-dispatch.ts +690 -0
- package/src/key-bindings.test.ts +156 -0
- package/src/key-bindings.ts +459 -0
- package/src/keymap.test.ts +456 -115
- package/src/keymap.ts +315 -81
- package/src/kitty-image.test.ts +66 -0
- package/src/kitty-image.ts +138 -0
- package/src/launch-layout.test.ts +67 -0
- package/src/launch-layout.ts +47 -0
- package/src/mark-runs.ts +121 -0
- 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/perf/otlp-trace.test.ts +116 -0
- package/src/perf/otlp-trace.ts +130 -0
- package/src/perf/perf-report.ts +17 -0
- package/src/perf/perf-timings.test.ts +18 -0
- package/src/perf/perf-timings.ts +38 -0
- package/src/prototype-browser.test.ts +81 -0
- package/src/prototype-browser.ts +329 -0
- package/src/prototype-pixels.tsx +35 -0
- package/src/pty.test.ts +81 -0
- package/src/pty.ts +245 -0
- package/src/ready-signal.test.ts +85 -0
- package/src/ready-signal.ts +52 -0
- package/src/run.ts +85 -12
- package/src/serve.test.ts +24 -12
- package/src/serve.ts +52 -10
- package/src/share.ts +237 -0
- package/src/skills.test.ts +74 -0
- package/src/skills.ts +75 -0
- package/src/slash-palette.test.ts +103 -0
- package/src/slash-palette.ts +129 -0
- package/src/split-diff.test.ts +110 -0
- package/src/split-diff.ts +149 -0
- package/src/test-support.test.ts +264 -0
- package/src/test-support.ts +369 -0
- package/src/theme-presets.test.ts +51 -0
- package/src/theme-presets.ts +219 -0
- package/src/theme.ts +88 -19
- package/src/thread-chords.test.ts +79 -0
- package/src/thread-chords.ts +163 -0
- package/src/thread-code-syntax.ts +45 -0
- package/src/thread-controller.curate.test.ts +476 -0
- package/src/thread-controller.derivation.test.ts +62 -0
- package/src/thread-controller.live-diff.test.ts +322 -0
- package/src/thread-controller.optimistic.test.ts +69 -0
- package/src/thread-controller.share.test.ts +561 -0
- package/src/thread-controller.switch-cache.test.ts +176 -0
- package/src/thread-controller.tree.test.ts +261 -0
- package/src/thread-controller.ts +2050 -0
- package/src/thread-selection.test.ts +210 -0
- package/src/thread-selection.ts +243 -0
- package/src/tree-view.test.ts +128 -0
- package/src/tree-view.ts +153 -0
- package/src/use-annotation-surface.tsx +975 -0
- package/src/use-changes-workbench.ts +190 -0
- package/src/use-frame-measure.ts +41 -0
- package/src/use-remember-layout.ts +31 -0
- package/src/use-repo-changes.ts +87 -0
- package/src/use-settings-dialog.tsx +364 -0
- package/src/use-terminal-virtualizer.ts +259 -0
- package/src/use-thread-body-editing.tsx +58 -0
- package/src/version.ts +5 -0
- package/src/view-diff.test.ts +279 -9
- package/src/view-diff.ts +460 -24
- package/src/view-plan.regression.test.ts +60 -0
- package/src/view-plan.test.ts +545 -0
- package/src/view-plan.ts +1019 -0
- package/src/visual/contrast.test.ts +35 -0
- package/src/visual/contrast.ts +33 -0
- package/src/walk.test.ts +121 -0
- package/src/walk.ts +74 -0
- package/src/workbench-snapshot.test.ts +99 -0
- package/src/workbench-snapshot.ts +59 -0
- package/src/session-controller.ts +0 -398
- 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 -364
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { allFolderIds, flattenTree, statusMeta, type TreeNode } from "./tree-model";
|
|
3
|
+
|
|
4
|
+
const TREE: TreeNode[] = [
|
|
5
|
+
{
|
|
6
|
+
id: "src",
|
|
7
|
+
label: "src",
|
|
8
|
+
children: [
|
|
9
|
+
{ id: "src/a.ts", label: "a.ts", status: "modified" },
|
|
10
|
+
{
|
|
11
|
+
id: "src/nested",
|
|
12
|
+
label: "nested",
|
|
13
|
+
children: [
|
|
14
|
+
{
|
|
15
|
+
id: "src/nested/only",
|
|
16
|
+
label: "only",
|
|
17
|
+
children: [{ id: "deep", label: "deep.ts", status: "added" }],
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
{ id: "readme", label: "README.md" },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
describe("flattenTree", () => {
|
|
27
|
+
test("a collapsed folder shows only itself; expanding it reveals its children", () => {
|
|
28
|
+
// Act
|
|
29
|
+
const collapsed = flattenTree(TREE, { expandedIds: new Set() });
|
|
30
|
+
const rows = flattenTree(TREE, { expandedIds: new Set(["src"]) });
|
|
31
|
+
|
|
32
|
+
// Assert
|
|
33
|
+
expect(collapsed.map((row) => row.label)).toEqual(["src", "README.md"]);
|
|
34
|
+
expect(rows.map((row) => [row.label, row.depth, row.isFolder])).toEqual([
|
|
35
|
+
["src", 0, true],
|
|
36
|
+
["a.ts", 1, false],
|
|
37
|
+
["nested", 1, true],
|
|
38
|
+
["README.md", 0, false],
|
|
39
|
+
]);
|
|
40
|
+
expect(rows.find((row) => row.label === "src")!.expanded).toBe(true);
|
|
41
|
+
expect(rows.find((row) => row.label === "a.ts")!.status).toBe("modified");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("flattenEmptyDirectories collapses a single-subfolder chain into one row", () => {
|
|
45
|
+
// Act - src open but the chain itself collapsed (none of its ids expanded)
|
|
46
|
+
const collapsed = flattenTree(TREE, {
|
|
47
|
+
expandedIds: new Set(["src"]),
|
|
48
|
+
flattenEmptyDirectories: true,
|
|
49
|
+
});
|
|
50
|
+
// Act - expanding via any id along the chain opens it
|
|
51
|
+
const expanded = flattenTree(TREE, {
|
|
52
|
+
expandedIds: new Set(["src", "src/nested/only"]),
|
|
53
|
+
flattenEmptyDirectories: true,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Assert
|
|
57
|
+
expect(collapsed.map((row) => row.label)).toEqual(["src", "a.ts", "nested/only", "README.md"]);
|
|
58
|
+
expect(expanded.map((row) => row.label)).toEqual([
|
|
59
|
+
"src",
|
|
60
|
+
"a.ts",
|
|
61
|
+
"nested/only",
|
|
62
|
+
"deep.ts",
|
|
63
|
+
"README.md",
|
|
64
|
+
]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("without the flag the chain stays one folder per row", () => {
|
|
68
|
+
// Act
|
|
69
|
+
const rows = flattenTree(TREE, {
|
|
70
|
+
expandedIds: new Set(["src", "src/nested", "src/nested/only"]),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Assert
|
|
74
|
+
expect(rows.map((row) => [row.label, row.depth])).toEqual([
|
|
75
|
+
["src", 0],
|
|
76
|
+
["a.ts", 1],
|
|
77
|
+
["nested", 1],
|
|
78
|
+
["only", 2],
|
|
79
|
+
["deep.ts", 3],
|
|
80
|
+
["README.md", 0],
|
|
81
|
+
]);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("allFolderIds", () => {
|
|
86
|
+
test("collects every folder, not the leaves", () => {
|
|
87
|
+
// Assert
|
|
88
|
+
expect([...allFolderIds(TREE)].sort()).toEqual(["src", "src/nested", "src/nested/only"]);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("statusMeta", () => {
|
|
93
|
+
test("maps each status to a letter and a tone", () => {
|
|
94
|
+
// Assert
|
|
95
|
+
expect(statusMeta("added")).toEqual({ letter: "A", tone: "green" });
|
|
96
|
+
expect(statusMeta("deleted")).toEqual({ letter: "D", tone: "red" });
|
|
97
|
+
expect(statusMeta("ignored")).toEqual({ letter: "I", tone: "muted" });
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Headless tree model mirrored from @pierre/trees (DOM-only): share the model, render our own.
|
|
2
|
+
|
|
3
|
+
export type GitStatus = "added" | "modified" | "deleted" | "renamed" | "untracked" | "ignored";
|
|
4
|
+
|
|
5
|
+
export interface TreeNode {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
children?: TreeNode[];
|
|
9
|
+
status?: GitStatus;
|
|
10
|
+
badge?: string;
|
|
11
|
+
icon?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface FlattenOptions {
|
|
15
|
+
expandedIds: ReadonlySet<string>;
|
|
16
|
+
flattenEmptyDirectories?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface VisibleRow {
|
|
20
|
+
id: string;
|
|
21
|
+
label: string;
|
|
22
|
+
depth: number;
|
|
23
|
+
isFolder: boolean;
|
|
24
|
+
expanded: boolean;
|
|
25
|
+
status?: GitStatus;
|
|
26
|
+
badge?: string;
|
|
27
|
+
icon?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isFolder(node: TreeNode): boolean {
|
|
31
|
+
return node.children !== undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface CollapsedChain {
|
|
35
|
+
label: string;
|
|
36
|
+
tail: TreeNode;
|
|
37
|
+
chainIds: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// flattenEmptyDirectories: fold a chain of single-subfolder folders into one row.
|
|
41
|
+
function collapseChain(node: TreeNode): CollapsedChain {
|
|
42
|
+
let tail = node;
|
|
43
|
+
const labels = [node.label];
|
|
44
|
+
const chainIds = [node.id];
|
|
45
|
+
|
|
46
|
+
while (tail.children?.length === 1 && isFolder(tail.children[0]!)) {
|
|
47
|
+
tail = tail.children[0]!;
|
|
48
|
+
labels.push(tail.label);
|
|
49
|
+
chainIds.push(tail.id);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return { label: labels.join("/"), tail, chainIds };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function flattenTree(nodes: readonly TreeNode[], options: FlattenOptions): VisibleRow[] {
|
|
56
|
+
const rows: VisibleRow[] = [];
|
|
57
|
+
const walk = (list: readonly TreeNode[], depth: number): void => {
|
|
58
|
+
for (const node of list) {
|
|
59
|
+
if (!isFolder(node)) {
|
|
60
|
+
const leaf: VisibleRow = {
|
|
61
|
+
id: node.id,
|
|
62
|
+
label: node.label,
|
|
63
|
+
depth,
|
|
64
|
+
isFolder: false,
|
|
65
|
+
expanded: false,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
if (node.status !== undefined) leaf.status = node.status;
|
|
69
|
+
if (node.badge !== undefined) leaf.badge = node.badge;
|
|
70
|
+
if (node.icon !== undefined) leaf.icon = node.icon;
|
|
71
|
+
rows.push(leaf);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const collapsed = options.flattenEmptyDirectories
|
|
75
|
+
? collapseChain(node)
|
|
76
|
+
: { label: node.label, tail: node, chainIds: [node.id] };
|
|
77
|
+
// open when any id along a collapsed chain is expanded
|
|
78
|
+
const expanded = collapsed.chainIds.some((id) => options.expandedIds.has(id));
|
|
79
|
+
const row: VisibleRow = {
|
|
80
|
+
id: node.id,
|
|
81
|
+
label: collapsed.label,
|
|
82
|
+
depth,
|
|
83
|
+
isFolder: true,
|
|
84
|
+
expanded,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
if (node.badge !== undefined) row.badge = node.badge;
|
|
88
|
+
if (node.icon !== undefined) row.icon = node.icon;
|
|
89
|
+
rows.push(row);
|
|
90
|
+
if (expanded) walk(collapsed.tail.children ?? [], depth + 1);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
walk(nodes, 0);
|
|
95
|
+
|
|
96
|
+
return rows;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function allFolderIds(nodes: readonly TreeNode[]): Set<string> {
|
|
100
|
+
const ids = new Set<string>();
|
|
101
|
+
const walk = (list: readonly TreeNode[]): void => {
|
|
102
|
+
for (const node of list) {
|
|
103
|
+
if (isFolder(node)) {
|
|
104
|
+
ids.add(node.id);
|
|
105
|
+
walk(node.children ?? []);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
walk(nodes);
|
|
111
|
+
|
|
112
|
+
return ids;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type TreeTone = "green" | "blue" | "red" | "muted";
|
|
116
|
+
|
|
117
|
+
export interface StatusMeta {
|
|
118
|
+
letter: string;
|
|
119
|
+
tone: TreeTone;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function statusMeta(status: GitStatus): StatusMeta {
|
|
123
|
+
switch (status) {
|
|
124
|
+
case "added":
|
|
125
|
+
return { letter: "A", tone: "green" };
|
|
126
|
+
case "modified":
|
|
127
|
+
return { letter: "M", tone: "blue" };
|
|
128
|
+
case "deleted":
|
|
129
|
+
return { letter: "D", tone: "red" };
|
|
130
|
+
case "renamed":
|
|
131
|
+
return { letter: "R", tone: "blue" };
|
|
132
|
+
case "untracked":
|
|
133
|
+
return { letter: "U", tone: "green" };
|
|
134
|
+
case "ignored":
|
|
135
|
+
return { letter: "I", tone: "muted" };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
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: "Chrome/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
|
+
expandedField="prompt"
|
|
28
|
+
onToggleExpand={noop}
|
|
29
|
+
onEditPrompt={() => {}}
|
|
30
|
+
onEditMetadata={noop}
|
|
31
|
+
onReset={noop}
|
|
32
|
+
onAdd={noop}
|
|
33
|
+
theme={DARK}
|
|
34
|
+
/>
|
|
35
|
+
</BodyFrame>
|
|
36
|
+
),
|
|
37
|
+
size: { width: 54, height: 22 },
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const RowExpanded: Story = {
|
|
41
|
+
render: () => (
|
|
42
|
+
<BodyFrame>
|
|
43
|
+
<QuickActionsEditor
|
|
44
|
+
actions={DEFAULT_QUICK_ACTIONS}
|
|
45
|
+
selectedIndex={1}
|
|
46
|
+
expandedIndex={1}
|
|
47
|
+
expandedField="prompt"
|
|
48
|
+
onToggleExpand={noop}
|
|
49
|
+
onEditPrompt={() => {}}
|
|
50
|
+
onEditMetadata={noop}
|
|
51
|
+
onReset={noop}
|
|
52
|
+
onAdd={noop}
|
|
53
|
+
theme={DARK}
|
|
54
|
+
/>
|
|
55
|
+
</BodyFrame>
|
|
56
|
+
),
|
|
57
|
+
size: { width: 54, height: 22 },
|
|
58
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/** The Actions editor scrolls the list, and an expanded row edits both the title and the description. */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, test } from "bun:test";
|
|
4
|
+
import React from "react";
|
|
5
|
+
import { testRender } from "@opentui/react/test-utils";
|
|
6
|
+
import type { QuickAction } from "../config";
|
|
7
|
+
import { locateText, settle, typeText } from "../test-support";
|
|
8
|
+
import { QuickActionsEditor } from "./quick-actions-editor";
|
|
9
|
+
|
|
10
|
+
const MANY: QuickAction[] = Array.from({ length: 14 }, (_, index) => ({
|
|
11
|
+
prompt: `Action number ${index}`,
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
async function mount(selectedIndex: number) {
|
|
15
|
+
const setup = await testRender(
|
|
16
|
+
<QuickActionsEditor
|
|
17
|
+
actions={MANY}
|
|
18
|
+
selectedIndex={selectedIndex}
|
|
19
|
+
expandedIndex={null}
|
|
20
|
+
expandedField="prompt"
|
|
21
|
+
onToggleExpand={() => {}}
|
|
22
|
+
onEditPrompt={() => {}}
|
|
23
|
+
onEditMetadata={() => {}}
|
|
24
|
+
onReset={() => {}}
|
|
25
|
+
onAdd={() => {}}
|
|
26
|
+
/>,
|
|
27
|
+
{ width: 60, height: 10 },
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
await settle(setup);
|
|
31
|
+
await settle(setup);
|
|
32
|
+
await settle(setup);
|
|
33
|
+
await settle(setup);
|
|
34
|
+
|
|
35
|
+
return setup;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("QuickActionsEditor scrolling", () => {
|
|
39
|
+
test("a list too long for the pane clips into the scroll area instead of overlapping rows", async () => {
|
|
40
|
+
const setup = await mount(0);
|
|
41
|
+
const lines = setup.captureCharFrame().split("\n");
|
|
42
|
+
|
|
43
|
+
// the top rows read cleanly on their own lines - the overflow no longer paints rows on top of each other
|
|
44
|
+
expect(lines.some((line) => line.includes("Action number 0"))).toBe(true);
|
|
45
|
+
expect(lines.some((line) => line.includes("Action number 1"))).toBe(true);
|
|
46
|
+
// a bottom row is scrolled out of the clipped pane, not overlaid onto a visible one
|
|
47
|
+
expect(setup.captureCharFrame()).not.toContain("Action number 13");
|
|
48
|
+
|
|
49
|
+
setup.renderer.destroy();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("editing an expanded action", () => {
|
|
54
|
+
test("an expanded row edits both the title and the description", async () => {
|
|
55
|
+
const prompts: string[] = [];
|
|
56
|
+
const metadatas: string[] = [];
|
|
57
|
+
const setup = await testRender(
|
|
58
|
+
<QuickActionsEditor
|
|
59
|
+
actions={[{ prompt: "Ship it", metadata: "be terse" }]}
|
|
60
|
+
selectedIndex={0}
|
|
61
|
+
expandedIndex={0}
|
|
62
|
+
expandedField="prompt"
|
|
63
|
+
onToggleExpand={() => {}}
|
|
64
|
+
onEditPrompt={(_index, prompt) => prompts.push(prompt)}
|
|
65
|
+
onEditMetadata={(_index, metadata) => metadatas.push(metadata)}
|
|
66
|
+
onReset={() => {}}
|
|
67
|
+
onAdd={() => {}}
|
|
68
|
+
/>,
|
|
69
|
+
{ width: 60, height: 8 },
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
await settle(setup);
|
|
73
|
+
await settle(setup);
|
|
74
|
+
|
|
75
|
+
await typeText(setup, "!");
|
|
76
|
+
expect(prompts.at(-1)).toBe("Ship it!");
|
|
77
|
+
|
|
78
|
+
setup.mockInput.pressKey("RETURN");
|
|
79
|
+
await settle(setup);
|
|
80
|
+
await typeText(setup, "X");
|
|
81
|
+
expect(metadatas.at(-1)).toBe("be terseX");
|
|
82
|
+
|
|
83
|
+
setup.renderer.destroy();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("clicking the description row opens the editor on the metadata field directly", async () => {
|
|
87
|
+
const toggles: Array<{ index: number; field: "prompt" | "metadata" }> = [];
|
|
88
|
+
const setup = await testRender(
|
|
89
|
+
<QuickActionsEditor
|
|
90
|
+
actions={[{ prompt: "Ship it", metadata: "be terse" }]}
|
|
91
|
+
selectedIndex={0}
|
|
92
|
+
expandedIndex={null}
|
|
93
|
+
expandedField="prompt"
|
|
94
|
+
onToggleExpand={(index, field) => toggles.push({ index, field })}
|
|
95
|
+
onEditPrompt={() => {}}
|
|
96
|
+
onEditMetadata={() => {}}
|
|
97
|
+
onReset={() => {}}
|
|
98
|
+
onAdd={() => {}}
|
|
99
|
+
/>,
|
|
100
|
+
{ width: 60, height: 8 },
|
|
101
|
+
);
|
|
102
|
+
await settle(setup);
|
|
103
|
+
|
|
104
|
+
// clicking the title opens the title field; clicking the description opens the metadata field
|
|
105
|
+
const title = locateText(setup, "Ship it");
|
|
106
|
+
await setup.mockMouse.click(title.column, title.row);
|
|
107
|
+
const description = locateText(setup, "be terse");
|
|
108
|
+
await setup.mockMouse.click(description.column, description.row);
|
|
109
|
+
|
|
110
|
+
expect(toggles).toEqual([
|
|
111
|
+
{ index: 0, field: "prompt" },
|
|
112
|
+
{ index: 0, field: "metadata" },
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
setup.renderer.destroy();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,226 @@
|
|
|
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, { useEffect, useRef, useState } from "react";
|
|
10
|
+
import type { KeyBinding, ScrollBoxRenderable, TextareaRenderable } from "@opentui/core";
|
|
11
|
+
import type { QuickAction } from "../config";
|
|
12
|
+
import type { Theme } from "../theme";
|
|
13
|
+
import { ScrollArea } from "./ScrollArea";
|
|
14
|
+
import { useComponentTheme } from "./theme-context";
|
|
15
|
+
|
|
16
|
+
// ⏎ never inserts a newline: a system prompt is one line, esc closes the editor.
|
|
17
|
+
const ACTION_INPUT_KEY_BINDINGS: KeyBinding[] = [{ name: "return", action: "submit" }];
|
|
18
|
+
|
|
19
|
+
export interface QuickActionsEditorProps {
|
|
20
|
+
actions: QuickAction[];
|
|
21
|
+
/** The highlighted row; equals the settings body row index. */
|
|
22
|
+
selectedIndex: number;
|
|
23
|
+
/** The row whose system-prompt input is open and focused, or null. */
|
|
24
|
+
expandedIndex: number | null;
|
|
25
|
+
/** Which field the open editor focuses first - set by the clicked row. */
|
|
26
|
+
expandedField: "prompt" | "metadata";
|
|
27
|
+
/** Select and toggle a row's editor open/closed, focusing the given field. */
|
|
28
|
+
onToggleExpand: (index: number, field: "prompt" | "metadata") => void;
|
|
29
|
+
onEditPrompt: (index: number, prompt: string) => void;
|
|
30
|
+
onEditMetadata: (index: number, metadata: string) => void;
|
|
31
|
+
onReset: () => void;
|
|
32
|
+
onAdd: () => void;
|
|
33
|
+
theme?: Theme;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function QuickActionsEditor({
|
|
37
|
+
actions,
|
|
38
|
+
selectedIndex,
|
|
39
|
+
expandedIndex,
|
|
40
|
+
expandedField,
|
|
41
|
+
onToggleExpand,
|
|
42
|
+
onEditPrompt,
|
|
43
|
+
onEditMetadata,
|
|
44
|
+
onReset,
|
|
45
|
+
onAdd,
|
|
46
|
+
theme,
|
|
47
|
+
}: QuickActionsEditorProps): React.ReactNode {
|
|
48
|
+
const tokens = useComponentTheme(theme);
|
|
49
|
+
const scrollRef = useRef<ScrollBoxRenderable | null>(null);
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
scrollRef.current?.scrollChildIntoView(`action-row-${selectedIndex}`);
|
|
53
|
+
}, [selectedIndex]);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<box style={{ flexDirection: "column", flexGrow: 1 }}>
|
|
57
|
+
<box style={{ flexDirection: "row" }}>
|
|
58
|
+
<box style={{ flexGrow: 1 }} />
|
|
59
|
+
<box onMouseUp={onReset}>
|
|
60
|
+
<text fg={tokens.textDim}>reset</text>
|
|
61
|
+
</box>
|
|
62
|
+
</box>
|
|
63
|
+
<box style={{ height: 1 }} />
|
|
64
|
+
<ScrollArea scrollRef={scrollRef}>
|
|
65
|
+
{actions.map((action, index) => (
|
|
66
|
+
<ActionRow
|
|
67
|
+
key={index}
|
|
68
|
+
rowId={`action-row-${index}`}
|
|
69
|
+
action={action}
|
|
70
|
+
isSelected={index === selectedIndex}
|
|
71
|
+
isExpanded={index === expandedIndex}
|
|
72
|
+
initialField={expandedField}
|
|
73
|
+
onToggleExpand={(field) => onToggleExpand(index, field)}
|
|
74
|
+
onEditPrompt={(prompt) => onEditPrompt(index, prompt)}
|
|
75
|
+
onEditMetadata={(metadata) => onEditMetadata(index, metadata)}
|
|
76
|
+
theme={theme}
|
|
77
|
+
/>
|
|
78
|
+
))}
|
|
79
|
+
<box
|
|
80
|
+
id={`action-row-${actions.length}`}
|
|
81
|
+
style={{ backgroundColor: selectedIndex === actions.length ? tokens.border : undefined }}
|
|
82
|
+
onMouseUp={onAdd}
|
|
83
|
+
>
|
|
84
|
+
<text fg={tokens.textMuted}>+ Add action</text>
|
|
85
|
+
</box>
|
|
86
|
+
</ScrollArea>
|
|
87
|
+
</box>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function ActionRow({
|
|
92
|
+
rowId,
|
|
93
|
+
action,
|
|
94
|
+
isSelected,
|
|
95
|
+
isExpanded,
|
|
96
|
+
initialField,
|
|
97
|
+
onToggleExpand,
|
|
98
|
+
onEditPrompt,
|
|
99
|
+
onEditMetadata,
|
|
100
|
+
theme,
|
|
101
|
+
}: {
|
|
102
|
+
rowId: string;
|
|
103
|
+
action: QuickAction;
|
|
104
|
+
isSelected: boolean;
|
|
105
|
+
isExpanded: boolean;
|
|
106
|
+
initialField: "prompt" | "metadata";
|
|
107
|
+
onToggleExpand: (field: "prompt" | "metadata") => void;
|
|
108
|
+
onEditPrompt: (prompt: string) => void;
|
|
109
|
+
onEditMetadata: (metadata: string) => void;
|
|
110
|
+
theme?: Theme;
|
|
111
|
+
}): React.ReactNode {
|
|
112
|
+
const tokens = useComponentTheme(theme);
|
|
113
|
+
|
|
114
|
+
if (isExpanded) {
|
|
115
|
+
return (
|
|
116
|
+
<box id={rowId} style={{ flexDirection: "column" }}>
|
|
117
|
+
<ActionEditor
|
|
118
|
+
action={action}
|
|
119
|
+
isSelected={isSelected}
|
|
120
|
+
initialField={initialField}
|
|
121
|
+
onEditPrompt={onEditPrompt}
|
|
122
|
+
onEditMetadata={onEditMetadata}
|
|
123
|
+
onDone={() => onToggleExpand("prompt")}
|
|
124
|
+
tokens={tokens}
|
|
125
|
+
/>
|
|
126
|
+
</box>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<box id={rowId} style={{ flexDirection: "column" }}>
|
|
132
|
+
<box
|
|
133
|
+
style={{ backgroundColor: isSelected ? tokens.border : undefined }}
|
|
134
|
+
onMouseUp={() => onToggleExpand("prompt")}
|
|
135
|
+
>
|
|
136
|
+
<text fg={isSelected ? tokens.text : tokens.textMuted}>{action.prompt}</text>
|
|
137
|
+
</box>
|
|
138
|
+
{action.metadata ? (
|
|
139
|
+
<box onMouseUp={() => onToggleExpand("metadata")}>
|
|
140
|
+
<text fg={tokens.textDim}>{truncateMetadata(action.metadata)}</text>
|
|
141
|
+
</box>
|
|
142
|
+
) : null}
|
|
143
|
+
</box>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The two-field editor for an expanded row: the action title over its system
|
|
149
|
+
* prompt. Mounts fresh per expand, so focus starts on the clicked field without a
|
|
150
|
+
* reset effect; ⏎ steps title -> description -> close, and a click focuses either.
|
|
151
|
+
*/
|
|
152
|
+
function ActionEditor({
|
|
153
|
+
action,
|
|
154
|
+
isSelected,
|
|
155
|
+
initialField,
|
|
156
|
+
onEditPrompt,
|
|
157
|
+
onEditMetadata,
|
|
158
|
+
onDone,
|
|
159
|
+
tokens,
|
|
160
|
+
}: {
|
|
161
|
+
action: QuickAction;
|
|
162
|
+
isSelected: boolean;
|
|
163
|
+
initialField: "prompt" | "metadata";
|
|
164
|
+
onEditPrompt: (prompt: string) => void;
|
|
165
|
+
onEditMetadata: (metadata: string) => void;
|
|
166
|
+
onDone: () => void;
|
|
167
|
+
tokens: Theme;
|
|
168
|
+
}): React.ReactNode {
|
|
169
|
+
const promptRef = useRef<TextareaRenderable | null>(null);
|
|
170
|
+
const metadataRef = useRef<TextareaRenderable | null>(null);
|
|
171
|
+
const [activeField, setActiveField] = useState<"prompt" | "metadata">(initialField);
|
|
172
|
+
|
|
173
|
+
// the focused field types from its end, not from the caret parked at the start
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
const editor = activeField === "prompt" ? promptRef.current : metadataRef.current;
|
|
176
|
+
|
|
177
|
+
if (editor) editor.cursorOffset = editor.plainText.length;
|
|
178
|
+
}, [activeField]);
|
|
179
|
+
|
|
180
|
+
const fieldStyle = {
|
|
181
|
+
height: 1,
|
|
182
|
+
flexGrow: 1,
|
|
183
|
+
backgroundColor: tokens.elevated,
|
|
184
|
+
focusedBackgroundColor: tokens.elevated,
|
|
185
|
+
textColor: tokens.text,
|
|
186
|
+
focusedTextColor: tokens.text,
|
|
187
|
+
} as const;
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<>
|
|
191
|
+
<box
|
|
192
|
+
style={{ flexDirection: "row", backgroundColor: isSelected ? tokens.border : undefined }}
|
|
193
|
+
>
|
|
194
|
+
<textarea
|
|
195
|
+
ref={promptRef}
|
|
196
|
+
focused={activeField === "prompt"}
|
|
197
|
+
initialValue={action.prompt}
|
|
198
|
+
placeholder="action title"
|
|
199
|
+
keyBindings={ACTION_INPUT_KEY_BINDINGS}
|
|
200
|
+
onMouseUp={() => setActiveField("prompt")}
|
|
201
|
+
onSubmit={() => setActiveField("metadata")}
|
|
202
|
+
onContentChange={() => onEditPrompt(promptRef.current?.plainText ?? "")}
|
|
203
|
+
style={fieldStyle}
|
|
204
|
+
/>
|
|
205
|
+
</box>
|
|
206
|
+
<box style={{ flexDirection: "row" }}>
|
|
207
|
+
<textarea
|
|
208
|
+
ref={metadataRef}
|
|
209
|
+
focused={activeField === "metadata"}
|
|
210
|
+
initialValue={action.metadata ?? ""}
|
|
211
|
+
placeholder="extra system prompt appended to this comment (optional)"
|
|
212
|
+
keyBindings={ACTION_INPUT_KEY_BINDINGS}
|
|
213
|
+
onMouseUp={() => setActiveField("metadata")}
|
|
214
|
+
onSubmit={onDone}
|
|
215
|
+
onContentChange={() => onEditMetadata(metadataRef.current?.plainText ?? "")}
|
|
216
|
+
style={fieldStyle}
|
|
217
|
+
/>
|
|
218
|
+
</box>
|
|
219
|
+
</>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** A one-line preview of the system prompt under a collapsed row. */
|
|
224
|
+
function truncateMetadata(metadata: string): string {
|
|
225
|
+
return metadata.length > 44 ? `${metadata.slice(0, 43)}…` : metadata;
|
|
226
|
+
}
|
|
@@ -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
|
+
}
|