@cueloop/client 0.1.0-alpha.10 → 0.1.0-alpha.101
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/annotations/annotation-palette.ts +62 -0
- package/src/annotations/components/AnnotationCards.stories.tsx +86 -0
- package/src/annotations/components/AnnotationCards.test.tsx +42 -0
- package/src/annotations/components/AnnotationCards.tsx +369 -0
- package/src/annotations/components/DiscussionMarkerRail.stories.tsx +54 -0
- package/src/annotations/components/DiscussionMarkerRail.tsx +186 -0
- package/src/annotations/discussions.ts +77 -0
- package/src/annotations/thread-selection.test.ts +210 -0
- package/src/annotations/thread-selection.ts +242 -0
- package/src/annotations/use-annotation-surface.tsx +1243 -0
- package/src/app/App.appearance.test.tsx +53 -0
- package/src/{App.autoclose.test.tsx → app/App.autoclose.test.tsx} +77 -51
- package/src/app/App.deleted-scroll.test.tsx +187 -0
- package/src/app/App.diff.test.tsx +313 -0
- package/src/app/App.export.test.tsx +105 -0
- package/src/app/App.focus.test.ts +89 -0
- package/src/app/App.identity.test.tsx +84 -0
- package/src/app/App.plan-render.test.tsx +209 -0
- package/src/app/App.plan-review.test.tsx +428 -0
- package/src/app/App.prototype-markdown.test.tsx +128 -0
- package/src/app/App.prototype.test.tsx +176 -0
- package/src/app/App.readonly.test.tsx +210 -0
- package/src/app/App.regression.test.tsx +99 -0
- package/src/app/App.rendering.test.tsx +134 -0
- package/src/app/App.share-dialog.test.tsx +177 -0
- package/src/app/App.submit-confirm.test.tsx +267 -0
- package/src/app/App.test.tsx +496 -0
- package/src/app/App.thread-agent.test.ts +20 -0
- package/src/app/App.toast.test.tsx +124 -0
- package/src/app/App.tsx +1667 -0
- package/src/app/App.visual.test.tsx +59 -0
- package/src/app/App.walk.test.tsx +299 -0
- package/src/app/App.workbench.test.tsx +1149 -0
- package/src/app/app-key-state.ts +61 -0
- package/src/app/app-screens.tsx +606 -0
- package/src/app/app-view-model.ts +168 -0
- package/src/app/components/AppHeader.stories.tsx +50 -0
- package/src/app/components/AppHeader.tsx +81 -0
- package/src/app/components/AppShell.brand.test.tsx +58 -0
- package/src/app/components/AppShell.extensions.test.tsx +191 -0
- package/src/app/components/AppShell.stories.tsx +89 -0
- package/src/app/components/AppShell.tsx +358 -0
- package/src/app/components/AppShell.zoom.test.tsx +149 -0
- package/src/app/components/Breadcrumb.stories.tsx +19 -0
- package/src/app/components/Breadcrumb.tsx +45 -0
- package/src/app/components/MenuBar.stories.tsx +28 -0
- package/src/app/components/MenuBar.tsx +44 -0
- package/src/app/components/PaneGrid.stories.tsx +84 -0
- package/src/app/components/PaneGrid.tsx +128 -0
- package/src/app/components/PanelColumn.stories.tsx +25 -0
- package/src/app/components/PanelColumn.tsx +93 -0
- package/src/app/components/ShellHeader.stories.tsx +50 -0
- package/src/app/components/ShellHeader.tsx +137 -0
- package/src/app/components/WorkspacePanels.stories.tsx +36 -0
- package/src/app/components/WorkspacePanels.tsx +120 -0
- package/src/app/components/pages/Workspace.stories.tsx +181 -0
- package/src/app/intent-dispatch.test.ts +728 -0
- package/src/app/intent-dispatch.ts +687 -0
- package/src/app/launch-layout.test.ts +78 -0
- package/src/app/launch-layout.ts +57 -0
- package/src/app/ready-signal.test.ts +85 -0
- package/src/app/ready-signal.ts +52 -0
- package/src/app/run.ts +109 -0
- package/src/{serve.test.ts → app/serve.test.ts} +24 -12
- package/src/app/serve.ts +111 -0
- package/src/app/version.ts +5 -0
- package/src/appearance/components/theme-context.stories.tsx +46 -0
- package/src/appearance/components/theme-context.tsx +37 -0
- package/src/appearance/theme-presets.test.ts +51 -0
- package/src/appearance/theme-presets.ts +219 -0
- package/src/appearance/theme.ts +112 -0
- package/src/appearance/visual/contrast.test.ts +35 -0
- package/src/appearance/visual/contrast.ts +33 -0
- package/src/diff/components/AnnotatableFileView.stories.tsx +42 -0
- package/src/diff/components/AnnotatableFileView.test.tsx +157 -0
- package/src/diff/components/AnnotatableFileView.tsx +141 -0
- package/src/diff/components/BareWorkbenchFileView.stories.tsx +27 -0
- package/src/diff/components/BareWorkbenchFileView.tsx +63 -0
- package/src/diff/components/DiffContentView.scroll.test.tsx +434 -0
- package/src/diff/components/DiffContentView.split.test.tsx +140 -0
- package/src/diff/components/DiffContentView.stories.tsx +80 -0
- package/src/diff/components/DiffContentView.syntax.test.tsx +82 -0
- package/src/diff/components/DiffContentView.tsx +1055 -0
- package/src/diff/components/WalkWizard.stories.tsx +100 -0
- package/src/diff/components/WalkWizard.tsx +146 -0
- package/src/diff/components/diff-content-view-layout.test.ts +198 -0
- package/src/diff/components/diff-content-view-layout.ts +84 -0
- package/src/diff/diff-fold.test.ts +78 -0
- package/src/diff/diff-fold.ts +126 -0
- package/src/diff/diff-hunk-curate.test.ts +315 -0
- package/src/diff/diff-hunk-curate.ts +152 -0
- package/src/diff/diff-intraline.test.ts +239 -0
- package/src/diff/diff-intraline.ts +238 -0
- package/src/diff/diff-syntax.test.ts +125 -0
- package/src/diff/diff-syntax.ts +186 -0
- package/src/diff/split-diff.test.ts +110 -0
- package/src/diff/split-diff.ts +144 -0
- package/src/diff/view-diff.test.ts +325 -0
- package/src/diff/view-diff.ts +522 -0
- package/src/diff/walk.test.ts +121 -0
- package/src/diff/walk.ts +73 -0
- package/src/index.ts +31 -2
- package/src/integrations/attribution.test.ts +31 -0
- package/src/integrations/attribution.ts +42 -0
- package/src/integrations/client-extension-registry.test.tsx +61 -0
- package/src/integrations/client-extension-registry.ts +159 -0
- package/src/integrations/clipboard.test.ts +23 -0
- package/src/integrations/clipboard.ts +33 -0
- package/src/integrations/components/ExtensionZone.stories.tsx +28 -0
- package/src/integrations/components/ExtensionZone.tsx +145 -0
- package/src/integrations/components/ShareDialog.agent.test.tsx +50 -0
- package/src/integrations/components/ShareDialog.stories.tsx +121 -0
- package/src/integrations/components/ShareDialog.tsx +574 -0
- package/src/integrations/components/share-dialog-store.ts +59 -0
- package/src/integrations/editor.test.ts +149 -0
- package/src/integrations/editor.ts +160 -0
- package/src/integrations/github-identity.test.ts +58 -0
- package/src/integrations/github-identity.ts +41 -0
- package/src/integrations/herdr.test.ts +43 -0
- package/src/integrations/herdr.ts +38 -0
- package/src/integrations/integrations.test.ts +41 -0
- package/src/integrations/integrations.ts +32 -0
- package/src/integrations/pasted-image.test.ts +67 -0
- package/src/integrations/pasted-image.ts +60 -0
- package/src/integrations/share.ts +238 -0
- package/src/keyboard/components/CompletionOverlay.stories.tsx +37 -0
- package/src/keyboard/components/CompletionOverlay.tsx +75 -0
- package/src/keyboard/components/HandleChipsInput.stories.tsx +24 -0
- package/src/keyboard/components/HandleChipsInput.test.tsx +35 -0
- package/src/keyboard/components/HandleChipsInput.tsx +143 -0
- package/src/keyboard/components/KeybindsDialog.stories.tsx +31 -0
- package/src/keyboard/components/KeybindsDialog.tsx +103 -0
- package/src/keyboard/components/NavModeHint.stories.tsx +40 -0
- package/src/keyboard/components/NavModeHint.tsx +31 -0
- package/src/keyboard/components/SlashComposer.stories.tsx +37 -0
- package/src/keyboard/components/SlashComposer.tsx +87 -0
- package/src/keyboard/key-bindings.test.ts +156 -0
- package/src/keyboard/key-bindings.ts +459 -0
- package/src/keyboard/keymap.test.ts +611 -0
- package/src/keyboard/keymap.ts +395 -0
- package/src/keyboard/skills.test.ts +74 -0
- package/src/keyboard/skills.ts +75 -0
- package/src/keyboard/slash-palette.test.ts +103 -0
- package/src/keyboard/slash-palette.ts +129 -0
- package/src/keyboard/thread-chords.test.ts +79 -0
- package/src/keyboard/thread-chords.ts +163 -0
- package/src/keyboard/use-shared-keyboard.ts +55 -0
- package/src/markdown/__snapshots__/view-plan.regression.test.ts.snap +100 -0
- package/src/markdown/components/MarkdownGridBlock.stories.tsx +33 -0
- package/src/markdown/components/MarkdownGridBlock.tsx +72 -0
- package/src/markdown/components/MarkdownThreadEditor.stories.tsx +15 -0
- package/src/markdown/components/MarkdownThreadEditor.test.tsx +98 -0
- package/src/markdown/components/MarkdownThreadEditor.tsx +160 -0
- package/src/markdown/components/ThreadView.selection.test.tsx +951 -0
- package/src/markdown/components/ThreadView.slash.test.tsx +146 -0
- package/src/markdown/components/ThreadView.stories.tsx +26 -0
- package/src/markdown/components/ThreadView.tsx +560 -0
- package/src/markdown/mark-runs.ts +121 -0
- package/src/markdown/markdown-editor-syntax-style.ts +50 -0
- package/src/markdown/markdown-grid.test.ts +101 -0
- package/src/markdown/markdown-grid.ts +203 -0
- package/src/markdown/markdown-highlight.test.ts +61 -0
- package/src/markdown/markdown-highlight.ts +124 -0
- package/src/markdown/thread-code-syntax.ts +45 -0
- package/src/markdown/use-thread-body-editing.tsx +54 -0
- package/src/markdown/view-plan.regression.test.ts +60 -0
- package/src/markdown/view-plan.test.ts +545 -0
- package/src/markdown/view-plan.ts +1009 -0
- package/src/performance/otlp-trace.test.ts +119 -0
- package/src/performance/otlp-trace.ts +134 -0
- package/src/performance/perf-report.ts +18 -0
- package/src/performance/perf-timings.test.ts +18 -0
- package/src/performance/perf-timings.ts +39 -0
- package/src/prototype/components/PrototypeContentView.stories.tsx +19 -0
- package/src/prototype/components/PrototypeContentView.tsx +442 -0
- package/src/prototype/prototype-browser.test.ts +81 -0
- package/src/prototype/prototype-browser.ts +321 -0
- package/src/prototype/prototype-pixels.tsx +35 -0
- package/src/settings/components/SettingsDialog.stories.tsx +117 -0
- package/src/settings/components/SettingsDialog.tsx +183 -0
- package/src/settings/components/SettingsRows.stories.tsx +18 -0
- package/src/settings/components/SettingsRows.tsx +112 -0
- package/src/settings/components/quick-actions-editor.stories.tsx +58 -0
- package/src/settings/components/quick-actions-editor.test.tsx +120 -0
- package/src/settings/components/quick-actions-editor.tsx +222 -0
- package/src/settings/config-identity.test.ts +78 -0
- package/src/settings/config.test.ts +676 -0
- package/src/settings/config.ts +628 -0
- package/src/settings/use-settings-dialog.tsx +351 -0
- package/src/stories/__snapshots__/stories.test.tsx.snap +2358 -0
- package/src/stories/stories-app.tsx +196 -0
- package/src/stories/stories.test.tsx +74 -0
- package/src/stories/story-fixtures.ts +102 -0
- package/src/stories/story-tree.test.ts +108 -0
- package/src/stories/story.ts +154 -0
- package/src/terminal/ansi-screen.test.ts +28 -0
- package/src/terminal/ansi-screen.ts +72 -0
- package/src/terminal/components/AnsiScreen.stories.tsx +12 -0
- package/src/terminal/components/AnsiScreen.tsx +28 -0
- package/src/terminal/components/terminal-pane.test.tsx +40 -0
- package/src/terminal/components/terminal-pane.ts +196 -0
- package/src/terminal/ghostty-terminal.test.ts +85 -0
- package/src/terminal/ghostty-terminal.ts +197 -0
- package/src/terminal/kitty-image.test.ts +66 -0
- package/src/terminal/kitty-image.ts +137 -0
- package/src/terminal/pty.test.ts +81 -0
- package/src/terminal/pty.ts +246 -0
- package/src/testing/test-support.test.ts +264 -0
- package/src/testing/test-support.ts +367 -0
- package/src/thread/agent-continuation.test.ts +32 -0
- package/src/thread/agent-continuation.ts +13 -0
- package/src/thread/agent-transcript.test.ts +161 -0
- package/src/thread/agent-transcript.ts +336 -0
- package/src/thread/components/AgentConfigControls.stories.tsx +39 -0
- package/src/thread/components/AgentConfigControls.test.tsx +112 -0
- package/src/thread/components/AgentConfigControls.tsx +114 -0
- package/src/thread/components/AgentThreadPane.stories.tsx +105 -0
- package/src/thread/components/AgentThreadPane.test.tsx +935 -0
- package/src/thread/components/AgentThreadPane.tsx +377 -0
- package/src/thread/components/ThreadFooter.stories.tsx +30 -0
- package/src/thread/components/ThreadFooter.test.tsx +49 -0
- package/src/thread/components/ThreadFooter.tsx +77 -0
- package/src/thread/components/ThreadTree.stories.tsx +46 -0
- package/src/thread/components/ThreadTree.test.tsx +41 -0
- package/src/thread/components/ThreadTree.tsx +267 -0
- package/src/thread/components/thread-tree.test.ts +137 -0
- package/src/thread/components/thread-tree.ts +112 -0
- package/src/thread/thread-controller.curate.test.ts +502 -0
- package/src/thread/thread-controller.derivation.test.ts +62 -0
- package/src/thread/thread-controller.live-diff.test.ts +357 -0
- package/src/thread/thread-controller.optimistic.test.ts +69 -0
- package/src/thread/thread-controller.share.test.ts +573 -0
- package/src/thread/thread-controller.switch-cache.test.ts +174 -0
- package/src/thread/thread-controller.tree.test.ts +263 -0
- package/src/thread/thread-controller.ts +2026 -0
- package/src/thread/tree-view.test.ts +128 -0
- package/src/thread/tree-view.ts +146 -0
- package/src/thread/use-thread-agent.test.tsx +208 -0
- package/src/thread/use-thread-agent.ts +117 -0
- package/src/ui/components/CodeBlock.stories.tsx +42 -0
- package/src/ui/components/CodeBlock.tsx +80 -0
- package/src/ui/components/ConfirmCard.stories.tsx +37 -0
- package/src/ui/components/ConfirmCard.tsx +151 -0
- package/src/ui/components/ConfirmDialog.stories.tsx +21 -0
- package/src/ui/components/ConfirmDialog.tsx +72 -0
- package/src/ui/components/OverlayScrollbar.stories.tsx +33 -0
- package/src/ui/components/OverlayScrollbar.test.tsx +139 -0
- package/src/ui/components/OverlayScrollbar.tsx +129 -0
- package/src/ui/components/PromptDialog.stories.tsx +36 -0
- package/src/ui/components/PromptDialog.test.tsx +65 -0
- package/src/ui/components/PromptDialog.tsx +99 -0
- package/src/ui/components/RootOverlay.stories.tsx +45 -0
- package/src/ui/components/RootOverlay.tsx +49 -0
- package/src/ui/components/ScrollArea.stories.tsx +20 -0
- package/src/ui/components/ScrollArea.tsx +61 -0
- package/src/ui/components/Toast.stories.tsx +19 -0
- package/src/ui/components/Toast.tsx +54 -0
- package/src/ui/components/Tooltip.stories.tsx +26 -0
- package/src/ui/components/Tooltip.tsx +72 -0
- package/src/ui/components/menu-control.stories.tsx +35 -0
- package/src/ui/components/menu-control.tsx +43 -0
- package/src/ui/components/primitives/Button.stories.tsx +32 -0
- package/src/ui/components/primitives/Button.tsx +57 -0
- package/src/ui/components/primitives/Card.stories.tsx +25 -0
- package/src/ui/components/primitives/Card.tsx +62 -0
- package/src/ui/components/primitives/Dialog.stories.tsx +24 -0
- package/src/ui/components/primitives/Dialog.test.tsx +38 -0
- package/src/ui/components/primitives/Dialog.tsx +69 -0
- package/src/ui/components/primitives/DialogActions.stories.tsx +20 -0
- package/src/ui/components/primitives/DialogActions.tsx +39 -0
- package/src/ui/components/primitives/IconButton.stories.tsx +36 -0
- package/src/ui/components/primitives/IconButton.tsx +58 -0
- package/src/ui/components/primitives/StatusBar.stories.tsx +11 -0
- package/src/ui/components/primitives/StatusBar.tsx +23 -0
- package/src/ui/components/primitives/Switch.stories.tsx +17 -0
- package/src/ui/components/primitives/Switch.tsx +32 -0
- package/src/ui/components/primitives/Tabs.stories.tsx +29 -0
- package/src/ui/components/primitives/Tabs.tsx +101 -0
- package/src/ui/components/primitives/Toolbar.stories.tsx +17 -0
- package/src/ui/components/primitives/Toolbar.tsx +14 -0
- package/src/ui/components/primitives/Tree.stories.tsx +124 -0
- package/src/ui/components/primitives/Tree.tsx +133 -0
- package/src/ui/components/primitives/frame.test.ts +12 -0
- package/src/ui/components/primitives/frame.ts +17 -0
- package/src/ui/components/primitives/icons.ts +61 -0
- package/src/ui/components/primitives/tree-model.test.ts +99 -0
- package/src/ui/components/primitives/tree-model.ts +137 -0
- package/src/ui/components/syntax-highlight.ts +133 -0
- package/src/ui/components/truncate-text.ts +7 -0
- package/src/ui/components/truncate-title.test.ts +31 -0
- package/src/ui/components/truncate-title.ts +15 -0
- package/src/ui/use-frame-measure.ts +74 -0
- package/src/ui/use-terminal-virtualizer.ts +259 -0
- package/src/workbench/components/ChangesColumn.stories.tsx +44 -0
- package/src/workbench/components/ChangesColumn.tsx +203 -0
- package/src/workbench/components/ChangesFileTree.test.tsx +181 -0
- package/src/workbench/components/EditorGrid.split.test.tsx +118 -0
- package/src/workbench/components/EditorGrid.stories.tsx +86 -0
- package/src/workbench/components/EditorGrid.tabs.test.tsx +118 -0
- package/src/workbench/components/EditorGrid.tsx +544 -0
- package/src/workbench/components/EditorGrid.zoom.test.tsx +132 -0
- package/src/workbench/components/FileContentsView.stories.tsx +16 -0
- package/src/workbench/components/FileContentsView.test.tsx +29 -0
- package/src/workbench/components/FileContentsView.tsx +100 -0
- package/src/workbench/components/GridTabContent.actions.test.tsx +151 -0
- package/src/workbench/components/GridTabContent.stories.tsx +59 -0
- package/src/workbench/components/GridTabContent.tsx +193 -0
- package/src/workbench/components/ProjectTreeView.stories.tsx +25 -0
- package/src/workbench/components/ProjectTreeView.test.tsx +236 -0
- package/src/workbench/components/ProjectTreeView.tsx +151 -0
- package/src/workbench/components/TreePane.stories.tsx +106 -0
- package/src/workbench/components/TreePane.tsx +155 -0
- package/src/workbench/components/WelcomePlayground.stories.tsx +12 -0
- package/src/workbench/components/WelcomePlayground.test.tsx +83 -0
- package/src/workbench/components/WelcomePlayground.tsx +138 -0
- package/src/workbench/components/editor-grid.test.ts +184 -0
- package/src/workbench/components/editor-grid.ts +281 -0
- package/src/workbench/components/file-tree.test.ts +60 -0
- package/src/workbench/components/file-tree.ts +112 -0
- package/src/workbench/use-changes-workbench.ts +205 -0
- package/src/workbench/use-remember-layout.ts +31 -0
- package/src/workbench/use-repo-changes.ts +91 -0
- package/src/workbench/workbench-snapshot.test.ts +99 -0
- package/src/workbench/workbench-snapshot.ts +59 -0
- package/src/App.diff.test.tsx +0 -99
- package/src/App.export.test.tsx +0 -88
- package/src/App.plan-review.test.tsx +0 -259
- package/src/App.readonly.test.tsx +0 -148
- package/src/App.rendering.test.tsx +0 -108
- package/src/App.submit-confirm.test.tsx +0 -183
- package/src/App.test.tsx +0 -218
- package/src/App.tsx +0 -1249
- package/src/config.test.ts +0 -116
- package/src/config.ts +0 -150
- package/src/editor.ts +0 -34
- package/src/herdr.test.ts +0 -68
- package/src/herdr.ts +0 -34
- package/src/keymap.test.ts +0 -270
- package/src/keymap.ts +0 -173
- package/src/run.ts +0 -30
- package/src/serve.ts +0 -75
- package/src/session-controller.ts +0 -398
- package/src/syntax.test.ts +0 -41
- package/src/syntax.ts +0 -88
- package/src/theme.ts +0 -46
- package/src/view-diff.test.ts +0 -55
- package/src/view-diff.ts +0 -86
- package/src/view.test.ts +0 -157
- package/src/view.ts +0 -364
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// A project file shown as its own annotatable surface: the file's lines render through the diff
|
|
2
|
+
// sheet as context rows (one line-number gutter, no +/- sign), so selecting text leaves a comment
|
|
3
|
+
// anchored to the file exactly as a plan or diff comment is. Used where a thread exists to
|
|
4
|
+
// hold the notes; the bare-launch welcome shell uses the read-only FileContentsView instead.
|
|
5
|
+
|
|
6
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
7
|
+
import {
|
|
8
|
+
makeAnchor,
|
|
9
|
+
annotationTarget,
|
|
10
|
+
type Anchor,
|
|
11
|
+
type Annotation,
|
|
12
|
+
type Thread,
|
|
13
|
+
} from "@cueloop/schema";
|
|
14
|
+
import type { Theme } from "../../appearance/theme";
|
|
15
|
+
import type { QuickAction } from "../../settings/config";
|
|
16
|
+
import { useComponentTheme } from "../../appearance/components/theme-context";
|
|
17
|
+
import { DiffContentView } from "./DiffContentView";
|
|
18
|
+
import { diffRowBlocks, fileContentsRows, marksByRows } from "../view-diff";
|
|
19
|
+
|
|
20
|
+
export interface AnnotatableFileViewProps {
|
|
21
|
+
path: string;
|
|
22
|
+
loadContents: (path: string) => Promise<string | null>;
|
|
23
|
+
session: Thread;
|
|
24
|
+
quickActions: QuickAction[];
|
|
25
|
+
observer: boolean;
|
|
26
|
+
resolved?: boolean;
|
|
27
|
+
suspended?: boolean;
|
|
28
|
+
focusedAnnotationId?: string;
|
|
29
|
+
onFocusAnnotation?: (annotationId: string | undefined) => void;
|
|
30
|
+
onComposingChange?: (composing: boolean) => void;
|
|
31
|
+
onCursorChange?: (rowIndex: number) => void;
|
|
32
|
+
onObserverBlocked?: (reason: "observer" | "resolved") => void;
|
|
33
|
+
onAddComment: (anchor: Anchor, body: string) => void;
|
|
34
|
+
onReply: (rootAnnotationId: string, body: string) => void;
|
|
35
|
+
onUpdateAnnotation: (id: string, body: string) => void;
|
|
36
|
+
onInvoke?: () => void;
|
|
37
|
+
annotationAction?: (id: string) => { label: string; run: () => void } | undefined;
|
|
38
|
+
isAnnotationReadOnly?: (id: string) => boolean;
|
|
39
|
+
resolveAuthorLabel?: (annotation: Annotation) => string | undefined;
|
|
40
|
+
onExit: () => void;
|
|
41
|
+
theme?: Theme;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A finished read for a specific path; `lines` is null when the file could not be read. */
|
|
45
|
+
interface FileLoad {
|
|
46
|
+
path: string;
|
|
47
|
+
lines: string[] | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function AnnotatableFileView(props: AnnotatableFileViewProps): React.ReactNode {
|
|
51
|
+
const tokens = useComponentTheme(props.theme);
|
|
52
|
+
const [loaded, setLoaded] = useState<FileLoad | null>(null);
|
|
53
|
+
const loadRef = useRef(props.loadContents);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
loadRef.current = props.loadContents;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
let alive = true;
|
|
61
|
+
|
|
62
|
+
void loadRef.current(props.path).then(
|
|
63
|
+
(contents) => {
|
|
64
|
+
if (alive)
|
|
65
|
+
setLoaded({ path: props.path, lines: contents === null ? null : contents.split("\n") });
|
|
66
|
+
},
|
|
67
|
+
() => {
|
|
68
|
+
if (alive) setLoaded({ path: props.path, lines: null });
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
return () => {
|
|
73
|
+
alive = false;
|
|
74
|
+
};
|
|
75
|
+
}, [props.path]);
|
|
76
|
+
|
|
77
|
+
if (loaded === null || loaded.path !== props.path) {
|
|
78
|
+
return (
|
|
79
|
+
<box style={{ flexGrow: 1, paddingLeft: 1, paddingTop: 1 }}>
|
|
80
|
+
<text fg={tokens.textDim}>loading...</text>
|
|
81
|
+
</box>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
if (loaded.lines === null) {
|
|
85
|
+
return (
|
|
86
|
+
<box
|
|
87
|
+
style={{ flexGrow: 1, alignItems: "center", justifyContent: "center", paddingBottom: 2 }}
|
|
88
|
+
>
|
|
89
|
+
<text fg={tokens.textDim}>File deleted</text>
|
|
90
|
+
</box>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const rows = fileContentsRows(props.path, loaded.lines.join("\n"));
|
|
95
|
+
// the file's worktree notes: these rows are the current file, so a head-side (deleted-line) note
|
|
96
|
+
// must stay orphaned here rather than rebind to similar surviving text
|
|
97
|
+
const fileNotes = props.session.annotations.filter((annotation) => {
|
|
98
|
+
const target = annotationTarget(annotation);
|
|
99
|
+
|
|
100
|
+
return target.kind === "file" && target.path === props.path && target.rev === "worktree";
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<DiffContentView
|
|
105
|
+
rows={rows}
|
|
106
|
+
session={props.session}
|
|
107
|
+
marks={marksByRows(fileNotes, rows, props.focusedAnnotationId)}
|
|
108
|
+
quickActions={props.quickActions}
|
|
109
|
+
observer={props.observer}
|
|
110
|
+
commentsEnabled
|
|
111
|
+
resolved={props.resolved}
|
|
112
|
+
suspended={props.suspended}
|
|
113
|
+
fileView
|
|
114
|
+
onComposingChange={props.onComposingChange}
|
|
115
|
+
onObserverBlocked={props.onObserverBlocked}
|
|
116
|
+
onCursorChange={props.onCursorChange}
|
|
117
|
+
focusedAnnotationId={props.focusedAnnotationId}
|
|
118
|
+
onFocusAnnotation={props.onFocusAnnotation}
|
|
119
|
+
onAnnotate={(span, body) =>
|
|
120
|
+
props.onAddComment(
|
|
121
|
+
makeAnchor(
|
|
122
|
+
diffRowBlocks(rows),
|
|
123
|
+
span.start.blockIndex,
|
|
124
|
+
span.start.char,
|
|
125
|
+
span.end.char,
|
|
126
|
+
span.end.blockIndex,
|
|
127
|
+
),
|
|
128
|
+
body,
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
onReply={props.onReply}
|
|
132
|
+
onUpdateAnnotation={props.onUpdateAnnotation}
|
|
133
|
+
onInvoke={props.onInvoke}
|
|
134
|
+
isAnnotationReadOnly={props.isAnnotationReadOnly}
|
|
135
|
+
annotationAction={props.annotationAction}
|
|
136
|
+
resolveAuthorLabel={props.resolveAuthorLabel}
|
|
137
|
+
onExit={props.onExit}
|
|
138
|
+
theme={props.theme}
|
|
139
|
+
/>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../../appearance/theme";
|
|
3
|
+
import type { Story, StoryMeta } from "../../stories/story";
|
|
4
|
+
import { BareWorkbenchFileView, type WorkbenchCommenter } from "./BareWorkbenchFileView";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "Surfaces/BareWorkbenchFileView" };
|
|
7
|
+
|
|
8
|
+
const SAMPLE = `export function add(a: number, b: number): number {\n return a + b;\n}\n`;
|
|
9
|
+
|
|
10
|
+
const controller: WorkbenchCommenter = {
|
|
11
|
+
repoReadFile: () => Promise.resolve(SAMPLE),
|
|
12
|
+
commentOnWorkbench: () => Promise.resolve(),
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const Loaded: Story = {
|
|
16
|
+
render: () => (
|
|
17
|
+
<BareWorkbenchFileView
|
|
18
|
+
path="src/add.ts"
|
|
19
|
+
controller={controller}
|
|
20
|
+
quickActions={[]}
|
|
21
|
+
onExit={() => {}}
|
|
22
|
+
theme={DARK}
|
|
23
|
+
/>
|
|
24
|
+
),
|
|
25
|
+
expectedColors: [DARK.textDim],
|
|
26
|
+
size: { width: 60, height: 12 },
|
|
27
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// A project file in the bare launch shell: annotatable like any file, but no thread exists yet. The
|
|
2
|
+
// first comment find-or-creates the per-repo workbench thread (controller.commentOnWorkbench), which
|
|
3
|
+
// opens it - so this ephemeral view lives only until that first note promotes it to a real thread.
|
|
4
|
+
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { SCHEMA_VERSION, type Anchor, type AnnotationTarget, type Thread } from "@cueloop/schema";
|
|
7
|
+
import type { Theme } from "../../appearance/theme";
|
|
8
|
+
import type { QuickAction } from "../../settings/config";
|
|
9
|
+
import { AnnotatableFileView } from "./AnnotatableFileView";
|
|
10
|
+
|
|
11
|
+
/** The controller slice this view needs: read a file, and route a comment to the workbench thread. */
|
|
12
|
+
export interface WorkbenchCommenter {
|
|
13
|
+
repoReadFile(path: string): Promise<string | null>;
|
|
14
|
+
commentOnWorkbench(anchor: Anchor, target: AnnotationTarget, body: string): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** An empty, unsaved thread: it holds no notes, just enough shape for the annotation surface. */
|
|
18
|
+
export function draftThread(): Thread {
|
|
19
|
+
return {
|
|
20
|
+
schemaVersion: SCHEMA_VERSION,
|
|
21
|
+
id: "workbench-draft",
|
|
22
|
+
workspace: { repoRoot: "", branch: "main" },
|
|
23
|
+
artifact: { type: "diff", content: "", meta: {} },
|
|
24
|
+
revisions: [],
|
|
25
|
+
annotations: [],
|
|
26
|
+
message: null,
|
|
27
|
+
status: "pending",
|
|
28
|
+
createdAt: "1970-01-01T00:00:00.000Z",
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface BareWorkbenchFileViewProps {
|
|
33
|
+
path: string;
|
|
34
|
+
controller: WorkbenchCommenter;
|
|
35
|
+
quickActions: QuickAction[];
|
|
36
|
+
onComposingChange?: (composing: boolean) => void;
|
|
37
|
+
onExit: () => void;
|
|
38
|
+
theme?: Theme;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function BareWorkbenchFileView(props: BareWorkbenchFileViewProps): React.ReactNode {
|
|
42
|
+
return (
|
|
43
|
+
<AnnotatableFileView
|
|
44
|
+
path={props.path}
|
|
45
|
+
loadContents={(path) => props.controller.repoReadFile(path)}
|
|
46
|
+
session={draftThread()}
|
|
47
|
+
quickActions={props.quickActions}
|
|
48
|
+
observer={false}
|
|
49
|
+
onComposingChange={props.onComposingChange}
|
|
50
|
+
onAddComment={(anchor: Anchor, body: string) =>
|
|
51
|
+
void props.controller.commentOnWorkbench(
|
|
52
|
+
anchor,
|
|
53
|
+
{ kind: "file", path: props.path, rev: "worktree" },
|
|
54
|
+
body,
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
onReply={() => {}}
|
|
58
|
+
onUpdateAnnotation={() => {}}
|
|
59
|
+
onExit={props.onExit}
|
|
60
|
+
theme={props.theme}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
/** Keyboard scroll is smooth: the caret-follow reveal never scrolls the caret off screen, nor rebounds. */
|
|
2
|
+
|
|
3
|
+
import { expect, test } from "bun:test";
|
|
4
|
+
import React from "react";
|
|
5
|
+
import { testRender } from "@opentui/react/test-utils";
|
|
6
|
+
import { ScrollBoxRenderable, type Renderable } from "@opentui/core";
|
|
7
|
+
import type { Annotation } from "@cueloop/schema";
|
|
8
|
+
import { DiffContentView } from "./DiffContentView";
|
|
9
|
+
import { diffRows, marksByRows } from "../view-diff";
|
|
10
|
+
import { DARK } from "../../appearance/theme";
|
|
11
|
+
import { annotationPaletteFor } from "../../annotations/annotation-palette";
|
|
12
|
+
import { press, settle, typeText } from "../../testing/test-support";
|
|
13
|
+
import { fixtureDiffSession } from "../../stories/story-fixtures";
|
|
14
|
+
|
|
15
|
+
const noop = (): void => {};
|
|
16
|
+
|
|
17
|
+
/** Find a renderable by id anywhere in the tree (getRenderable is not recursive). */
|
|
18
|
+
function findById(node: Renderable, id: string): Renderable | undefined {
|
|
19
|
+
if (node.id === id) return node;
|
|
20
|
+
for (const child of node.getChildren()) {
|
|
21
|
+
const found = findById(child, id);
|
|
22
|
+
|
|
23
|
+
if (found) return found;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function hex(color: { toInts(): [number, number, number, number] }): string {
|
|
30
|
+
const [red, green, blue] = color.toInts();
|
|
31
|
+
|
|
32
|
+
return "#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join("");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A single-hunk patch of `count` added lines, taller than any test viewport. */
|
|
36
|
+
function tallPatch(count: number): string {
|
|
37
|
+
const adds = Array.from(
|
|
38
|
+
{ length: count },
|
|
39
|
+
(_, index) => `+line ${String(index).padStart(2, "0")}`,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
return [
|
|
43
|
+
"diff --git a/f.ts b/f.ts",
|
|
44
|
+
"--- a/f.ts",
|
|
45
|
+
"+++ b/f.ts",
|
|
46
|
+
`@@ -0,0 +1,${count} @@`,
|
|
47
|
+
...adds,
|
|
48
|
+
"",
|
|
49
|
+
].join("\n");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
test("walking the caret down past wrapped discussion cards keeps it on screen and the scroll monotonic", async () => {
|
|
53
|
+
// Arrange - a tall diff whose comment bodies wrap at this width, so the cards
|
|
54
|
+
// between rows are taller than one line
|
|
55
|
+
const rows = diffRows(tallPatch(40));
|
|
56
|
+
const longBody =
|
|
57
|
+
"this is a deliberately long annotation body that would wrap across the narrow test viewport";
|
|
58
|
+
const annotations: Annotation[] = [8, 16, 24].map((rowIndex) => ({
|
|
59
|
+
id: `a${rowIndex}`,
|
|
60
|
+
kind: "comment",
|
|
61
|
+
body: longBody,
|
|
62
|
+
anchor: { quote: rows[rowIndex]!.text.replace(/\n$/, ""), prefix: "", suffix: "" },
|
|
63
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
64
|
+
}));
|
|
65
|
+
const setup = await testRender(
|
|
66
|
+
<DiffContentView
|
|
67
|
+
rows={rows}
|
|
68
|
+
session={fixtureDiffSession({ annotations })}
|
|
69
|
+
marks={marksByRows(annotations, rows)}
|
|
70
|
+
quickActions={[]}
|
|
71
|
+
observer={false}
|
|
72
|
+
onAnnotate={noop}
|
|
73
|
+
onReply={noop}
|
|
74
|
+
onUpdateAnnotation={noop}
|
|
75
|
+
onExit={noop}
|
|
76
|
+
/>,
|
|
77
|
+
{ width: 60, height: 12 },
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
await setup.waitForVisualIdle();
|
|
81
|
+
const found = findById(setup.renderer.root, "diff-scroll");
|
|
82
|
+
|
|
83
|
+
if (!(found instanceof ScrollBoxRenderable)) throw new Error("diff-scroll is not a scrollbox");
|
|
84
|
+
const scrollbox = found;
|
|
85
|
+
const caretCell = annotationPaletteFor(DARK).caretCell;
|
|
86
|
+
|
|
87
|
+
// the caret row is the one painting the caret cell
|
|
88
|
+
const caretScreenRow = (): number => {
|
|
89
|
+
const lines = setup.captureSpans().lines;
|
|
90
|
+
|
|
91
|
+
for (let index = 0; index < lines.length; index++) {
|
|
92
|
+
if (lines[index]!.spans.some((span) => hex(span.bg) === caretCell)) return index;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return -1;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Act - walk the caret down through every code row, sampling the scroll each step
|
|
99
|
+
const scrollTops: number[] = [];
|
|
100
|
+
const screenRows: number[] = [];
|
|
101
|
+
const codeRowCount = rows.filter((row) => row.kind === "add").length;
|
|
102
|
+
|
|
103
|
+
for (let step = 0; step < codeRowCount; step++) {
|
|
104
|
+
await setup.waitForVisualIdle();
|
|
105
|
+
scrollTops.push(scrollbox.scrollTop);
|
|
106
|
+
screenRows.push(caretScreenRow());
|
|
107
|
+
await press(setup, "down");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// A key advances the viewport by at most one visual line, even across a tall card.
|
|
111
|
+
for (let step = 1; step < scrollTops.length; step++) {
|
|
112
|
+
expect(scrollTops[step]!).toBeGreaterThanOrEqual(scrollTops[step - 1]!);
|
|
113
|
+
expect(scrollTops[step]! - scrollTops[step - 1]!).toBeLessThanOrEqual(1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Walk back through the same cards: upward reveal must also stay one row at a time.
|
|
117
|
+
const upwardTops: number[] = [];
|
|
118
|
+
|
|
119
|
+
for (let step = 0; step < codeRowCount + 20; step++) {
|
|
120
|
+
upwardTops.push(scrollbox.scrollTop);
|
|
121
|
+
screenRows.push(caretScreenRow());
|
|
122
|
+
await press(setup, "up");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
for (let step = 1; step < upwardTops.length; step++) {
|
|
126
|
+
expect(upwardTops[step]!).toBeLessThanOrEqual(upwardTops[step - 1]!);
|
|
127
|
+
expect(upwardTops[step - 1]! - upwardTops[step]!).toBeLessThanOrEqual(1);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Assert - the caret never scrolls off screen while it walks past each wrapped card
|
|
131
|
+
expect(screenRows.every((row) => row >= 0)).toBe(true);
|
|
132
|
+
// and the walk did scroll: the tall diff does not fit the viewport
|
|
133
|
+
expect(scrollTops[scrollTops.length - 1]!).toBeGreaterThan(0);
|
|
134
|
+
|
|
135
|
+
setup.renderer.destroy();
|
|
136
|
+
}, 25000);
|
|
137
|
+
|
|
138
|
+
test("wheel input accelerates a continuous gesture and settles back to precise steps", async () => {
|
|
139
|
+
const rows = diffRows(tallPatch(80));
|
|
140
|
+
const setup = await testRender(
|
|
141
|
+
<DiffContentView
|
|
142
|
+
rows={rows}
|
|
143
|
+
session={fixtureDiffSession()}
|
|
144
|
+
marks={marksByRows([], rows)}
|
|
145
|
+
quickActions={[]}
|
|
146
|
+
observer={false}
|
|
147
|
+
onAnnotate={noop}
|
|
148
|
+
onReply={noop}
|
|
149
|
+
onUpdateAnnotation={noop}
|
|
150
|
+
onExit={noop}
|
|
151
|
+
/>,
|
|
152
|
+
{ width: 60, height: 12 },
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
await settle(setup);
|
|
156
|
+
const found = findById(setup.renderer.root, "diff-scroll");
|
|
157
|
+
|
|
158
|
+
if (!(found instanceof ScrollBoxRenderable)) throw new Error("diff-scroll is not a scrollbox");
|
|
159
|
+
const scrollbox = found;
|
|
160
|
+
const acceleration = scrollbox.scrollAcceleration;
|
|
161
|
+
const first = acceleration.tick(1000);
|
|
162
|
+
const burst = Array.from({ length: 10 }, (_, index) => acceleration.tick(1016 + index * 16));
|
|
163
|
+
const tail = acceleration.tick(1400);
|
|
164
|
+
|
|
165
|
+
expect(scrollbox.verticalScrollBar.scrollStep).toBe(1);
|
|
166
|
+
expect(first).toBe(1);
|
|
167
|
+
expect(Math.max(...burst)).toBeGreaterThan(first);
|
|
168
|
+
expect(tail).toBe(1);
|
|
169
|
+
|
|
170
|
+
acceleration.reset();
|
|
171
|
+
await setup.mockMouse.scroll(20, 5, "down");
|
|
172
|
+
await settle(setup);
|
|
173
|
+
expect(scrollbox.scrollTop).toBeGreaterThan(0);
|
|
174
|
+
|
|
175
|
+
setup.renderer.destroy();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("wheel scrolling up keeps the caret on the bottom visible code row", async () => {
|
|
179
|
+
const rows = diffRows(tallPatch(80));
|
|
180
|
+
const setup = await testRender(
|
|
181
|
+
<DiffContentView
|
|
182
|
+
rows={rows}
|
|
183
|
+
session={fixtureDiffSession()}
|
|
184
|
+
marks={marksByRows([], rows)}
|
|
185
|
+
quickActions={[]}
|
|
186
|
+
observer={false}
|
|
187
|
+
onAnnotate={noop}
|
|
188
|
+
onReply={noop}
|
|
189
|
+
onUpdateAnnotation={noop}
|
|
190
|
+
onExit={noop}
|
|
191
|
+
/>,
|
|
192
|
+
{ width: 60, height: 12 },
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
await settle(setup);
|
|
196
|
+
const found = findById(setup.renderer.root, "diff-scroll");
|
|
197
|
+
|
|
198
|
+
if (!(found instanceof ScrollBoxRenderable)) throw new Error("diff-scroll is not a scrollbox");
|
|
199
|
+
const scrollbox = found;
|
|
200
|
+
|
|
201
|
+
for (const key of Array.from({ length: 30 }, () => "down" as const)) {
|
|
202
|
+
// eslint-disable-next-line no-await-in-loop
|
|
203
|
+
await press(setup, key);
|
|
204
|
+
}
|
|
205
|
+
const before = scrollbox.scrollTop;
|
|
206
|
+
const markerRows = (): number[] =>
|
|
207
|
+
setup
|
|
208
|
+
.captureCharFrame()
|
|
209
|
+
.split("\n")
|
|
210
|
+
.flatMap((line, row) => (line.includes("▎") ? [row] : []));
|
|
211
|
+
const top = scrollbox.viewport.screenY;
|
|
212
|
+
const bottom = top + scrollbox.viewport.height - 1;
|
|
213
|
+
|
|
214
|
+
for (const direction of Array.from({ length: 8 }, () => "up" as const)) {
|
|
215
|
+
// eslint-disable-next-line no-await-in-loop
|
|
216
|
+
await setup.mockMouse.scroll(20, 5, direction);
|
|
217
|
+
// eslint-disable-next-line no-await-in-loop
|
|
218
|
+
await settle(setup);
|
|
219
|
+
expect(markerRows()).toEqual([bottom]);
|
|
220
|
+
}
|
|
221
|
+
expect(scrollbox.scrollTop).toBeLessThan(before);
|
|
222
|
+
for (const direction of Array.from({ length: 20 }, () => "down" as const)) {
|
|
223
|
+
// eslint-disable-next-line no-await-in-loop
|
|
224
|
+
await setup.mockMouse.scroll(20, 5, direction);
|
|
225
|
+
// eslint-disable-next-line no-await-in-loop
|
|
226
|
+
await settle(setup);
|
|
227
|
+
expect(markerRows()).toHaveLength(1);
|
|
228
|
+
}
|
|
229
|
+
expect(markerRows()).toEqual([top]);
|
|
230
|
+
setup.renderer.destroy();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("rapid wheel scrolling through a large wrapped diff does not loop React updates", async () => {
|
|
234
|
+
const filePatches = Array.from({ length: 80 }, (_, fileIndex) => [
|
|
235
|
+
`diff --git a/file-${fileIndex}.ts b/file-${fileIndex}.ts`,
|
|
236
|
+
`--- a/file-${fileIndex}.ts`,
|
|
237
|
+
`+++ b/file-${fileIndex}.ts`,
|
|
238
|
+
"@@ -0,0 +1,12 @@",
|
|
239
|
+
...Array.from(
|
|
240
|
+
{ length: 12 },
|
|
241
|
+
(_, lineIndex) => `+line ${lineIndex} ${"dense source text ".repeat(80)}`,
|
|
242
|
+
),
|
|
243
|
+
]).flat();
|
|
244
|
+
const rows = diffRows([...filePatches, ""].join("\n"));
|
|
245
|
+
const StatefulDiff = (): React.ReactNode => {
|
|
246
|
+
const [, setCursor] = React.useState(0);
|
|
247
|
+
|
|
248
|
+
return (
|
|
249
|
+
<DiffContentView
|
|
250
|
+
rows={rows}
|
|
251
|
+
session={fixtureDiffSession()}
|
|
252
|
+
marks={marksByRows([], rows)}
|
|
253
|
+
quickActions={[]}
|
|
254
|
+
observer={false}
|
|
255
|
+
onCursorChange={setCursor}
|
|
256
|
+
onAnnotate={noop}
|
|
257
|
+
onReply={noop}
|
|
258
|
+
onUpdateAnnotation={noop}
|
|
259
|
+
onExit={noop}
|
|
260
|
+
/>
|
|
261
|
+
);
|
|
262
|
+
};
|
|
263
|
+
const setup = await testRender(<StatefulDiff />, { width: 60, height: 12 });
|
|
264
|
+
|
|
265
|
+
await settle(setup);
|
|
266
|
+
const found = findById(setup.renderer.root, "diff-scroll");
|
|
267
|
+
|
|
268
|
+
if (!(found instanceof ScrollBoxRenderable)) throw new Error("diff-scroll is not a scrollbox");
|
|
269
|
+
|
|
270
|
+
for (const [index, direction] of Array.from({ length: 240 }, () => "down" as const).entries()) {
|
|
271
|
+
// eslint-disable-next-line no-await-in-loop
|
|
272
|
+
await setup.mockMouse.scroll(20, 5, direction);
|
|
273
|
+
if (index % 12 === 0) {
|
|
274
|
+
// eslint-disable-next-line no-await-in-loop
|
|
275
|
+
await settle(setup);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
await settle(setup);
|
|
279
|
+
expect(found.scrollTop).toBeGreaterThan(0);
|
|
280
|
+
setup.renderer.destroy();
|
|
281
|
+
}, 60000);
|
|
282
|
+
|
|
283
|
+
test("walking long wrapped code lines advances the viewport one visual row per key", async () => {
|
|
284
|
+
const lines = Array.from(
|
|
285
|
+
{ length: 36 },
|
|
286
|
+
(_, index) =>
|
|
287
|
+
`+line ${String(index).padStart(2, "0")} ${"a paragraph with several wrapped phrases ".repeat(5)}`,
|
|
288
|
+
);
|
|
289
|
+
const rows = diffRows(
|
|
290
|
+
[
|
|
291
|
+
"diff --git a/long.md b/long.md",
|
|
292
|
+
"--- a/long.md",
|
|
293
|
+
"+++ b/long.md",
|
|
294
|
+
"@@ -0,0 +1,36 @@",
|
|
295
|
+
...lines,
|
|
296
|
+
"",
|
|
297
|
+
].join("\n"),
|
|
298
|
+
);
|
|
299
|
+
const setup = await testRender(
|
|
300
|
+
<DiffContentView
|
|
301
|
+
rows={rows}
|
|
302
|
+
session={fixtureDiffSession()}
|
|
303
|
+
marks={marksByRows([], rows)}
|
|
304
|
+
quickActions={[]}
|
|
305
|
+
observer={false}
|
|
306
|
+
onAnnotate={noop}
|
|
307
|
+
onReply={noop}
|
|
308
|
+
onUpdateAnnotation={noop}
|
|
309
|
+
onExit={noop}
|
|
310
|
+
/>,
|
|
311
|
+
{ width: 60, height: 12 },
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
await settle(setup);
|
|
315
|
+
const found = findById(setup.renderer.root, "diff-scroll");
|
|
316
|
+
|
|
317
|
+
if (!(found instanceof ScrollBoxRenderable)) throw new Error("diff-scroll is not a scrollbox");
|
|
318
|
+
const scrollbox = found;
|
|
319
|
+
const steps: number[] = [];
|
|
320
|
+
const caretCell = annotationPaletteFor(DARK).caretCell;
|
|
321
|
+
|
|
322
|
+
for (let index = 0; index < 45; index++) {
|
|
323
|
+
const before = scrollbox.scrollTop;
|
|
324
|
+
|
|
325
|
+
await press(setup, "down");
|
|
326
|
+
steps.push(scrollbox.scrollTop - before);
|
|
327
|
+
const caretRow = setup
|
|
328
|
+
.captureSpans()
|
|
329
|
+
.lines.findIndex((line) => line.spans.some((span) => hex(span.bg) === caretCell));
|
|
330
|
+
const markerRows = setup
|
|
331
|
+
.captureCharFrame()
|
|
332
|
+
.split("\n")
|
|
333
|
+
.flatMap((line, row) => (line.includes("▎") ? [row] : []));
|
|
334
|
+
|
|
335
|
+
expect(caretRow).toBeGreaterThanOrEqual(0);
|
|
336
|
+
expect(markerRows).toEqual([caretRow]);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
expect(Math.max(...steps)).toBeLessThanOrEqual(1);
|
|
340
|
+
expect(steps.some((step) => step > 0)).toBe(true);
|
|
341
|
+
await setup.mockMouse.scroll(20, 5, "up");
|
|
342
|
+
await settle(setup);
|
|
343
|
+
const wrappedMarkerRows = setup
|
|
344
|
+
.captureCharFrame()
|
|
345
|
+
.split("\n")
|
|
346
|
+
.flatMap((line, row) => (line.includes("▎") ? [row] : []));
|
|
347
|
+
|
|
348
|
+
expect(wrappedMarkerRows).toEqual([scrollbox.viewport.screenY + scrollbox.viewport.height - 1]);
|
|
349
|
+
setup.renderer.destroy();
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("opening a comment on the diff's last line reveals its composer", async () => {
|
|
353
|
+
const rows = diffRows(tallPatch(24));
|
|
354
|
+
const setup = await testRender(
|
|
355
|
+
<DiffContentView
|
|
356
|
+
rows={rows}
|
|
357
|
+
session={fixtureDiffSession()}
|
|
358
|
+
marks={marksByRows([], rows)}
|
|
359
|
+
quickActions={[]}
|
|
360
|
+
observer={false}
|
|
361
|
+
onAnnotate={noop}
|
|
362
|
+
onReply={noop}
|
|
363
|
+
onUpdateAnnotation={noop}
|
|
364
|
+
onExit={noop}
|
|
365
|
+
/>,
|
|
366
|
+
{ width: 60, height: 10 },
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
await settle(setup);
|
|
370
|
+
for (let step = 0; step < rows.length + 2; step++) {
|
|
371
|
+
// eslint-disable-next-line no-await-in-loop
|
|
372
|
+
await press(setup, "down");
|
|
373
|
+
}
|
|
374
|
+
await typeText(setup, "bottom comment");
|
|
375
|
+
|
|
376
|
+
expect(setup.captureCharFrame()).toContain("● bottom comment");
|
|
377
|
+
setup.renderer.destroy();
|
|
378
|
+
}, 25000);
|
|
379
|
+
|
|
380
|
+
test("Up keeps the caret on the first screen row while wrapped lines scroll", async () => {
|
|
381
|
+
const lines = Array.from({ length: 24 }, (_, index) =>
|
|
382
|
+
index % 3 === 2 ? "-" : `-line ${index} ${"wrapped words here ".repeat(5)}`,
|
|
383
|
+
);
|
|
384
|
+
const rows = diffRows(
|
|
385
|
+
[
|
|
386
|
+
"diff --git a/long.md b/long.md",
|
|
387
|
+
"--- a/long.md",
|
|
388
|
+
"+++ /dev/null",
|
|
389
|
+
"@@ -1,24 +0,0 @@",
|
|
390
|
+
...lines,
|
|
391
|
+
"",
|
|
392
|
+
].join("\n"),
|
|
393
|
+
);
|
|
394
|
+
const setup = await testRender(
|
|
395
|
+
<DiffContentView
|
|
396
|
+
rows={rows}
|
|
397
|
+
session={fixtureDiffSession()}
|
|
398
|
+
marks={marksByRows([], rows)}
|
|
399
|
+
quickActions={[]}
|
|
400
|
+
observer={false}
|
|
401
|
+
onAnnotate={noop}
|
|
402
|
+
onReply={noop}
|
|
403
|
+
onUpdateAnnotation={noop}
|
|
404
|
+
onExit={noop}
|
|
405
|
+
/>,
|
|
406
|
+
{ width: 60, height: 12 },
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
await settle(setup);
|
|
410
|
+
const found = findById(setup.renderer.root, "diff-scroll");
|
|
411
|
+
|
|
412
|
+
if (!(found instanceof ScrollBoxRenderable)) throw new Error("diff-scroll is not a scrollbox");
|
|
413
|
+
const scrollbox = found;
|
|
414
|
+
|
|
415
|
+
for (let index = 0; index < 40; index++) await press(setup, "down");
|
|
416
|
+
let previousTop = scrollbox.scrollTop;
|
|
417
|
+
let scrolled = false;
|
|
418
|
+
|
|
419
|
+
for (let index = 0; index < 80; index++) {
|
|
420
|
+
await press(setup, "up");
|
|
421
|
+
const frame = setup.captureCharFrame().split("\n");
|
|
422
|
+
const marker = frame.findIndex((line) => line.includes("▎"));
|
|
423
|
+
const top = scrollbox.scrollTop;
|
|
424
|
+
|
|
425
|
+
expect(previousTop - top).toBeLessThanOrEqual(1);
|
|
426
|
+
if (top < previousTop) {
|
|
427
|
+
expect(marker).toBe(0);
|
|
428
|
+
scrolled = true;
|
|
429
|
+
}
|
|
430
|
+
previousTop = top;
|
|
431
|
+
}
|
|
432
|
+
expect(scrolled).toBe(true);
|
|
433
|
+
setup.renderer.destroy();
|
|
434
|
+
});
|