@cueloop/client 0.1.0-alpha.9 → 0.1.0-alpha.91
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/App.appearance.test.tsx +53 -0
- package/src/App.autoclose.test.tsx +77 -51
- package/src/App.diff.test.tsx +259 -45
- package/src/App.export.test.tsx +58 -41
- package/src/App.focus.test.ts +89 -0
- package/src/App.identity.test.tsx +78 -0
- package/src/App.plan-render.test.tsx +209 -0
- package/src/App.plan-review.test.tsx +330 -161
- package/src/App.prototype-markdown.test.tsx +128 -0
- package/src/App.prototype.test.tsx +176 -0
- package/src/App.readonly.test.tsx +129 -67
- package/src/App.regression.test.tsx +94 -0
- package/src/App.rendering.test.tsx +61 -35
- package/src/App.share-dialog.test.tsx +172 -0
- package/src/App.submit-confirm.test.tsx +264 -0
- package/src/App.test.tsx +402 -126
- package/src/App.toast.test.tsx +98 -0
- package/src/App.tsx +1406 -1060
- package/src/App.visual.test.tsx +57 -0
- package/src/App.walk.test.tsx +299 -0
- package/src/App.workbench.test.tsx +787 -0
- package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
- package/src/annotation-palette.ts +62 -0
- package/src/ansi-screen.test.ts +28 -0
- package/src/ansi-screen.ts +72 -0
- package/src/app-key-state.ts +61 -0
- package/src/app-screens.tsx +577 -0
- package/src/app-view-model.ts +168 -0
- package/src/attribution.test.ts +31 -0
- package/src/attribution.ts +42 -0
- package/src/client-extension-registry.test.tsx +61 -0
- package/src/client-extension-registry.ts +159 -0
- package/src/clipboard.test.ts +23 -0
- package/src/clipboard.ts +33 -0
- package/src/components/AnnotatableFileView.stories.tsx +42 -0
- package/src/components/AnnotatableFileView.test.tsx +102 -0
- package/src/components/AnnotatableFileView.tsx +134 -0
- package/src/components/AnnotationCards.stories.tsx +86 -0
- package/src/components/AnnotationCards.tsx +341 -0
- package/src/components/AnsiScreen.stories.tsx +12 -0
- package/src/components/AnsiScreen.tsx +28 -0
- package/src/components/AppHeader.stories.tsx +50 -0
- package/src/components/AppHeader.tsx +83 -0
- package/src/components/AppShell.brand.test.tsx +58 -0
- package/src/components/AppShell.extensions.test.tsx +191 -0
- package/src/components/AppShell.stories.tsx +89 -0
- package/src/components/AppShell.tsx +362 -0
- package/src/components/AppShell.zoom.test.tsx +135 -0
- package/src/components/BareWorkbenchFileView.stories.tsx +27 -0
- package/src/components/BareWorkbenchFileView.tsx +64 -0
- package/src/components/Breadcrumb.stories.tsx +19 -0
- package/src/components/Breadcrumb.tsx +45 -0
- package/src/components/ChangesColumn.stories.tsx +44 -0
- package/src/components/ChangesColumn.tsx +199 -0
- package/src/components/ChangesFileTree.test.tsx +61 -0
- package/src/components/CodeBlock.stories.tsx +42 -0
- package/src/components/CodeBlock.tsx +84 -0
- package/src/components/CompletionOverlay.stories.tsx +37 -0
- package/src/components/CompletionOverlay.tsx +77 -0
- package/src/components/ConfirmCard.stories.tsx +37 -0
- package/src/components/ConfirmCard.tsx +150 -0
- package/src/components/ConfirmDialog.stories.tsx +21 -0
- package/src/components/ConfirmDialog.tsx +72 -0
- package/src/components/DiffContentView.scroll.test.tsx +149 -0
- package/src/components/DiffContentView.split.test.tsx +140 -0
- package/src/components/DiffContentView.stories.tsx +80 -0
- package/src/components/DiffContentView.syntax.test.tsx +82 -0
- package/src/components/DiffContentView.tsx +906 -0
- package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
- package/src/components/DiscussionMarkerRail.tsx +186 -0
- package/src/components/EditorGrid.stories.tsx +59 -0
- package/src/components/EditorGrid.tabs.test.tsx +118 -0
- package/src/components/EditorGrid.tsx +426 -0
- package/src/components/EditorGrid.zoom.test.tsx +68 -0
- package/src/components/ExtensionZone.stories.tsx +28 -0
- package/src/components/ExtensionZone.tsx +145 -0
- package/src/components/FileContentsView.stories.tsx +16 -0
- package/src/components/FileContentsView.test.tsx +29 -0
- package/src/components/FileContentsView.tsx +98 -0
- package/src/components/GridTabContent.stories.tsx +59 -0
- package/src/components/GridTabContent.tsx +182 -0
- package/src/components/HandleChipsInput.stories.tsx +24 -0
- package/src/components/HandleChipsInput.test.tsx +35 -0
- package/src/components/HandleChipsInput.tsx +145 -0
- package/src/components/KeybindsDialog.stories.tsx +31 -0
- package/src/components/KeybindsDialog.tsx +103 -0
- package/src/components/MarkdownGridBlock.stories.tsx +33 -0
- package/src/components/MarkdownGridBlock.tsx +72 -0
- package/src/components/MarkdownThreadEditor.stories.tsx +15 -0
- package/src/components/MarkdownThreadEditor.test.tsx +98 -0
- package/src/components/MarkdownThreadEditor.tsx +162 -0
- package/src/components/MenuBar.stories.tsx +28 -0
- package/src/components/MenuBar.tsx +44 -0
- package/src/components/NavModeHint.stories.tsx +40 -0
- package/src/components/NavModeHint.tsx +31 -0
- package/src/components/OverlayScrollbar.stories.tsx +33 -0
- package/src/components/OverlayScrollbar.test.tsx +139 -0
- package/src/components/OverlayScrollbar.tsx +129 -0
- package/src/components/PaneGrid.stories.tsx +84 -0
- package/src/components/PaneGrid.tsx +128 -0
- package/src/components/PanelColumn.stories.tsx +25 -0
- package/src/components/PanelColumn.tsx +98 -0
- package/src/components/ProjectTreeView.stories.tsx +25 -0
- package/src/components/ProjectTreeView.test.tsx +75 -0
- package/src/components/ProjectTreeView.tsx +125 -0
- package/src/components/PromptDialog.stories.tsx +36 -0
- package/src/components/PromptDialog.test.tsx +61 -0
- package/src/components/PromptDialog.tsx +99 -0
- package/src/components/PrototypeContentView.stories.tsx +19 -0
- package/src/components/PrototypeContentView.tsx +446 -0
- package/src/components/RootOverlay.stories.tsx +45 -0
- package/src/components/RootOverlay.tsx +48 -0
- package/src/components/ScrollArea.stories.tsx +20 -0
- package/src/components/ScrollArea.tsx +44 -0
- package/src/components/SettingsDialog.stories.tsx +117 -0
- package/src/components/SettingsDialog.tsx +186 -0
- package/src/components/SettingsRows.stories.tsx +18 -0
- package/src/components/SettingsRows.tsx +112 -0
- package/src/components/ShareDialog.stories.tsx +121 -0
- package/src/components/ShareDialog.tsx +552 -0
- package/src/components/ShellHeader.stories.tsx +50 -0
- package/src/components/ShellHeader.tsx +137 -0
- package/src/components/SlashComposer.stories.tsx +37 -0
- package/src/components/SlashComposer.tsx +87 -0
- package/src/components/ThreadFooter.stories.tsx +30 -0
- package/src/components/ThreadFooter.test.tsx +47 -0
- package/src/components/ThreadFooter.tsx +76 -0
- package/src/components/ThreadTree.stories.tsx +46 -0
- package/src/components/ThreadTree.tsx +255 -0
- package/src/components/ThreadView.selection.test.tsx +951 -0
- package/src/components/ThreadView.slash.test.tsx +146 -0
- package/src/components/ThreadView.stories.tsx +26 -0
- package/src/components/ThreadView.tsx +525 -0
- package/src/components/Toast.stories.tsx +19 -0
- package/src/components/Toast.tsx +52 -0
- package/src/components/Tooltip.stories.tsx +26 -0
- package/src/components/Tooltip.tsx +72 -0
- package/src/components/TreePane.stories.tsx +106 -0
- package/src/components/TreePane.tsx +157 -0
- package/src/components/WalkWizard.stories.tsx +100 -0
- package/src/components/WalkWizard.tsx +150 -0
- package/src/components/WelcomePlayground.stories.tsx +12 -0
- package/src/components/WelcomePlayground.test.tsx +49 -0
- package/src/components/WelcomePlayground.tsx +139 -0
- package/src/components/WorkspacePanels.stories.tsx +36 -0
- package/src/components/WorkspacePanels.tsx +116 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +2264 -0
- package/src/components/diff-content-view-layout.test.ts +198 -0
- package/src/components/diff-content-view-layout.ts +84 -0
- package/src/components/editor-grid.test.ts +86 -0
- package/src/components/editor-grid.ts +138 -0
- package/src/components/file-tree.test.ts +60 -0
- package/src/components/file-tree.ts +112 -0
- package/src/components/menu-control.stories.tsx +35 -0
- package/src/components/menu-control.tsx +43 -0
- package/src/components/pages/Workspace.stories.tsx +181 -0
- package/src/components/primitives/Button.stories.tsx +32 -0
- package/src/components/primitives/Button.tsx +59 -0
- package/src/components/primitives/Card.stories.tsx +25 -0
- package/src/components/primitives/Card.tsx +63 -0
- package/src/components/primitives/Dialog.stories.tsx +24 -0
- package/src/components/primitives/Dialog.test.tsx +37 -0
- package/src/components/primitives/Dialog.tsx +71 -0
- package/src/components/primitives/DialogActions.stories.tsx +20 -0
- package/src/components/primitives/DialogActions.tsx +39 -0
- package/src/components/primitives/IconButton.stories.tsx +36 -0
- package/src/components/primitives/IconButton.tsx +62 -0
- package/src/components/primitives/StatusBar.stories.tsx +11 -0
- package/src/components/primitives/StatusBar.tsx +23 -0
- package/src/components/primitives/Switch.stories.tsx +17 -0
- package/src/components/primitives/Switch.tsx +32 -0
- package/src/components/primitives/Tabs.stories.tsx +29 -0
- package/src/components/primitives/Tabs.tsx +102 -0
- package/src/components/primitives/Toolbar.stories.tsx +17 -0
- package/src/components/primitives/Toolbar.tsx +14 -0
- package/src/components/primitives/Tree.stories.tsx +124 -0
- package/src/components/primitives/Tree.tsx +100 -0
- package/src/components/primitives/frame.test.ts +12 -0
- package/src/components/primitives/frame.ts +17 -0
- package/src/components/primitives/icons.ts +61 -0
- package/src/components/primitives/tree-model.test.ts +99 -0
- package/src/components/primitives/tree-model.ts +137 -0
- package/src/components/quick-actions-editor.stories.tsx +58 -0
- package/src/components/quick-actions-editor.test.tsx +117 -0
- package/src/components/quick-actions-editor.tsx +226 -0
- package/src/components/share-dialog-store.ts +58 -0
- package/src/components/stories-app.tsx +193 -0
- package/src/components/stories.test.tsx +74 -0
- package/src/components/story-fixtures.ts +102 -0
- package/src/components/story-tree.test.ts +108 -0
- package/src/components/story.ts +159 -0
- package/src/components/syntax-highlight.ts +133 -0
- package/src/components/terminal-pane.test.tsx +40 -0
- package/src/components/terminal-pane.ts +199 -0
- package/src/components/theme-context.stories.tsx +46 -0
- package/src/components/theme-context.tsx +37 -0
- package/src/components/thread-tree.test.ts +134 -0
- package/src/components/thread-tree.ts +109 -0
- package/src/components/truncate-text.ts +7 -0
- package/src/components/truncate-title.test.ts +31 -0
- package/src/components/truncate-title.ts +15 -0
- package/src/config-identity.test.ts +78 -0
- package/src/config.test.ts +579 -34
- package/src/config.ts +533 -57
- package/src/diff-fold.test.ts +78 -0
- package/src/diff-fold.ts +126 -0
- package/src/diff-hunk-curate.test.ts +315 -0
- package/src/diff-hunk-curate.ts +153 -0
- package/src/diff-intraline.test.ts +238 -0
- package/src/diff-intraline.ts +236 -0
- package/src/diff-syntax.test.ts +112 -0
- package/src/diff-syntax.ts +179 -0
- package/src/discussions.ts +79 -0
- package/src/editor.test.ts +149 -0
- package/src/editor.ts +139 -9
- package/src/ghostty-terminal.test.ts +85 -0
- package/src/ghostty-terminal.ts +199 -0
- package/src/github-identity.test.ts +58 -0
- package/src/github-identity.ts +41 -0
- package/src/herdr.test.ts +9 -34
- package/src/herdr.ts +25 -21
- package/src/index.ts +29 -0
- package/src/integrations.test.ts +41 -0
- package/src/integrations.ts +32 -0
- package/src/intent-dispatch.test.ts +724 -0
- package/src/intent-dispatch.ts +690 -0
- package/src/key-bindings.test.ts +156 -0
- package/src/key-bindings.ts +459 -0
- package/src/keymap.test.ts +456 -115
- package/src/keymap.ts +315 -81
- package/src/kitty-image.test.ts +66 -0
- package/src/kitty-image.ts +138 -0
- package/src/launch-layout.test.ts +78 -0
- package/src/launch-layout.ts +62 -0
- package/src/mark-runs.ts +121 -0
- package/src/markdown-editor-syntax-style.ts +50 -0
- package/src/markdown-grid.test.ts +101 -0
- package/src/markdown-grid.ts +203 -0
- package/src/markdown-highlight.test.ts +61 -0
- package/src/markdown-highlight.ts +122 -0
- package/src/pasted-image.test.ts +67 -0
- package/src/pasted-image.ts +60 -0
- package/src/perf/otlp-trace.test.ts +116 -0
- package/src/perf/otlp-trace.ts +130 -0
- package/src/perf/perf-report.ts +17 -0
- package/src/perf/perf-timings.test.ts +18 -0
- package/src/perf/perf-timings.ts +38 -0
- package/src/prototype-browser.test.ts +81 -0
- package/src/prototype-browser.ts +329 -0
- package/src/prototype-pixels.tsx +35 -0
- package/src/pty.test.ts +81 -0
- package/src/pty.ts +245 -0
- package/src/ready-signal.test.ts +85 -0
- package/src/ready-signal.ts +52 -0
- package/src/run.ts +85 -12
- package/src/serve.test.ts +24 -12
- package/src/serve.ts +52 -10
- package/src/share.ts +237 -0
- package/src/skills.test.ts +74 -0
- package/src/skills.ts +75 -0
- package/src/slash-palette.test.ts +103 -0
- package/src/slash-palette.ts +129 -0
- package/src/split-diff.test.ts +110 -0
- package/src/split-diff.ts +149 -0
- package/src/test-support.test.ts +264 -0
- package/src/test-support.ts +369 -0
- package/src/theme-presets.test.ts +51 -0
- package/src/theme-presets.ts +219 -0
- package/src/theme.ts +88 -19
- package/src/thread-chords.test.ts +79 -0
- package/src/thread-chords.ts +163 -0
- package/src/thread-code-syntax.ts +45 -0
- package/src/thread-controller.curate.test.ts +476 -0
- package/src/thread-controller.derivation.test.ts +62 -0
- package/src/thread-controller.live-diff.test.ts +322 -0
- package/src/thread-controller.optimistic.test.ts +69 -0
- package/src/thread-controller.share.test.ts +561 -0
- package/src/thread-controller.switch-cache.test.ts +176 -0
- package/src/thread-controller.tree.test.ts +261 -0
- package/src/thread-controller.ts +2050 -0
- package/src/thread-selection.test.ts +210 -0
- package/src/thread-selection.ts +243 -0
- package/src/tree-view.test.ts +128 -0
- package/src/tree-view.ts +153 -0
- package/src/use-annotation-surface.tsx +975 -0
- package/src/use-changes-workbench.ts +190 -0
- package/src/use-frame-measure.ts +41 -0
- package/src/use-remember-layout.ts +31 -0
- package/src/use-repo-changes.ts +87 -0
- package/src/use-settings-dialog.tsx +364 -0
- package/src/use-terminal-virtualizer.ts +259 -0
- package/src/use-thread-body-editing.tsx +58 -0
- package/src/version.ts +5 -0
- package/src/view-diff.test.ts +279 -9
- package/src/view-diff.ts +460 -24
- package/src/view-plan.regression.test.ts +60 -0
- package/src/view-plan.test.ts +545 -0
- package/src/view-plan.ts +1019 -0
- package/src/visual/contrast.test.ts +35 -0
- package/src/visual/contrast.ts +33 -0
- package/src/walk.test.ts +121 -0
- package/src/walk.ts +74 -0
- package/src/workbench-snapshot.test.ts +99 -0
- package/src/workbench-snapshot.ts +59 -0
- package/src/session-controller.ts +0 -398
- package/src/syntax.test.ts +0 -41
- package/src/syntax.ts +0 -88
- package/src/view.test.ts +0 -157
- package/src/view.ts +0 -364
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Submit button expanded into a bordered confirm card at the rail bottom:
|
|
3
|
+
* the message selector (arrow keys or click), the optional summary, and plain
|
|
4
|
+
* word-buttons - key hints live in the status line only. The bordered height
|
|
5
|
+
* derives from the content rows through Card, so layout and render never drift.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useContext } from "react";
|
|
9
|
+
import { useTerminalDimensions } from "@opentui/react";
|
|
10
|
+
import { MESSAGE_OUTCOMES, type MessageOutcome } from "@cueloop/schema";
|
|
11
|
+
import type { Theme } from "../theme";
|
|
12
|
+
import type { QuickAction } from "../config";
|
|
13
|
+
import { SlashSkillsContext } from "../skills";
|
|
14
|
+
import { activeSlashToken, mergeSlashItems, slashFilter, slashItemsFrom } from "../slash-palette";
|
|
15
|
+
import { useComponentTheme } from "./theme-context";
|
|
16
|
+
import { Card } from "./primitives/Card";
|
|
17
|
+
import { composeRowCount } from "./AnnotationCards";
|
|
18
|
+
import { SlashComposer } from "./SlashComposer";
|
|
19
|
+
import { DialogActions } from "./primitives/DialogActions";
|
|
20
|
+
|
|
21
|
+
const SUBMIT_CARD_MAX_WIDTH = 52;
|
|
22
|
+
const PALETTE_WINDOW = 5;
|
|
23
|
+
|
|
24
|
+
/** Selector words in the confirm card - one word per message. */
|
|
25
|
+
export const MESSAGE_OUTCOME_LABEL: Record<MessageOutcome, string> = {
|
|
26
|
+
comment: "comment",
|
|
27
|
+
approved: "approve",
|
|
28
|
+
changes_requested: "changes",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export interface ConfirmCardProps {
|
|
32
|
+
message: MessageOutcome;
|
|
33
|
+
summary: string;
|
|
34
|
+
/**
|
|
35
|
+
* The guided walk's honest coverage line for diff sessions, e.g.
|
|
36
|
+
* "2/3 files viewed". Undefined = no walk data, the row does not render.
|
|
37
|
+
*/
|
|
38
|
+
viewedSummary?: string;
|
|
39
|
+
onInput: (summary: string) => void;
|
|
40
|
+
onSelectMessage: (message: MessageOutcome) => void;
|
|
41
|
+
onSubmit: () => void;
|
|
42
|
+
onCancel: () => void;
|
|
43
|
+
quickActions: QuickAction[];
|
|
44
|
+
theme?: Theme;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 1-row message selector, spacer, summary input, spacer, buttons. */
|
|
48
|
+
const CONFIRM_CONTENT_ROWS = 5;
|
|
49
|
+
|
|
50
|
+
export function outcomeColor(message: MessageOutcome, tokens: Theme): string {
|
|
51
|
+
return message === "approved"
|
|
52
|
+
? tokens.green
|
|
53
|
+
: message === "changes_requested"
|
|
54
|
+
? tokens.red
|
|
55
|
+
: tokens.blue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The message selector: one row of pressable words in reading order.
|
|
60
|
+
* Selection stays controlled by the grammar (←/→ cycle the message); a click
|
|
61
|
+
* selects directly. Every message keeps its brackets so active color never changes the row's
|
|
62
|
+
* width or moves a neighboring choice.
|
|
63
|
+
*/
|
|
64
|
+
function MessageSelector({
|
|
65
|
+
message,
|
|
66
|
+
onSelectMessage,
|
|
67
|
+
theme,
|
|
68
|
+
}: {
|
|
69
|
+
message: MessageOutcome;
|
|
70
|
+
onSelectMessage: (message: MessageOutcome) => void;
|
|
71
|
+
theme?: Theme;
|
|
72
|
+
}): React.ReactNode {
|
|
73
|
+
const tokens = useComponentTheme(theme);
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<box style={{ flexDirection: "row", height: 1, width: "100%", justifyContent: "center" }}>
|
|
77
|
+
{MESSAGE_OUTCOMES.map((candidate) => (
|
|
78
|
+
<box
|
|
79
|
+
key={candidate}
|
|
80
|
+
style={{ paddingRight: 1 }}
|
|
81
|
+
onMouseUp={() => onSelectMessage(candidate)}
|
|
82
|
+
>
|
|
83
|
+
<text fg={candidate === message ? outcomeColor(candidate, tokens) : tokens.textDim}>
|
|
84
|
+
{`[${MESSAGE_OUTCOME_LABEL[candidate]}]`}
|
|
85
|
+
</text>
|
|
86
|
+
</box>
|
|
87
|
+
))}
|
|
88
|
+
</box>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function ConfirmCard({
|
|
93
|
+
message,
|
|
94
|
+
summary,
|
|
95
|
+
viewedSummary,
|
|
96
|
+
onInput,
|
|
97
|
+
onSelectMessage,
|
|
98
|
+
onSubmit,
|
|
99
|
+
onCancel,
|
|
100
|
+
quickActions,
|
|
101
|
+
theme,
|
|
102
|
+
}: ConfirmCardProps): React.ReactNode {
|
|
103
|
+
const tokens = useComponentTheme(theme);
|
|
104
|
+
const skills = useContext(SlashSkillsContext);
|
|
105
|
+
const { width: terminalWidth } = useTerminalDimensions();
|
|
106
|
+
const cardWidth = Math.max(24, Math.min(terminalWidth - 2, SUBMIT_CARD_MAX_WIDTH));
|
|
107
|
+
|
|
108
|
+
const composerRows = composeRowCount(summary, cardWidth - 4);
|
|
109
|
+
const token = activeSlashToken(summary, summary.length);
|
|
110
|
+
const paletteItems =
|
|
111
|
+
token !== null
|
|
112
|
+
? slashFilter(mergeSlashItems(slashItemsFrom(quickActions), skills), token.slice(1))
|
|
113
|
+
: [];
|
|
114
|
+
const paletteRows =
|
|
115
|
+
paletteItems.length > 0
|
|
116
|
+
? Math.min(PALETTE_WINDOW, paletteItems.length) +
|
|
117
|
+
(paletteItems.length > PALETTE_WINDOW ? 1 : 0)
|
|
118
|
+
: 0;
|
|
119
|
+
const contentRows =
|
|
120
|
+
CONFIRM_CONTENT_ROWS - 1 + composerRows + paletteRows + (viewedSummary !== undefined ? 2 : 0);
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<Card
|
|
124
|
+
contentRows={contentRows}
|
|
125
|
+
width={cardWidth}
|
|
126
|
+
borderColor={tokens.accent}
|
|
127
|
+
backgroundColor={tokens.elevated}
|
|
128
|
+
theme={theme}
|
|
129
|
+
>
|
|
130
|
+
{viewedSummary !== undefined ? <text fg={tokens.textDim}>{viewedSummary}</text> : null}
|
|
131
|
+
{viewedSummary !== undefined ? <box style={{ height: 1 }} /> : null}
|
|
132
|
+
<MessageSelector message={message} onSelectMessage={onSelectMessage} theme={theme} />
|
|
133
|
+
<box style={{ height: 1 }} />
|
|
134
|
+
<SlashComposer
|
|
135
|
+
seed={summary}
|
|
136
|
+
glyph=""
|
|
137
|
+
quickActions={quickActions}
|
|
138
|
+
tokens={tokens}
|
|
139
|
+
placeholder="summary for the agent (optional)"
|
|
140
|
+
onSubmit={(text) => {
|
|
141
|
+
onInput(text);
|
|
142
|
+
onSubmit();
|
|
143
|
+
}}
|
|
144
|
+
onInput={onInput}
|
|
145
|
+
/>
|
|
146
|
+
<box style={{ height: 1 }} />
|
|
147
|
+
<DialogActions confirmLabel="send" onConfirm={onSubmit} onCancel={onCancel} theme={theme} />
|
|
148
|
+
</Card>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "./story";
|
|
4
|
+
import { ConfirmDialog } from "./ConfirmDialog";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "Overlays/ConfirmDialog" };
|
|
7
|
+
|
|
8
|
+
const callbacks = { onConfirm: () => {}, onCancel: () => {} };
|
|
9
|
+
|
|
10
|
+
export const DeleteThread: Story = {
|
|
11
|
+
render: () => (
|
|
12
|
+
<ConfirmDialog
|
|
13
|
+
isOpen
|
|
14
|
+
title=" delete thread "
|
|
15
|
+
message={'Delete "Auth rollout"? This removes the thread.'}
|
|
16
|
+
{...callbacks}
|
|
17
|
+
/>
|
|
18
|
+
),
|
|
19
|
+
expectedColors: [DARK.accent],
|
|
20
|
+
size: { width: 64, height: 16 },
|
|
21
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A centered yes/no confirmation on the shared Dialog overlay. Presentational:
|
|
3
|
+
* the key grammar (⏎ confirm, esc cancel) is driven by the reducer; the buttons
|
|
4
|
+
* carry the same intents for the mouse. Used for destructive actions like the
|
|
5
|
+
* inbox delete.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from "react";
|
|
9
|
+
import { useTerminalDimensions } from "@opentui/react";
|
|
10
|
+
import type { Theme } from "../theme";
|
|
11
|
+
import { useComponentTheme } from "./theme-context";
|
|
12
|
+
import { Dialog } from "./primitives/Dialog";
|
|
13
|
+
import { DialogActions } from "./primitives/DialogActions";
|
|
14
|
+
|
|
15
|
+
export interface ConfirmDialogProps {
|
|
16
|
+
isOpen: boolean;
|
|
17
|
+
title?: string;
|
|
18
|
+
message: string;
|
|
19
|
+
confirmLabel?: string;
|
|
20
|
+
cancelLabel?: string;
|
|
21
|
+
onConfirm: () => void;
|
|
22
|
+
onCancel: () => void;
|
|
23
|
+
theme?: Theme;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function ConfirmDialog({
|
|
27
|
+
isOpen,
|
|
28
|
+
title,
|
|
29
|
+
message,
|
|
30
|
+
confirmLabel = "delete",
|
|
31
|
+
cancelLabel = "cancel",
|
|
32
|
+
onConfirm,
|
|
33
|
+
onCancel,
|
|
34
|
+
theme,
|
|
35
|
+
}: ConfirmDialogProps): React.ReactNode {
|
|
36
|
+
const tokens = useComponentTheme(theme);
|
|
37
|
+
const { width: terminalWidth } = useTerminalDimensions();
|
|
38
|
+
|
|
39
|
+
if (!isOpen) return null;
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<Dialog
|
|
43
|
+
isOpen
|
|
44
|
+
title={title}
|
|
45
|
+
width={Math.min(54, terminalWidth - 6)}
|
|
46
|
+
height={7}
|
|
47
|
+
background={tokens.elevated}
|
|
48
|
+
onDismiss={onCancel}
|
|
49
|
+
theme={theme}
|
|
50
|
+
>
|
|
51
|
+
<box
|
|
52
|
+
style={{
|
|
53
|
+
flexDirection: "column",
|
|
54
|
+
flexGrow: 1,
|
|
55
|
+
paddingLeft: 1,
|
|
56
|
+
paddingRight: 1,
|
|
57
|
+
paddingTop: 1,
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
<text fg={tokens.text}>{message}</text>
|
|
61
|
+
<box style={{ flexGrow: 1 }} />
|
|
62
|
+
<DialogActions
|
|
63
|
+
confirmLabel={confirmLabel}
|
|
64
|
+
cancelLabel={cancelLabel}
|
|
65
|
+
onConfirm={onConfirm}
|
|
66
|
+
onCancel={onCancel}
|
|
67
|
+
theme={theme}
|
|
68
|
+
/>
|
|
69
|
+
</box>
|
|
70
|
+
</Dialog>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/** Keyboard scroll is smooth: the caret-follow reveal never scrolls the caret off screen, nor rebounds. */
|
|
2
|
+
|
|
3
|
+
import { expect, test } from "bun:test";
|
|
4
|
+
import React from "react";
|
|
5
|
+
import { testRender } from "@opentui/react/test-utils";
|
|
6
|
+
import { ScrollBoxRenderable, type Renderable } from "@opentui/core";
|
|
7
|
+
import type { Annotation } from "@cueloop/schema";
|
|
8
|
+
import { DiffContentView } from "./DiffContentView";
|
|
9
|
+
import { diffRows, marksByRows } from "../view-diff";
|
|
10
|
+
import { DARK } from "../theme";
|
|
11
|
+
import { annotationPaletteFor } from "../annotation-palette";
|
|
12
|
+
import { press, settle, typeText } from "../test-support";
|
|
13
|
+
import { fixtureDiffSession } from "./story-fixtures";
|
|
14
|
+
|
|
15
|
+
const noop = (): void => {};
|
|
16
|
+
|
|
17
|
+
/** Find a renderable by id anywhere in the tree (getRenderable is not recursive). */
|
|
18
|
+
function findById(node: Renderable, id: string): Renderable | undefined {
|
|
19
|
+
if (node.id === id) return node;
|
|
20
|
+
for (const child of node.getChildren()) {
|
|
21
|
+
const found = findById(child, id);
|
|
22
|
+
|
|
23
|
+
if (found) return found;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function hex(color: { toInts(): [number, number, number, number] }): string {
|
|
30
|
+
const [red, green, blue] = color.toInts();
|
|
31
|
+
|
|
32
|
+
return "#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join("");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A single-hunk patch of `count` added lines, taller than any test viewport. */
|
|
36
|
+
function tallPatch(count: number): string {
|
|
37
|
+
const adds = Array.from(
|
|
38
|
+
{ length: count },
|
|
39
|
+
(_, index) => `+line ${String(index).padStart(2, "0")}`,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
return [
|
|
43
|
+
"diff --git a/f.ts b/f.ts",
|
|
44
|
+
"--- a/f.ts",
|
|
45
|
+
"+++ b/f.ts",
|
|
46
|
+
`@@ -0,0 +1,${count} @@`,
|
|
47
|
+
...adds,
|
|
48
|
+
"",
|
|
49
|
+
].join("\n");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
test("walking the caret down past wrapped discussion cards keeps it on screen and the scroll monotonic", async () => {
|
|
53
|
+
// Arrange - a tall diff whose comment bodies wrap at this width, so the cards
|
|
54
|
+
// between rows are taller than one line
|
|
55
|
+
const rows = diffRows(tallPatch(40));
|
|
56
|
+
const longBody =
|
|
57
|
+
"this is a deliberately long annotation body that would wrap across the narrow test viewport";
|
|
58
|
+
const annotations: Annotation[] = [8, 16, 24].map((rowIndex) => ({
|
|
59
|
+
id: `a${rowIndex}`,
|
|
60
|
+
kind: "comment",
|
|
61
|
+
body: longBody,
|
|
62
|
+
anchor: { quote: rows[rowIndex]!.text.replace(/\n$/, ""), prefix: "", suffix: "" },
|
|
63
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
64
|
+
}));
|
|
65
|
+
const setup = await testRender(
|
|
66
|
+
<DiffContentView
|
|
67
|
+
rows={rows}
|
|
68
|
+
session={fixtureDiffSession({ annotations })}
|
|
69
|
+
marks={marksByRows(annotations, rows)}
|
|
70
|
+
quickActions={[]}
|
|
71
|
+
observer={false}
|
|
72
|
+
onAnnotate={noop}
|
|
73
|
+
onReply={noop}
|
|
74
|
+
onUpdateAnnotation={noop}
|
|
75
|
+
onExit={noop}
|
|
76
|
+
/>,
|
|
77
|
+
{ width: 60, height: 12 },
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
await setup.waitForVisualIdle();
|
|
81
|
+
const found = findById(setup.renderer.root, "diff-scroll");
|
|
82
|
+
|
|
83
|
+
if (!(found instanceof ScrollBoxRenderable)) throw new Error("diff-scroll is not a scrollbox");
|
|
84
|
+
const scrollbox = found;
|
|
85
|
+
const caretCell = annotationPaletteFor(DARK).caretCell;
|
|
86
|
+
|
|
87
|
+
// the caret row is the one painting the caret cell
|
|
88
|
+
const caretScreenRow = (): number => {
|
|
89
|
+
const lines = setup.captureSpans().lines;
|
|
90
|
+
|
|
91
|
+
for (let index = 0; index < lines.length; index++) {
|
|
92
|
+
if (lines[index]!.spans.some((span) => hex(span.bg) === caretCell)) return index;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return -1;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Act - walk the caret down through every code row, sampling the scroll each step
|
|
99
|
+
const scrollTops: number[] = [];
|
|
100
|
+
const screenRows: number[] = [];
|
|
101
|
+
const codeRowCount = rows.filter((row) => row.kind === "add").length;
|
|
102
|
+
|
|
103
|
+
for (let step = 0; step < codeRowCount; step++) {
|
|
104
|
+
await setup.waitForVisualIdle();
|
|
105
|
+
scrollTops.push(scrollbox.scrollTop);
|
|
106
|
+
screenRows.push(caretScreenRow());
|
|
107
|
+
await press(setup, "down");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Assert - scrollTop only ever grows going down (never rebounds/jitters)
|
|
111
|
+
for (let step = 1; step < scrollTops.length; step++) {
|
|
112
|
+
expect(scrollTops[step]!).toBeGreaterThanOrEqual(scrollTops[step - 1]!);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Assert - the caret never scrolls off screen while it walks past each wrapped card
|
|
116
|
+
expect(screenRows.every((row) => row >= 0)).toBe(true);
|
|
117
|
+
// and the walk did scroll: the tall diff does not fit the viewport
|
|
118
|
+
expect(scrollTops[scrollTops.length - 1]!).toBeGreaterThan(0);
|
|
119
|
+
|
|
120
|
+
setup.renderer.destroy();
|
|
121
|
+
}, 25000);
|
|
122
|
+
|
|
123
|
+
test("opening a comment on the diff's last line reveals its composer", async () => {
|
|
124
|
+
const rows = diffRows(tallPatch(24));
|
|
125
|
+
const setup = await testRender(
|
|
126
|
+
<DiffContentView
|
|
127
|
+
rows={rows}
|
|
128
|
+
session={fixtureDiffSession()}
|
|
129
|
+
marks={marksByRows([], rows)}
|
|
130
|
+
quickActions={[]}
|
|
131
|
+
observer={false}
|
|
132
|
+
onAnnotate={noop}
|
|
133
|
+
onReply={noop}
|
|
134
|
+
onUpdateAnnotation={noop}
|
|
135
|
+
onExit={noop}
|
|
136
|
+
/>,
|
|
137
|
+
{ width: 60, height: 10 },
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
await settle(setup);
|
|
141
|
+
for (let step = 0; step < rows.length + 2; step++) {
|
|
142
|
+
// eslint-disable-next-line no-await-in-loop
|
|
143
|
+
await press(setup, "down");
|
|
144
|
+
}
|
|
145
|
+
await typeText(setup, "bottom comment");
|
|
146
|
+
|
|
147
|
+
expect(setup.captureCharFrame()).toContain("● bottom comment");
|
|
148
|
+
setup.renderer.destroy();
|
|
149
|
+
}, 25000);
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visual regression for the side-by-side split: the divider must be one unbroken
|
|
3
|
+
* column with no holes on wrapped continuation lines, and the two columns must not
|
|
4
|
+
* merge into each other. Rendered in a virtual terminal and asserted on the char frame.
|
|
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 { DiffContentView } from "./DiffContentView";
|
|
11
|
+
import { diffRows } from "../view-diff";
|
|
12
|
+
import { allowEventLoopUpdates } from "../test-support";
|
|
13
|
+
import { fixtureDiffSession } from "./story-fixtures";
|
|
14
|
+
import { annotationPaletteFor } from "../annotation-palette";
|
|
15
|
+
import { DARK } from "../theme";
|
|
16
|
+
|
|
17
|
+
const noop = (): void => {};
|
|
18
|
+
|
|
19
|
+
function hex(color: { toInts(): [number, number, number, number] }): string {
|
|
20
|
+
const [red, green, blue] = color.toInts();
|
|
21
|
+
|
|
22
|
+
return "#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join("");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// spaced words so each side word-wraps several visual lines, plus an unbalanced block (filler);
|
|
26
|
+
// spaced (not one long token) so a whole word lands on a continuation line for the assertions
|
|
27
|
+
const WRAPPING_PATCH = `diff --git a/app.ts b/app.ts
|
|
28
|
+
--- a/app.ts
|
|
29
|
+
+++ b/app.ts
|
|
30
|
+
@@ -1,4 +1,5 @@
|
|
31
|
+
const alpha = the quick brown fox jumps over the lazy dog and then keeps running past the far edge
|
|
32
|
+
-const beta = old value that used to sit on this single line before the review had changed it wholly
|
|
33
|
+
+const beta = new value that now sits upon this single line after the review has changed it entirely
|
|
34
|
+
+const gamma = an added line with no deletion partner so the whole left column stays blank filler now
|
|
35
|
+
export { alpha, beta };
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
async function splitFrameLines(width: number): Promise<string[]> {
|
|
39
|
+
const setup = await testRender(
|
|
40
|
+
<DiffContentView
|
|
41
|
+
rows={diffRows(WRAPPING_PATCH)}
|
|
42
|
+
session={fixtureDiffSession()}
|
|
43
|
+
marks={new Map()}
|
|
44
|
+
quickActions={[]}
|
|
45
|
+
observer={false}
|
|
46
|
+
onAnnotate={noop}
|
|
47
|
+
onReply={noop}
|
|
48
|
+
onUpdateAnnotation={noop}
|
|
49
|
+
onExit={noop}
|
|
50
|
+
split
|
|
51
|
+
/>,
|
|
52
|
+
{ width, height: 18 },
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
allowEventLoopUpdates();
|
|
56
|
+
await setup.waitForVisualIdle();
|
|
57
|
+
const frame = setup.captureCharFrame();
|
|
58
|
+
|
|
59
|
+
setup.renderer.destroy();
|
|
60
|
+
|
|
61
|
+
return frame.split("\n");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
describe("split diff rendering", () => {
|
|
65
|
+
test("the caret marks only the change side, not both columns", async () => {
|
|
66
|
+
// Arrange - the caret opens on a context row, which is shown in both columns
|
|
67
|
+
const setup = await testRender(
|
|
68
|
+
<DiffContentView
|
|
69
|
+
rows={diffRows(WRAPPING_PATCH)}
|
|
70
|
+
session={fixtureDiffSession()}
|
|
71
|
+
marks={new Map()}
|
|
72
|
+
quickActions={[]}
|
|
73
|
+
observer={false}
|
|
74
|
+
onAnnotate={noop}
|
|
75
|
+
onReply={noop}
|
|
76
|
+
onUpdateAnnotation={noop}
|
|
77
|
+
onExit={noop}
|
|
78
|
+
split
|
|
79
|
+
/>,
|
|
80
|
+
{ width: 100, height: 18 },
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
allowEventLoopUpdates();
|
|
84
|
+
await setup.waitForVisualIdle();
|
|
85
|
+
const caretCell = annotationPaletteFor(DARK).caretCell;
|
|
86
|
+
let cells = 0;
|
|
87
|
+
|
|
88
|
+
for (const line of setup.captureSpans().lines) {
|
|
89
|
+
for (const span of line.spans) {
|
|
90
|
+
if (span.bg && hex(span.bg) === caretCell) cells += span.text.length;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Assert - one caret cell (the change side), not one per column
|
|
95
|
+
expect(cells).toBe(1);
|
|
96
|
+
setup.renderer.destroy();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("the divider is one unbroken column across wrapped lines", async () => {
|
|
100
|
+
// Act
|
|
101
|
+
const lines = await splitFrameLines(80);
|
|
102
|
+
const dividerLines = lines.filter((line) => line.includes("│"));
|
|
103
|
+
const dividerColumns = new Set(dividerLines.map((line) => line.indexOf("│")));
|
|
104
|
+
|
|
105
|
+
// Assert - every divider sits in the same column (the columns never drift or merge)
|
|
106
|
+
expect(dividerColumns.size).toBe(1);
|
|
107
|
+
// and the code region is contiguous: no code row is missing its divider
|
|
108
|
+
const firstDivider = lines.findIndex((line) => line.includes("│"));
|
|
109
|
+
const lastDivider = lines.map((line) => line.includes("│")).lastIndexOf(true);
|
|
110
|
+
|
|
111
|
+
for (let index = firstDivider; index <= lastDivider; index++) {
|
|
112
|
+
expect(lines[index]).toContain("│");
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("wrapped continuation lines carry the divider, not just the first line", async () => {
|
|
117
|
+
// Act - the alpha line wraps, so a late word sits on a continuation line (no gutter number)
|
|
118
|
+
const lines = await splitFrameLines(80);
|
|
119
|
+
const continuation = lines.find(
|
|
120
|
+
(line) => line.includes("running") && !line.includes("const alpha"),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// Assert - the continuation still shows the divider
|
|
124
|
+
expect(continuation).toBeDefined();
|
|
125
|
+
expect(continuation).toContain("│");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("an addition with no deletion partner keeps the divider over blank left filler", async () => {
|
|
129
|
+
// Act - the gamma line has no left partner, so its left column is blank filler
|
|
130
|
+
const lines = await splitFrameLines(80);
|
|
131
|
+
const fillerRow = lines.find((line) => line.includes("no deletion partner"));
|
|
132
|
+
|
|
133
|
+
// Assert - the row still has a divider, and the left of it is blank (no merged text)
|
|
134
|
+
expect(fillerRow).toBeDefined();
|
|
135
|
+
const dividerAt = fillerRow!.indexOf("│");
|
|
136
|
+
|
|
137
|
+
expect(dividerAt).toBeGreaterThan(0);
|
|
138
|
+
expect(fillerRow!.slice(0, dividerAt).trim()).toBe("");
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Annotation } from "@cueloop/schema";
|
|
3
|
+
import { DARK } from "../theme";
|
|
4
|
+
import { annotationPaletteFor } from "../annotation-palette";
|
|
5
|
+
import { DEFAULT_QUICK_ACTIONS } from "../config";
|
|
6
|
+
import { marksByRows } from "../view-diff";
|
|
7
|
+
import type { Story, StoryMeta } from "./story";
|
|
8
|
+
import { DiffContentView } from "./DiffContentView";
|
|
9
|
+
import { fixtureDiffRows, fixtureDiffSession } from "./story-fixtures";
|
|
10
|
+
|
|
11
|
+
export const meta: StoryMeta = { title: "Surfaces/DiffContentView" };
|
|
12
|
+
|
|
13
|
+
const palette = annotationPaletteFor(DARK);
|
|
14
|
+
const noop = (): void => {};
|
|
15
|
+
|
|
16
|
+
/** A comment on the added line, anchored the way the surface anchors a marked word. */
|
|
17
|
+
const LINE_COMMENT: Annotation = {
|
|
18
|
+
id: "a_diff_1",
|
|
19
|
+
kind: "comment",
|
|
20
|
+
anchor: { quote: "new Map()", prefix: " private items = ", suffix: ";" },
|
|
21
|
+
body: "Map needs an eviction story.",
|
|
22
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function sheet(annotations: Annotation[], split = false): React.ReactNode {
|
|
26
|
+
const rows = fixtureDiffRows();
|
|
27
|
+
const session = fixtureDiffSession({ annotations });
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<DiffContentView
|
|
31
|
+
rows={rows}
|
|
32
|
+
session={session}
|
|
33
|
+
marks={marksByRows(annotations, rows)}
|
|
34
|
+
quickActions={DEFAULT_QUICK_ACTIONS}
|
|
35
|
+
observer={false}
|
|
36
|
+
onAnnotate={noop}
|
|
37
|
+
onReply={noop}
|
|
38
|
+
onUpdateAnnotation={noop}
|
|
39
|
+
onExit={noop}
|
|
40
|
+
split={split}
|
|
41
|
+
/>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const SignedRows: Story = {
|
|
46
|
+
render: () => sheet([]),
|
|
47
|
+
expectedColors: [DARK.insertedForeground, DARK.deletedForeground],
|
|
48
|
+
size: { width: 90, height: 16 },
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// A modified line keeps the diff color on the changed words and dims the
|
|
52
|
+
// unchanged prefix to the dim token, so the intra-line change stands out.
|
|
53
|
+
export const IntralineWordDiff: Story = {
|
|
54
|
+
render: () => sheet([]),
|
|
55
|
+
expectedColors: [DARK.insertedForeground, DARK.deletedForeground, DARK.textDim],
|
|
56
|
+
size: { width: 90, height: 16 },
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// The caret opens on the first code line: its cell wears the caret backdrop.
|
|
60
|
+
export const CaretOnFirstLine: Story = {
|
|
61
|
+
render: () => sheet([]),
|
|
62
|
+
expectedColors: [palette.caretCell],
|
|
63
|
+
size: { width: 90, height: 16 },
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// A saved comment marks its words (backdrop + underline) and hangs its discussion
|
|
67
|
+
// card under the line, exactly as the thread view does for prose.
|
|
68
|
+
export const AnnotatedLine: Story = {
|
|
69
|
+
render: () => sheet([LINE_COMMENT]),
|
|
70
|
+
expectedColors: [palette.markBackdrop, palette.cardEdge],
|
|
71
|
+
size: { width: 90, height: 18 },
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Side by side: old on the left, new on the right, divided by a thin rule; the
|
|
75
|
+
// comment card spans the full width under its pair.
|
|
76
|
+
export const SplitView: Story = {
|
|
77
|
+
render: () => sheet([LINE_COMMENT], true),
|
|
78
|
+
expectedColors: [DARK.insertedForeground, DARK.deletedForeground, palette.markBackdrop],
|
|
79
|
+
size: { width: 120, height: 18 },
|
|
80
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { afterEach, beforeEach, expect, test } from "bun:test";
|
|
2
|
+
import { testRender } from "@opentui/react/test-utils";
|
|
3
|
+
import type { RGBA } from "@opentui/core";
|
|
4
|
+
import React from "react";
|
|
5
|
+
import { DiffContentView } from "./DiffContentView";
|
|
6
|
+
import { fixtureDiffSession } from "./story-fixtures";
|
|
7
|
+
import { diffRows } from "../view-diff";
|
|
8
|
+
import { DARK } from "../theme";
|
|
9
|
+
import { allowEventLoopUpdates } from "../test-support";
|
|
10
|
+
|
|
11
|
+
const PATCH = `diff --git a/src/x.ts b/src/x.ts
|
|
12
|
+
--- a/src/x.ts
|
|
13
|
+
+++ b/src/x.ts
|
|
14
|
+
@@ -1,3 +1,3 @@
|
|
15
|
+
export class Store {
|
|
16
|
+
- const limit = 100;
|
|
17
|
+
+ const limit = 250;
|
|
18
|
+
}
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
function hex(color: RGBA): string {
|
|
22
|
+
const [red, green, blue] = color.toInts();
|
|
23
|
+
|
|
24
|
+
return "#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join("");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
beforeEach(() => allowEventLoopUpdates());
|
|
28
|
+
afterEach(() => allowEventLoopUpdates());
|
|
29
|
+
|
|
30
|
+
test("renders tree-sitter colors, with the changed word in the diff color on top", async () => {
|
|
31
|
+
// Arrange
|
|
32
|
+
const noop = (): void => {};
|
|
33
|
+
const setup = await testRender(
|
|
34
|
+
<DiffContentView
|
|
35
|
+
rows={diffRows(PATCH)}
|
|
36
|
+
session={fixtureDiffSession()}
|
|
37
|
+
marks={new Map()}
|
|
38
|
+
quickActions={[]}
|
|
39
|
+
observer={false}
|
|
40
|
+
onAnnotate={noop}
|
|
41
|
+
onReply={noop}
|
|
42
|
+
onUpdateAnnotation={noop}
|
|
43
|
+
onExit={noop}
|
|
44
|
+
/>,
|
|
45
|
+
{ width: 60, height: 12 },
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// Act
|
|
49
|
+
// syntax highlighting resolves off the render path; pump until a keyword paints
|
|
50
|
+
let keywordAccent = false;
|
|
51
|
+
|
|
52
|
+
for (let attempt = 0; attempt < 60 && !keywordAccent; attempt++) {
|
|
53
|
+
await setup.renderOnce();
|
|
54
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
55
|
+
// the caret cell splits the keyword's first character into its own span, so read the
|
|
56
|
+
// accent-colored text of a whole line rather than one span
|
|
57
|
+
for (const line of setup.captureSpans().lines) {
|
|
58
|
+
const accentText = line.spans
|
|
59
|
+
.filter((span) => hex(span.fg) === DARK.accent)
|
|
60
|
+
.map((span) => span.text)
|
|
61
|
+
.join("");
|
|
62
|
+
|
|
63
|
+
if (accentText.includes("export")) keywordAccent = true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Assert
|
|
68
|
+
expect(keywordAccent).toBe(true);
|
|
69
|
+
// the intra-line changed number keeps the diff color over its syntax color
|
|
70
|
+
const changedColors = new Set<string>();
|
|
71
|
+
|
|
72
|
+
for (const line of setup.captureSpans().lines) {
|
|
73
|
+
for (const span of line.spans) {
|
|
74
|
+
if (span.text.includes("250")) changedColors.add(hex(span.fg));
|
|
75
|
+
if (span.text.includes("100")) changedColors.add(hex(span.fg));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
expect(changedColors.has(DARK.insertedForeground)).toBe(true);
|
|
79
|
+
expect(changedColors.has(DARK.deletedForeground)).toBe(true);
|
|
80
|
+
|
|
81
|
+
setup.renderer.destroy();
|
|
82
|
+
}, 25000);
|