@cueloop/client 0.1.0-alpha.8 → 0.1.0-alpha.81
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 +40 -32
- package/src/App.appearance.test.tsx +53 -0
- package/src/App.autoclose.test.tsx +77 -51
- package/src/App.diff.test.tsx +176 -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 +428 -0
- package/src/App.prototype-markdown.test.tsx +92 -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 +222 -0
- package/src/App.test.tsx +333 -131
- package/src/App.toast.test.tsx +98 -0
- package/src/App.tsx +1338 -608
- package/src/App.visual.test.tsx +57 -0
- package/src/App.walk.test.tsx +299 -0
- package/src/App.workbench.test.tsx +701 -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 +574 -0
- package/src/app-view-model.ts +162 -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 +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.stories.tsx +89 -0
- package/src/components/AppShell.tsx +329 -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 +153 -0
- package/src/components/ConfirmDialog.stories.tsx +21 -0
- package/src/components/ConfirmDialog.tsx +72 -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 +876 -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/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 +59 -0
- package/src/components/ProjectTreeView.tsx +111 -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 +899 -0
- package/src/components/ThreadView.slash.test.tsx +146 -0
- package/src/components/ThreadView.stories.tsx +26 -0
- package/src/components/ThreadView.tsx +472 -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 +47 -0
- package/src/components/WelcomePlayground.tsx +142 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +2238 -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 +56 -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 +517 -33
- package/src/config.ts +467 -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 +13 -0
- package/src/integrations.test.ts +41 -0
- package/src/integrations.ts +32 -0
- package/src/intent-dispatch.test.ts +723 -0
- package/src/intent-dispatch.ts +692 -0
- package/src/key-bindings.test.ts +156 -0
- package/src/key-bindings.ts +459 -0
- package/src/keymap.test.ts +461 -113
- package/src/keymap.ts +321 -79
- package/src/kitty-image.test.ts +66 -0
- package/src/kitty-image.ts +138 -0
- package/src/launch-layout.test.ts +52 -0
- package/src/launch-layout.ts +42 -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 +64 -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 +78 -13
- 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 +214 -0
- package/src/theme.ts +84 -19
- package/src/thread-chords.test.ts +79 -0
- package/src/thread-chords.ts +163 -0
- package/src/thread-controller.curate.test.ts +450 -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 +1893 -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 +915 -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-settings-dialog.tsx +320 -0
- package/src/use-terminal-virtualizer.ts +233 -0
- package/src/use-thread-body-editing.tsx +58 -0
- package/src/version.ts +5 -0
- package/src/view-diff.test.ts +137 -9
- package/src/view-diff.ts +269 -23
- package/src/view-plan.regression.test.ts +60 -0
- package/src/view-plan.test.ts +456 -0
- package/src/view-plan.ts +719 -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 +81 -0
- package/src/workbench-snapshot.ts +29 -0
- package/src/session-controller.ts +0 -345
- 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,146 @@
|
|
|
1
|
+
/** The "/" palette in the composer: it reopens for each "/word" the caret writes, so skills chain. */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, test } from "bun:test";
|
|
4
|
+
import React from "react";
|
|
5
|
+
import type { RGBA } from "@opentui/core";
|
|
6
|
+
import { testRender } from "@opentui/react/test-utils";
|
|
7
|
+
import type { QuickAction } from "../config";
|
|
8
|
+
import { settle, typeText } from "../test-support";
|
|
9
|
+
import { DARK } from "../theme";
|
|
10
|
+
import { buildDisplay, marksByDisplay } from "../view-plan";
|
|
11
|
+
import { fixturePlanSession } from "./story-fixtures";
|
|
12
|
+
import { ThreadView } from "./ThreadView";
|
|
13
|
+
import { mergeSlashItems, slashItemsFrom, type SlashItem } from "../slash-palette";
|
|
14
|
+
import { PaletteNamesContext, SlashSkillsContext } from "../skills";
|
|
15
|
+
|
|
16
|
+
const SKILLS: QuickAction[] = [
|
|
17
|
+
{ prompt: "typescript magician" },
|
|
18
|
+
{ prompt: "implement" },
|
|
19
|
+
{ prompt: "write discoverable code" },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const PLAN = "# Plan\n\nRefine the store before the rewrite lands.\n";
|
|
23
|
+
|
|
24
|
+
function colorToHex(color: RGBA): string {
|
|
25
|
+
const [red, green, blue] = color.toInts();
|
|
26
|
+
|
|
27
|
+
return "#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join("");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Render a plan composer, wiring the palette names into context exactly as the app does. */
|
|
31
|
+
async function mountComposerWithSkills(skills: SlashItem[] = [], width = 72) {
|
|
32
|
+
const display = buildDisplay(PLAN, undefined);
|
|
33
|
+
const names = new Set(mergeSlashItems(slashItemsFrom(SKILLS), skills).map((item) => item.name));
|
|
34
|
+
const setup = await testRender(
|
|
35
|
+
<SlashSkillsContext.Provider value={skills}>
|
|
36
|
+
<PaletteNamesContext.Provider value={names}>
|
|
37
|
+
<ThreadView
|
|
38
|
+
session={fixturePlanSession({
|
|
39
|
+
artifact: { type: "plan", content: PLAN, meta: { title: "Plan" } },
|
|
40
|
+
})}
|
|
41
|
+
display={display}
|
|
42
|
+
marks={marksByDisplay([], display)}
|
|
43
|
+
quickActions={SKILLS}
|
|
44
|
+
observer={false}
|
|
45
|
+
onAnnotate={() => {}}
|
|
46
|
+
onReply={() => {}}
|
|
47
|
+
onUpdateAnnotation={() => {}}
|
|
48
|
+
onExit={() => {}}
|
|
49
|
+
/>
|
|
50
|
+
</PaletteNamesContext.Provider>
|
|
51
|
+
</SlashSkillsContext.Provider>,
|
|
52
|
+
{ width, height: 20 },
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
await settle(setup);
|
|
56
|
+
await settle(setup);
|
|
57
|
+
|
|
58
|
+
return setup;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const mountComposer = () => mountComposerWithSkills();
|
|
62
|
+
|
|
63
|
+
describe("skills in the palette", () => {
|
|
64
|
+
test("a skill from context lists in the / palette and a pick inserts its /name", async () => {
|
|
65
|
+
const setup = await mountComposerWithSkills([
|
|
66
|
+
{ name: "vitest-patterns", description: "patterns for vitest", body: "" },
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
await typeText(setup, "/vitest");
|
|
70
|
+
expect(setup.captureCharFrame()).toContain("/vitest-patterns");
|
|
71
|
+
|
|
72
|
+
setup.mockInput.pressKey("RETURN");
|
|
73
|
+
await settle(setup);
|
|
74
|
+
// the reference lands in the draft, exactly like a quick action
|
|
75
|
+
expect(setup.captureCharFrame()).toContain("/vitest-patterns");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("the palette reopens per token and chains skills", () => {
|
|
80
|
+
test("a mid-sentence / opens the list, tab completes it, and a second / opens the list again", async () => {
|
|
81
|
+
const setup = await mountComposer();
|
|
82
|
+
|
|
83
|
+
// a trailing "/type" opens the full list under the composer, closest skill first
|
|
84
|
+
await typeText(setup, "run /type");
|
|
85
|
+
const firstList = setup.captureCharFrame();
|
|
86
|
+
|
|
87
|
+
expect(firstList).toContain("/typescript-magician");
|
|
88
|
+
|
|
89
|
+
// tab completes only that token, leaving the prose before it
|
|
90
|
+
setup.mockInput.pressKey("TAB");
|
|
91
|
+
await settle(setup);
|
|
92
|
+
const completed = setup.captureCharFrame();
|
|
93
|
+
|
|
94
|
+
expect(completed).toContain("run /typescript-magician");
|
|
95
|
+
|
|
96
|
+
// a second "/" reopens the palette so another skill chains onto the draft
|
|
97
|
+
await typeText(setup, "/impl");
|
|
98
|
+
const secondList = setup.captureCharFrame();
|
|
99
|
+
|
|
100
|
+
expect(secondList).toContain("/implement");
|
|
101
|
+
|
|
102
|
+
setup.mockInput.pressKey("TAB");
|
|
103
|
+
await settle(setup);
|
|
104
|
+
expect(setup.captureCharFrame()).toContain("run /typescript-magician /implement");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("several skills picked on enter, with prose between, all persist in the draft", async () => {
|
|
108
|
+
const setup = await mountComposerWithSkills([], 120);
|
|
109
|
+
|
|
110
|
+
// pick the first skill, keep typing prose, pick the next, and so on
|
|
111
|
+
await typeText(setup, "start /type");
|
|
112
|
+
setup.mockInput.pressKey("RETURN");
|
|
113
|
+
await settle(setup);
|
|
114
|
+
|
|
115
|
+
await typeText(setup, "then run /impl");
|
|
116
|
+
setup.mockInput.pressKey("RETURN");
|
|
117
|
+
await settle(setup);
|
|
118
|
+
|
|
119
|
+
await typeText(setup, "and finally /write");
|
|
120
|
+
setup.mockInput.pressKey("RETURN");
|
|
121
|
+
await settle(setup);
|
|
122
|
+
|
|
123
|
+
// every pick landed as its full "/name", and the prose between them survived
|
|
124
|
+
expect(setup.captureCharFrame()).toContain(
|
|
125
|
+
"start /typescript-magician then run /implement and finally /write-discoverable-code",
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("a completed reference paints in the accent color", async () => {
|
|
130
|
+
const setup = await mountComposer();
|
|
131
|
+
|
|
132
|
+
await typeText(setup, "please /type");
|
|
133
|
+
setup.mockInput.pressKey("RETURN");
|
|
134
|
+
await settle(setup);
|
|
135
|
+
|
|
136
|
+
const accentText = setup
|
|
137
|
+
.captureSpans()
|
|
138
|
+
.lines.flatMap((line) => line.spans.filter((span) => colorToHex(span.fg) === DARK.accent))
|
|
139
|
+
.map((span) => span.text)
|
|
140
|
+
.join("");
|
|
141
|
+
|
|
142
|
+
expect(accentText).toContain("/typescript-magician");
|
|
143
|
+
// the prose around the reference is not accented
|
|
144
|
+
expect(accentText).not.toContain("please");
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -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,472 @@
|
|
|
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 { useTerminalDimensions } from "@opentui/react";
|
|
17
|
+
import type { KeyEvent, ScrollBoxRenderable } from "@opentui/core";
|
|
18
|
+
import type { Annotation, Thread } from "@cueloop/schema";
|
|
19
|
+
import {
|
|
20
|
+
displayText,
|
|
21
|
+
renderedText,
|
|
22
|
+
renderedStyleRuns,
|
|
23
|
+
styledRunsFor,
|
|
24
|
+
type DisplayBlock,
|
|
25
|
+
type Mark,
|
|
26
|
+
type RenderedRun,
|
|
27
|
+
type RunRole,
|
|
28
|
+
type StyledRun,
|
|
29
|
+
} from "../view-plan";
|
|
30
|
+
import type { TextSpan } from "../thread-selection";
|
|
31
|
+
import type { QuickAction } from "../config";
|
|
32
|
+
import type { CheatsheetSection } from "../key-bindings";
|
|
33
|
+
import type { Theme } from "../theme";
|
|
34
|
+
import { BOLD, CUT, ITALIC, UNDERLINE } from "../annotation-palette";
|
|
35
|
+
import { wrapLines, type MarkRange, type VisualLine } from "../mark-runs";
|
|
36
|
+
import { MarkdownGridBlock } from "./MarkdownGridBlock";
|
|
37
|
+
import { useFrameMeasure } from "../use-frame-measure";
|
|
38
|
+
import { useTerminalVirtualizer } from "../use-terminal-virtualizer";
|
|
39
|
+
import { useAnnotationSurface, type LineSource } from "../use-annotation-surface";
|
|
40
|
+
import { NavModeHint } from "./NavModeHint";
|
|
41
|
+
import { DiscussionMarkerRail } from "./DiscussionMarkerRail";
|
|
42
|
+
import { useComponentTheme } from "./theme-context";
|
|
43
|
+
|
|
44
|
+
/** Blocks kept mounted beyond the viewport, so a scroll step or a drag past the edge never shows a gap. */
|
|
45
|
+
const OVERSCAN_BLOCKS = 8;
|
|
46
|
+
|
|
47
|
+
export { lighten } from "../annotation-palette";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* How a block's rows are painted: heading weight, muted kinds, and the list or
|
|
51
|
+
* quote marker. A cut block reads dim and struck through; other edits are
|
|
52
|
+
* tracked into the working copy but not tagged in the read-only view.
|
|
53
|
+
*/
|
|
54
|
+
interface BlockStyle {
|
|
55
|
+
baseFg: string;
|
|
56
|
+
baseAttributes: number;
|
|
57
|
+
marker: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function blockStyle(block: DisplayBlock, tokens: Theme): BlockStyle {
|
|
61
|
+
const isHeading = block.kind === "h1" || block.kind === "h2" || block.kind === "h3";
|
|
62
|
+
const isCut = block.type === "del";
|
|
63
|
+
const muted = block.kind === "h2" || block.kind === "h3" || block.kind === "code";
|
|
64
|
+
const marker =
|
|
65
|
+
block.kind === "li"
|
|
66
|
+
? "· "
|
|
67
|
+
: block.kind === "oli"
|
|
68
|
+
? `${block.orderedItemNumber ?? 1}. `
|
|
69
|
+
: block.kind === "quote"
|
|
70
|
+
? "▏ "
|
|
71
|
+
: "";
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
baseFg: isCut ? tokens.textDim : muted ? tokens.textMuted : tokens.text,
|
|
75
|
+
baseAttributes: (isHeading ? BOLD : 0) | (isCut ? CUT : 0),
|
|
76
|
+
marker,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The read-only color of an inline markdown role: links accent, code and removals dim, insertions green. */
|
|
81
|
+
function roleForeground(role: RunRole, baseFg: string, tokens: Theme): string {
|
|
82
|
+
if (role === "link") return tokens.blue;
|
|
83
|
+
if (role === "ins") return tokens.green;
|
|
84
|
+
if (role === "code" || role === "del") return tokens.textDim;
|
|
85
|
+
|
|
86
|
+
return baseFg;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The text attributes an inline role adds over a block's base attributes. */
|
|
90
|
+
function roleAttributes(role: RunRole, baseAttributes: number): number {
|
|
91
|
+
if (role === "strong") return baseAttributes | BOLD;
|
|
92
|
+
if (role === "em") return baseAttributes | ITALIC;
|
|
93
|
+
if (role === "strike" || role === "del") return baseAttributes | CUT;
|
|
94
|
+
|
|
95
|
+
return baseAttributes;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isGridKind(kind: DisplayBlock["kind"]): boolean {
|
|
99
|
+
return kind === "table" || kind === "frontmatter";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* ------------------------------------------------------------------ view */
|
|
103
|
+
|
|
104
|
+
/** The grammar as the keybinds dialog lists it; the view owns these keys, so they are not rebindable. */
|
|
105
|
+
export const THREAD_VIEW_CHEATSHEET: CheatsheetSection[] = [
|
|
106
|
+
{
|
|
107
|
+
title: "Thread (type)",
|
|
108
|
+
entries: [
|
|
109
|
+
{ keys: "click", label: "place the caret" },
|
|
110
|
+
{ keys: "drag", label: "mark text, across blocks" },
|
|
111
|
+
{ keys: "dbl-click", label: "mark the word" },
|
|
112
|
+
{ keys: "← / →", label: "move by word" },
|
|
113
|
+
{ keys: "⇧← / ⇧→", label: "hold a mark" },
|
|
114
|
+
{ keys: "↑ / ↓", label: "move by block" },
|
|
115
|
+
{ keys: "type", label: "comment on the mark" },
|
|
116
|
+
{ keys: "enter", label: "reply to the comment" },
|
|
117
|
+
{ keys: "esc", label: "nav mode" },
|
|
118
|
+
{ keys: "tab", label: "cycle panes" },
|
|
119
|
+
{ keys: "⌃q", label: "quit" },
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
title: "Comment",
|
|
124
|
+
entries: [
|
|
125
|
+
{ keys: "⌘enter", label: "send (⌃enter as well)" },
|
|
126
|
+
{ keys: "enter", label: "new line" },
|
|
127
|
+
{ keys: "backspace", label: "dismiss an empty draft" },
|
|
128
|
+
{ keys: "/", label: "actions" },
|
|
129
|
+
{ keys: "click away", label: "save" },
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
export interface ThreadViewProps {
|
|
135
|
+
session: Thread;
|
|
136
|
+
display: DisplayBlock[];
|
|
137
|
+
marks: Map<number, Mark[]>;
|
|
138
|
+
quickActions: QuickAction[];
|
|
139
|
+
observer: boolean;
|
|
140
|
+
/** True while a menu, dialog, or overlay owns the keyboard. */
|
|
141
|
+
suspended?: boolean;
|
|
142
|
+
/** Comments an edit orphaned: their passage is gone from the working copy. */
|
|
143
|
+
editOrphanCount?: number;
|
|
144
|
+
/** Reports whether a composer is open, so session chords can yield to typing. */
|
|
145
|
+
onComposingChange?: (composing: boolean) => void;
|
|
146
|
+
/** A message is in: no draft may open; the app answers with its read-only status. */
|
|
147
|
+
resolved?: boolean;
|
|
148
|
+
/** An observer or a resolved review refused a draft; the app shows why. */
|
|
149
|
+
onObserverBlocked?: (reason: "observer" | "resolved") => void;
|
|
150
|
+
/** Reports the caret's block, so block-level primitives (cut, restore) act where the caret is. */
|
|
151
|
+
onCursorChange?: (blockIndex: number) => void;
|
|
152
|
+
/** The rail's focused card; the discussion holding it takes focus here. */
|
|
153
|
+
focusedAnnotationId?: string;
|
|
154
|
+
/** Reports the focused discussion's root comment, so the rail follows. */
|
|
155
|
+
onFocusAnnotation?: (annotationId: string | undefined) => void;
|
|
156
|
+
onAnnotate: (span: TextSpan, body: string) => void;
|
|
157
|
+
onReply: (rootAnnotationId: string, body: string) => void;
|
|
158
|
+
onUpdateAnnotation: (id: string, body: string) => void;
|
|
159
|
+
/** The author's display name for a comment's hover tooltip. */
|
|
160
|
+
resolveAuthorLabel?: (annotation: Annotation) => string | undefined;
|
|
161
|
+
onNavCommand?: (key: KeyEvent) => boolean;
|
|
162
|
+
onExit: () => void;
|
|
163
|
+
theme?: Theme;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function ThreadView({
|
|
167
|
+
session,
|
|
168
|
+
display,
|
|
169
|
+
marks,
|
|
170
|
+
quickActions,
|
|
171
|
+
observer,
|
|
172
|
+
resolved = false,
|
|
173
|
+
suspended = false,
|
|
174
|
+
editOrphanCount = 0,
|
|
175
|
+
onComposingChange,
|
|
176
|
+
onObserverBlocked,
|
|
177
|
+
onCursorChange,
|
|
178
|
+
focusedAnnotationId,
|
|
179
|
+
onFocusAnnotation,
|
|
180
|
+
onAnnotate,
|
|
181
|
+
onReply,
|
|
182
|
+
onUpdateAnnotation,
|
|
183
|
+
resolveAuthorLabel,
|
|
184
|
+
onNavCommand,
|
|
185
|
+
onExit,
|
|
186
|
+
theme,
|
|
187
|
+
}: ThreadViewProps): React.ReactNode {
|
|
188
|
+
const tokens = useComponentTheme(theme);
|
|
189
|
+
const source: LineSource = {
|
|
190
|
+
count: display.length,
|
|
191
|
+
// the surface hit-tests and paints in rendered text (inline markers concealed); a grid anchors on its raw source
|
|
192
|
+
textAt: (blockIndex) => renderedText(display[blockIndex]!),
|
|
193
|
+
annotatable: () => true,
|
|
194
|
+
};
|
|
195
|
+
const surface = useAnnotationSurface({
|
|
196
|
+
source,
|
|
197
|
+
session,
|
|
198
|
+
marks,
|
|
199
|
+
quickActions,
|
|
200
|
+
tokens,
|
|
201
|
+
observer,
|
|
202
|
+
resolved,
|
|
203
|
+
suspended,
|
|
204
|
+
onComposingChange,
|
|
205
|
+
onObserverBlocked,
|
|
206
|
+
onCursorChange,
|
|
207
|
+
focusedAnnotationId,
|
|
208
|
+
onFocusAnnotation,
|
|
209
|
+
onAnnotate,
|
|
210
|
+
onReply,
|
|
211
|
+
onUpdateAnnotation,
|
|
212
|
+
resolveAuthorLabel,
|
|
213
|
+
onNavCommand,
|
|
214
|
+
onExit,
|
|
215
|
+
});
|
|
216
|
+
const { palette, discussions } = surface;
|
|
217
|
+
const scrollRef = useRef<ScrollBoxRenderable | null>(null);
|
|
218
|
+
const { height: terminalHeight } = useTerminalDimensions();
|
|
219
|
+
const viewWidth = useFrameMeasure(
|
|
220
|
+
() => scrollRef.current?.content?.width ?? 0,
|
|
221
|
+
(left, right) => left === right,
|
|
222
|
+
0,
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// only the viewport's blocks (plus an overscan) mount; a block is estimated at its wrapped-line
|
|
226
|
+
// count until its box, cards included, is measured. The initial viewport seeds the first render so a
|
|
227
|
+
// freshly mounted thread (a switch from a diff, another project) windows its first paint rather than
|
|
228
|
+
// building every block, and never flashes blank.
|
|
229
|
+
const estimateBlockRows = (index: number): number => {
|
|
230
|
+
const block = display[index];
|
|
231
|
+
|
|
232
|
+
if (!block) return 1;
|
|
233
|
+
const usable = viewWidth > 0 ? Math.max(1, viewWidth - 6) : 40;
|
|
234
|
+
const lines = Math.max(1, Math.ceil(displayText(block).length / usable));
|
|
235
|
+
|
|
236
|
+
return lines + (index === 0 ? 0 : 1);
|
|
237
|
+
};
|
|
238
|
+
const virtual = useTerminalVirtualizer({
|
|
239
|
+
scrollbox: scrollRef,
|
|
240
|
+
count: display.length,
|
|
241
|
+
estimateSize: estimateBlockRows,
|
|
242
|
+
overscan: OVERSCAN_BLOCKS,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
useEffect(() => {
|
|
246
|
+
virtual.scrollToIndex(surface.revealBlockIndex);
|
|
247
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
248
|
+
}, [surface.revealBlockIndex]);
|
|
249
|
+
|
|
250
|
+
interface LineContext {
|
|
251
|
+
blockIndex: number;
|
|
252
|
+
roleRuns: RenderedRun[];
|
|
253
|
+
line: VisualLine;
|
|
254
|
+
lineIndex: number;
|
|
255
|
+
ranges: MarkRange[];
|
|
256
|
+
marker: string;
|
|
257
|
+
baseFg: string;
|
|
258
|
+
baseAttributes: number;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const paintedSpan = (
|
|
262
|
+
run: StyledRun,
|
|
263
|
+
runIndex: number,
|
|
264
|
+
baseFg: string,
|
|
265
|
+
baseAttributes: number,
|
|
266
|
+
) => (
|
|
267
|
+
<span
|
|
268
|
+
key={runIndex}
|
|
269
|
+
fg={roleForeground(run.role, baseFg, tokens)}
|
|
270
|
+
bg={run.caretOnly ? palette.caretCell : run.marked ? palette.markBackdrop : undefined}
|
|
271
|
+
attributes={roleAttributes(run.role, baseAttributes) | (run.marked ? UNDERLINE : 0)}
|
|
272
|
+
>
|
|
273
|
+
{run.text}
|
|
274
|
+
</span>
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
const lineRowFor = (context: LineContext): React.ReactNode => {
|
|
278
|
+
const { blockIndex, roleRuns, line, lineIndex, ranges, marker, baseFg, baseAttributes } =
|
|
279
|
+
context;
|
|
280
|
+
|
|
281
|
+
return (
|
|
282
|
+
<box key={`line-${lineIndex}`} style={{ flexDirection: "row" }}>
|
|
283
|
+
<text selectable={false}>
|
|
284
|
+
<span fg={tokens.textDim}>{" "}</span>
|
|
285
|
+
<span fg={tokens.textDim}>{lineIndex === 0 ? marker : " ".repeat(marker.length)}</span>
|
|
286
|
+
</text>
|
|
287
|
+
<text
|
|
288
|
+
selectable={false}
|
|
289
|
+
style={{ flexGrow: 1, flexShrink: 1 }}
|
|
290
|
+
ref={surface.registerLine(blockIndex, lineIndex, line)}
|
|
291
|
+
onMouseDown={surface.onLineMouseDown}
|
|
292
|
+
>
|
|
293
|
+
{styledRunsFor(roleRuns, line, ranges).map((run, runIndex) =>
|
|
294
|
+
paintedSpan(run, runIndex, baseFg, baseAttributes),
|
|
295
|
+
)}
|
|
296
|
+
</text>
|
|
297
|
+
</box>
|
|
298
|
+
);
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// an h1 or h2 sits over a dim rule; h3 gets weight only. The row is
|
|
302
|
+
// always present for those kinds (only its width tracks the measured viewport) so a late width
|
|
303
|
+
// measurement never adds a row and shifts the blocks below it out from under a pending click
|
|
304
|
+
const headingRule = (block: DisplayBlock): React.ReactNode => {
|
|
305
|
+
if (block.kind !== "h1" && block.kind !== "h2") return null;
|
|
306
|
+
const width = Math.max(1, (viewWidth > 6 ? viewWidth : 46) - 6);
|
|
307
|
+
|
|
308
|
+
return (
|
|
309
|
+
<box key="heading-rule" style={{ flexDirection: "row" }}>
|
|
310
|
+
<text selectable={false} fg={tokens.textDim}>
|
|
311
|
+
{" " + "─".repeat(width)}
|
|
312
|
+
</text>
|
|
313
|
+
</box>
|
|
314
|
+
);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const blockNodeFor = (blockIndex: number): React.ReactNode => {
|
|
318
|
+
const block = display[blockIndex]!;
|
|
319
|
+
const { baseFg, baseAttributes, marker } = blockStyle(block, tokens);
|
|
320
|
+
// list items of one list stay tight; every other block sits a blank row below its neighbour
|
|
321
|
+
const previous = display[blockIndex - 1];
|
|
322
|
+
const tight =
|
|
323
|
+
previous !== undefined &&
|
|
324
|
+
block.kind === previous.kind &&
|
|
325
|
+
(block.kind === "li" || block.kind === "oli");
|
|
326
|
+
// the leading gap rides inside the measured box (not marginTop, which sits outside it) so the
|
|
327
|
+
// virtualizer's spacer math counts it
|
|
328
|
+
const leadingGap =
|
|
329
|
+
blockIndex === 0 || tight ? null : <box key="lead-gap" style={{ height: 1 }} />;
|
|
330
|
+
const measuredBox = (children: React.ReactNode): React.ReactNode => (
|
|
331
|
+
<box
|
|
332
|
+
key={`discussion-block-${blockIndex}`}
|
|
333
|
+
id={`discussion-block-${blockIndex}`}
|
|
334
|
+
ref={virtual.measureRef(blockIndex)}
|
|
335
|
+
style={{ flexDirection: "column" }}
|
|
336
|
+
>
|
|
337
|
+
{leadingGap}
|
|
338
|
+
{children}
|
|
339
|
+
</box>
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
if (isGridKind(block.kind)) {
|
|
343
|
+
// a grid annotates as one unit: every row hit-tests to the same block span, a mark tints the grid,
|
|
344
|
+
// and the block's discussion cards slot under it, so annotations resolving into a grid stay visible
|
|
345
|
+
const gridLine: VisualLine = { start: 0, end: renderedText(block).length };
|
|
346
|
+
const marked = surface.rangesFor(blockIndex).some((range) => !range.caretOnly);
|
|
347
|
+
|
|
348
|
+
return measuredBox(
|
|
349
|
+
<>
|
|
350
|
+
<MarkdownGridBlock
|
|
351
|
+
block={block}
|
|
352
|
+
theme={tokens}
|
|
353
|
+
contentWidth={viewWidth > 0 ? viewWidth - 6 : 40}
|
|
354
|
+
marked={marked}
|
|
355
|
+
markBackdrop={palette.markBackdrop}
|
|
356
|
+
registerRow={(rowIndex) => surface.registerLine(blockIndex, rowIndex, gridLine)}
|
|
357
|
+
onRowMouseDown={surface.onLineMouseDown}
|
|
358
|
+
/>
|
|
359
|
+
{surface.cardsAfterLine(blockIndex, gridLine, true)}
|
|
360
|
+
</>,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const roleRuns = renderedStyleRuns(block);
|
|
365
|
+
const ranges = surface.rangesFor(blockIndex);
|
|
366
|
+
const lines = wrapLines(renderedText(block), viewWidth > 0 ? viewWidth - marker.length - 6 : 0);
|
|
367
|
+
const lineRows: React.ReactNode[] = [];
|
|
368
|
+
|
|
369
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
370
|
+
const line = lines[lineIndex]!;
|
|
371
|
+
const isLastLine = lineIndex === lines.length - 1;
|
|
372
|
+
|
|
373
|
+
lineRows.push(
|
|
374
|
+
lineRowFor({
|
|
375
|
+
blockIndex,
|
|
376
|
+
roleRuns,
|
|
377
|
+
line,
|
|
378
|
+
lineIndex,
|
|
379
|
+
ranges,
|
|
380
|
+
marker,
|
|
381
|
+
baseFg,
|
|
382
|
+
baseAttributes,
|
|
383
|
+
}),
|
|
384
|
+
);
|
|
385
|
+
if (isLastLine) lineRows.push(headingRule(block));
|
|
386
|
+
const cards = surface.cardsAfterLine(blockIndex, line, isLastLine);
|
|
387
|
+
|
|
388
|
+
lineRows.push(...cards);
|
|
389
|
+
if (cards.length > 0 && !isLastLine) {
|
|
390
|
+
lineRows.push(<box key={`gap-${lineIndex}`} style={{ height: 1 }} />);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// a code block announces its language first
|
|
395
|
+
const languageRow =
|
|
396
|
+
block.kind === "code" ? (
|
|
397
|
+
<text key="language" fg={tokens.textDim} selectable={false}>
|
|
398
|
+
{` ${block.work?.lang ?? block.base?.lang ?? "code"}`}
|
|
399
|
+
</text>
|
|
400
|
+
) : null;
|
|
401
|
+
|
|
402
|
+
return measuredBox(
|
|
403
|
+
<>
|
|
404
|
+
{languageRow}
|
|
405
|
+
{lineRows}
|
|
406
|
+
</>,
|
|
407
|
+
);
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const virtualBlocks = (): React.ReactNode[] => {
|
|
411
|
+
const nodes: React.ReactNode[] = [];
|
|
412
|
+
const firstItem = virtual.items[0];
|
|
413
|
+
const lastItem = virtual.items.at(-1);
|
|
414
|
+
|
|
415
|
+
// the very first render has no measured viewport yet, so the virtualizer's window is empty; paint a
|
|
416
|
+
// viewport-worth from the top rather than blank or the whole document, then it takes over next frame
|
|
417
|
+
if (!firstItem || !lastItem) {
|
|
418
|
+
let mountedRows = 0;
|
|
419
|
+
|
|
420
|
+
for (let index = 0; index < display.length; index++) {
|
|
421
|
+
nodes.push(blockNodeFor(index));
|
|
422
|
+
mountedRows += estimateBlockRows(index);
|
|
423
|
+
if (mountedRows > terminalHeight + OVERSCAN_BLOCKS) break;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return nodes;
|
|
427
|
+
}
|
|
428
|
+
if (firstItem.start > 0) {
|
|
429
|
+
nodes.push(<box key="spacer-above" style={{ height: firstItem.start }} />);
|
|
430
|
+
}
|
|
431
|
+
for (const item of virtual.items) nodes.push(blockNodeFor(item.index));
|
|
432
|
+
const below = virtual.totalSize - lastItem.end;
|
|
433
|
+
|
|
434
|
+
if (below > 0) nodes.push(<box key="spacer-below" style={{ height: below }} />);
|
|
435
|
+
|
|
436
|
+
return nodes;
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
return (
|
|
440
|
+
<box style={{ flexGrow: 1, flexDirection: "row" }} {...surface.rootMouseProps}>
|
|
441
|
+
<box style={{ flexGrow: 1, flexDirection: "column" }}>
|
|
442
|
+
{editOrphanCount > 0 ? (
|
|
443
|
+
<box style={{ height: 1, backgroundColor: palette.markBackdrop, paddingLeft: 2 }}>
|
|
444
|
+
<text fg={tokens.red}>
|
|
445
|
+
{`${editOrphanCount} annotation${editOrphanCount === 1 ? "" : "s"} no longer match - the passage was removed.`}
|
|
446
|
+
</text>
|
|
447
|
+
</box>
|
|
448
|
+
) : null}
|
|
449
|
+
{/* the rail draws this box's scrollbar past the dots, so the built-in one stays hidden */}
|
|
450
|
+
<scrollbox
|
|
451
|
+
ref={scrollRef}
|
|
452
|
+
style={{ flexGrow: 1, paddingTop: 1 }}
|
|
453
|
+
focused={false}
|
|
454
|
+
verticalScrollbarOptions={{ visible: false }}
|
|
455
|
+
>
|
|
456
|
+
{virtualBlocks()}
|
|
457
|
+
</scrollbox>
|
|
458
|
+
{suspended ? null : (
|
|
459
|
+
<NavModeHint navMode={surface.navMode} surface="thread" theme={tokens} />
|
|
460
|
+
)}
|
|
461
|
+
</box>
|
|
462
|
+
<DiscussionMarkerRail
|
|
463
|
+
discussions={discussions}
|
|
464
|
+
spanQuote={surface.spanQuote}
|
|
465
|
+
focusedKey={surface.focusedDiscussion}
|
|
466
|
+
onJump={surface.jumpToDiscussion}
|
|
467
|
+
scrollbox={scrollRef}
|
|
468
|
+
theme={theme}
|
|
469
|
+
/>
|
|
470
|
+
</box>
|
|
471
|
+
);
|
|
472
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
};
|
|
13
|
+
|
|
14
|
+
export const LongMessage: Story = {
|
|
15
|
+
render: () => (
|
|
16
|
+
<Toast body="could not stop sharing: gateway revoke failed - the share may already be gone, try again in a moment" />
|
|
17
|
+
),
|
|
18
|
+
size: { width: 90, height: 14 },
|
|
19
|
+
};
|