@cueloop/client 0.1.0-alpha.7 → 0.1.0-alpha.71
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 +39 -32
- package/src/App.appearance.test.tsx +53 -0
- package/src/App.autoclose.test.tsx +76 -50
- package/src/App.diff.test.tsx +156 -42
- package/src/App.export.test.tsx +58 -40
- package/src/App.plan-review.test.tsx +363 -0
- package/src/App.prototype-markdown.test.tsx +92 -0
- package/src/App.prototype.test.tsx +176 -0
- package/src/App.readonly.test.tsx +128 -67
- package/src/App.rendering.test.tsx +61 -35
- package/src/App.submit-confirm.test.tsx +191 -0
- package/src/App.test.tsx +310 -114
- package/src/App.toast.test.tsx +89 -0
- package/src/App.tsx +1043 -798
- package/src/App.walk.test.tsx +298 -0
- package/src/App.workbench.test.tsx +495 -0
- package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
- package/src/annotation-palette.ts +61 -0
- package/src/app-key-state.ts +61 -0
- package/src/app-screens.tsx +432 -0
- package/src/app-view-model.ts +159 -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/AnnotatableFileView.stories.tsx +42 -0
- package/src/components/AnnotatableFileView.test.tsx +102 -0
- package/src/components/AnnotatableFileView.tsx +126 -0
- package/src/components/AnnotationCards.stories.tsx +87 -0
- package/src/components/AnnotationCards.tsx +303 -0
- package/src/components/AppHeader.stories.tsx +50 -0
- package/src/components/AppHeader.tsx +83 -0
- package/src/components/AppShell.stories.tsx +89 -0
- package/src/components/AppShell.tsx +290 -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 +39 -0
- package/src/components/ChangesColumn.tsx +171 -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 +128 -0
- package/src/components/ConfirmDialog.stories.tsx +21 -0
- package/src/components/ConfirmDialog.tsx +66 -0
- package/src/components/DiffContentView.scroll.test.tsx +121 -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 +859 -0
- package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
- package/src/components/DiscussionMarkerRail.tsx +181 -0
- package/src/components/EditorGrid.stories.tsx +59 -0
- package/src/components/EditorGrid.tabs.test.tsx +118 -0
- package/src/components/EditorGrid.tsx +368 -0
- package/src/components/EditorGrid.zoom.test.tsx +68 -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/KeybindsDialog.stories.tsx +31 -0
- package/src/components/KeybindsDialog.tsx +103 -0
- package/src/components/MenuBar.stories.tsx +28 -0
- package/src/components/MenuBar.tsx +44 -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 +95 -0
- package/src/components/ProjectTreeView.stories.tsx +25 -0
- package/src/components/ProjectTreeView.test.tsx +32 -0
- package/src/components/ProjectTreeView.tsx +83 -0
- package/src/components/PromptDialog.stories.tsx +36 -0
- package/src/components/PromptDialog.tsx +87 -0
- package/src/components/PrototypeContentView.stories.tsx +19 -0
- package/src/components/PrototypeContentView.tsx +446 -0
- package/src/components/RootOverlay.stories.tsx +44 -0
- package/src/components/RootOverlay.tsx +35 -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 +195 -0
- package/src/components/SettingsRows.stories.tsx +18 -0
- package/src/components/SettingsRows.tsx +112 -0
- package/src/components/ShellHeader.stories.tsx +50 -0
- package/src/components/ShellHeader.tsx +137 -0
- package/src/components/ThreadFooter.stories.tsx +30 -0
- package/src/components/ThreadFooter.test.tsx +31 -0
- package/src/components/ThreadFooter.tsx +54 -0
- package/src/components/ThreadTree.stories.tsx +46 -0
- package/src/components/ThreadTree.tsx +219 -0
- package/src/components/ThreadView.selection.test.tsx +812 -0
- package/src/components/ThreadView.slash.test.tsx +158 -0
- package/src/components/ThreadView.stories.tsx +26 -0
- package/src/components/ThreadView.tsx +330 -0
- package/src/components/ThreadsSidebar.stories.tsx +36 -0
- package/src/components/ThreadsSidebar.tsx +71 -0
- package/src/components/Toast.stories.tsx +12 -0
- package/src/components/Toast.tsx +45 -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 +152 -0
- package/src/components/WelcomePlayground.stories.tsx +12 -0
- package/src/components/WelcomePlayground.test.tsx +47 -0
- package/src/components/WelcomePlayground.tsx +140 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +1845 -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-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/fade-title.test.ts +49 -0
- package/src/components/fade-title.ts +61 -0
- package/src/components/file-tree.test.ts +60 -0
- package/src/components/file-tree.ts +112 -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 +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/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/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 +96 -0
- package/src/components/primitives/frame.test.ts +12 -0
- package/src/components/primitives/frame.ts +14 -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 +54 -0
- package/src/components/quick-actions-editor.test.tsx +49 -0
- package/src/components/quick-actions-editor.tsx +150 -0
- package/src/components/session-tree.test.ts +134 -0
- package/src/components/session-tree.ts +108 -0
- package/src/components/stories-app.tsx +184 -0
- package/src/components/stories.test.tsx +66 -0
- package/src/components/story-fixtures.ts +102 -0
- package/src/components/story-tree.test.ts +100 -0
- package/src/components/story.ts +142 -0
- package/src/components/syntax-highlight.ts +110 -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/truncate-text.ts +7 -0
- package/src/config.test.ts +461 -31
- package/src/config.ts +389 -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 +165 -0
- package/src/diff-intraline.ts +146 -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/herdr.test.ts +9 -34
- package/src/herdr.ts +25 -21
- package/src/index.ts +11 -0
- package/src/integrations.test.ts +41 -0
- package/src/integrations.ts +32 -0
- package/src/intent-dispatch.test.ts +698 -0
- package/src/intent-dispatch.ts +700 -0
- package/src/key-bindings.test.ts +156 -0
- package/src/key-bindings.ts +465 -0
- package/src/keymap.test.ts +611 -0
- package/src/keymap.ts +410 -0
- package/src/kitty-image.test.ts +66 -0
- package/src/kitty-image.ts +138 -0
- package/src/mark-runs.ts +120 -0
- package/src/prototype-browser.test.ts +56 -0
- package/src/prototype-browser.ts +307 -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 +30 -9
- package/src/serve.test.ts +24 -12
- package/src/serve.ts +20 -9
- package/src/share.ts +215 -0
- package/src/skills.test.ts +74 -0
- package/src/skills.ts +68 -0
- package/src/slash-palette.test.ts +54 -0
- package/src/slash-palette.ts +104 -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 +355 -0
- package/src/theme-presets.test.ts +51 -0
- package/src/theme-presets.ts +214 -0
- package/src/theme.ts +84 -19
- package/src/thread-chords.test.ts +170 -0
- package/src/thread-chords.ts +169 -0
- package/src/thread-controller.curate.test.ts +438 -0
- package/src/thread-controller.live-diff.test.ts +199 -0
- package/src/thread-controller.share.test.ts +472 -0
- package/src/thread-controller.tree.test.ts +255 -0
- package/src/thread-controller.ts +1509 -0
- package/src/thread-selection.test.ts +210 -0
- package/src/thread-selection.ts +243 -0
- package/src/tree-view.test.ts +128 -0
- package/src/tree-view.ts +148 -0
- package/src/use-annotation-surface.tsx +851 -0
- package/src/use-changes-workbench.ts +153 -0
- package/src/use-frame-measure.ts +38 -0
- package/src/use-settings-dialog.tsx +274 -0
- package/src/use-terminal-virtualizer.ts +233 -0
- package/src/version.ts +5 -0
- package/src/view-diff.test.ts +66 -9
- package/src/view-diff.ts +231 -23
- package/src/view-plan.regression.test.ts +60 -0
- package/src/view-plan.test.ts +406 -0
- package/src/view-plan.ts +570 -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 -312
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/** Inline skill completion in the composer (#25): the trailing "/word" token and its match. */
|
|
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 { settle, typeText } from "../test-support";
|
|
8
|
+
import { buildDisplay, marksByDisplay } from "../view-plan";
|
|
9
|
+
import { fixturePlanSession } from "./story-fixtures";
|
|
10
|
+
import { inlineSlashToken, resolveInlineSuggestion, ThreadView } from "./ThreadView";
|
|
11
|
+
import { slashItemsFrom, type SlashItem } from "../slash-palette";
|
|
12
|
+
import { SlashSkillsContext } from "../skills";
|
|
13
|
+
|
|
14
|
+
const SKILLS: QuickAction[] = [
|
|
15
|
+
{ prompt: "typescript magician" },
|
|
16
|
+
{ prompt: "implement" },
|
|
17
|
+
{ prompt: "write discoverable code" },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const PLAN = "# Plan\n\nRefine the store before the rewrite lands.\n";
|
|
21
|
+
|
|
22
|
+
async function mountComposer() {
|
|
23
|
+
const display = buildDisplay(PLAN, undefined);
|
|
24
|
+
const setup = await testRender(
|
|
25
|
+
<ThreadView
|
|
26
|
+
session={fixturePlanSession({
|
|
27
|
+
artifact: { type: "plan", content: PLAN, meta: { title: "Plan" } },
|
|
28
|
+
})}
|
|
29
|
+
display={display}
|
|
30
|
+
marks={marksByDisplay([], display)}
|
|
31
|
+
quickActions={SKILLS}
|
|
32
|
+
observer={false}
|
|
33
|
+
onAnnotate={() => {}}
|
|
34
|
+
onReply={() => {}}
|
|
35
|
+
onUpdateAnnotation={() => {}}
|
|
36
|
+
onExit={() => {}}
|
|
37
|
+
/>,
|
|
38
|
+
{ width: 72, height: 20 },
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
await settle(setup);
|
|
42
|
+
await settle(setup);
|
|
43
|
+
|
|
44
|
+
return setup;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe("inlineSlashToken", () => {
|
|
48
|
+
test("finds the trailing slash token when text precedes it", () => {
|
|
49
|
+
expect(inlineSlashToken("please run /type")).toBe("/type");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("ignores a draft that is itself a leading slash (that is the palette)", () => {
|
|
53
|
+
expect(inlineSlashToken("/type")).toBeNull();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("is newline-safe: a token at the start of a later line still resolves", () => {
|
|
57
|
+
expect(inlineSlashToken("first line\n/impl")).toBe("/impl");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("returns null when the trailing word is not a slash token", () => {
|
|
61
|
+
expect(inlineSlashToken("just some prose")).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("a bare slash mid-sentence is a valid, empty-query token", () => {
|
|
65
|
+
expect(inlineSlashToken("run /")).toBe("/");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("resolveInlineSuggestion", () => {
|
|
70
|
+
test("offers the fuzzy closest skill for the trailing token", () => {
|
|
71
|
+
const inline = resolveInlineSuggestion(false, "run /type", slashItemsFrom(SKILLS));
|
|
72
|
+
|
|
73
|
+
expect(inline?.token).toBe("/type");
|
|
74
|
+
expect(inline?.suggestion.name).toBe("typescript-magician");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("prefers a prefix match over a subsequence match", () => {
|
|
78
|
+
const inline = resolveInlineSuggestion(false, "run /impl", slashItemsFrom(SKILLS));
|
|
79
|
+
|
|
80
|
+
expect(inline?.suggestion.name).toBe("implement");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("stays silent while the palette owns a leading slash", () => {
|
|
84
|
+
expect(resolveInlineSuggestion(true, "/type", slashItemsFrom(SKILLS))).toBeNull();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("returns null when nothing matches the token", () => {
|
|
88
|
+
expect(resolveInlineSuggestion(false, "run /zzzz", slashItemsFrom(SKILLS))).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
async function mountComposerWithSkills(skills: SlashItem[]) {
|
|
93
|
+
const display = buildDisplay(PLAN, undefined);
|
|
94
|
+
const setup = await testRender(
|
|
95
|
+
<SlashSkillsContext.Provider value={skills}>
|
|
96
|
+
<ThreadView
|
|
97
|
+
session={fixturePlanSession({
|
|
98
|
+
artifact: { type: "plan", content: PLAN, meta: { title: "Plan" } },
|
|
99
|
+
})}
|
|
100
|
+
display={display}
|
|
101
|
+
marks={marksByDisplay([], display)}
|
|
102
|
+
quickActions={SKILLS}
|
|
103
|
+
observer={false}
|
|
104
|
+
onAnnotate={() => {}}
|
|
105
|
+
onReply={() => {}}
|
|
106
|
+
onUpdateAnnotation={() => {}}
|
|
107
|
+
onExit={() => {}}
|
|
108
|
+
/>
|
|
109
|
+
</SlashSkillsContext.Provider>,
|
|
110
|
+
{ width: 72, height: 20 },
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
await settle(setup);
|
|
114
|
+
await settle(setup);
|
|
115
|
+
|
|
116
|
+
return setup;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
describe("skills in the palette", () => {
|
|
120
|
+
test("a skill from context lists in the / palette and a pick inserts its /name", async () => {
|
|
121
|
+
const setup = await mountComposerWithSkills([
|
|
122
|
+
{ name: "vitest-patterns", description: "patterns for vitest", body: "" },
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
await typeText(setup, "/vitest");
|
|
126
|
+
expect(setup.captureCharFrame()).toContain("/vitest-patterns");
|
|
127
|
+
|
|
128
|
+
setup.mockInput.pressKey("RETURN");
|
|
129
|
+
await settle(setup);
|
|
130
|
+
// the reference lands in the draft, exactly like a quick action
|
|
131
|
+
expect(setup.captureCharFrame()).toContain("/vitest-patterns");
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("inline completion in the composer", () => {
|
|
136
|
+
test("typing an inline slash shows the tab-hint, and tab completes to the full name", async () => {
|
|
137
|
+
// Arrange - open a composer and type text with a trailing slash token
|
|
138
|
+
const setup = await mountComposer();
|
|
139
|
+
|
|
140
|
+
await typeText(setup, "run /type");
|
|
141
|
+
|
|
142
|
+
// Assert - the hint offers the closest skill, marked with the tab glyph
|
|
143
|
+
const withHint = setup.captureCharFrame();
|
|
144
|
+
|
|
145
|
+
expect(withHint).toContain("⇥");
|
|
146
|
+
expect(withHint).toContain("/typescript-magician");
|
|
147
|
+
|
|
148
|
+
// Act - tab completes the token
|
|
149
|
+
setup.mockInput.pressKey("TAB");
|
|
150
|
+
await settle(setup);
|
|
151
|
+
|
|
152
|
+
// Assert - the hint is gone and the composer now carries the full skill name
|
|
153
|
+
const completed = setup.captureCharFrame();
|
|
154
|
+
|
|
155
|
+
expect(completed).not.toContain("⇥");
|
|
156
|
+
expect(completed).toContain("typescript-magician");
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DEFAULT_QUICK_ACTIONS } from "../config";
|
|
3
|
+
import { DARK } from "../theme";
|
|
4
|
+
import type { Story, StoryMeta } from "./story";
|
|
5
|
+
import { fixtureDisplay, fixtureMarks, fixturePlanSession } from "./story-fixtures";
|
|
6
|
+
import { ThreadView } from "./ThreadView";
|
|
7
|
+
|
|
8
|
+
export const meta: StoryMeta = { title: "Surfaces/ThreadView" };
|
|
9
|
+
|
|
10
|
+
export const InlineThreads: Story = {
|
|
11
|
+
render: () => (
|
|
12
|
+
<ThreadView
|
|
13
|
+
session={fixturePlanSession()}
|
|
14
|
+
display={fixtureDisplay()}
|
|
15
|
+
marks={fixtureMarks()}
|
|
16
|
+
quickActions={DEFAULT_QUICK_ACTIONS}
|
|
17
|
+
observer={false}
|
|
18
|
+
onAnnotate={() => {}}
|
|
19
|
+
onReply={() => {}}
|
|
20
|
+
onUpdateAnnotation={() => {}}
|
|
21
|
+
onExit={() => {}}
|
|
22
|
+
/>
|
|
23
|
+
),
|
|
24
|
+
expectedColors: [DARK.text],
|
|
25
|
+
size: { width: 100, height: 34 },
|
|
26
|
+
};
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The thread view: the plan surface where discussions live inline in the
|
|
3
|
+
* document instead of the rail. The caret, marks, composer, palette, and cards
|
|
4
|
+
* come from the shared annotation surface (the diff sheet drives the same hook);
|
|
5
|
+
* this view paints the plan's blocks - headings, lists, quotes, tracked changes -
|
|
6
|
+
* and the right-edge scroll markers that navigate between discussions.
|
|
7
|
+
*
|
|
8
|
+
* Grammar: arrows move (caret flows across blocks) · shift+arrows select ·
|
|
9
|
+
* click/drag mark · typing comments (or edits my trailing comment / replies
|
|
10
|
+
* in a focused discussion) · "/" opens the quick-action palette · cmd+option+m
|
|
11
|
+
* comments on selection · cmd+enter sends, enter breaks the line · esc
|
|
12
|
+
* dismisses · tab folds · cmd+[ / cmd+] cycle discussions · blur-save on click.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import React, { useEffect, useRef } from "react";
|
|
16
|
+
import type { ScrollBoxRenderable } from "@opentui/core";
|
|
17
|
+
import type { Thread } from "@cueloop/schema";
|
|
18
|
+
import { displayText, type DisplayBlock, type Mark } from "../view-plan";
|
|
19
|
+
import type { TextSpan } from "../thread-selection";
|
|
20
|
+
import type { QuickAction } from "../config";
|
|
21
|
+
import type { CheatsheetSection } from "../key-bindings";
|
|
22
|
+
import type { Theme } from "../theme";
|
|
23
|
+
import { BOLD, CUT, UNDERLINE } from "../annotation-palette";
|
|
24
|
+
import { lineMarkRanges, runsFor, wrapLines, type MarkRange, type VisualLine } from "../mark-runs";
|
|
25
|
+
import { useFrameMeasure } from "../use-frame-measure";
|
|
26
|
+
import { useAnnotationSurface, type LineSource } from "../use-annotation-surface";
|
|
27
|
+
import { DiscussionMarkerRail } from "./DiscussionMarkerRail";
|
|
28
|
+
import { useComponentTheme } from "./theme-context";
|
|
29
|
+
|
|
30
|
+
export { lighten } from "../annotation-palette";
|
|
31
|
+
export { inlineSlashToken, resolveInlineSuggestion } from "../slash-palette";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* How a block's rows are painted: heading weight, muted kinds, the list or
|
|
35
|
+
* quote marker, and tracked changes as the plan sheet drew them - a cut block
|
|
36
|
+
* dim and struck through, an added or edited block tagged on its first row.
|
|
37
|
+
*/
|
|
38
|
+
interface BlockStyle {
|
|
39
|
+
baseFg: string;
|
|
40
|
+
baseAttributes: number;
|
|
41
|
+
marker: string;
|
|
42
|
+
changeTag: { text: string; fg: string } | null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function blockStyle(block: DisplayBlock, tokens: Theme): BlockStyle {
|
|
46
|
+
const isHeading = block.kind === "h1" || block.kind === "h2" || block.kind === "h3";
|
|
47
|
+
const isCut = block.type === "del";
|
|
48
|
+
const muted = block.kind === "h2" || block.kind === "h3" || block.kind === "code";
|
|
49
|
+
const marker =
|
|
50
|
+
block.kind === "li"
|
|
51
|
+
? "· "
|
|
52
|
+
: block.kind === "oli"
|
|
53
|
+
? `${block.orderedItemNumber ?? 1}. `
|
|
54
|
+
: block.kind === "quote"
|
|
55
|
+
? "▏ "
|
|
56
|
+
: "";
|
|
57
|
+
const changeTag =
|
|
58
|
+
block.type === "add"
|
|
59
|
+
? { text: " [new]", fg: tokens.green }
|
|
60
|
+
: block.type === "mod"
|
|
61
|
+
? { text: " [edited]", fg: tokens.accent }
|
|
62
|
+
: null;
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
baseFg: isCut ? tokens.textDim : muted ? tokens.textMuted : tokens.text,
|
|
66
|
+
baseAttributes: (isHeading ? BOLD : 0) | (isCut ? CUT : 0),
|
|
67
|
+
marker,
|
|
68
|
+
changeTag,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* ------------------------------------------------------------------ view */
|
|
73
|
+
|
|
74
|
+
/** The grammar as the keybinds dialog lists it; the view owns these keys, so they are not rebindable. */
|
|
75
|
+
export const THREAD_VIEW_CHEATSHEET: CheatsheetSection[] = [
|
|
76
|
+
{
|
|
77
|
+
title: "Thread",
|
|
78
|
+
entries: [
|
|
79
|
+
{ keys: "click", label: "place the caret" },
|
|
80
|
+
{ keys: "drag", label: "mark text, across blocks" },
|
|
81
|
+
{ keys: "dbl-click", label: "mark the word" },
|
|
82
|
+
{ keys: "← / →", label: "move by word" },
|
|
83
|
+
{ keys: "⇧← / ⇧→", label: "hold a mark" },
|
|
84
|
+
{ keys: "↑ / ↓", label: "move by block" },
|
|
85
|
+
{ keys: "type", label: "comment on the mark" },
|
|
86
|
+
{ keys: "⌘⌥m", label: "comment" },
|
|
87
|
+
{ keys: "enter", label: "reply to the comment" },
|
|
88
|
+
{ keys: "tab", label: "fold / unfold" },
|
|
89
|
+
{ keys: "⌘] / ⌘[", label: "next / previous comment" },
|
|
90
|
+
{ keys: "esc", label: "drop the mark" },
|
|
91
|
+
{ keys: "⌃q", label: "quit" },
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
title: "Comment",
|
|
96
|
+
entries: [
|
|
97
|
+
{ keys: "⌘enter", label: "send (⌃enter as well)" },
|
|
98
|
+
{ keys: "enter", label: "new line" },
|
|
99
|
+
{ keys: "backspace", label: "dismiss an empty draft" },
|
|
100
|
+
{ keys: "/", label: "actions" },
|
|
101
|
+
{ keys: "click away", label: "save" },
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
export interface ThreadViewProps {
|
|
107
|
+
session: Thread;
|
|
108
|
+
display: DisplayBlock[];
|
|
109
|
+
marks: Map<number, Mark[]>;
|
|
110
|
+
quickActions: QuickAction[];
|
|
111
|
+
observer: boolean;
|
|
112
|
+
/** True while a menu, dialog, or overlay owns the keyboard. */
|
|
113
|
+
suspended?: boolean;
|
|
114
|
+
/** Comments an edit orphaned: their passage is gone from the working copy. */
|
|
115
|
+
editOrphanCount?: number;
|
|
116
|
+
/** Reports whether a composer is open, so session chords can yield to typing. */
|
|
117
|
+
onComposingChange?: (composing: boolean) => void;
|
|
118
|
+
/** A verdict is in: no draft may open; the app answers with its read-only status. */
|
|
119
|
+
resolved?: boolean;
|
|
120
|
+
/** An observer or a resolved review refused a draft; the app shows why. */
|
|
121
|
+
onObserverBlocked?: (reason: "observer" | "resolved") => void;
|
|
122
|
+
/** Reports the caret's block, so block-level primitives (cut, restore) act where the caret is. */
|
|
123
|
+
onCursorChange?: (blockIndex: number) => void;
|
|
124
|
+
/** The rail's focused card; the discussion holding it takes focus here. */
|
|
125
|
+
focusedAnnotationId?: string;
|
|
126
|
+
/** Reports the focused discussion's root comment, so the rail follows. */
|
|
127
|
+
onFocusAnnotation?: (annotationId: string | undefined) => void;
|
|
128
|
+
onAnnotate: (span: TextSpan, body: string) => void;
|
|
129
|
+
onReply: (rootAnnotationId: string, body: string) => void;
|
|
130
|
+
onUpdateAnnotation: (id: string, body: string) => void;
|
|
131
|
+
onExit: () => void;
|
|
132
|
+
theme?: Theme;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function ThreadView({
|
|
136
|
+
session,
|
|
137
|
+
display,
|
|
138
|
+
marks,
|
|
139
|
+
quickActions,
|
|
140
|
+
observer,
|
|
141
|
+
resolved = false,
|
|
142
|
+
suspended = false,
|
|
143
|
+
editOrphanCount = 0,
|
|
144
|
+
onComposingChange,
|
|
145
|
+
onObserverBlocked,
|
|
146
|
+
onCursorChange,
|
|
147
|
+
focusedAnnotationId,
|
|
148
|
+
onFocusAnnotation,
|
|
149
|
+
onAnnotate,
|
|
150
|
+
onReply,
|
|
151
|
+
onUpdateAnnotation,
|
|
152
|
+
onExit,
|
|
153
|
+
theme,
|
|
154
|
+
}: ThreadViewProps): React.ReactNode {
|
|
155
|
+
const tokens = useComponentTheme(theme);
|
|
156
|
+
const source: LineSource = {
|
|
157
|
+
count: display.length,
|
|
158
|
+
textAt: (blockIndex) => displayText(display[blockIndex]!),
|
|
159
|
+
annotatable: () => true,
|
|
160
|
+
};
|
|
161
|
+
const surface = useAnnotationSurface({
|
|
162
|
+
source,
|
|
163
|
+
session,
|
|
164
|
+
marks,
|
|
165
|
+
quickActions,
|
|
166
|
+
tokens,
|
|
167
|
+
observer,
|
|
168
|
+
resolved,
|
|
169
|
+
suspended,
|
|
170
|
+
onComposingChange,
|
|
171
|
+
onObserverBlocked,
|
|
172
|
+
onCursorChange,
|
|
173
|
+
focusedAnnotationId,
|
|
174
|
+
onFocusAnnotation,
|
|
175
|
+
onAnnotate,
|
|
176
|
+
onReply,
|
|
177
|
+
onUpdateAnnotation,
|
|
178
|
+
onExit,
|
|
179
|
+
});
|
|
180
|
+
const { palette, discussions } = surface;
|
|
181
|
+
const scrollRef = useRef<ScrollBoxRenderable | null>(null);
|
|
182
|
+
const viewWidth = useFrameMeasure(
|
|
183
|
+
() => scrollRef.current?.content?.width ?? 0,
|
|
184
|
+
(left, right) => left === right,
|
|
185
|
+
0,
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
try {
|
|
190
|
+
scrollRef.current?.scrollChildIntoView(`discussion-block-${surface.revealBlockIndex}`);
|
|
191
|
+
} catch {
|
|
192
|
+
// best-effort reveal
|
|
193
|
+
}
|
|
194
|
+
}, [surface.revealBlockIndex]);
|
|
195
|
+
|
|
196
|
+
interface LineContext {
|
|
197
|
+
blockIndex: number;
|
|
198
|
+
text: string;
|
|
199
|
+
line: VisualLine;
|
|
200
|
+
lineIndex: number;
|
|
201
|
+
ranges: MarkRange[];
|
|
202
|
+
marker: string;
|
|
203
|
+
baseFg: string;
|
|
204
|
+
baseAttributes: number;
|
|
205
|
+
/** A tag after the row's text, such as the tracked-change label. */
|
|
206
|
+
trailing: { text: string; fg: string } | null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const lineRowFor = (context: LineContext): React.ReactNode => {
|
|
210
|
+
const { blockIndex, text, line, lineIndex, ranges, marker, baseFg, baseAttributes, trailing } =
|
|
211
|
+
context;
|
|
212
|
+
const lineRanges = lineMarkRanges(ranges, line);
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<box key={`line-${lineIndex}`} style={{ flexDirection: "row" }}>
|
|
216
|
+
<text selectable={false}>
|
|
217
|
+
<span fg={tokens.textDim}>{" "}</span>
|
|
218
|
+
<span fg={tokens.textDim}>{lineIndex === 0 ? marker : " ".repeat(marker.length)}</span>
|
|
219
|
+
</text>
|
|
220
|
+
<text
|
|
221
|
+
selectable={false}
|
|
222
|
+
style={{ flexGrow: 1, flexShrink: 1 }}
|
|
223
|
+
ref={surface.registerLine(blockIndex, lineIndex, line)}
|
|
224
|
+
onMouseDown={surface.onLineMouseDown}
|
|
225
|
+
>
|
|
226
|
+
{runsFor(text.slice(line.start, line.end), lineRanges).map((run, runIndex) => (
|
|
227
|
+
<span
|
|
228
|
+
key={runIndex}
|
|
229
|
+
fg={baseFg}
|
|
230
|
+
bg={run.caretOnly ? palette.caretCell : run.marked ? palette.markBackdrop : undefined}
|
|
231
|
+
attributes={baseAttributes | (run.marked ? UNDERLINE : 0)}
|
|
232
|
+
>
|
|
233
|
+
{run.text}
|
|
234
|
+
</span>
|
|
235
|
+
))}
|
|
236
|
+
{trailing ? <span fg={trailing.fg}>{trailing.text}</span> : null}
|
|
237
|
+
</text>
|
|
238
|
+
</box>
|
|
239
|
+
);
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const blockNodeFor = (blockIndex: number): React.ReactNode => {
|
|
243
|
+
const block = display[blockIndex]!;
|
|
244
|
+
const text = displayText(block);
|
|
245
|
+
const ranges = surface.rangesFor(blockIndex);
|
|
246
|
+
const { baseFg, baseAttributes, marker, changeTag } = blockStyle(block, tokens);
|
|
247
|
+
const lines = wrapLines(text, viewWidth > 0 ? viewWidth - marker.length - 6 : 0);
|
|
248
|
+
const lineRows: React.ReactNode[] = [];
|
|
249
|
+
|
|
250
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
251
|
+
const line = lines[lineIndex]!;
|
|
252
|
+
const isLastLine = lineIndex === lines.length - 1;
|
|
253
|
+
|
|
254
|
+
lineRows.push(
|
|
255
|
+
lineRowFor({
|
|
256
|
+
blockIndex,
|
|
257
|
+
text,
|
|
258
|
+
line,
|
|
259
|
+
lineIndex,
|
|
260
|
+
ranges,
|
|
261
|
+
marker,
|
|
262
|
+
baseFg,
|
|
263
|
+
baseAttributes,
|
|
264
|
+
trailing: lineIndex === 0 ? changeTag : null,
|
|
265
|
+
}),
|
|
266
|
+
);
|
|
267
|
+
const cards = surface.cardsAfterLine(blockIndex, line, isLastLine);
|
|
268
|
+
|
|
269
|
+
lineRows.push(...cards);
|
|
270
|
+
if (cards.length > 0 && !isLastLine) {
|
|
271
|
+
lineRows.push(<box key={`gap-${lineIndex}`} style={{ height: 1 }} />);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// list items of one list stay tight; every other block sits a blank row
|
|
276
|
+
// below its neighbour, and a code block announces its language first
|
|
277
|
+
const previous = display[blockIndex - 1];
|
|
278
|
+
const tight =
|
|
279
|
+
previous !== undefined &&
|
|
280
|
+
block.kind === previous.kind &&
|
|
281
|
+
(block.kind === "li" || block.kind === "oli");
|
|
282
|
+
const languageRow =
|
|
283
|
+
block.kind === "code" ? (
|
|
284
|
+
<text key="language" fg={tokens.textDim} selectable={false}>
|
|
285
|
+
{` ${block.work?.lang ?? block.base?.lang ?? "code"}`}
|
|
286
|
+
</text>
|
|
287
|
+
) : null;
|
|
288
|
+
|
|
289
|
+
return (
|
|
290
|
+
<box
|
|
291
|
+
key={`discussion-block-${blockIndex}`}
|
|
292
|
+
id={`discussion-block-${blockIndex}`}
|
|
293
|
+
style={{ flexDirection: "column", marginTop: blockIndex === 0 || tight ? 0 : 1 }}
|
|
294
|
+
>
|
|
295
|
+
{languageRow}
|
|
296
|
+
{lineRows}
|
|
297
|
+
</box>
|
|
298
|
+
);
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
return (
|
|
302
|
+
<box style={{ flexGrow: 1, flexDirection: "row" }} {...surface.rootMouseProps}>
|
|
303
|
+
<box style={{ flexGrow: 1, flexDirection: "column" }}>
|
|
304
|
+
{editOrphanCount > 0 ? (
|
|
305
|
+
<box style={{ height: 1, backgroundColor: palette.markBackdrop, paddingLeft: 2 }}>
|
|
306
|
+
<text fg={tokens.red}>
|
|
307
|
+
{`${editOrphanCount} annotation${editOrphanCount === 1 ? "" : "s"} no longer match - the passage was removed.`}
|
|
308
|
+
</text>
|
|
309
|
+
</box>
|
|
310
|
+
) : null}
|
|
311
|
+
{/* the rail draws this box's scrollbar past the dots, so the built-in one stays hidden */}
|
|
312
|
+
<scrollbox
|
|
313
|
+
ref={scrollRef}
|
|
314
|
+
style={{ flexGrow: 1, paddingTop: 1 }}
|
|
315
|
+
focused={false}
|
|
316
|
+
verticalScrollbarOptions={{ visible: false }}
|
|
317
|
+
>
|
|
318
|
+
{display.map((_block, blockIndex) => blockNodeFor(blockIndex))}
|
|
319
|
+
</scrollbox>
|
|
320
|
+
</box>
|
|
321
|
+
<DiscussionMarkerRail
|
|
322
|
+
discussions={discussions}
|
|
323
|
+
spanQuote={surface.spanQuote}
|
|
324
|
+
onJump={surface.jumpToDiscussion}
|
|
325
|
+
scrollbox={scrollRef}
|
|
326
|
+
theme={theme}
|
|
327
|
+
/>
|
|
328
|
+
</box>
|
|
329
|
+
);
|
|
330
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "./story";
|
|
4
|
+
import { ThreadsSidebar } from "./ThreadsSidebar";
|
|
5
|
+
import { groupInbox } from "./session-tree";
|
|
6
|
+
import { fixtureDiffSession, fixturePlanSession } from "./story-fixtures";
|
|
7
|
+
|
|
8
|
+
export const meta: StoryMeta = { title: "Surfaces/ThreadsSidebar" };
|
|
9
|
+
|
|
10
|
+
function Frame({ children }: { children: React.ReactNode }): React.ReactNode {
|
|
11
|
+
return <box style={{ width: "100%", height: "100%", flexDirection: "row" }}>{children}</box>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const plan = fixturePlanSession();
|
|
15
|
+
const projectPlan = {
|
|
16
|
+
...plan,
|
|
17
|
+
workspace: { ...plan.workspace, rootCommit: "root-1", remote: "git@github.com:acme/widget.git" },
|
|
18
|
+
};
|
|
19
|
+
const { rows, ordered } = groupInbox([projectPlan, fixtureDiffSession()]);
|
|
20
|
+
|
|
21
|
+
export const Open: Story = {
|
|
22
|
+
render: () => (
|
|
23
|
+
<Frame>
|
|
24
|
+
<ThreadsSidebar
|
|
25
|
+
open
|
|
26
|
+
rows={rows}
|
|
27
|
+
cursor={0}
|
|
28
|
+
activeId={ordered[0]!.id}
|
|
29
|
+
onSelect={() => {}}
|
|
30
|
+
theme={DARK}
|
|
31
|
+
/>
|
|
32
|
+
</Frame>
|
|
33
|
+
),
|
|
34
|
+
expectedColors: [DARK.accent, DARK.border],
|
|
35
|
+
size: { width: 60, height: 12 },
|
|
36
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// The left Projects and Threads column: a collapsible, scrollable tree beside the
|
|
2
|
+
// thread. A click jumps to that thread; the open thread stays highlighted. Renders
|
|
3
|
+
// nothing when collapsed so the thread reclaims the width.
|
|
4
|
+
|
|
5
|
+
import { ScrollArea } from "./ScrollArea";
|
|
6
|
+
import React from "react";
|
|
7
|
+
import type { Theme } from "../theme";
|
|
8
|
+
import { ThreadTree } from "./ThreadTree";
|
|
9
|
+
import type { InboxRow } from "./session-tree";
|
|
10
|
+
|
|
11
|
+
export interface ThreadsSidebarProps {
|
|
12
|
+
open: boolean;
|
|
13
|
+
rows: InboxRow[];
|
|
14
|
+
cursor: number;
|
|
15
|
+
/** The open thread's id, highlighted in the list. */
|
|
16
|
+
activeId?: string;
|
|
17
|
+
/** Ids of pinned threads. */
|
|
18
|
+
pinnedIds?: ReadonlySet<string>;
|
|
19
|
+
onSelect: (sessionId: string) => void;
|
|
20
|
+
/** Ask to delete a thread (the row menu's Delete, wired to the confirm dialog). */
|
|
21
|
+
onRequestDelete?: (id: string, title: string) => void;
|
|
22
|
+
/** Toggle a thread's pinned state (the row menu's Pin/Unpin). */
|
|
23
|
+
onPin?: (id: string) => void;
|
|
24
|
+
/** Rename a thread's title (the row menu's Rename). */
|
|
25
|
+
onRename?: (id: string, title: string) => void;
|
|
26
|
+
width?: number;
|
|
27
|
+
theme?: Theme;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function ThreadsSidebar({
|
|
31
|
+
open,
|
|
32
|
+
rows,
|
|
33
|
+
cursor,
|
|
34
|
+
activeId,
|
|
35
|
+
pinnedIds,
|
|
36
|
+
onSelect,
|
|
37
|
+
onRequestDelete,
|
|
38
|
+
onPin,
|
|
39
|
+
onRename,
|
|
40
|
+
width = 30,
|
|
41
|
+
theme,
|
|
42
|
+
}: ThreadsSidebarProps): React.ReactNode {
|
|
43
|
+
if (!open) return null;
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<box
|
|
47
|
+
style={{
|
|
48
|
+
width,
|
|
49
|
+
flexDirection: "column",
|
|
50
|
+
borderStyle: "single",
|
|
51
|
+
border: ["right"],
|
|
52
|
+
borderColor: theme?.border,
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
<ScrollArea>
|
|
56
|
+
<ThreadTree
|
|
57
|
+
rows={rows}
|
|
58
|
+
cursor={cursor}
|
|
59
|
+
activeId={activeId}
|
|
60
|
+
pinnedIds={pinnedIds}
|
|
61
|
+
width={width}
|
|
62
|
+
onSelect={onSelect}
|
|
63
|
+
onRequestDelete={onRequestDelete}
|
|
64
|
+
onPin={onPin}
|
|
65
|
+
onRename={onRename}
|
|
66
|
+
theme={theme}
|
|
67
|
+
/>
|
|
68
|
+
</ScrollArea>
|
|
69
|
+
</box>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -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: "Overlays/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
|
+
}
|