@cueloop/client 0.1.0-alpha.11 → 0.1.0-alpha.111
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 +63 -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 +383 -0
- package/src/annotations/components/DiscussionMarkerRail.stories.tsx +54 -0
- package/src/annotations/components/DiscussionMarkerRail.tsx +189 -0
- package/src/annotations/discussions.ts +78 -0
- package/src/annotations/thread-selection.test.ts +210 -0
- package/src/annotations/thread-selection.ts +248 -0
- package/src/annotations/use-annotation-surface.tsx +1378 -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 +190 -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.harness-settings.test.tsx +77 -0
- package/src/app/App.identity.test.tsx +84 -0
- package/src/app/App.plan-render.test.tsx +211 -0
- package/src/app/App.plan-review.test.tsx +429 -0
- package/src/app/App.prototype-markdown.test.tsx +129 -0
- package/src/app/App.prototype.test.tsx +176 -0
- package/src/app/App.readonly.test.tsx +274 -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 +497 -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 +1758 -0
- package/src/app/App.visual.test.tsx +60 -0
- package/src/app/App.walk.test.tsx +300 -0
- package/src/app/App.workbench.test.tsx +1153 -0
- package/src/app/app-key-state.ts +61 -0
- package/src/app/app-screens.tsx +607 -0
- package/src/app/app-view-model.ts +178 -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 +121 -0
- package/src/app/components/pages/Workspace.stories.tsx +181 -0
- package/src/app/intent-dispatch.test.ts +729 -0
- package/src/app/intent-dispatch.ts +713 -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 +54 -0
- package/src/app/run.ts +111 -0
- package/src/{serve.test.ts → app/serve.test.ts} +28 -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 +160 -0
- package/src/diff/components/AnnotatableFileView.tsx +142 -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 +443 -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 +85 -0
- package/src/diff/components/DiffContentView.tsx +1080 -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 +87 -0
- package/src/diff/diff-fold.test.ts +78 -0
- package/src/diff/diff-fold.ts +130 -0
- package/src/diff/diff-hunk-curate.test.ts +315 -0
- package/src/diff/diff-hunk-curate.ts +155 -0
- package/src/diff/diff-intraline.test.ts +239 -0
- package/src/diff/diff-intraline.ts +244 -0
- package/src/diff/diff-syntax.test.ts +125 -0
- package/src/diff/diff-syntax.ts +194 -0
- package/src/diff/split-diff.test.ts +110 -0
- package/src/diff/split-diff.ts +150 -0
- package/src/diff/view-diff.test.ts +325 -0
- package/src/diff/view-diff.ts +540 -0
- package/src/diff/walk.test.ts +121 -0
- package/src/diff/walk.ts +76 -0
- package/src/index.ts +32 -2
- package/src/integrations/attribution.test.ts +31 -0
- package/src/integrations/attribution.ts +44 -0
- package/src/integrations/client-extension-registry.test.tsx +61 -0
- package/src/integrations/client-extension-registry.ts +163 -0
- package/src/integrations/clipboard.test.ts +23 -0
- package/src/integrations/clipboard.ts +34 -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 +52 -0
- package/src/integrations/components/ShareDialog.stories.tsx +121 -0
- package/src/integrations/components/ShareDialog.tsx +602 -0
- package/src/integrations/components/share-dialog-store.ts +59 -0
- package/src/integrations/editor.test.ts +149 -0
- package/src/integrations/editor.ts +163 -0
- package/src/integrations/github-identity.test.ts +59 -0
- package/src/integrations/github-identity.ts +42 -0
- package/src/integrations/herdr.test.ts +43 -0
- package/src/integrations/herdr.ts +40 -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 +62 -0
- package/src/integrations/share.ts +283 -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 +147 -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 +90 -0
- package/src/keyboard/key-bindings.test.ts +156 -0
- package/src/keyboard/key-bindings.ts +462 -0
- package/src/keyboard/keymap.test.ts +611 -0
- package/src/keyboard/keymap.ts +446 -0
- package/src/keyboard/skills.test.ts +74 -0
- package/src/keyboard/skills.ts +77 -0
- package/src/keyboard/slash-palette.test.ts +103 -0
- package/src/keyboard/slash-palette.ts +137 -0
- package/src/keyboard/thread-chords.test.ts +79 -0
- package/src/keyboard/thread-chords.ts +168 -0
- package/src/keyboard/use-shared-keyboard.ts +56 -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 +165 -0
- package/src/markdown/components/ThreadView.selection.test.tsx +954 -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 +577 -0
- package/src/markdown/mark-runs.ts +125 -0
- package/src/markdown/markdown-editor-syntax-style.ts +66 -0
- package/src/markdown/markdown-grid.test.ts +101 -0
- package/src/markdown/markdown-grid.ts +209 -0
- package/src/markdown/markdown-highlight.test.ts +61 -0
- package/src/markdown/markdown-highlight.ts +125 -0
- package/src/markdown/thread-code-syntax.ts +48 -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 +1061 -0
- package/src/performance/otlp-trace.test.ts +119 -0
- package/src/performance/otlp-trace.ts +134 -0
- package/src/performance/perf-report.ts +19 -0
- package/src/performance/perf-timings.test.ts +18 -0
- package/src/performance/perf-timings.ts +40 -0
- package/src/prototype/components/PrototypeContentView.stories.tsx +19 -0
- package/src/prototype/components/PrototypeContentView.tsx +459 -0
- package/src/prototype/prototype-browser.test.ts +81 -0
- package/src/prototype/prototype-browser.ts +332 -0
- package/src/prototype/prototype-pixels.tsx +35 -0
- package/src/settings/components/SettingsDialog.stories.tsx +117 -0
- package/src/settings/components/SettingsDialog.tsx +186 -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 +771 -0
- package/src/settings/config.ts +722 -0
- package/src/settings/use-settings-dialog.test.tsx +172 -0
- package/src/settings/use-settings-dialog.tsx +465 -0
- package/src/settings/use-thread-harness-setting.test.tsx +87 -0
- package/src/settings/use-thread-harness-setting.ts +86 -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 +78 -0
- package/src/stories/story-fixtures.ts +102 -0
- package/src/stories/story-tree.test.ts +108 -0
- package/src/stories/story.ts +160 -0
- package/src/terminal/ansi-screen.test.ts +28 -0
- package/src/terminal/ansi-screen.ts +77 -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 +208 -0
- package/src/terminal/ghostty-terminal.test.ts +85 -0
- package/src/terminal/ghostty-terminal.ts +207 -0
- package/src/terminal/kitty-image.test.ts +66 -0
- package/src/terminal/kitty-image.ts +140 -0
- package/src/terminal/pty.test.ts +81 -0
- package/src/terminal/pty.ts +255 -0
- package/src/testing/test-support.test.ts +266 -0
- package/src/testing/test-support.ts +449 -0
- package/src/thread/agent-continuation.test.ts +34 -0
- package/src/thread/agent-continuation.ts +13 -0
- package/src/thread/agent-transcript.test.ts +161 -0
- package/src/thread/agent-transcript.ts +344 -0
- package/src/thread/components/AgentConfigControls.stories.tsx +39 -0
- package/src/thread/components/AgentConfigControls.test.tsx +153 -0
- package/src/thread/components/AgentConfigControls.tsx +121 -0
- package/src/thread/components/AgentThreadPane.stories.tsx +105 -0
- package/src/thread/components/AgentThreadPane.test.tsx +1248 -0
- package/src/thread/components/AgentThreadPane.tsx +458 -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 +268 -0
- package/src/thread/components/thread-tree.test.ts +138 -0
- package/src/thread/components/thread-tree.ts +112 -0
- package/src/thread/thread-controller.curate.test.ts +504 -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 +618 -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 +2151 -0
- package/src/thread/tree-view.test.ts +128 -0
- package/src/thread/tree-view.ts +150 -0
- package/src/thread/use-thread-agent.test.tsx +209 -0
- package/src/thread/use-thread-agent.ts +122 -0
- package/src/ui/components/CodeBlock.stories.tsx +42 -0
- package/src/ui/components/CodeBlock.tsx +82 -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 +142 -0
- package/src/ui/components/OverlayScrollbar.tsx +134 -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 +101 -0
- package/src/ui/components/RootOverlay.stories.tsx +45 -0
- package/src/ui/components/RootOverlay.tsx +50 -0
- package/src/ui/components/ScrollArea.stories.tsx +20 -0
- package/src/ui/components/ScrollArea.tsx +63 -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 +93 -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 +58 -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 +102 -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 +137 -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 +144 -0
- package/src/ui/components/syntax-highlight.ts +154 -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 +16 -0
- package/src/ui/use-frame-measure.ts +88 -0
- package/src/ui/use-terminal-virtualizer.ts +286 -0
- package/src/workbench/components/ChangesColumn.stories.tsx +44 -0
- package/src/workbench/components/ChangesColumn.tsx +210 -0
- package/src/workbench/components/ChangesFileTree.test.tsx +184 -0
- package/src/workbench/components/EditorGrid.split.test.tsx +119 -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 +552 -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 +104 -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 +195 -0
- package/src/workbench/components/ProjectTreeView.stories.tsx +25 -0
- package/src/workbench/components/ProjectTreeView.test.tsx +239 -0
- package/src/workbench/components/ProjectTreeView.tsx +159 -0
- package/src/workbench/components/TreePane.stories.tsx +106 -0
- package/src/workbench/components/TreePane.tsx +157 -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 +284 -0
- package/src/workbench/components/file-tree.test.ts +60 -0
- package/src/workbench/components/file-tree.ts +114 -0
- package/src/workbench/use-changes-workbench.ts +210 -0
- package/src/workbench/use-remember-layout.ts +33 -0
- package/src/workbench/use-repo-changes.ts +93 -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,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression: the header controls (search, zoom, split) stay pinned to the right
|
|
3
|
+
* edge whatever the container width parity. A lone flexGrow tab strip used to
|
|
4
|
+
* underfill by a cell at even widths, dragging the zoom icon a column left when
|
|
5
|
+
* zoom widened the pane; space-between pins the controls so the column is stable.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from "bun:test";
|
|
9
|
+
import { testRender } from "@opentui/react/test-utils";
|
|
10
|
+
import React from "react";
|
|
11
|
+
import { EditorGrid } from "./EditorGrid";
|
|
12
|
+
import { changesTab, fileTab, makeGroup, splitGroup } from "./editor-grid";
|
|
13
|
+
import { NERD } from "../../ui/components/primitives/icons";
|
|
14
|
+
import { allowEventLoopUpdates } from "../../testing/test-support";
|
|
15
|
+
|
|
16
|
+
const grid = makeGroup([changesTab(), fileTab("App.tsx", "src/App.tsx", "diff")]);
|
|
17
|
+
|
|
18
|
+
async function zoomIconInsetFromRight(width: number, zoomed: boolean): Promise<number> {
|
|
19
|
+
const setup = await testRender(
|
|
20
|
+
<EditorGrid
|
|
21
|
+
tree={grid}
|
|
22
|
+
focusedGroupId={grid.id}
|
|
23
|
+
onFocusGroup={() => {}}
|
|
24
|
+
onActivateTab={() => {}}
|
|
25
|
+
onCloseTab={() => {}}
|
|
26
|
+
onSplit={() => {}}
|
|
27
|
+
onZoom={() => {}}
|
|
28
|
+
zoomed={zoomed}
|
|
29
|
+
renderTab={() => <text>body</text>}
|
|
30
|
+
/>,
|
|
31
|
+
{ width, height: 6 },
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
allowEventLoopUpdates();
|
|
35
|
+
await setup.waitForVisualIdle();
|
|
36
|
+
const line = setup
|
|
37
|
+
.captureCharFrame()
|
|
38
|
+
.split("\n")
|
|
39
|
+
.find((row) => row.includes("⛶"));
|
|
40
|
+
const inset = line ? width - line.indexOf("⛶") : -1;
|
|
41
|
+
|
|
42
|
+
setup.renderer.destroy();
|
|
43
|
+
|
|
44
|
+
return inset;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe("zoom icon position", () => {
|
|
48
|
+
test("stays a fixed inset from the right edge across width parities", async () => {
|
|
49
|
+
// Act - even and odd widths flip the flexGrow rounding the old layout was sensitive to
|
|
50
|
+
const insets = await Promise.all([
|
|
51
|
+
zoomIconInsetFromRight(100, false),
|
|
52
|
+
zoomIconInsetFromRight(101, false),
|
|
53
|
+
zoomIconInsetFromRight(102, false),
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
// Assert - one inset for all widths: the icon is pinned right, not drifting with parity
|
|
57
|
+
expect(new Set(insets).size).toBe(1);
|
|
58
|
+
expect(insets[0]).toBeGreaterThan(0);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("does not shift when zoom becomes active", async () => {
|
|
62
|
+
// Act
|
|
63
|
+
const inactive = await zoomIconInsetFromRight(100, false);
|
|
64
|
+
const active = await zoomIconInsetFromRight(100, true);
|
|
65
|
+
|
|
66
|
+
// Assert - active only recolours the glyph; it must not move
|
|
67
|
+
expect(active).toBe(inactive);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("split stays in every header while zoom appears only in the upper-right group", async () => {
|
|
72
|
+
const first = makeGroup([changesTab()]);
|
|
73
|
+
const right = splitGroup(first, first.id, "right");
|
|
74
|
+
const upperRight = splitGroup(right.tree, right.focusGroupId, "up");
|
|
75
|
+
const setup = await testRender(
|
|
76
|
+
<EditorGrid
|
|
77
|
+
tree={upperRight.tree}
|
|
78
|
+
focusedGroupId={upperRight.focusGroupId}
|
|
79
|
+
onFocusGroup={() => {}}
|
|
80
|
+
onActivateTab={() => {}}
|
|
81
|
+
onCloseTab={() => {}}
|
|
82
|
+
onSplit={() => {}}
|
|
83
|
+
onZoom={() => {}}
|
|
84
|
+
zoomed={false}
|
|
85
|
+
renderTab={() => <text>body</text>}
|
|
86
|
+
/>,
|
|
87
|
+
{ width: 100, height: 16 },
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
allowEventLoopUpdates();
|
|
91
|
+
await setup.waitForVisualIdle();
|
|
92
|
+
const rows = setup.captureCharFrame().split("\n");
|
|
93
|
+
const headers = rows.filter((row) => row.includes("split"));
|
|
94
|
+
|
|
95
|
+
expect([...rows.join("\n").matchAll(/split/g)]).toHaveLength(3);
|
|
96
|
+
expect(headers[0]).toContain(NERD.zoom);
|
|
97
|
+
expect(headers[1]).not.toContain(NERD.zoom);
|
|
98
|
+
expect(rows.filter((row) => row.includes(NERD.zoom))).toHaveLength(1);
|
|
99
|
+
setup.renderer.destroy();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("zoom keeps a full cell at the right edge of a narrow split", async () => {
|
|
103
|
+
const first = makeGroup([changesTab()]);
|
|
104
|
+
const split = splitGroup(first, first.id, "right");
|
|
105
|
+
|
|
106
|
+
for (const width of [50, 51, 100]) {
|
|
107
|
+
for (const zoomed of [false, true]) {
|
|
108
|
+
const setup = await testRender(
|
|
109
|
+
<EditorGrid
|
|
110
|
+
tree={split.tree}
|
|
111
|
+
focusedGroupId={split.focusGroupId}
|
|
112
|
+
onFocusGroup={() => {}}
|
|
113
|
+
onActivateTab={() => {}}
|
|
114
|
+
onCloseTab={() => {}}
|
|
115
|
+
onSplit={() => {}}
|
|
116
|
+
onZoom={() => {}}
|
|
117
|
+
zoomed={zoomed}
|
|
118
|
+
renderTab={() => <text>body</text>}
|
|
119
|
+
/>,
|
|
120
|
+
{ width, height: 8 },
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
allowEventLoopUpdates();
|
|
124
|
+
await setup.waitForVisualIdle();
|
|
125
|
+
const header = setup.captureCharFrame().split("\n")[0]!;
|
|
126
|
+
|
|
127
|
+
expect(header.match(/⛶/g)).toHaveLength(1);
|
|
128
|
+
expect(width - header.indexOf(NERD.zoom)).toBe(2);
|
|
129
|
+
setup.renderer.destroy();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../../appearance/theme";
|
|
3
|
+
import type { Story, StoryMeta } from "../../stories/story";
|
|
4
|
+
import { FileContentsView } from "./FileContentsView";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "Surfaces/FileContentsView" };
|
|
7
|
+
|
|
8
|
+
const SAMPLE = `export function add(a: number, b: number): number {\n return a + b;\n}\n`;
|
|
9
|
+
|
|
10
|
+
export const Loaded: Story = {
|
|
11
|
+
render: () => (
|
|
12
|
+
<FileContentsView path="src/add.ts" loadContents={() => Promise.resolve(SAMPLE)} theme={DARK} />
|
|
13
|
+
),
|
|
14
|
+
expectedColors: [DARK.textDim],
|
|
15
|
+
size: { width: 60, height: 12 },
|
|
16
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { testRender } from "@opentui/react/test-utils";
|
|
4
|
+
import { FileContentsView } from "./FileContentsView";
|
|
5
|
+
import { waitForText } from "../../testing/test-support";
|
|
6
|
+
import { DARK } from "../../appearance/theme";
|
|
7
|
+
|
|
8
|
+
test("renders the loaded file contents", async () => {
|
|
9
|
+
const setup = await testRender(
|
|
10
|
+
<FileContentsView
|
|
11
|
+
path="src/add.ts"
|
|
12
|
+
loadContents={() => Promise.resolve("const answer = 42;\n")}
|
|
13
|
+
theme={DARK}
|
|
14
|
+
/>,
|
|
15
|
+
{ width: 60, height: 10 },
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
await waitForText(setup, "const answer = 42;");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("shows a centered File deleted message when the file cannot be read", async () => {
|
|
22
|
+
const setup = await testRender(
|
|
23
|
+
<FileContentsView path="missing.ts" loadContents={() => Promise.resolve(null)} theme={DARK} />,
|
|
24
|
+
{ width: 60, height: 10 },
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
await waitForText(setup, "File deleted");
|
|
28
|
+
expect(setup.captureCharFrame()).toContain("File deleted");
|
|
29
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// A Changes/Project file tab in contents mode: a workspace file rendered read-only with line
|
|
2
|
+
// numbers, for surfaces with no thread to anchor notes to (the bare-launch welcome shell).
|
|
3
|
+
// Loads on mount and whenever the path changes; a null read renders a centered "File deleted".
|
|
4
|
+
// Syntax highlighting comes from the native code renderable (tree-sitter), the language
|
|
5
|
+
// auto-detected from the path; unknown languages simply render unstyled.
|
|
6
|
+
|
|
7
|
+
import { ScrollArea } from "../../ui/components/ScrollArea";
|
|
8
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
9
|
+
import type { CodeRenderable } from "@opentui/core";
|
|
10
|
+
import { useRenderer } from "@opentui/react";
|
|
11
|
+
import type { Theme } from "../../appearance/theme";
|
|
12
|
+
import { useComponentTheme } from "../../appearance/components/theme-context";
|
|
13
|
+
import { filetypeForPath, syntaxStyleFor } from "../../ui/components/syntax-highlight";
|
|
14
|
+
|
|
15
|
+
export interface FileContentsViewProps {
|
|
16
|
+
path: string;
|
|
17
|
+
loadContents: (path: string) => Promise<string | null>;
|
|
18
|
+
theme?: Theme;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A finished read for a specific path; `lines` is null when the file could not be read. */
|
|
22
|
+
interface FileLoad {
|
|
23
|
+
path: string;
|
|
24
|
+
lines: string[] | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function FileContentsView({
|
|
28
|
+
path,
|
|
29
|
+
loadContents,
|
|
30
|
+
theme,
|
|
31
|
+
}: FileContentsViewProps): React.ReactNode {
|
|
32
|
+
const tokens = useComponentTheme(theme);
|
|
33
|
+
const renderer = useRenderer();
|
|
34
|
+
const [loaded, setLoaded] = useState<FileLoad | null>(null);
|
|
35
|
+
// The gutter mirrors the code renderable's line info, so it needs the mounted
|
|
36
|
+
// instance; a state-backed ref rebinds the target once the code renderable exists.
|
|
37
|
+
const [codeTarget, setCodeTarget] = useState<CodeRenderable | null>(null);
|
|
38
|
+
const loadRef = useRef(loadContents);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
loadRef.current = loadContents;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
let alive = true;
|
|
46
|
+
|
|
47
|
+
void loadRef.current(path).then(
|
|
48
|
+
(contents) => {
|
|
49
|
+
if (alive) setLoaded({ path, lines: contents === null ? null : contents.split("\n") });
|
|
50
|
+
},
|
|
51
|
+
() => {
|
|
52
|
+
if (alive) setLoaded({ path, lines: null });
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return () => {
|
|
57
|
+
alive = false;
|
|
58
|
+
};
|
|
59
|
+
}, [path]);
|
|
60
|
+
|
|
61
|
+
if (loaded === null || loaded.path !== path) {
|
|
62
|
+
return (
|
|
63
|
+
<box style={{ flexGrow: 1, paddingLeft: 1, paddingTop: 1 }}>
|
|
64
|
+
<text fg={tokens.textDim}>loading...</text>
|
|
65
|
+
</box>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (loaded.lines === null) {
|
|
70
|
+
// a file that no longer reads in the Changes/Project view is one the working tree deleted;
|
|
71
|
+
// the bottom pad lifts the text one row so it lines up with the footer-shortened thread empty state
|
|
72
|
+
return (
|
|
73
|
+
<box
|
|
74
|
+
style={{ flexGrow: 1, alignItems: "center", justifyContent: "center", paddingBottom: 2 }}
|
|
75
|
+
>
|
|
76
|
+
<text fg={tokens.textDim}>File deleted</text>
|
|
77
|
+
</box>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const content = loaded.lines.join("\n");
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<ScrollArea>
|
|
85
|
+
<line-number
|
|
86
|
+
target={codeTarget ?? undefined}
|
|
87
|
+
showLineNumbers
|
|
88
|
+
fg={tokens.textDim}
|
|
89
|
+
minWidth={5}
|
|
90
|
+
paddingRight={1}
|
|
91
|
+
style={{ paddingTop: 1, paddingLeft: 1 }}
|
|
92
|
+
>
|
|
93
|
+
<code
|
|
94
|
+
ref={setCodeTarget}
|
|
95
|
+
content={content}
|
|
96
|
+
filetype={filetypeForPath(path)}
|
|
97
|
+
syntaxStyle={syntaxStyleFor(tokens, renderer.nativeScene)}
|
|
98
|
+
selectable={false}
|
|
99
|
+
style={{ wrapMode: "none", fg: tokens.text }}
|
|
100
|
+
/>
|
|
101
|
+
</line-number>
|
|
102
|
+
</ScrollArea>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/** File tabs keep path actions while only the aggregate Changes tab can fold a file. */
|
|
2
|
+
|
|
3
|
+
import { expect, test } from "bun:test";
|
|
4
|
+
import React from "react";
|
|
5
|
+
import { testRender } from "@opentui/react/test-utils";
|
|
6
|
+
import { changesTab, fileTab } from "./editor-grid";
|
|
7
|
+
import { GridTabContent, type DiffSurfaceProps } from "./GridTabContent";
|
|
8
|
+
import { FIXTURE_PATCH, fixtureDiffSession } from "../../stories/story-fixtures";
|
|
9
|
+
import { NERD } from "../../ui/components/primitives/icons";
|
|
10
|
+
import { TooltipProvider } from "../../ui/components/Tooltip";
|
|
11
|
+
import { locateText, waitForText } from "../../testing/test-support";
|
|
12
|
+
import { DARK } from "../../appearance/theme";
|
|
13
|
+
import { diffRows } from "../../diff/view-diff";
|
|
14
|
+
|
|
15
|
+
const noop = (): void => {};
|
|
16
|
+
const rows = diffRows(
|
|
17
|
+
FIXTURE_PATCH.replaceAll("src/store.ts", "src/store.txt").replace("-1,4 +1,4", "-1,3 +1,3"),
|
|
18
|
+
);
|
|
19
|
+
const surface: DiffSurfaceProps = {
|
|
20
|
+
session: fixtureDiffSession(),
|
|
21
|
+
quickActions: [],
|
|
22
|
+
observer: false,
|
|
23
|
+
onAnnotate: noop,
|
|
24
|
+
onReply: noop,
|
|
25
|
+
onUpdateAnnotation: noop,
|
|
26
|
+
onExit: noop,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
test("a single-file diff keeps copy and expand without a collapse action", async () => {
|
|
30
|
+
const copied: string[] = [];
|
|
31
|
+
const setup = await testRender(
|
|
32
|
+
<TooltipProvider theme={DARK}>
|
|
33
|
+
<GridTabContent
|
|
34
|
+
tab={fileTab("store.txt", "src/store.txt", "diff")}
|
|
35
|
+
rows={rows}
|
|
36
|
+
surface={surface}
|
|
37
|
+
rejectedRows={new Set()}
|
|
38
|
+
fold={{
|
|
39
|
+
isCollapsed: () => false,
|
|
40
|
+
isExpanded: () => false,
|
|
41
|
+
canExpand: () => true,
|
|
42
|
+
onToggleCollapse: noop,
|
|
43
|
+
onToggleExpand: noop,
|
|
44
|
+
onCopyPath: (file) => {
|
|
45
|
+
copied.push(file);
|
|
46
|
+
|
|
47
|
+
return true;
|
|
48
|
+
},
|
|
49
|
+
}}
|
|
50
|
+
dimmed={false}
|
|
51
|
+
readFile={async () => null}
|
|
52
|
+
onAddFileComment={noop}
|
|
53
|
+
theme={DARK}
|
|
54
|
+
/>
|
|
55
|
+
</TooltipProvider>,
|
|
56
|
+
{ width: 80, height: 12 },
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
await waitForText(setup, "src/store.txt");
|
|
60
|
+
const frame = setup.captureCharFrame();
|
|
61
|
+
|
|
62
|
+
expect(frame).toContain(NERD.copy);
|
|
63
|
+
expect(frame).not.toContain("v src/store.txt");
|
|
64
|
+
expect(frame).toContain("expand");
|
|
65
|
+
const copy = locateText(setup, NERD.copy);
|
|
66
|
+
|
|
67
|
+
await setup.mockMouse.moveTo(copy.column, copy.row);
|
|
68
|
+
await waitForText(setup, "Copy path");
|
|
69
|
+
await setup.mockMouse.click(copy.column, copy.row);
|
|
70
|
+
expect(copied).toEqual(["src/store.txt"]);
|
|
71
|
+
await waitForText(setup, "copied");
|
|
72
|
+
await setup.waitForVisualIdle();
|
|
73
|
+
setup.renderer.destroy();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("the full Changes tab retains fold and expand controls", async () => {
|
|
77
|
+
const setup = await testRender(
|
|
78
|
+
<GridTabContent
|
|
79
|
+
tab={changesTab()}
|
|
80
|
+
rows={rows}
|
|
81
|
+
surface={surface}
|
|
82
|
+
rejectedRows={new Set()}
|
|
83
|
+
fold={{
|
|
84
|
+
isCollapsed: () => false,
|
|
85
|
+
isExpanded: () => false,
|
|
86
|
+
canExpand: () => true,
|
|
87
|
+
onToggleCollapse: noop,
|
|
88
|
+
onToggleExpand: noop,
|
|
89
|
+
onCopyPath: () => true,
|
|
90
|
+
}}
|
|
91
|
+
dimmed={false}
|
|
92
|
+
readFile={async () => null}
|
|
93
|
+
onAddFileComment={noop}
|
|
94
|
+
theme={DARK}
|
|
95
|
+
/>,
|
|
96
|
+
{ width: 80, height: 12 },
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
await waitForText(setup, "src/store.txt");
|
|
100
|
+
const frame = setup.captureCharFrame();
|
|
101
|
+
|
|
102
|
+
expect(frame).toContain("src/store.txt");
|
|
103
|
+
expect(frame).not.toContain("v src/store.txt");
|
|
104
|
+
expect(frame).toContain(NERD.copy);
|
|
105
|
+
expect(frame).toContain("expand");
|
|
106
|
+
await setup.waitForVisualIdle();
|
|
107
|
+
setup.renderer.destroy();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("the file title tooltip follows its collapse action", async () => {
|
|
111
|
+
function TestChanges(): React.ReactNode {
|
|
112
|
+
const [collapsed, setCollapsed] = React.useState(false);
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<TooltipProvider theme={DARK}>
|
|
116
|
+
<GridTabContent
|
|
117
|
+
tab={changesTab()}
|
|
118
|
+
rows={rows}
|
|
119
|
+
surface={surface}
|
|
120
|
+
rejectedRows={new Set()}
|
|
121
|
+
fold={{
|
|
122
|
+
isCollapsed: () => collapsed,
|
|
123
|
+
isExpanded: () => false,
|
|
124
|
+
canExpand: () => true,
|
|
125
|
+
onToggleCollapse: () => setCollapsed((current) => !current),
|
|
126
|
+
onToggleExpand: noop,
|
|
127
|
+
onCopyPath: () => true,
|
|
128
|
+
}}
|
|
129
|
+
dimmed={false}
|
|
130
|
+
readFile={async () => null}
|
|
131
|
+
onAddFileComment={noop}
|
|
132
|
+
theme={DARK}
|
|
133
|
+
/>
|
|
134
|
+
</TooltipProvider>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const setup = await testRender(<TestChanges />, { width: 80, height: 12 });
|
|
139
|
+
|
|
140
|
+
await waitForText(setup, "src/store.txt");
|
|
141
|
+
const title = locateText(setup, "src/store.txt");
|
|
142
|
+
|
|
143
|
+
await setup.mockMouse.moveTo(title.column, title.row);
|
|
144
|
+
await waitForText(setup, "collapse");
|
|
145
|
+
await setup.mockMouse.click(title.column, title.row);
|
|
146
|
+
await waitForText(setup, "uncollapse");
|
|
147
|
+
await setup.mockMouse.click(title.column, title.row);
|
|
148
|
+
await waitForText(setup, "collapse");
|
|
149
|
+
expect(setup.captureCharFrame()).not.toContain("uncollapse");
|
|
150
|
+
setup.renderer.destroy();
|
|
151
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../../appearance/theme";
|
|
3
|
+
import { DEFAULT_QUICK_ACTIONS } from "../../settings/config";
|
|
4
|
+
import type { Story, StoryMeta } from "../../stories/story";
|
|
5
|
+
import { GridTabContent, type DiffSurfaceProps } from "./GridTabContent";
|
|
6
|
+
import { fixtureDiffRows, fixtureDiffSession } from "../../stories/story-fixtures";
|
|
7
|
+
import { changesTab, fileTab } from "./editor-grid";
|
|
8
|
+
|
|
9
|
+
export const meta: StoryMeta = { title: "Surfaces/GridTabContent" };
|
|
10
|
+
|
|
11
|
+
const noop = (): void => {};
|
|
12
|
+
|
|
13
|
+
function surface(): DiffSurfaceProps {
|
|
14
|
+
return {
|
|
15
|
+
session: fixtureDiffSession({ annotations: [] }),
|
|
16
|
+
quickActions: DEFAULT_QUICK_ACTIONS,
|
|
17
|
+
observer: false,
|
|
18
|
+
onAnnotate: noop,
|
|
19
|
+
onReply: noop,
|
|
20
|
+
onUpdateAnnotation: noop,
|
|
21
|
+
onExit: noop,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The whole-diff Changes tab: every changed file in one scroll container. */
|
|
26
|
+
export const ChangesTab: Story = {
|
|
27
|
+
render: () => (
|
|
28
|
+
<GridTabContent
|
|
29
|
+
tab={changesTab()}
|
|
30
|
+
rows={fixtureDiffRows()}
|
|
31
|
+
surface={surface()}
|
|
32
|
+
rejectedRows={new Set()}
|
|
33
|
+
dimmed={false}
|
|
34
|
+
readFile={async () => null}
|
|
35
|
+
onAddFileComment={noop}
|
|
36
|
+
theme={DARK}
|
|
37
|
+
/>
|
|
38
|
+
),
|
|
39
|
+
expectedColors: [DARK.textMuted],
|
|
40
|
+
size: { width: 80, height: 18 },
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** A single-file diff tab: that file's rows alone, indexed on their own. */
|
|
44
|
+
export const FileDiffTab: Story = {
|
|
45
|
+
render: () => (
|
|
46
|
+
<GridTabContent
|
|
47
|
+
tab={fileTab("store.ts", "src/store.ts", "diff")}
|
|
48
|
+
rows={fixtureDiffRows()}
|
|
49
|
+
surface={surface()}
|
|
50
|
+
rejectedRows={new Set()}
|
|
51
|
+
dimmed={false}
|
|
52
|
+
readFile={async () => null}
|
|
53
|
+
onAddFileComment={noop}
|
|
54
|
+
theme={DARK}
|
|
55
|
+
/>
|
|
56
|
+
),
|
|
57
|
+
expectedColors: [DARK.textMuted],
|
|
58
|
+
size: { width: 80, height: 18 },
|
|
59
|
+
};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The body an editor tab paints: the whole working-tree/artifact diff for the
|
|
3
|
+
* Changes tab, or a single file's diff (or read-only contents) for a file tab.
|
|
4
|
+
* Shared by the thread workbench and the bare-launch shell so both render a
|
|
5
|
+
* changed file the same way, with comments routed back through one surface.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useMemo } from "react";
|
|
9
|
+
import { type Anchor } from "@cueloop/schema";
|
|
10
|
+
import { dimmedTheme, type Theme } from "../../appearance/theme";
|
|
11
|
+
import { changesMarks, type DiffRow } from "../../diff/view-diff";
|
|
12
|
+
import {
|
|
13
|
+
DiffContentView,
|
|
14
|
+
type DiffFoldControls,
|
|
15
|
+
type DiffContentViewProps,
|
|
16
|
+
} from "../../diff/components/DiffContentView";
|
|
17
|
+
import { AnnotatableFileView } from "../../diff/components/AnnotatableFileView";
|
|
18
|
+
import type { EditorTab } from "./editor-grid";
|
|
19
|
+
|
|
20
|
+
/** The inline-commenting props the diff sheet shares with the thread view, wired once by the app. */
|
|
21
|
+
export type DiffSurfaceProps = Pick<
|
|
22
|
+
DiffContentViewProps,
|
|
23
|
+
| "session"
|
|
24
|
+
| "quickActions"
|
|
25
|
+
| "observer"
|
|
26
|
+
| "commentsEnabled"
|
|
27
|
+
| "resolved"
|
|
28
|
+
| "suspended"
|
|
29
|
+
| "onComposingChange"
|
|
30
|
+
| "onObserverBlocked"
|
|
31
|
+
| "onCursorChange"
|
|
32
|
+
| "focusedAnnotationId"
|
|
33
|
+
| "onFocusAnnotation"
|
|
34
|
+
| "onAnnotate"
|
|
35
|
+
| "onReply"
|
|
36
|
+
| "onUpdateAnnotation"
|
|
37
|
+
| "isAnnotationReadOnly"
|
|
38
|
+
| "annotationAction"
|
|
39
|
+
| "onInvoke"
|
|
40
|
+
| "resolveAuthorLabel"
|
|
41
|
+
| "onNavCommand"
|
|
42
|
+
| "onExit"
|
|
43
|
+
>;
|
|
44
|
+
|
|
45
|
+
/** The Changes tab body: the whole diff in one scroll container, or a bare hint when nothing changed. */
|
|
46
|
+
export function ChangesTabBody(props: {
|
|
47
|
+
rows: DiffRow[];
|
|
48
|
+
surface: DiffSurfaceProps;
|
|
49
|
+
rejectedRows: Set<number>;
|
|
50
|
+
fold?: DiffFoldControls;
|
|
51
|
+
showFileCollapse?: boolean;
|
|
52
|
+
fileStats?: ReadonlyMap<string, { additions: number; deletions: number }>;
|
|
53
|
+
split?: boolean;
|
|
54
|
+
dimmed: boolean;
|
|
55
|
+
/** Rows the sibling Thread pane spends on its footer, so this empty hint centers level with it. */
|
|
56
|
+
emptyBottomPadding?: number;
|
|
57
|
+
theme: Theme;
|
|
58
|
+
}): React.ReactNode {
|
|
59
|
+
const session = props.surface.session;
|
|
60
|
+
const marks = useMemo(
|
|
61
|
+
() => changesMarks(session, props.rows, props.surface.focusedAnnotationId),
|
|
62
|
+
[session, props.rows, props.surface.focusedAnnotationId],
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
if (props.rows.length === 0) {
|
|
66
|
+
return (
|
|
67
|
+
<box
|
|
68
|
+
style={{
|
|
69
|
+
flexGrow: 1,
|
|
70
|
+
flexDirection: "column",
|
|
71
|
+
alignItems: "center",
|
|
72
|
+
justifyContent: "center",
|
|
73
|
+
paddingBottom: props.emptyBottomPadding,
|
|
74
|
+
}}
|
|
75
|
+
>
|
|
76
|
+
<text fg={props.theme.textDim}>No changes</text>
|
|
77
|
+
</box>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<DiffContentView
|
|
83
|
+
rows={props.rows}
|
|
84
|
+
marks={marks}
|
|
85
|
+
{...props.surface}
|
|
86
|
+
rejectedRows={props.rejectedRows}
|
|
87
|
+
fold={props.fold}
|
|
88
|
+
showFileCollapse={props.showFileCollapse}
|
|
89
|
+
fileStats={props.fileStats}
|
|
90
|
+
split={props.split}
|
|
91
|
+
theme={props.dimmed ? dimmedTheme(props.theme) : undefined}
|
|
92
|
+
/>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** An editor tab's body: the whole diff for the Changes tab, a file's diff or contents for a file tab. */
|
|
97
|
+
export function GridTabContent(props: {
|
|
98
|
+
tab: EditorTab;
|
|
99
|
+
rows: DiffRow[];
|
|
100
|
+
surface: DiffSurfaceProps;
|
|
101
|
+
rejectedRows: Set<number>;
|
|
102
|
+
fold?: DiffFoldControls;
|
|
103
|
+
fileStats?: ReadonlyMap<string, { additions: number; deletions: number }>;
|
|
104
|
+
split?: boolean;
|
|
105
|
+
dimmed: boolean;
|
|
106
|
+
readFile: (path: string) => Promise<string | null>;
|
|
107
|
+
/** Persist a comment on a project file; the file view built the anchor against its own lines. */
|
|
108
|
+
onAddFileComment: (path: string, anchor: Anchor, body: string) => void;
|
|
109
|
+
/** Rows the sibling Thread pane spends on its footer, so the empty hint centers level with it. */
|
|
110
|
+
emptyBottomPadding?: number;
|
|
111
|
+
theme: Theme;
|
|
112
|
+
}): React.ReactNode {
|
|
113
|
+
const { tab } = props;
|
|
114
|
+
|
|
115
|
+
if (tab.kind === "file" && tab.fileView === "contents" && tab.path !== undefined) {
|
|
116
|
+
const filePath = tab.path;
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<AnnotatableFileView
|
|
120
|
+
path={filePath}
|
|
121
|
+
loadContents={props.readFile}
|
|
122
|
+
session={props.surface.session}
|
|
123
|
+
quickActions={props.surface.quickActions}
|
|
124
|
+
observer={props.surface.observer}
|
|
125
|
+
resolved={props.surface.resolved}
|
|
126
|
+
suspended={props.surface.suspended}
|
|
127
|
+
focusedAnnotationId={props.surface.focusedAnnotationId}
|
|
128
|
+
onFocusAnnotation={props.surface.onFocusAnnotation}
|
|
129
|
+
onComposingChange={props.surface.onComposingChange}
|
|
130
|
+
onCursorChange={props.surface.onCursorChange}
|
|
131
|
+
onObserverBlocked={props.surface.onObserverBlocked}
|
|
132
|
+
onAddComment={(anchor, body) => props.onAddFileComment(filePath, anchor, body)}
|
|
133
|
+
onReply={props.surface.onReply}
|
|
134
|
+
onUpdateAnnotation={props.surface.onUpdateAnnotation}
|
|
135
|
+
isAnnotationReadOnly={props.surface.isAnnotationReadOnly}
|
|
136
|
+
annotationAction={props.surface.annotationAction}
|
|
137
|
+
onInvoke={props.surface.onInvoke}
|
|
138
|
+
resolveAuthorLabel={props.surface.resolveAuthorLabel}
|
|
139
|
+
onExit={props.surface.onExit}
|
|
140
|
+
theme={props.theme}
|
|
141
|
+
/>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (tab.kind !== "file") {
|
|
146
|
+
return (
|
|
147
|
+
<ChangesTabBody
|
|
148
|
+
rows={props.rows}
|
|
149
|
+
surface={props.surface}
|
|
150
|
+
rejectedRows={props.rejectedRows}
|
|
151
|
+
fold={props.fold}
|
|
152
|
+
fileStats={props.fileStats}
|
|
153
|
+
split={props.split}
|
|
154
|
+
dimmed={props.dimmed}
|
|
155
|
+
emptyBottomPadding={props.emptyBottomPadding}
|
|
156
|
+
theme={props.theme}
|
|
157
|
+
/>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// a single-file tab shows that file's rows alone, so its row indices are its own: comments and
|
|
162
|
+
// the caret report back in whole-diff indices; a single-file tab omits only the collapse action
|
|
163
|
+
const fileRowIndices = props.rows.flatMap((row, index) => (row.file === tab.path ? [index] : []));
|
|
164
|
+
const rows = fileRowIndices.map((index) => props.rows[index]!);
|
|
165
|
+
const wholeIndex = (rowIndex: number): number => fileRowIndices[rowIndex] ?? rowIndex;
|
|
166
|
+
const rejectedRows = new Set(
|
|
167
|
+
fileRowIndices.flatMap((index, rowIndex) => (props.rejectedRows.has(index) ? [rowIndex] : [])),
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<ChangesTabBody
|
|
172
|
+
rows={rows}
|
|
173
|
+
surface={{
|
|
174
|
+
...props.surface,
|
|
175
|
+
onCursorChange: (rowIndex) => props.surface.onCursorChange?.(wholeIndex(rowIndex)),
|
|
176
|
+
onAnnotate: (span, body) =>
|
|
177
|
+
props.surface.onAnnotate(
|
|
178
|
+
{
|
|
179
|
+
start: { blockIndex: wholeIndex(span.start.blockIndex), char: span.start.char },
|
|
180
|
+
end: { blockIndex: wholeIndex(span.end.blockIndex), char: span.end.char },
|
|
181
|
+
},
|
|
182
|
+
body,
|
|
183
|
+
),
|
|
184
|
+
}}
|
|
185
|
+
rejectedRows={rejectedRows}
|
|
186
|
+
fileStats={props.fileStats}
|
|
187
|
+
fold={props.fold}
|
|
188
|
+
showFileCollapse={false}
|
|
189
|
+
split={props.split}
|
|
190
|
+
dimmed={props.dimmed}
|
|
191
|
+
emptyBottomPadding={props.emptyBottomPadding}
|
|
192
|
+
theme={props.theme}
|
|
193
|
+
/>
|
|
194
|
+
);
|
|
195
|
+
}
|