@cueloop/client 0.1.0-alpha.10 → 0.1.0-alpha.101
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/native/build-pty.sh +51 -0
- package/native/build.sh +82 -0
- package/native/darwin-arm64/libcuelooppty.dylib +0 -0
- package/native/darwin-arm64/libcueloopvt.dylib +0 -0
- package/native/src/pty.zig +193 -0
- package/native/src/shim.zig +114 -0
- package/package.json +42 -34
- package/src/annotations/annotation-palette.ts +62 -0
- package/src/annotations/components/AnnotationCards.stories.tsx +86 -0
- package/src/annotations/components/AnnotationCards.test.tsx +42 -0
- package/src/annotations/components/AnnotationCards.tsx +369 -0
- package/src/annotations/components/DiscussionMarkerRail.stories.tsx +54 -0
- package/src/annotations/components/DiscussionMarkerRail.tsx +186 -0
- package/src/annotations/discussions.ts +77 -0
- package/src/annotations/thread-selection.test.ts +210 -0
- package/src/annotations/thread-selection.ts +242 -0
- package/src/annotations/use-annotation-surface.tsx +1243 -0
- package/src/app/App.appearance.test.tsx +53 -0
- package/src/{App.autoclose.test.tsx → app/App.autoclose.test.tsx} +77 -51
- package/src/app/App.deleted-scroll.test.tsx +187 -0
- package/src/app/App.diff.test.tsx +313 -0
- package/src/app/App.export.test.tsx +105 -0
- package/src/app/App.focus.test.ts +89 -0
- package/src/app/App.identity.test.tsx +84 -0
- package/src/app/App.plan-render.test.tsx +209 -0
- package/src/app/App.plan-review.test.tsx +428 -0
- package/src/app/App.prototype-markdown.test.tsx +128 -0
- package/src/app/App.prototype.test.tsx +176 -0
- package/src/app/App.readonly.test.tsx +210 -0
- package/src/app/App.regression.test.tsx +99 -0
- package/src/app/App.rendering.test.tsx +134 -0
- package/src/app/App.share-dialog.test.tsx +177 -0
- package/src/app/App.submit-confirm.test.tsx +267 -0
- package/src/app/App.test.tsx +496 -0
- package/src/app/App.thread-agent.test.ts +20 -0
- package/src/app/App.toast.test.tsx +124 -0
- package/src/app/App.tsx +1667 -0
- package/src/app/App.visual.test.tsx +59 -0
- package/src/app/App.walk.test.tsx +299 -0
- package/src/app/App.workbench.test.tsx +1149 -0
- package/src/app/app-key-state.ts +61 -0
- package/src/app/app-screens.tsx +606 -0
- package/src/app/app-view-model.ts +168 -0
- package/src/app/components/AppHeader.stories.tsx +50 -0
- package/src/app/components/AppHeader.tsx +81 -0
- package/src/app/components/AppShell.brand.test.tsx +58 -0
- package/src/app/components/AppShell.extensions.test.tsx +191 -0
- package/src/app/components/AppShell.stories.tsx +89 -0
- package/src/app/components/AppShell.tsx +358 -0
- package/src/app/components/AppShell.zoom.test.tsx +149 -0
- package/src/app/components/Breadcrumb.stories.tsx +19 -0
- package/src/app/components/Breadcrumb.tsx +45 -0
- package/src/app/components/MenuBar.stories.tsx +28 -0
- package/src/app/components/MenuBar.tsx +44 -0
- package/src/app/components/PaneGrid.stories.tsx +84 -0
- package/src/app/components/PaneGrid.tsx +128 -0
- package/src/app/components/PanelColumn.stories.tsx +25 -0
- package/src/app/components/PanelColumn.tsx +93 -0
- package/src/app/components/ShellHeader.stories.tsx +50 -0
- package/src/app/components/ShellHeader.tsx +137 -0
- package/src/app/components/WorkspacePanels.stories.tsx +36 -0
- package/src/app/components/WorkspacePanels.tsx +120 -0
- package/src/app/components/pages/Workspace.stories.tsx +181 -0
- package/src/app/intent-dispatch.test.ts +728 -0
- package/src/app/intent-dispatch.ts +687 -0
- package/src/app/launch-layout.test.ts +78 -0
- package/src/app/launch-layout.ts +57 -0
- package/src/app/ready-signal.test.ts +85 -0
- package/src/app/ready-signal.ts +52 -0
- package/src/app/run.ts +109 -0
- package/src/{serve.test.ts → app/serve.test.ts} +24 -12
- package/src/app/serve.ts +111 -0
- package/src/app/version.ts +5 -0
- package/src/appearance/components/theme-context.stories.tsx +46 -0
- package/src/appearance/components/theme-context.tsx +37 -0
- package/src/appearance/theme-presets.test.ts +51 -0
- package/src/appearance/theme-presets.ts +219 -0
- package/src/appearance/theme.ts +112 -0
- package/src/appearance/visual/contrast.test.ts +35 -0
- package/src/appearance/visual/contrast.ts +33 -0
- package/src/diff/components/AnnotatableFileView.stories.tsx +42 -0
- package/src/diff/components/AnnotatableFileView.test.tsx +157 -0
- package/src/diff/components/AnnotatableFileView.tsx +141 -0
- package/src/diff/components/BareWorkbenchFileView.stories.tsx +27 -0
- package/src/diff/components/BareWorkbenchFileView.tsx +63 -0
- package/src/diff/components/DiffContentView.scroll.test.tsx +434 -0
- package/src/diff/components/DiffContentView.split.test.tsx +140 -0
- package/src/diff/components/DiffContentView.stories.tsx +80 -0
- package/src/diff/components/DiffContentView.syntax.test.tsx +82 -0
- package/src/diff/components/DiffContentView.tsx +1055 -0
- package/src/diff/components/WalkWizard.stories.tsx +100 -0
- package/src/diff/components/WalkWizard.tsx +146 -0
- package/src/diff/components/diff-content-view-layout.test.ts +198 -0
- package/src/diff/components/diff-content-view-layout.ts +84 -0
- package/src/diff/diff-fold.test.ts +78 -0
- package/src/diff/diff-fold.ts +126 -0
- package/src/diff/diff-hunk-curate.test.ts +315 -0
- package/src/diff/diff-hunk-curate.ts +152 -0
- package/src/diff/diff-intraline.test.ts +239 -0
- package/src/diff/diff-intraline.ts +238 -0
- package/src/diff/diff-syntax.test.ts +125 -0
- package/src/diff/diff-syntax.ts +186 -0
- package/src/diff/split-diff.test.ts +110 -0
- package/src/diff/split-diff.ts +144 -0
- package/src/diff/view-diff.test.ts +325 -0
- package/src/diff/view-diff.ts +522 -0
- package/src/diff/walk.test.ts +121 -0
- package/src/diff/walk.ts +73 -0
- package/src/index.ts +31 -2
- package/src/integrations/attribution.test.ts +31 -0
- package/src/integrations/attribution.ts +42 -0
- package/src/integrations/client-extension-registry.test.tsx +61 -0
- package/src/integrations/client-extension-registry.ts +159 -0
- package/src/integrations/clipboard.test.ts +23 -0
- package/src/integrations/clipboard.ts +33 -0
- package/src/integrations/components/ExtensionZone.stories.tsx +28 -0
- package/src/integrations/components/ExtensionZone.tsx +145 -0
- package/src/integrations/components/ShareDialog.agent.test.tsx +50 -0
- package/src/integrations/components/ShareDialog.stories.tsx +121 -0
- package/src/integrations/components/ShareDialog.tsx +574 -0
- package/src/integrations/components/share-dialog-store.ts +59 -0
- package/src/integrations/editor.test.ts +149 -0
- package/src/integrations/editor.ts +160 -0
- package/src/integrations/github-identity.test.ts +58 -0
- package/src/integrations/github-identity.ts +41 -0
- package/src/integrations/herdr.test.ts +43 -0
- package/src/integrations/herdr.ts +38 -0
- package/src/integrations/integrations.test.ts +41 -0
- package/src/integrations/integrations.ts +32 -0
- package/src/integrations/pasted-image.test.ts +67 -0
- package/src/integrations/pasted-image.ts +60 -0
- package/src/integrations/share.ts +238 -0
- package/src/keyboard/components/CompletionOverlay.stories.tsx +37 -0
- package/src/keyboard/components/CompletionOverlay.tsx +75 -0
- package/src/keyboard/components/HandleChipsInput.stories.tsx +24 -0
- package/src/keyboard/components/HandleChipsInput.test.tsx +35 -0
- package/src/keyboard/components/HandleChipsInput.tsx +143 -0
- package/src/keyboard/components/KeybindsDialog.stories.tsx +31 -0
- package/src/keyboard/components/KeybindsDialog.tsx +103 -0
- package/src/keyboard/components/NavModeHint.stories.tsx +40 -0
- package/src/keyboard/components/NavModeHint.tsx +31 -0
- package/src/keyboard/components/SlashComposer.stories.tsx +37 -0
- package/src/keyboard/components/SlashComposer.tsx +87 -0
- package/src/keyboard/key-bindings.test.ts +156 -0
- package/src/keyboard/key-bindings.ts +459 -0
- package/src/keyboard/keymap.test.ts +611 -0
- package/src/keyboard/keymap.ts +395 -0
- package/src/keyboard/skills.test.ts +74 -0
- package/src/keyboard/skills.ts +75 -0
- package/src/keyboard/slash-palette.test.ts +103 -0
- package/src/keyboard/slash-palette.ts +129 -0
- package/src/keyboard/thread-chords.test.ts +79 -0
- package/src/keyboard/thread-chords.ts +163 -0
- package/src/keyboard/use-shared-keyboard.ts +55 -0
- package/src/markdown/__snapshots__/view-plan.regression.test.ts.snap +100 -0
- package/src/markdown/components/MarkdownGridBlock.stories.tsx +33 -0
- package/src/markdown/components/MarkdownGridBlock.tsx +72 -0
- package/src/markdown/components/MarkdownThreadEditor.stories.tsx +15 -0
- package/src/markdown/components/MarkdownThreadEditor.test.tsx +98 -0
- package/src/markdown/components/MarkdownThreadEditor.tsx +160 -0
- package/src/markdown/components/ThreadView.selection.test.tsx +951 -0
- package/src/markdown/components/ThreadView.slash.test.tsx +146 -0
- package/src/markdown/components/ThreadView.stories.tsx +26 -0
- package/src/markdown/components/ThreadView.tsx +560 -0
- package/src/markdown/mark-runs.ts +121 -0
- package/src/markdown/markdown-editor-syntax-style.ts +50 -0
- package/src/markdown/markdown-grid.test.ts +101 -0
- package/src/markdown/markdown-grid.ts +203 -0
- package/src/markdown/markdown-highlight.test.ts +61 -0
- package/src/markdown/markdown-highlight.ts +124 -0
- package/src/markdown/thread-code-syntax.ts +45 -0
- package/src/markdown/use-thread-body-editing.tsx +54 -0
- package/src/markdown/view-plan.regression.test.ts +60 -0
- package/src/markdown/view-plan.test.ts +545 -0
- package/src/markdown/view-plan.ts +1009 -0
- package/src/performance/otlp-trace.test.ts +119 -0
- package/src/performance/otlp-trace.ts +134 -0
- package/src/performance/perf-report.ts +18 -0
- package/src/performance/perf-timings.test.ts +18 -0
- package/src/performance/perf-timings.ts +39 -0
- package/src/prototype/components/PrototypeContentView.stories.tsx +19 -0
- package/src/prototype/components/PrototypeContentView.tsx +442 -0
- package/src/prototype/prototype-browser.test.ts +81 -0
- package/src/prototype/prototype-browser.ts +321 -0
- package/src/prototype/prototype-pixels.tsx +35 -0
- package/src/settings/components/SettingsDialog.stories.tsx +117 -0
- package/src/settings/components/SettingsDialog.tsx +183 -0
- package/src/settings/components/SettingsRows.stories.tsx +18 -0
- package/src/settings/components/SettingsRows.tsx +112 -0
- package/src/settings/components/quick-actions-editor.stories.tsx +58 -0
- package/src/settings/components/quick-actions-editor.test.tsx +120 -0
- package/src/settings/components/quick-actions-editor.tsx +222 -0
- package/src/settings/config-identity.test.ts +78 -0
- package/src/settings/config.test.ts +676 -0
- package/src/settings/config.ts +628 -0
- package/src/settings/use-settings-dialog.tsx +351 -0
- package/src/stories/__snapshots__/stories.test.tsx.snap +2358 -0
- package/src/stories/stories-app.tsx +196 -0
- package/src/stories/stories.test.tsx +74 -0
- package/src/stories/story-fixtures.ts +102 -0
- package/src/stories/story-tree.test.ts +108 -0
- package/src/stories/story.ts +154 -0
- package/src/terminal/ansi-screen.test.ts +28 -0
- package/src/terminal/ansi-screen.ts +72 -0
- package/src/terminal/components/AnsiScreen.stories.tsx +12 -0
- package/src/terminal/components/AnsiScreen.tsx +28 -0
- package/src/terminal/components/terminal-pane.test.tsx +40 -0
- package/src/terminal/components/terminal-pane.ts +196 -0
- package/src/terminal/ghostty-terminal.test.ts +85 -0
- package/src/terminal/ghostty-terminal.ts +197 -0
- package/src/terminal/kitty-image.test.ts +66 -0
- package/src/terminal/kitty-image.ts +137 -0
- package/src/terminal/pty.test.ts +81 -0
- package/src/terminal/pty.ts +246 -0
- package/src/testing/test-support.test.ts +264 -0
- package/src/testing/test-support.ts +367 -0
- package/src/thread/agent-continuation.test.ts +32 -0
- package/src/thread/agent-continuation.ts +13 -0
- package/src/thread/agent-transcript.test.ts +161 -0
- package/src/thread/agent-transcript.ts +336 -0
- package/src/thread/components/AgentConfigControls.stories.tsx +39 -0
- package/src/thread/components/AgentConfigControls.test.tsx +112 -0
- package/src/thread/components/AgentConfigControls.tsx +114 -0
- package/src/thread/components/AgentThreadPane.stories.tsx +105 -0
- package/src/thread/components/AgentThreadPane.test.tsx +935 -0
- package/src/thread/components/AgentThreadPane.tsx +377 -0
- package/src/thread/components/ThreadFooter.stories.tsx +30 -0
- package/src/thread/components/ThreadFooter.test.tsx +49 -0
- package/src/thread/components/ThreadFooter.tsx +77 -0
- package/src/thread/components/ThreadTree.stories.tsx +46 -0
- package/src/thread/components/ThreadTree.test.tsx +41 -0
- package/src/thread/components/ThreadTree.tsx +267 -0
- package/src/thread/components/thread-tree.test.ts +137 -0
- package/src/thread/components/thread-tree.ts +112 -0
- package/src/thread/thread-controller.curate.test.ts +502 -0
- package/src/thread/thread-controller.derivation.test.ts +62 -0
- package/src/thread/thread-controller.live-diff.test.ts +357 -0
- package/src/thread/thread-controller.optimistic.test.ts +69 -0
- package/src/thread/thread-controller.share.test.ts +573 -0
- package/src/thread/thread-controller.switch-cache.test.ts +174 -0
- package/src/thread/thread-controller.tree.test.ts +263 -0
- package/src/thread/thread-controller.ts +2026 -0
- package/src/thread/tree-view.test.ts +128 -0
- package/src/thread/tree-view.ts +146 -0
- package/src/thread/use-thread-agent.test.tsx +208 -0
- package/src/thread/use-thread-agent.ts +117 -0
- package/src/ui/components/CodeBlock.stories.tsx +42 -0
- package/src/ui/components/CodeBlock.tsx +80 -0
- package/src/ui/components/ConfirmCard.stories.tsx +37 -0
- package/src/ui/components/ConfirmCard.tsx +151 -0
- package/src/ui/components/ConfirmDialog.stories.tsx +21 -0
- package/src/ui/components/ConfirmDialog.tsx +72 -0
- package/src/ui/components/OverlayScrollbar.stories.tsx +33 -0
- package/src/ui/components/OverlayScrollbar.test.tsx +139 -0
- package/src/ui/components/OverlayScrollbar.tsx +129 -0
- package/src/ui/components/PromptDialog.stories.tsx +36 -0
- package/src/ui/components/PromptDialog.test.tsx +65 -0
- package/src/ui/components/PromptDialog.tsx +99 -0
- package/src/ui/components/RootOverlay.stories.tsx +45 -0
- package/src/ui/components/RootOverlay.tsx +49 -0
- package/src/ui/components/ScrollArea.stories.tsx +20 -0
- package/src/ui/components/ScrollArea.tsx +61 -0
- package/src/ui/components/Toast.stories.tsx +19 -0
- package/src/ui/components/Toast.tsx +54 -0
- package/src/ui/components/Tooltip.stories.tsx +26 -0
- package/src/ui/components/Tooltip.tsx +72 -0
- package/src/ui/components/menu-control.stories.tsx +35 -0
- package/src/ui/components/menu-control.tsx +43 -0
- package/src/ui/components/primitives/Button.stories.tsx +32 -0
- package/src/ui/components/primitives/Button.tsx +57 -0
- package/src/ui/components/primitives/Card.stories.tsx +25 -0
- package/src/ui/components/primitives/Card.tsx +62 -0
- package/src/ui/components/primitives/Dialog.stories.tsx +24 -0
- package/src/ui/components/primitives/Dialog.test.tsx +38 -0
- package/src/ui/components/primitives/Dialog.tsx +69 -0
- package/src/ui/components/primitives/DialogActions.stories.tsx +20 -0
- package/src/ui/components/primitives/DialogActions.tsx +39 -0
- package/src/ui/components/primitives/IconButton.stories.tsx +36 -0
- package/src/ui/components/primitives/IconButton.tsx +58 -0
- package/src/ui/components/primitives/StatusBar.stories.tsx +11 -0
- package/src/ui/components/primitives/StatusBar.tsx +23 -0
- package/src/ui/components/primitives/Switch.stories.tsx +17 -0
- package/src/ui/components/primitives/Switch.tsx +32 -0
- package/src/ui/components/primitives/Tabs.stories.tsx +29 -0
- package/src/ui/components/primitives/Tabs.tsx +101 -0
- package/src/ui/components/primitives/Toolbar.stories.tsx +17 -0
- package/src/ui/components/primitives/Toolbar.tsx +14 -0
- package/src/ui/components/primitives/Tree.stories.tsx +124 -0
- package/src/ui/components/primitives/Tree.tsx +133 -0
- package/src/ui/components/primitives/frame.test.ts +12 -0
- package/src/ui/components/primitives/frame.ts +17 -0
- package/src/ui/components/primitives/icons.ts +61 -0
- package/src/ui/components/primitives/tree-model.test.ts +99 -0
- package/src/ui/components/primitives/tree-model.ts +137 -0
- package/src/ui/components/syntax-highlight.ts +133 -0
- package/src/ui/components/truncate-text.ts +7 -0
- package/src/ui/components/truncate-title.test.ts +31 -0
- package/src/ui/components/truncate-title.ts +15 -0
- package/src/ui/use-frame-measure.ts +74 -0
- package/src/ui/use-terminal-virtualizer.ts +259 -0
- package/src/workbench/components/ChangesColumn.stories.tsx +44 -0
- package/src/workbench/components/ChangesColumn.tsx +203 -0
- package/src/workbench/components/ChangesFileTree.test.tsx +181 -0
- package/src/workbench/components/EditorGrid.split.test.tsx +118 -0
- package/src/workbench/components/EditorGrid.stories.tsx +86 -0
- package/src/workbench/components/EditorGrid.tabs.test.tsx +118 -0
- package/src/workbench/components/EditorGrid.tsx +544 -0
- package/src/workbench/components/EditorGrid.zoom.test.tsx +132 -0
- package/src/workbench/components/FileContentsView.stories.tsx +16 -0
- package/src/workbench/components/FileContentsView.test.tsx +29 -0
- package/src/workbench/components/FileContentsView.tsx +100 -0
- package/src/workbench/components/GridTabContent.actions.test.tsx +151 -0
- package/src/workbench/components/GridTabContent.stories.tsx +59 -0
- package/src/workbench/components/GridTabContent.tsx +193 -0
- package/src/workbench/components/ProjectTreeView.stories.tsx +25 -0
- package/src/workbench/components/ProjectTreeView.test.tsx +236 -0
- package/src/workbench/components/ProjectTreeView.tsx +151 -0
- package/src/workbench/components/TreePane.stories.tsx +106 -0
- package/src/workbench/components/TreePane.tsx +155 -0
- package/src/workbench/components/WelcomePlayground.stories.tsx +12 -0
- package/src/workbench/components/WelcomePlayground.test.tsx +83 -0
- package/src/workbench/components/WelcomePlayground.tsx +138 -0
- package/src/workbench/components/editor-grid.test.ts +184 -0
- package/src/workbench/components/editor-grid.ts +281 -0
- package/src/workbench/components/file-tree.test.ts +60 -0
- package/src/workbench/components/file-tree.ts +112 -0
- package/src/workbench/use-changes-workbench.ts +205 -0
- package/src/workbench/use-remember-layout.ts +31 -0
- package/src/workbench/use-repo-changes.ts +91 -0
- package/src/workbench/workbench-snapshot.test.ts +99 -0
- package/src/workbench/workbench-snapshot.ts +59 -0
- package/src/App.diff.test.tsx +0 -99
- package/src/App.export.test.tsx +0 -88
- package/src/App.plan-review.test.tsx +0 -259
- package/src/App.readonly.test.tsx +0 -148
- package/src/App.rendering.test.tsx +0 -108
- package/src/App.submit-confirm.test.tsx +0 -183
- package/src/App.test.tsx +0 -218
- package/src/App.tsx +0 -1249
- package/src/config.test.ts +0 -116
- package/src/config.ts +0 -150
- package/src/editor.ts +0 -34
- package/src/herdr.test.ts +0 -68
- package/src/herdr.ts +0 -34
- package/src/keymap.test.ts +0 -270
- package/src/keymap.ts +0 -173
- package/src/run.ts +0 -30
- package/src/serve.ts +0 -75
- package/src/session-controller.ts +0 -398
- package/src/syntax.test.ts +0 -41
- package/src/syntax.ts +0 -88
- package/src/theme.ts +0 -46
- package/src/view-diff.test.ts +0 -55
- package/src/view-diff.ts +0 -86
- package/src/view.test.ts +0 -157
- package/src/view.ts +0 -364
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/** The Project tree navigates by keyboard when focused: j/k move the cursor, Enter folds a folder or
|
|
2
|
+
* 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, type Renderable } from "@opentui/core";
|
|
8
|
+
import { ProjectTreeView } from "./ProjectTreeView";
|
|
9
|
+
import { locateText, settle, waitForText, waitForTextGone } from "../../testing/test-support";
|
|
10
|
+
import { DARK } from "../../appearance/theme";
|
|
11
|
+
|
|
12
|
+
function findById(node: Renderable, id: string): Renderable | undefined {
|
|
13
|
+
if (node.id === id) return node;
|
|
14
|
+
for (const child of node.getChildren()) {
|
|
15
|
+
const found = findById(child, id);
|
|
16
|
+
|
|
17
|
+
if (found) return found;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test("Enter expands a folder, then j moves down and Enter opens the file", async () => {
|
|
24
|
+
const opened: string[] = [];
|
|
25
|
+
const setup = await testRender(
|
|
26
|
+
<ProjectTreeView
|
|
27
|
+
loadFiles={async () => ["src/a.ts", "README.md"]}
|
|
28
|
+
onSelectFile={(path) => opened.push(path)}
|
|
29
|
+
focused
|
|
30
|
+
theme={DARK}
|
|
31
|
+
/>,
|
|
32
|
+
{ width: 40, height: 12 },
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
await settle(setup);
|
|
36
|
+
await waitForText(setup, "src");
|
|
37
|
+
|
|
38
|
+
// cursor starts on the "src" folder; Enter expands it
|
|
39
|
+
setup.mockInput.pressKey("RETURN");
|
|
40
|
+
await waitForText(setup, "a.ts");
|
|
41
|
+
|
|
42
|
+
// j moves onto the file, Enter opens it
|
|
43
|
+
setup.mockInput.pressKey("j");
|
|
44
|
+
await settle(setup);
|
|
45
|
+
setup.mockInput.pressKey("RETURN");
|
|
46
|
+
await settle(setup);
|
|
47
|
+
|
|
48
|
+
expect(opened).toEqual(["src/a.ts"]);
|
|
49
|
+
|
|
50
|
+
setup.renderer.destroy();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("keyboard navigation reveals the selected project row", async () => {
|
|
54
|
+
const paths = Array.from(
|
|
55
|
+
{ length: 35 },
|
|
56
|
+
(_, index) => `file-${String(index).padStart(2, "0")}.ts`,
|
|
57
|
+
);
|
|
58
|
+
const setup = await testRender(
|
|
59
|
+
<ProjectTreeView loadFiles={async () => paths} onSelectFile={() => {}} focused theme={DARK} />,
|
|
60
|
+
{ width: 28, height: 8 },
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
await waitForText(setup, "file-00.ts");
|
|
64
|
+
const scrollbox = findById(setup.renderer.root, "tree-scroll");
|
|
65
|
+
|
|
66
|
+
if (!(scrollbox instanceof ScrollBoxRenderable)) throw new Error("tree scrollbox missing");
|
|
67
|
+
const scrollSteps: number[] = [];
|
|
68
|
+
|
|
69
|
+
for (const [index, key] of Array.from({ length: 15 }, () => "j").entries()) {
|
|
70
|
+
const before = scrollbox.scrollTop;
|
|
71
|
+
|
|
72
|
+
setup.mockInput.pressKey(key);
|
|
73
|
+
// eslint-disable-next-line no-await-in-loop
|
|
74
|
+
await settle(setup);
|
|
75
|
+
scrollSteps.push(scrollbox.scrollTop - before);
|
|
76
|
+
const row = scrollbox.content.findDescendantById(
|
|
77
|
+
`tree-row-file-${String(index + 1).padStart(2, "0")}.ts`,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
expect(row?.y).toBeGreaterThanOrEqual(scrollbox.viewport.y);
|
|
81
|
+
expect(row?.y).toBeLessThan(scrollbox.viewport.y + scrollbox.viewport.height);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
expect(scrollbox.scrollTop).toBeGreaterThan(0);
|
|
85
|
+
expect(Math.max(...scrollSteps)).toBe(1);
|
|
86
|
+
expect(setup.captureCharFrame()).toContain("file-15.ts");
|
|
87
|
+
setup.renderer.destroy();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("project wheel scrolling accelerates during a gesture and resets after a pause", async () => {
|
|
91
|
+
const paths = Array.from(
|
|
92
|
+
{ length: 35 },
|
|
93
|
+
(_, index) => `file-${String(index).padStart(2, "0")}.ts`,
|
|
94
|
+
);
|
|
95
|
+
const setup = await testRender(
|
|
96
|
+
<ProjectTreeView loadFiles={async () => paths} onSelectFile={() => {}} theme={DARK} />,
|
|
97
|
+
{ width: 28, height: 8 },
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
await waitForText(setup, "file-00.ts");
|
|
101
|
+
const scrollbox = findById(setup.renderer.root, "tree-scroll");
|
|
102
|
+
|
|
103
|
+
if (!(scrollbox instanceof ScrollBoxRenderable)) throw new Error("tree scrollbox missing");
|
|
104
|
+
const acceleration = scrollbox.scrollAcceleration;
|
|
105
|
+
const first = acceleration.tick(1000);
|
|
106
|
+
const burst = Array.from({ length: 10 }, (_, index) => acceleration.tick(1016 + index * 16));
|
|
107
|
+
const tail = acceleration.tick(1400);
|
|
108
|
+
|
|
109
|
+
expect(first).toBe(1);
|
|
110
|
+
expect(Math.max(...burst)).toBeGreaterThan(first);
|
|
111
|
+
expect(tail).toBe(1);
|
|
112
|
+
acceleration.reset();
|
|
113
|
+
await setup.mockMouse.scroll(10, 4, "down");
|
|
114
|
+
await settle(setup);
|
|
115
|
+
expect(scrollbox.scrollTop).toBeGreaterThan(0);
|
|
116
|
+
setup.renderer.destroy();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("long project filenames stay on one row without an ellipsis", async () => {
|
|
120
|
+
const path = "src/ChangesFileTree.test.tsx";
|
|
121
|
+
const setup = await testRender(
|
|
122
|
+
<ProjectTreeView loadFiles={async () => [path]} onSelectFile={() => {}} focused theme={DARK} />,
|
|
123
|
+
{ width: 22, height: 8 },
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
await waitForText(setup, "src");
|
|
127
|
+
setup.mockInput.pressKey("RETURN");
|
|
128
|
+
await settle(setup);
|
|
129
|
+
const frame = setup.captureCharFrame();
|
|
130
|
+
const row = findById(setup.renderer.root, `tree-row-${path}`);
|
|
131
|
+
|
|
132
|
+
expect(row?.height).toBe(1);
|
|
133
|
+
expect(frame).not.toContain("…");
|
|
134
|
+
expect(frame).toContain("ChangesFile");
|
|
135
|
+
setup.renderer.destroy();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("without focus the keyboard does nothing", async () => {
|
|
139
|
+
const opened: string[] = [];
|
|
140
|
+
const setup = await testRender(
|
|
141
|
+
<ProjectTreeView
|
|
142
|
+
loadFiles={async () => ["src/a.ts"]}
|
|
143
|
+
onSelectFile={(path) => opened.push(path)}
|
|
144
|
+
theme={DARK}
|
|
145
|
+
/>,
|
|
146
|
+
{ width: 40, height: 12 },
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
await settle(setup);
|
|
150
|
+
await waitForText(setup, "src");
|
|
151
|
+
|
|
152
|
+
setup.mockInput.pressKey("RETURN");
|
|
153
|
+
await settle(setup);
|
|
154
|
+
expect(setup.captureCharFrame()).not.toContain("a.ts");
|
|
155
|
+
expect(opened).toEqual([]);
|
|
156
|
+
|
|
157
|
+
setup.renderer.destroy();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("the empty project hint stays centered in the panel", async () => {
|
|
161
|
+
const setup = await testRender(
|
|
162
|
+
<ProjectTreeView loadFiles={async () => []} onSelectFile={() => {}} theme={DARK} />,
|
|
163
|
+
{ width: 40, height: 12 },
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
await waitForText(setup, "empty");
|
|
167
|
+
const position = locateText(setup, "empty");
|
|
168
|
+
|
|
169
|
+
expect(position.column).toBeGreaterThanOrEqual(17);
|
|
170
|
+
expect(position.column).toBeLessThanOrEqual(18);
|
|
171
|
+
expect(position.row).toBeGreaterThanOrEqual(5);
|
|
172
|
+
expect(position.row).toBeLessThanOrEqual(6);
|
|
173
|
+
setup.renderer.destroy();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("the visible tree removes deleted files without remounting", async () => {
|
|
177
|
+
let files = ["README.md", "gone.ts"];
|
|
178
|
+
const setup = await testRender(
|
|
179
|
+
<ProjectTreeView loadFiles={async () => files} onSelectFile={() => {}} theme={DARK} />,
|
|
180
|
+
{ width: 40, height: 12 },
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
await waitForText(setup, "gone.ts");
|
|
184
|
+
files = ["README.md"];
|
|
185
|
+
await waitForTextGone(setup, "gone.ts");
|
|
186
|
+
expect(setup.captureCharFrame()).toContain("README.md");
|
|
187
|
+
setup.renderer.destroy();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("refresh keeps the selected file when an earlier file disappears", async () => {
|
|
191
|
+
let files = ["a.ts", "b.ts", "c.ts"];
|
|
192
|
+
const opened: string[] = [];
|
|
193
|
+
const setup = await testRender(
|
|
194
|
+
<ProjectTreeView
|
|
195
|
+
loadFiles={async () => files}
|
|
196
|
+
onSelectFile={(path) => opened.push(path)}
|
|
197
|
+
focused
|
|
198
|
+
theme={DARK}
|
|
199
|
+
/>,
|
|
200
|
+
{ width: 40, height: 12 },
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
await waitForText(setup, "b.ts");
|
|
204
|
+
setup.mockInput.pressKey("j");
|
|
205
|
+
await settle(setup);
|
|
206
|
+
files = ["b.ts", "c.ts"];
|
|
207
|
+
await waitForTextGone(setup, "a.ts");
|
|
208
|
+
setup.mockInput.pressKey("RETURN");
|
|
209
|
+
await settle(setup);
|
|
210
|
+
|
|
211
|
+
expect(opened).toEqual(["b.ts"]);
|
|
212
|
+
setup.renderer.destroy();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("a double-click requests a persistent file tab", async () => {
|
|
216
|
+
const opened: { path: string; persistent: boolean }[] = [];
|
|
217
|
+
const setup = await testRender(
|
|
218
|
+
<ProjectTreeView
|
|
219
|
+
loadFiles={async () => ["README.md"]}
|
|
220
|
+
onSelectFile={(path, persistent) => opened.push({ path, persistent: persistent === true })}
|
|
221
|
+
theme={DARK}
|
|
222
|
+
/>,
|
|
223
|
+
{ width: 40, height: 12 },
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
await waitForText(setup, "README.md");
|
|
227
|
+
const file = locateText(setup, "README.md");
|
|
228
|
+
|
|
229
|
+
await setup.mockMouse.doubleClick(file.column, file.row);
|
|
230
|
+
expect(opened).toEqual([
|
|
231
|
+
{ path: "README.md", persistent: false },
|
|
232
|
+
{ path: "README.md", persistent: false },
|
|
233
|
+
{ path: "README.md", persistent: true },
|
|
234
|
+
]);
|
|
235
|
+
setup.renderer.destroy();
|
|
236
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// The Project pane in tree mode: the workspace's tracked files as a collapsible directory tree.
|
|
2
|
+
// Folders start collapsed and expand on click; a file click opens it as a tab. The caller keys this
|
|
3
|
+
// on the session id so switching sessions remounts it and never shows the previous repository's paths.
|
|
4
|
+
|
|
5
|
+
import { ScrollArea } from "../../ui/components/ScrollArea";
|
|
6
|
+
import React, { useEffect, useEffectEvent, useMemo, useState } from "react";
|
|
7
|
+
import { useKeyboard } from "@opentui/react";
|
|
8
|
+
import type { Theme } from "../../appearance/theme";
|
|
9
|
+
import { useComponentTheme } from "../../appearance/components/theme-context";
|
|
10
|
+
import { Tree } from "../../ui/components/primitives/Tree";
|
|
11
|
+
import { flattenTree } from "../../ui/components/primitives/tree-model";
|
|
12
|
+
import { buildPathTree } from "./file-tree";
|
|
13
|
+
|
|
14
|
+
const PROJECT_FILES_REFRESH_MS = 2000;
|
|
15
|
+
|
|
16
|
+
export interface ProjectTreeViewProps {
|
|
17
|
+
loadFiles: () => Promise<string[]>;
|
|
18
|
+
onSelectFile: (path: string, persistent?: boolean) => void;
|
|
19
|
+
focused?: boolean;
|
|
20
|
+
theme?: Theme;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function ProjectTreeView({
|
|
24
|
+
loadFiles,
|
|
25
|
+
onSelectFile,
|
|
26
|
+
focused = false,
|
|
27
|
+
theme,
|
|
28
|
+
}: ProjectTreeViewProps): React.ReactNode {
|
|
29
|
+
const tokens = useComponentTheme(theme);
|
|
30
|
+
const [paths, setPaths] = useState<readonly string[] | null>(null);
|
|
31
|
+
const [expandedIds, setExpandedIds] = useState<ReadonlySet<string>>(new Set());
|
|
32
|
+
const [cursorId, setCursorId] = useState<string | null>(null);
|
|
33
|
+
const nodes = useMemo(() => (paths ? buildPathTree(paths) : []), [paths]);
|
|
34
|
+
const rows = useMemo(() => flattenTree(nodes, { expandedIds }), [nodes, expandedIds]);
|
|
35
|
+
const cursorIndex = Math.max(
|
|
36
|
+
0,
|
|
37
|
+
rows.findIndex((row) => row.id === cursorId),
|
|
38
|
+
);
|
|
39
|
+
const loadLatestFiles = useEffectEvent(loadFiles);
|
|
40
|
+
|
|
41
|
+
const toggle = (id: string): void =>
|
|
42
|
+
setExpandedIds((current) => {
|
|
43
|
+
const next = new Set(current);
|
|
44
|
+
|
|
45
|
+
if (next.has(id)) next.delete(id);
|
|
46
|
+
else next.add(id);
|
|
47
|
+
|
|
48
|
+
return next;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
useKeyboard((key) => {
|
|
52
|
+
if (!focused || rows.length === 0) return;
|
|
53
|
+
if (key.name === "j" || key.name === "down")
|
|
54
|
+
return setCursorId(rows[Math.min(cursorIndex + 1, rows.length - 1)]!.id);
|
|
55
|
+
if (key.name === "k" || key.name === "up")
|
|
56
|
+
return setCursorId(rows[Math.max(cursorIndex - 1, 0)]!.id);
|
|
57
|
+
const row = rows[cursorIndex];
|
|
58
|
+
|
|
59
|
+
if (!row) return;
|
|
60
|
+
if (key.name === "return" || key.name === "enter")
|
|
61
|
+
return row.isFolder ? toggle(row.id) : onSelectFile(row.id, true);
|
|
62
|
+
if (key.name === "l" && row.isFolder && !expandedIds.has(row.id)) return toggle(row.id);
|
|
63
|
+
if (key.name === "h" && row.isFolder && expandedIds.has(row.id)) return toggle(row.id);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
let cancelled = false;
|
|
68
|
+
let requestInFlight = false;
|
|
69
|
+
|
|
70
|
+
const refreshFiles = async (): Promise<void> => {
|
|
71
|
+
if (requestInFlight) return;
|
|
72
|
+
requestInFlight = true;
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const files = await loadLatestFiles();
|
|
76
|
+
|
|
77
|
+
if (!cancelled) {
|
|
78
|
+
setPaths((previous) =>
|
|
79
|
+
previous?.length === files.length &&
|
|
80
|
+
previous.every((path, index) => path === files[index])
|
|
81
|
+
? previous
|
|
82
|
+
: files,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
if (!cancelled) setPaths((previous) => previous ?? []);
|
|
87
|
+
} finally {
|
|
88
|
+
requestInFlight = false;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
void refreshFiles();
|
|
93
|
+
const timer = setInterval(() => void refreshFiles(), PROJECT_FILES_REFRESH_MS);
|
|
94
|
+
|
|
95
|
+
return () => {
|
|
96
|
+
cancelled = true;
|
|
97
|
+
clearInterval(timer);
|
|
98
|
+
};
|
|
99
|
+
}, []);
|
|
100
|
+
|
|
101
|
+
if (paths === null) {
|
|
102
|
+
return (
|
|
103
|
+
<box
|
|
104
|
+
style={{
|
|
105
|
+
flexGrow: 1,
|
|
106
|
+
flexDirection: "column",
|
|
107
|
+
alignItems: "center",
|
|
108
|
+
justifyContent: "center",
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
<text fg={tokens.textDim}>loading...</text>
|
|
112
|
+
</box>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
if (paths.length === 0) {
|
|
116
|
+
return (
|
|
117
|
+
<box
|
|
118
|
+
style={{
|
|
119
|
+
flexGrow: 1,
|
|
120
|
+
flexDirection: "column",
|
|
121
|
+
alignItems: "center",
|
|
122
|
+
justifyContent: "center",
|
|
123
|
+
}}
|
|
124
|
+
>
|
|
125
|
+
<text fg={tokens.textDim}>empty</text>
|
|
126
|
+
</box>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<ScrollArea
|
|
132
|
+
id="tree-scroll"
|
|
133
|
+
revealId={focused ? rows[cursorIndex]?.id : undefined}
|
|
134
|
+
gestureWheel
|
|
135
|
+
>
|
|
136
|
+
<Tree
|
|
137
|
+
nodes={nodes}
|
|
138
|
+
expandedIds={expandedIds}
|
|
139
|
+
selectedId={focused ? rows[cursorIndex]?.id : undefined}
|
|
140
|
+
singleLine
|
|
141
|
+
onSelect={(id) => {
|
|
142
|
+
setCursorId(id);
|
|
143
|
+
onSelectFile(id);
|
|
144
|
+
}}
|
|
145
|
+
onDoubleSelect={(id) => onSelectFile(id, true)}
|
|
146
|
+
onToggle={toggle}
|
|
147
|
+
theme={theme}
|
|
148
|
+
/>
|
|
149
|
+
</ScrollArea>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../../appearance/theme";
|
|
3
|
+
import type { Story, StoryMeta } from "../../stories/story";
|
|
4
|
+
import { TreePane } from "./TreePane";
|
|
5
|
+
import type { TreeRow } from "../../thread/tree-view";
|
|
6
|
+
|
|
7
|
+
export const meta: StoryMeta = { title: "Surfaces/TreePane" };
|
|
8
|
+
|
|
9
|
+
const callbacks = {
|
|
10
|
+
onSelect: () => {},
|
|
11
|
+
onGo: () => {},
|
|
12
|
+
onBranch: () => {},
|
|
13
|
+
onLabel: () => {},
|
|
14
|
+
onFork: () => {},
|
|
15
|
+
onForkAndShare: () => {},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** rev1 -> comment (labelled, a branch hangs off it) -> the branch's comment -> rev2 as main's tip. */
|
|
19
|
+
const ROWS: TreeRow[] = [
|
|
20
|
+
{
|
|
21
|
+
entryId: "e1",
|
|
22
|
+
depth: 0,
|
|
23
|
+
glyph: "◉",
|
|
24
|
+
text: "revision 1",
|
|
25
|
+
onPath: true,
|
|
26
|
+
tips: [],
|
|
27
|
+
isCurrentTip: false,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
entryId: "e2",
|
|
31
|
+
depth: 0,
|
|
32
|
+
glyph: "·",
|
|
33
|
+
text: "comment",
|
|
34
|
+
onPath: true,
|
|
35
|
+
tips: [],
|
|
36
|
+
isCurrentTip: false,
|
|
37
|
+
label: "start",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
entryId: "e3",
|
|
41
|
+
depth: 1,
|
|
42
|
+
glyph: "·",
|
|
43
|
+
text: "comment",
|
|
44
|
+
onPath: false,
|
|
45
|
+
tips: ["alt"],
|
|
46
|
+
isCurrentTip: false,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
entryId: "e4",
|
|
50
|
+
depth: 0,
|
|
51
|
+
glyph: "◉",
|
|
52
|
+
text: "revision 2",
|
|
53
|
+
onPath: true,
|
|
54
|
+
tips: ["main"],
|
|
55
|
+
isCurrentTip: true,
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
/** The rail expects a height-constrained parent, like the app's main row. */
|
|
60
|
+
function PaneFrame({ children }: { children: React.ReactNode }): React.ReactNode {
|
|
61
|
+
return (
|
|
62
|
+
<box
|
|
63
|
+
style={{ flexDirection: "column", width: 40, height: "100%", backgroundColor: DARK.panel }}
|
|
64
|
+
>
|
|
65
|
+
{children}
|
|
66
|
+
</box>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const OwnerWithBranch: Story = {
|
|
71
|
+
render: () => (
|
|
72
|
+
<PaneFrame>
|
|
73
|
+
<TreePane rows={ROWS} selectedEntryId="e2" canMove canFork {...callbacks} />
|
|
74
|
+
</PaneFrame>
|
|
75
|
+
),
|
|
76
|
+
expectedColors: [DARK.accent],
|
|
77
|
+
size: { width: 40, height: 16 },
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const CollaboratorReadsOnly: Story = {
|
|
81
|
+
render: () => (
|
|
82
|
+
<PaneFrame>
|
|
83
|
+
<TreePane rows={ROWS} canMove={false} canFork={false} {...callbacks} />
|
|
84
|
+
</PaneFrame>
|
|
85
|
+
),
|
|
86
|
+
size: { width: 40, height: 16 },
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** A resolved review: the owner can still fork, not move. */
|
|
90
|
+
export const ResolvedForkOnly: Story = {
|
|
91
|
+
render: () => (
|
|
92
|
+
<PaneFrame>
|
|
93
|
+
<TreePane rows={ROWS} canMove={false} canFork {...callbacks} />
|
|
94
|
+
</PaneFrame>
|
|
95
|
+
),
|
|
96
|
+
size: { width: 40, height: 16 },
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const NoHistory: Story = {
|
|
100
|
+
render: () => (
|
|
101
|
+
<PaneFrame>
|
|
102
|
+
<TreePane rows={[]} canMove canFork {...callbacks} />
|
|
103
|
+
</PaneFrame>
|
|
104
|
+
),
|
|
105
|
+
size: { width: 40, height: 8 },
|
|
106
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rail's Tree tab: the session's history as one row per entry. The active
|
|
3
|
+
* path reads bright, everything else dim; a branch tip carries its name, the
|
|
4
|
+
* current tip a marker, a checkpoint its label. Clicking a row selects it,
|
|
5
|
+
* clicking the selected row goes there; the buttons underneath are the mouse
|
|
6
|
+
* path to the same primitives the option chords reach.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { ScrollArea } from "../../ui/components/ScrollArea";
|
|
10
|
+
import React, { useEffect, useRef } from "react";
|
|
11
|
+
import { createTextAttributes, type ScrollBoxRenderable } from "@opentui/core";
|
|
12
|
+
import type { Theme } from "../../appearance/theme";
|
|
13
|
+
import type { TreeRow } from "../../thread/tree-view";
|
|
14
|
+
import { useComponentTheme } from "../../appearance/components/theme-context";
|
|
15
|
+
import { Button } from "../../ui/components/primitives/Button";
|
|
16
|
+
import { Toolbar } from "../../ui/components/primitives/Toolbar";
|
|
17
|
+
|
|
18
|
+
export interface TreePaneProps {
|
|
19
|
+
rows: TreeRow[];
|
|
20
|
+
selectedEntryId?: string;
|
|
21
|
+
canMove: boolean;
|
|
22
|
+
canFork: boolean;
|
|
23
|
+
onSelect: (entryId: string) => void;
|
|
24
|
+
onGo: (entryId: string) => void;
|
|
25
|
+
onBranch: () => void;
|
|
26
|
+
onLabel: () => void;
|
|
27
|
+
onFork: () => void;
|
|
28
|
+
onForkAndShare: () => void;
|
|
29
|
+
theme?: Theme;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const DIM = createTextAttributes({ dim: true });
|
|
33
|
+
const BOLD = createTextAttributes({ bold: true });
|
|
34
|
+
|
|
35
|
+
/** The marker column: the current tip stands out, every other row keeps its kind's glyph. */
|
|
36
|
+
function markerFor(row: TreeRow): string {
|
|
37
|
+
return row.isCurrentTip ? "●" : row.glyph;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** What follows the entry's name: the branches whose tip it is, and its checkpoint. */
|
|
41
|
+
function trailerFor(row: TreeRow): string {
|
|
42
|
+
const parts: string[] = [];
|
|
43
|
+
|
|
44
|
+
if (row.tips.length) parts.push(`← ${row.tips.join(", ")}`);
|
|
45
|
+
if (row.label !== undefined) parts.push(`⚑ ${row.label}`);
|
|
46
|
+
|
|
47
|
+
return parts.length ? ` ${parts.join(" ")}` : "";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function TreePane({
|
|
51
|
+
rows,
|
|
52
|
+
selectedEntryId,
|
|
53
|
+
canMove,
|
|
54
|
+
canFork,
|
|
55
|
+
onSelect,
|
|
56
|
+
onGo,
|
|
57
|
+
onBranch,
|
|
58
|
+
onLabel,
|
|
59
|
+
onFork,
|
|
60
|
+
onForkAndShare,
|
|
61
|
+
theme,
|
|
62
|
+
}: TreePaneProps): React.ReactNode {
|
|
63
|
+
const tokens = useComponentTheme(theme);
|
|
64
|
+
const scrollRef = useRef<ScrollBoxRenderable | null>(null);
|
|
65
|
+
const revealId = selectedEntryId ?? rows.find((row) => row.isCurrentTip)?.entryId;
|
|
66
|
+
|
|
67
|
+
// the trunk draws last, so a long history opens with its tip below the fold: keep the row in view
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (revealId === undefined) return;
|
|
70
|
+
try {
|
|
71
|
+
scrollRef.current?.scrollChildIntoView(`tree-row-${revealId}`);
|
|
72
|
+
} catch {
|
|
73
|
+
// reveal is best-effort; the selection is already correct
|
|
74
|
+
}
|
|
75
|
+
}, [revealId]);
|
|
76
|
+
|
|
77
|
+
if (rows.length === 0)
|
|
78
|
+
return (
|
|
79
|
+
<box style={{ flexGrow: 1, alignItems: "center", justifyContent: "center" }}>
|
|
80
|
+
<text fg={tokens.textDim}>no history yet</text>
|
|
81
|
+
</box>
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<box style={{ flexGrow: 1, flexDirection: "column" }}>
|
|
86
|
+
<ScrollArea scrollRef={scrollRef}>
|
|
87
|
+
{rows.map((row) => {
|
|
88
|
+
const selected = row.entryId === selectedEntryId;
|
|
89
|
+
const foreground = row.onPath ? tokens.text : tokens.textDim;
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<box
|
|
93
|
+
key={row.entryId}
|
|
94
|
+
id={`tree-row-${row.entryId}`}
|
|
95
|
+
style={{
|
|
96
|
+
flexDirection: "row",
|
|
97
|
+
paddingLeft: 1 + row.depth * 2,
|
|
98
|
+
backgroundColor: selected ? tokens.elevated : undefined,
|
|
99
|
+
}}
|
|
100
|
+
onMouseUp={() => (selected && canMove ? onGo(row.entryId) : onSelect(row.entryId))}
|
|
101
|
+
>
|
|
102
|
+
<text
|
|
103
|
+
fg={row.isCurrentTip ? tokens.accent : foreground}
|
|
104
|
+
attributes={row.onPath ? undefined : DIM}
|
|
105
|
+
>
|
|
106
|
+
{markerFor(row)} {row.text}
|
|
107
|
+
</text>
|
|
108
|
+
<text fg={tokens.textMuted} attributes={row.label === undefined ? undefined : BOLD}>
|
|
109
|
+
{trailerFor(row)}
|
|
110
|
+
</text>
|
|
111
|
+
</box>
|
|
112
|
+
);
|
|
113
|
+
})}
|
|
114
|
+
</ScrollArea>
|
|
115
|
+
{canMove || canFork ? (
|
|
116
|
+
<box style={{ paddingLeft: 1 }}>
|
|
117
|
+
<Toolbar>
|
|
118
|
+
{canMove ? (
|
|
119
|
+
<Button
|
|
120
|
+
onPress={() => {
|
|
121
|
+
if (selectedEntryId !== undefined) onGo(selectedEntryId);
|
|
122
|
+
}}
|
|
123
|
+
isDisabled={selectedEntryId === undefined}
|
|
124
|
+
marginRight={1}
|
|
125
|
+
theme={theme}
|
|
126
|
+
>
|
|
127
|
+
Go
|
|
128
|
+
</Button>
|
|
129
|
+
) : null}
|
|
130
|
+
{canMove ? (
|
|
131
|
+
<Button onPress={onBranch} marginRight={1} theme={theme}>
|
|
132
|
+
Branch
|
|
133
|
+
</Button>
|
|
134
|
+
) : null}
|
|
135
|
+
{canMove ? (
|
|
136
|
+
<Button onPress={onLabel} marginRight={1} theme={theme}>
|
|
137
|
+
Label
|
|
138
|
+
</Button>
|
|
139
|
+
) : null}
|
|
140
|
+
{canFork ? (
|
|
141
|
+
<Button onPress={onFork} marginRight={1} theme={theme}>
|
|
142
|
+
Fork
|
|
143
|
+
</Button>
|
|
144
|
+
) : null}
|
|
145
|
+
{canFork ? (
|
|
146
|
+
<Button onPress={onForkAndShare} theme={theme}>
|
|
147
|
+
Fork+share
|
|
148
|
+
</Button>
|
|
149
|
+
) : null}
|
|
150
|
+
</Toolbar>
|
|
151
|
+
</box>
|
|
152
|
+
) : null}
|
|
153
|
+
</box>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../../appearance/theme";
|
|
3
|
+
import type { Story, StoryMeta } from "../../stories/story";
|
|
4
|
+
import { WelcomePlayground } from "./WelcomePlayground";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "Surfaces/WelcomePlayground" };
|
|
7
|
+
|
|
8
|
+
export const Welcome: Story = {
|
|
9
|
+
render: () => <WelcomePlayground quickActions={[]} theme={DARK} />,
|
|
10
|
+
expectedColors: [DARK.textDim],
|
|
11
|
+
size: { width: 72, height: 20 },
|
|
12
|
+
};
|