@cueloop/client 0.1.0-alpha.10 → 0.1.0-alpha.100
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 +364 -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 +1238 -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 +12 -0
- package/src/app/App.toast.test.tsx +124 -0
- package/src/app/App.tsx +1650 -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.stories.tsx +121 -0
- package/src/integrations/components/ShareDialog.tsx +550 -0
- package/src/integrations/components/share-dialog-store.ts +55 -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 +829 -0
- package/src/thread/components/AgentThreadPane.tsx +367 -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 +2018 -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 +111 -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,203 @@
|
|
|
1
|
+
// The right-hand Changes column: the diff's changed files as a directory tree.
|
|
2
|
+
// A click on a file scrolls the diff to that file. Renders nothing when collapsed
|
|
3
|
+
// so the diff reclaims the width. Mirrors the left Threads column on the far side.
|
|
4
|
+
// Co-located here: the state hook that opens it by context and the header toggle.
|
|
5
|
+
|
|
6
|
+
import { ScrollArea } from "../../ui/components/ScrollArea";
|
|
7
|
+
import React, { useMemo, useState } from "react";
|
|
8
|
+
import { useKeyboard } from "@opentui/react";
|
|
9
|
+
import type { DiffFileContents, Thread } from "@cueloop/schema";
|
|
10
|
+
import type { Theme } from "../../appearance/theme";
|
|
11
|
+
import type { DiffRow } from "../../diff/view-diff";
|
|
12
|
+
import { Tree } from "../../ui/components/primitives/Tree";
|
|
13
|
+
import { allFolderIds, flattenTree } from "../../ui/components/primitives/tree-model";
|
|
14
|
+
import { IconButton } from "../../ui/components/primitives/IconButton";
|
|
15
|
+
import { NERD } from "../../ui/components/primitives/icons";
|
|
16
|
+
import { buildFileTree } from "./file-tree";
|
|
17
|
+
|
|
18
|
+
export interface DiffColumnsState {
|
|
19
|
+
changesOpen: boolean;
|
|
20
|
+
toggleChanges: () => void;
|
|
21
|
+
currentFilePath?: string;
|
|
22
|
+
scrollToFile: (path: string) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Owns the right Changes column for a diff: context opens it when you enter a
|
|
27
|
+
* diff thread, and a file click scrolls the sheet to that file's first row.
|
|
28
|
+
* Resets on session change via the guarded render-time pattern, no effect.
|
|
29
|
+
*/
|
|
30
|
+
export function useDiffColumns(params: {
|
|
31
|
+
session: Thread | null;
|
|
32
|
+
rows: DiffRow[];
|
|
33
|
+
cursor: number;
|
|
34
|
+
setCursor: (index: number) => void;
|
|
35
|
+
}): DiffColumnsState {
|
|
36
|
+
const { session, rows, cursor, setCursor } = params;
|
|
37
|
+
const contextDefault = session?.artifact.type === "diff";
|
|
38
|
+
const [changesOpen, setChangesOpen] = useState(contextDefault);
|
|
39
|
+
const [seenSessionId, setSeenSessionId] = useState(session?.id);
|
|
40
|
+
|
|
41
|
+
// entering another thread lets context decide again; within a thread the manual toggle stays
|
|
42
|
+
if (session?.id !== seenSessionId) {
|
|
43
|
+
setSeenSessionId(session?.id);
|
|
44
|
+
setChangesOpen(contextDefault);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
changesOpen,
|
|
49
|
+
toggleChanges: () => setChangesOpen((open) => !open),
|
|
50
|
+
currentFilePath: rows[cursor]?.file,
|
|
51
|
+
scrollToFile: (path: string) => {
|
|
52
|
+
const fileRowIndex = rows.findIndex((row) => row.kind === "file" && row.file === path);
|
|
53
|
+
|
|
54
|
+
if (fileRowIndex >= 0) setCursor(fileRowIndex);
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The header toggle for the Changes column; shown only for a diff, where files exist to browse. */
|
|
60
|
+
export function DiffChangesToggle({
|
|
61
|
+
isDiff,
|
|
62
|
+
open,
|
|
63
|
+
onToggle,
|
|
64
|
+
theme,
|
|
65
|
+
}: {
|
|
66
|
+
isDiff: boolean;
|
|
67
|
+
open: boolean;
|
|
68
|
+
onToggle: () => void;
|
|
69
|
+
theme?: Theme;
|
|
70
|
+
}): React.ReactNode {
|
|
71
|
+
if (!isDiff) return null;
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<IconButton
|
|
75
|
+
glyph={open ? NERD.sidebarRight : NERD.sidebarRightOff}
|
|
76
|
+
onPress={onToggle}
|
|
77
|
+
marginLeft={2}
|
|
78
|
+
theme={theme}
|
|
79
|
+
/>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface ChangesColumnProps {
|
|
84
|
+
open: boolean;
|
|
85
|
+
files?: readonly DiffFileContents[];
|
|
86
|
+
selectedPath?: string;
|
|
87
|
+
onSelectFile: (path: string, persistent?: boolean) => void;
|
|
88
|
+
commentCounts?: ReadonlyMap<string, number>;
|
|
89
|
+
focused?: boolean;
|
|
90
|
+
width?: number;
|
|
91
|
+
theme?: Theme;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The changed-files tree on its own, with no surrounding column frame: the body
|
|
96
|
+
* a workbench Project pane renders. A file click scrolls the diff to that file.
|
|
97
|
+
*/
|
|
98
|
+
export function ChangesFileTree({
|
|
99
|
+
files = [],
|
|
100
|
+
selectedPath,
|
|
101
|
+
onSelectFile,
|
|
102
|
+
commentCounts,
|
|
103
|
+
focused = false,
|
|
104
|
+
theme,
|
|
105
|
+
}: Omit<ChangesColumnProps, "open" | "width">): React.ReactNode {
|
|
106
|
+
const nodes = useMemo(() => buildFileTree(files, commentCounts), [files, commentCounts]);
|
|
107
|
+
// every folder opens by default so each changed file is reachable; the user
|
|
108
|
+
// only ever names the folders they fold shut, so a new file set stays open
|
|
109
|
+
const [collapsedIds, setCollapsedIds] = useState<ReadonlySet<string>>(new Set());
|
|
110
|
+
const [cursor, setCursor] = useState(0);
|
|
111
|
+
const expandedIds = useMemo(() => {
|
|
112
|
+
const expanded = allFolderIds(nodes);
|
|
113
|
+
|
|
114
|
+
for (const id of collapsedIds) expanded.delete(id);
|
|
115
|
+
|
|
116
|
+
return expanded;
|
|
117
|
+
}, [nodes, collapsedIds]);
|
|
118
|
+
const rows = useMemo(
|
|
119
|
+
() => flattenTree(nodes, { expandedIds, flattenEmptyDirectories: true }),
|
|
120
|
+
[nodes, expandedIds],
|
|
121
|
+
);
|
|
122
|
+
const cursorIndex = Math.min(cursor, Math.max(0, rows.length - 1));
|
|
123
|
+
|
|
124
|
+
const toggle = (id: string): void =>
|
|
125
|
+
setCollapsedIds((current) => {
|
|
126
|
+
const next = new Set(current);
|
|
127
|
+
|
|
128
|
+
if (next.has(id)) next.delete(id);
|
|
129
|
+
else next.add(id);
|
|
130
|
+
|
|
131
|
+
return next;
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
useKeyboard((key) => {
|
|
135
|
+
if (!focused || rows.length === 0) return;
|
|
136
|
+
if (key.name === "j" || key.name === "down")
|
|
137
|
+
return setCursor(Math.min(cursorIndex + 1, rows.length - 1));
|
|
138
|
+
if (key.name === "k" || key.name === "up") return setCursor(Math.max(cursorIndex - 1, 0));
|
|
139
|
+
const row = rows[cursorIndex];
|
|
140
|
+
|
|
141
|
+
if (!row) return;
|
|
142
|
+
if (key.name === "return" || key.name === "enter")
|
|
143
|
+
return row.isFolder ? toggle(row.id) : onSelectFile(row.id, true);
|
|
144
|
+
if (key.name === "l" && row.isFolder && collapsedIds.has(row.id)) return toggle(row.id);
|
|
145
|
+
if (key.name === "h" && row.isFolder && !collapsedIds.has(row.id)) return toggle(row.id);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<ScrollArea
|
|
150
|
+
id="tree-scroll"
|
|
151
|
+
revealId={focused ? rows[cursorIndex]?.id : undefined}
|
|
152
|
+
gestureWheel
|
|
153
|
+
>
|
|
154
|
+
<Tree
|
|
155
|
+
nodes={nodes}
|
|
156
|
+
expandedIds={expandedIds}
|
|
157
|
+
selectedId={focused ? rows[cursorIndex]?.id : selectedPath}
|
|
158
|
+
singleLine
|
|
159
|
+
flattenEmptyDirectories
|
|
160
|
+
showStatus
|
|
161
|
+
onSelect={(id) => {
|
|
162
|
+
const index = rows.findIndex((row) => row.id === id);
|
|
163
|
+
|
|
164
|
+
if (index >= 0) setCursor(index);
|
|
165
|
+
onSelectFile(id);
|
|
166
|
+
}}
|
|
167
|
+
onDoubleSelect={(id) => onSelectFile(id, true)}
|
|
168
|
+
onToggle={toggle}
|
|
169
|
+
theme={theme}
|
|
170
|
+
/>
|
|
171
|
+
</ScrollArea>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function ChangesColumn({
|
|
176
|
+
open,
|
|
177
|
+
files = [],
|
|
178
|
+
selectedPath,
|
|
179
|
+
onSelectFile,
|
|
180
|
+
width = 32,
|
|
181
|
+
theme,
|
|
182
|
+
}: ChangesColumnProps): React.ReactNode {
|
|
183
|
+
if (!open) return null;
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<box
|
|
187
|
+
style={{
|
|
188
|
+
width,
|
|
189
|
+
flexDirection: "column",
|
|
190
|
+
borderStyle: "single",
|
|
191
|
+
border: ["left"],
|
|
192
|
+
borderColor: theme?.border,
|
|
193
|
+
}}
|
|
194
|
+
>
|
|
195
|
+
<ChangesFileTree
|
|
196
|
+
files={files}
|
|
197
|
+
selectedPath={selectedPath}
|
|
198
|
+
onSelectFile={onSelectFile}
|
|
199
|
+
theme={theme}
|
|
200
|
+
/>
|
|
201
|
+
</box>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/** The changed-files tree navigates by keyboard when focused: j/k move the cursor, Enter folds a folder
|
|
2
|
+
* or opens a file. */
|
|
3
|
+
|
|
4
|
+
import { test, expect } from "bun:test";
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { testRender } from "@opentui/react/test-utils";
|
|
7
|
+
import { ScrollBoxRenderable, TextRenderable, type Renderable } from "@opentui/core";
|
|
8
|
+
import type { DiffFileContents } from "@cueloop/schema";
|
|
9
|
+
import { ChangesFileTree } from "./ChangesColumn";
|
|
10
|
+
import { settle, waitForText } from "../../testing/test-support";
|
|
11
|
+
import { DARK } from "../../appearance/theme";
|
|
12
|
+
|
|
13
|
+
function findById(node: Renderable, id: string): Renderable | undefined {
|
|
14
|
+
if (node.id === id) return node;
|
|
15
|
+
for (const child of node.getChildren()) {
|
|
16
|
+
const found = findById(child, id);
|
|
17
|
+
|
|
18
|
+
if (found) return found;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const file = (path: string): DiffFileContents => ({
|
|
25
|
+
path,
|
|
26
|
+
oldContents: "",
|
|
27
|
+
newContents: "x",
|
|
28
|
+
status: "modified",
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const files = [file("src/a.ts"), file("src/b.ts")];
|
|
32
|
+
|
|
33
|
+
test("j moves onto a file and Enter opens it", async () => {
|
|
34
|
+
const opened: string[] = [];
|
|
35
|
+
const setup = await testRender(
|
|
36
|
+
<ChangesFileTree
|
|
37
|
+
files={files}
|
|
38
|
+
onSelectFile={(path) => opened.push(path)}
|
|
39
|
+
focused
|
|
40
|
+
theme={DARK}
|
|
41
|
+
/>,
|
|
42
|
+
{ width: 40, height: 12 },
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
await settle(setup);
|
|
46
|
+
await waitForText(setup, "a.ts");
|
|
47
|
+
|
|
48
|
+
// folders open by default; cursor starts on the "src" folder, j steps onto a.ts
|
|
49
|
+
setup.mockInput.pressKey("j");
|
|
50
|
+
await settle(setup);
|
|
51
|
+
setup.mockInput.pressKey("RETURN");
|
|
52
|
+
await settle(setup);
|
|
53
|
+
|
|
54
|
+
expect(opened).toEqual(["src/a.ts"]);
|
|
55
|
+
|
|
56
|
+
setup.renderer.destroy();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("Tab leaves the focused tree without opening a file", async () => {
|
|
60
|
+
const opened: string[] = [];
|
|
61
|
+
const setup = await testRender(
|
|
62
|
+
<ChangesFileTree
|
|
63
|
+
files={files}
|
|
64
|
+
onSelectFile={(path) => opened.push(path)}
|
|
65
|
+
focused
|
|
66
|
+
theme={DARK}
|
|
67
|
+
/>,
|
|
68
|
+
{ width: 40, height: 12 },
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
await waitForText(setup, "a.ts");
|
|
72
|
+
setup.mockInput.pressKey("j");
|
|
73
|
+
setup.mockInput.pressKey("TAB");
|
|
74
|
+
await settle(setup);
|
|
75
|
+
expect(opened).toEqual([]);
|
|
76
|
+
setup.renderer.destroy();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("keyboard navigation reveals the selected changed file", async () => {
|
|
80
|
+
const manyFiles = Array.from({ length: 35 }, (_, index) =>
|
|
81
|
+
file(`file-${String(index).padStart(2, "0")}.ts`),
|
|
82
|
+
);
|
|
83
|
+
const setup = await testRender(
|
|
84
|
+
<ChangesFileTree files={manyFiles} onSelectFile={() => {}} focused theme={DARK} />,
|
|
85
|
+
{ width: 28, height: 8 },
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
await waitForText(setup, "file-00.ts");
|
|
89
|
+
const scrollbox = findById(setup.renderer.root, "tree-scroll");
|
|
90
|
+
|
|
91
|
+
if (!(scrollbox instanceof ScrollBoxRenderable)) throw new Error("tree scrollbox missing");
|
|
92
|
+
const scrollSteps: number[] = [];
|
|
93
|
+
|
|
94
|
+
for (const [index, key] of Array.from({ length: 15 }, () => "j").entries()) {
|
|
95
|
+
const before = scrollbox.scrollTop;
|
|
96
|
+
|
|
97
|
+
setup.mockInput.pressKey(key);
|
|
98
|
+
// eslint-disable-next-line no-await-in-loop
|
|
99
|
+
await settle(setup);
|
|
100
|
+
scrollSteps.push(scrollbox.scrollTop - before);
|
|
101
|
+
const row = scrollbox.content.findDescendantById(
|
|
102
|
+
`tree-row-file-${String(index + 1).padStart(2, "0")}.ts`,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
expect(row?.y).toBeGreaterThanOrEqual(scrollbox.viewport.y);
|
|
106
|
+
expect(row?.y).toBeLessThan(scrollbox.viewport.y + scrollbox.viewport.height);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
expect(scrollbox.scrollTop).toBeGreaterThan(0);
|
|
110
|
+
expect(Math.max(...scrollSteps)).toBe(1);
|
|
111
|
+
expect(setup.captureCharFrame()).toContain("file-15.ts");
|
|
112
|
+
setup.renderer.destroy();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("without focus the keyboard opens nothing", async () => {
|
|
116
|
+
const opened: string[] = [];
|
|
117
|
+
const setup = await testRender(
|
|
118
|
+
<ChangesFileTree files={files} onSelectFile={(path) => opened.push(path)} theme={DARK} />,
|
|
119
|
+
{ width: 40, height: 12 },
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
await settle(setup);
|
|
123
|
+
await waitForText(setup, "a.ts");
|
|
124
|
+
|
|
125
|
+
setup.mockInput.pressKey("j");
|
|
126
|
+
setup.mockInput.pressKey("RETURN");
|
|
127
|
+
await settle(setup);
|
|
128
|
+
expect(opened).toEqual([]);
|
|
129
|
+
|
|
130
|
+
setup.renderer.destroy();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("a selected long filename has no empty continuation row", async () => {
|
|
134
|
+
for (const width of [33, 32, 31, 30, 29, 28, 27, 26, 25, 24]) {
|
|
135
|
+
const setup = await testRender(
|
|
136
|
+
<ChangesFileTree
|
|
137
|
+
files={[
|
|
138
|
+
file("packages/client/src/App.workbench.test.tsx"),
|
|
139
|
+
file("packages/client/src/next.ts"),
|
|
140
|
+
]}
|
|
141
|
+
onSelectFile={() => {}}
|
|
142
|
+
focused
|
|
143
|
+
theme={DARK}
|
|
144
|
+
/>,
|
|
145
|
+
{ width, height: 12 },
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
await settle(setup);
|
|
149
|
+
setup.mockInput.pressKey("j");
|
|
150
|
+
await settle(setup);
|
|
151
|
+
const frame = setup.captureCharFrame().split("\n");
|
|
152
|
+
const selectedLine = frame.findIndex((line) => line.includes("App.wor"));
|
|
153
|
+
const row = findById(
|
|
154
|
+
setup.renderer.root,
|
|
155
|
+
"tree-row-packages/client/src/App.workbench.test.tsx",
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
expect(selectedLine).toBeGreaterThanOrEqual(0);
|
|
159
|
+
expect(frame[selectedLine + 1]).toContain("next.ts");
|
|
160
|
+
expect(row?.height).toBe(1);
|
|
161
|
+
setup.renderer.destroy();
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("horizontal name scrolling reveals the final extension beside the status", async () => {
|
|
166
|
+
const path = "src/sidebar-scroll.test.tsx";
|
|
167
|
+
const setup = await testRender(
|
|
168
|
+
<ChangesFileTree files={[file(path)]} onSelectFile={() => {}} focused theme={DARK} />,
|
|
169
|
+
{ width: 24, height: 8 },
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
await waitForText(setup, "src");
|
|
173
|
+
const label = findById(setup.renderer.root, `tree-label-${path}`);
|
|
174
|
+
|
|
175
|
+
if (!(label instanceof TextRenderable)) throw new Error("file label missing");
|
|
176
|
+
expect(label.maxScrollX).toBeGreaterThan(0);
|
|
177
|
+
label.scrollX = label.maxScrollX;
|
|
178
|
+
await settle(setup);
|
|
179
|
+
expect(setup.captureCharFrame()).toContain(".tsx");
|
|
180
|
+
setup.renderer.destroy();
|
|
181
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { testRender } from "@opentui/react/test-utils";
|
|
4
|
+
import type { Renderable } from "@opentui/core";
|
|
5
|
+
import { allowEventLoopUpdates } from "../../testing/test-support";
|
|
6
|
+
import { EditorGrid } from "./EditorGrid";
|
|
7
|
+
import { makeGroup, splitGroup, welcomeTab, type EditorNode } from "./editor-grid";
|
|
8
|
+
import { WelcomePlayground } from "./WelcomePlayground";
|
|
9
|
+
|
|
10
|
+
function findById(node: Renderable, id: string): Renderable | undefined {
|
|
11
|
+
if (node.id === id) return node;
|
|
12
|
+
|
|
13
|
+
for (const child of node.getChildren()) {
|
|
14
|
+
const found = findById(child, id);
|
|
15
|
+
|
|
16
|
+
if (found) return found;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function groupIds(node: EditorNode): string[] {
|
|
23
|
+
if (node.type === "group") return [node.id];
|
|
24
|
+
|
|
25
|
+
return node.children.flatMap(groupIds);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
test("mixed splits keep each Welcome page inside its own tile", async () => {
|
|
29
|
+
const first = makeGroup([welcomeTab()]);
|
|
30
|
+
let tree: EditorNode = first;
|
|
31
|
+
let focusGroupId = first.id;
|
|
32
|
+
|
|
33
|
+
for (const direction of ["right", "up", "left", "down"] as const) {
|
|
34
|
+
const next = splitGroup(tree, focusGroupId, direction);
|
|
35
|
+
|
|
36
|
+
tree = next.tree;
|
|
37
|
+
focusGroupId = next.focusGroupId;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const setup = await testRender(
|
|
41
|
+
<EditorGrid
|
|
42
|
+
tree={tree}
|
|
43
|
+
focusedGroupId={focusGroupId}
|
|
44
|
+
onFocusGroup={() => {}}
|
|
45
|
+
onActivateTab={() => {}}
|
|
46
|
+
onCloseTab={() => {}}
|
|
47
|
+
onSplit={() => {}}
|
|
48
|
+
onZoom={() => {}}
|
|
49
|
+
zoomed={false}
|
|
50
|
+
renderTab={(_, focused) => <WelcomePlayground quickActions={[]} suspended={!focused} />}
|
|
51
|
+
/>,
|
|
52
|
+
{ width: 120, height: 48 },
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
allowEventLoopUpdates();
|
|
56
|
+
await setup.waitForVisualIdle();
|
|
57
|
+
expect(setup.renderer.listenerCount("frame")).toBeLessThan(10);
|
|
58
|
+
expect(setup.renderer.listenerCount("resize")).toBeLessThan(10);
|
|
59
|
+
expect(setup.renderer.keyInput.listenerCount("keypress")).toBeLessThan(10);
|
|
60
|
+
|
|
61
|
+
for (const id of groupIds(tree)) {
|
|
62
|
+
const group = findById(setup.renderer.root, `editor-group:${id}`);
|
|
63
|
+
const scroll = group && findById(group, "diff-scroll");
|
|
64
|
+
|
|
65
|
+
expect(group).toBeDefined();
|
|
66
|
+
expect(scroll).toBeDefined();
|
|
67
|
+
expect(scroll!.y).toBeGreaterThanOrEqual(group!.y + 2);
|
|
68
|
+
expect(scroll!.y + scroll!.height).toBeLessThanOrEqual(group!.y + group!.height);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
setup.renderer.destroy();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("Split is disabled at eight tiles and when a tile is too small", async () => {
|
|
75
|
+
const first = makeGroup([welcomeTab()]);
|
|
76
|
+
let tree: EditorNode = first;
|
|
77
|
+
let focusGroupId = first.id;
|
|
78
|
+
|
|
79
|
+
for (const direction of ["right", "up", "left", "down", "right", "up", "left"] as const) {
|
|
80
|
+
const next = splitGroup(tree, focusGroupId, direction);
|
|
81
|
+
|
|
82
|
+
tree = next.tree;
|
|
83
|
+
focusGroupId = next.focusGroupId;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (const [candidate, width, height] of [
|
|
87
|
+
[tree, 240, 100],
|
|
88
|
+
[first, 40, 12],
|
|
89
|
+
] as const) {
|
|
90
|
+
let splitCalls = 0;
|
|
91
|
+
const setup = await testRender(
|
|
92
|
+
<EditorGrid
|
|
93
|
+
tree={candidate}
|
|
94
|
+
focusedGroupId={first.id}
|
|
95
|
+
onFocusGroup={() => {}}
|
|
96
|
+
onActivateTab={() => {}}
|
|
97
|
+
onCloseTab={() => {}}
|
|
98
|
+
onSplit={() => {
|
|
99
|
+
splitCalls += 1;
|
|
100
|
+
}}
|
|
101
|
+
onZoom={() => {}}
|
|
102
|
+
zoomed={false}
|
|
103
|
+
renderTab={() => <text>body</text>}
|
|
104
|
+
/>,
|
|
105
|
+
{ width, height },
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
allowEventLoopUpdates();
|
|
109
|
+
await setup.waitForVisualIdle();
|
|
110
|
+
const button = findById(setup.renderer.root, `split-button:${first.id}`);
|
|
111
|
+
|
|
112
|
+
expect(button).toBeDefined();
|
|
113
|
+
await setup.mockMouse.click(button!.x, button!.y);
|
|
114
|
+
expect(setup.captureCharFrame()).not.toContain("right →");
|
|
115
|
+
expect(splitCalls).toBe(0);
|
|
116
|
+
setup.renderer.destroy();
|
|
117
|
+
}
|
|
118
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../../appearance/theme";
|
|
3
|
+
import type { Story, StoryMeta } from "../../stories/story";
|
|
4
|
+
import { EditorGrid } from "./EditorGrid";
|
|
5
|
+
import { changesTab, fileTab, makeGroup, splitGroup, type EditorTab } from "./editor-grid";
|
|
6
|
+
|
|
7
|
+
export const meta: StoryMeta = { title: "Surfaces/EditorGrid" };
|
|
8
|
+
|
|
9
|
+
const noop = (): void => {};
|
|
10
|
+
|
|
11
|
+
function renderTab(tab: EditorTab): React.ReactNode {
|
|
12
|
+
return (
|
|
13
|
+
<box style={{ flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
|
|
14
|
+
<text fg={DARK.textMuted}>{tab.kind === "changes" ? "the whole diff" : tab.path}</text>
|
|
15
|
+
</box>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const oneGroup = makeGroup([changesTab(), fileTab("App.tsx", "src/App.tsx", "diff")]);
|
|
20
|
+
const sourceGroup = makeGroup([fileTab("plugin.json", ".claude-plugin/plugin.json", "contents")]);
|
|
21
|
+
const splitTree = splitGroup(sourceGroup, sourceGroup.id, "right").tree;
|
|
22
|
+
|
|
23
|
+
export const OneGroup: Story = {
|
|
24
|
+
render: () => (
|
|
25
|
+
<EditorGrid
|
|
26
|
+
tree={oneGroup}
|
|
27
|
+
focusedGroupId={oneGroup.id}
|
|
28
|
+
onFocusGroup={noop}
|
|
29
|
+
onActivateTab={noop}
|
|
30
|
+
onCloseTab={noop}
|
|
31
|
+
onSplit={noop}
|
|
32
|
+
onZoom={noop}
|
|
33
|
+
zoomed={false}
|
|
34
|
+
renderTab={renderTab}
|
|
35
|
+
theme={DARK}
|
|
36
|
+
/>
|
|
37
|
+
),
|
|
38
|
+
expectedColors: [DARK.textMuted, DARK.border],
|
|
39
|
+
size: { width: 80, height: 16 },
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const SplitGroups: Story = {
|
|
43
|
+
render: () => (
|
|
44
|
+
<EditorGrid
|
|
45
|
+
tree={splitTree}
|
|
46
|
+
focusedGroupId={null}
|
|
47
|
+
onFocusGroup={noop}
|
|
48
|
+
onActivateTab={noop}
|
|
49
|
+
onCloseTab={noop}
|
|
50
|
+
onSplit={noop}
|
|
51
|
+
onZoom={noop}
|
|
52
|
+
zoomed={false}
|
|
53
|
+
renderTab={renderTab}
|
|
54
|
+
theme={DARK}
|
|
55
|
+
/>
|
|
56
|
+
),
|
|
57
|
+
expectedColors: [DARK.textMuted, DARK.border],
|
|
58
|
+
size: { width: 100, height: 16 },
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** The rightmost group takes the remainder after whole-cell leading groups and divider cells. */
|
|
62
|
+
export const WholeCellSizing: Story = {
|
|
63
|
+
render: () => (
|
|
64
|
+
<box style={{ flexDirection: "column", width: 100, height: 16 }}>
|
|
65
|
+
<text fg={DARK.text}>Whole-cell split sizing</text>
|
|
66
|
+
<text fg={DARK.textMuted}>100 cells = 49 left + 1 divider + 50 right</text>
|
|
67
|
+
<text fg={DARK.textDim}>Fix the leading width; let the last group take the remainder.</text>
|
|
68
|
+
<box style={{ flexGrow: 1, minHeight: 0 }}>
|
|
69
|
+
<EditorGrid
|
|
70
|
+
tree={splitTree}
|
|
71
|
+
focusedGroupId={null}
|
|
72
|
+
onFocusGroup={noop}
|
|
73
|
+
onActivateTab={noop}
|
|
74
|
+
onCloseTab={noop}
|
|
75
|
+
onSplit={noop}
|
|
76
|
+
onZoom={noop}
|
|
77
|
+
zoomed={false}
|
|
78
|
+
renderTab={renderTab}
|
|
79
|
+
theme={DARK}
|
|
80
|
+
/>
|
|
81
|
+
</box>
|
|
82
|
+
</box>
|
|
83
|
+
),
|
|
84
|
+
expectedColors: [DARK.textMuted, DARK.border],
|
|
85
|
+
size: { width: 100, height: 16 },
|
|
86
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression: file tabs keep the width of their name and the strip scrolls horizontally
|
|
3
|
+
* behind the header controls when the row overflows, revealing the active tab - instead of
|
|
4
|
+
* squeezing every tab and truncating its label.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from "bun:test";
|
|
8
|
+
import { testRender } from "@opentui/react/test-utils";
|
|
9
|
+
import React from "react";
|
|
10
|
+
import { EditorGrid, visibleTabWindow } from "./EditorGrid";
|
|
11
|
+
import { changesTab, fileTab, makeGroup, type EditorTab } from "./editor-grid";
|
|
12
|
+
import { NERD } from "../../ui/components/primitives/icons";
|
|
13
|
+
import { allowEventLoopUpdates } from "../../testing/test-support";
|
|
14
|
+
|
|
15
|
+
describe("visibleTabWindow", () => {
|
|
16
|
+
const tabs: EditorTab[] = ["aaaa", "bbbb", "cccc", "dddd", "eeee"].map((name) =>
|
|
17
|
+
fileTab(name, name, "diff"),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
test("an unmeasured strip shows every tab", () => {
|
|
21
|
+
// Arrange / Act / Assert - width 0 means the header clips until it is measured
|
|
22
|
+
expect(visibleTabWindow(tabs, 0, 0, 0)).toEqual({ first: 0, end: tabs.length });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("only the run that fits renders, and the active tab pulls the window to it", () => {
|
|
26
|
+
// Arrange - each tab is 9 cells (4 label + 5 chrome); a 30-cell strip fits about three
|
|
27
|
+
// Act - the last tab is active
|
|
28
|
+
const window = visibleTabWindow(tabs, 0, tabs.length - 1, 30);
|
|
29
|
+
|
|
30
|
+
// Assert - the window ends on the active tab and dropped earlier tabs
|
|
31
|
+
expect(window.end).toBe(tabs.length);
|
|
32
|
+
expect(window.first).toBeGreaterThan(0);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("a tab wider than the strip still shows on its own", () => {
|
|
36
|
+
// Arrange
|
|
37
|
+
const wide = [fileTab("a-very-long-file-name.tsx", "x", "diff")];
|
|
38
|
+
|
|
39
|
+
// Act / Assert
|
|
40
|
+
expect(visibleTabWindow(wide, 0, 0, 10)).toEqual({ first: 0, end: 1 });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("the comment badge counts toward a tab's width, so a commented run does not overflow", () => {
|
|
44
|
+
// Arrange - three 9-cell tabs fit a 27-cell strip exactly with no badge
|
|
45
|
+
const three = ["aaaa", "bbbb", "cccc"].map((name) => fileTab(name, name, "diff"));
|
|
46
|
+
|
|
47
|
+
// Act - without badges all three fit; a ` ● 2` badge on each widens them past the strip
|
|
48
|
+
const bare = visibleTabWindow(three, 0, 0, 27);
|
|
49
|
+
const badged = visibleTabWindow(three, 0, 0, 27, () => 2);
|
|
50
|
+
|
|
51
|
+
// Assert - the badge width narrows the window rather than clipping the last tab
|
|
52
|
+
expect(bare.end).toBe(3);
|
|
53
|
+
expect(badged.end).toBeLessThan(3);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const NAMES = [
|
|
58
|
+
"AppShell.tsx",
|
|
59
|
+
"DiffContentView.tsx",
|
|
60
|
+
"IconButton.tsx",
|
|
61
|
+
"EditorGrid.zoom.test.tsx",
|
|
62
|
+
"ThreadView.tsx",
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
async function headerRow(activeIndex: number): Promise<string> {
|
|
66
|
+
const group = makeGroup([
|
|
67
|
+
changesTab(),
|
|
68
|
+
...NAMES.map((name) => fileTab(name, `src/${name}`, "diff")),
|
|
69
|
+
]);
|
|
70
|
+
const tree = { ...group, activeTabId: group.tabs[activeIndex]!.id };
|
|
71
|
+
const setup = await testRender(
|
|
72
|
+
<EditorGrid
|
|
73
|
+
tree={tree}
|
|
74
|
+
focusedGroupId={tree.id}
|
|
75
|
+
onFocusGroup={() => {}}
|
|
76
|
+
onActivateTab={() => {}}
|
|
77
|
+
onCloseTab={() => {}}
|
|
78
|
+
onSplit={() => {}}
|
|
79
|
+
onZoom={() => {}}
|
|
80
|
+
zoomed={false}
|
|
81
|
+
renderTab={() => <text>body</text>}
|
|
82
|
+
/>,
|
|
83
|
+
{ width: 70, height: 4 },
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
allowEventLoopUpdates();
|
|
87
|
+
await setup.waitForVisualIdle();
|
|
88
|
+
// the strip reveals the active tab on the frame after layout
|
|
89
|
+
await setup.waitForVisualIdle();
|
|
90
|
+
const row = setup.captureCharFrame().split("\n")[0]!;
|
|
91
|
+
|
|
92
|
+
setup.renderer.destroy();
|
|
93
|
+
|
|
94
|
+
return row;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
describe("editor tab strip", () => {
|
|
98
|
+
test("tabs keep their full names and the controls stay pinned when the row overflows", async () => {
|
|
99
|
+
// Act
|
|
100
|
+
const row = await headerRow(0);
|
|
101
|
+
|
|
102
|
+
// Assert - no squeezed labels: every visible tab reads in full up to the clip edge
|
|
103
|
+
expect(row).toContain("Changes ");
|
|
104
|
+
expect(row).toContain("AppShell.tsx ");
|
|
105
|
+
expect(row).toContain("DiffContentView.tsx ");
|
|
106
|
+
// and the header controls sit at the right edge, past the clipped strip (search is out for now)
|
|
107
|
+
expect(row.trimEnd().endsWith(NERD.zoom)).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("the strip scrolls to reveal the active tab", async () => {
|
|
111
|
+
// Act
|
|
112
|
+
const row = await headerRow(NAMES.length);
|
|
113
|
+
|
|
114
|
+
// Assert - the last tab is on screen, the first has scrolled out behind the left edge
|
|
115
|
+
expect(row).toContain("ThreadView.tsx ");
|
|
116
|
+
expect(row).not.toContain("Changes ");
|
|
117
|
+
});
|
|
118
|
+
});
|