@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,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Direct kitty graphics protocol emission for a cell rectangle. OpenTUI's
|
|
3
|
+
* ImageRenderable does not display in every terminal, so prototype review paints
|
|
4
|
+
* the screenshot itself: transmit the PNG once under a fixed image AND placement
|
|
5
|
+
* id, then re-place that same placement after each frame over its reserved
|
|
6
|
+
* (never-repainted) cells. Reusing one placement id means each frame REPLACES
|
|
7
|
+
* the placement rather than stacking a new one, which is the protocol's
|
|
8
|
+
* flicker-free move/resize path. Locally the PNG travels out of band via a
|
|
9
|
+
* temporary file so its bytes never cross the pty; a base64 fallback covers the
|
|
10
|
+
* ssh sharing path where no shared filesystem exists.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
|
|
17
|
+
export interface CellRegion {
|
|
18
|
+
/** 0-based screen cell of the region's top-left corner. */
|
|
19
|
+
column: number;
|
|
20
|
+
row: number;
|
|
21
|
+
columns: number;
|
|
22
|
+
rows: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Writes a raw sequence ordered against the renderer's own frame output. */
|
|
26
|
+
export type OrderedWrite = (chunk: string) => void;
|
|
27
|
+
|
|
28
|
+
export type TransmitMedium = "file" | "base64";
|
|
29
|
+
|
|
30
|
+
const ESCAPE_STRING_TERMINATOR = "\x1b\\";
|
|
31
|
+
const CHUNK_BYTES = 4096;
|
|
32
|
+
// The protocol reserves values below INT32_MIN/2 for images that must sit
|
|
33
|
+
// beneath cells with painted backgrounds, such as the marker popover.
|
|
34
|
+
const IMAGE_Z_INDEX = -1_073_741_825;
|
|
35
|
+
// One fixed placement per image: reusing it makes every put replace that
|
|
36
|
+
// placement instead of accumulating a new one each frame.
|
|
37
|
+
const PLACEMENT_ID = 1;
|
|
38
|
+
const SYNCHRONIZED_UPDATE_START = "\x1b[?2026h";
|
|
39
|
+
const SYNCHRONIZED_UPDATE_END = "\x1b[?2026l";
|
|
40
|
+
|
|
41
|
+
let tempFileCounter = 0;
|
|
42
|
+
let privateTempDir: string | null = null;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* base64 is the safe default: it works over the pty everywhere, including the
|
|
46
|
+
* ssh sharing path. Out-of-band file transfer (t=t) is faster for the initial
|
|
47
|
+
* transmit but depends on the terminal trusting the temp-file path, so it is
|
|
48
|
+
* opt-in via `CUELOOP_KITTY_FILE=1` and never used over ssh (no shared disk).
|
|
49
|
+
*/
|
|
50
|
+
export function resolveTransmitMedium(env: NodeJS.ProcessEnv = process.env): TransmitMedium {
|
|
51
|
+
if (env.CUELOOP_KITTY_FILE === "1" && !env.SSH_CONNECTION && !env.SSH_TTY) return "file";
|
|
52
|
+
|
|
53
|
+
return "base64";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Save the cursor, run the placement at the region's corner, restore it. */
|
|
57
|
+
function atRegion(region: CellRegion, body: string): string {
|
|
58
|
+
const moveToCorner = `\x1b[${region.row + 1};${region.column + 1}H`;
|
|
59
|
+
|
|
60
|
+
return `\x1b7${moveToCorner}${body}\x1b8`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function graphicsCommand(keys: string, payload: string): string {
|
|
64
|
+
return `\x1b_G${keys};${payload}${ESCAPE_STRING_TERMINATOR}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function placementKeys(imageId: number, region: CellRegion): string {
|
|
68
|
+
return `i=${imageId},p=${PLACEMENT_ID},q=2,C=1,z=${IMAGE_Z_INDEX},c=${region.columns},r=${region.rows}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// A per-process 0700 directory with an unpredictable name, so a local attacker
|
|
72
|
+
// cannot pre-place a symlink at the screenshot path and redirect the write.
|
|
73
|
+
function writeTempPng(png: Uint8Array): string {
|
|
74
|
+
if (!privateTempDir) privateTempDir = mkdtempSync(join(tmpdir(), "cueloop-prototype-"));
|
|
75
|
+
const path = join(privateTempDir, `${tempFileCounter++}.png`);
|
|
76
|
+
|
|
77
|
+
writeFileSync(path, png, { flag: "wx" });
|
|
78
|
+
|
|
79
|
+
return path;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function base64Transmit(png: Uint8Array, region: CellRegion, imageId: number): string {
|
|
83
|
+
const base64 = Buffer.from(png).toString("base64");
|
|
84
|
+
const controlKeys = `a=T,f=100,${placementKeys(imageId, region)}`;
|
|
85
|
+
let sequence = "";
|
|
86
|
+
|
|
87
|
+
for (let offset = 0; offset < base64.length; offset += CHUNK_BYTES) {
|
|
88
|
+
const chunk = base64.slice(offset, offset + CHUNK_BYTES);
|
|
89
|
+
const isMore = offset + CHUNK_BYTES < base64.length ? 1 : 0;
|
|
90
|
+
|
|
91
|
+
sequence +=
|
|
92
|
+
offset === 0
|
|
93
|
+
? graphicsCommand(`${controlKeys},m=${isMore}`, chunk)
|
|
94
|
+
: graphicsCommand(`m=${isMore}`, chunk);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return sequence;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function fileTransmit(png: Uint8Array, region: CellRegion, imageId: number): string {
|
|
101
|
+
const path = writeTempPng(png);
|
|
102
|
+
const payload = Buffer.from(path).toString("base64");
|
|
103
|
+
|
|
104
|
+
return graphicsCommand(`a=T,f=100,t=t,${placementKeys(imageId, region)}`, payload);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Transmit a PNG and display it filling the region's cells (a=T). f=100 is PNG,
|
|
109
|
+
* C=1 keeps the cursor put so a full-region image cannot scroll the screen, and
|
|
110
|
+
* q=2 suppresses the terminal's replies. The whole write is bracketed in
|
|
111
|
+
* synchronized-update mode so the terminal composites it atomically. `file`
|
|
112
|
+
* hands over a temp path (t=t: the terminal deletes it after reading); `base64`
|
|
113
|
+
* inlines the data, chunked at 4096 bytes.
|
|
114
|
+
*/
|
|
115
|
+
export function transmitKittyImage(
|
|
116
|
+
write: OrderedWrite,
|
|
117
|
+
png: Uint8Array,
|
|
118
|
+
region: CellRegion,
|
|
119
|
+
imageId: number,
|
|
120
|
+
medium: TransmitMedium = "base64",
|
|
121
|
+
): void {
|
|
122
|
+
if (region.columns < 1 || region.rows < 1) return;
|
|
123
|
+
const body =
|
|
124
|
+
medium === "file" ? fileTransmit(png, region, imageId) : base64Transmit(png, region, imageId);
|
|
125
|
+
|
|
126
|
+
write(SYNCHRONIZED_UPDATE_START + atRegion(region, body) + SYNCHRONIZED_UPDATE_END);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Re-place the already-transmitted image over its region, cheaply (a=p, no data). */
|
|
130
|
+
export function placeKittyImage(write: OrderedWrite, region: CellRegion, imageId: number): void {
|
|
131
|
+
if (region.columns < 1 || region.rows < 1) return;
|
|
132
|
+
write(atRegion(region, graphicsCommand(`a=p,${placementKeys(imageId, region)}`, "")));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Remove the image so it does not linger after the sheet closes (a=d). */
|
|
136
|
+
export function deleteKittyImage(write: OrderedWrite, imageId: number): void {
|
|
137
|
+
write(graphicsCommand(`a=d,d=i,i=${imageId},q=2`, ""));
|
|
138
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
defaultLayout,
|
|
4
|
+
layoutFromPanes,
|
|
5
|
+
pairLayout,
|
|
6
|
+
planLayout,
|
|
7
|
+
pullRequestReviewLayout,
|
|
8
|
+
reviewLayout,
|
|
9
|
+
} from "./launch-layout";
|
|
10
|
+
|
|
11
|
+
describe("launch layout factories", () => {
|
|
12
|
+
test("review opens the diff zoomed with the Changes panel", () => {
|
|
13
|
+
// Assert
|
|
14
|
+
expect(reviewLayout()).toEqual({ threads: true, rightSidebar: "changes", zoomChanges: true });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("pair opens the Project tree focused beside a zoomed Changes editor", () => {
|
|
18
|
+
expect(pairLayout()).toEqual({
|
|
19
|
+
threads: true,
|
|
20
|
+
rightSidebar: "changes",
|
|
21
|
+
zoomChanges: true,
|
|
22
|
+
projectMode: "tree",
|
|
23
|
+
focusPane: "project",
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("pull request review keeps its brief beside the Changes panel", () => {
|
|
28
|
+
// Assert
|
|
29
|
+
expect(pullRequestReviewLayout()).toEqual({
|
|
30
|
+
threads: true,
|
|
31
|
+
rightSidebar: "changes",
|
|
32
|
+
zoomChanges: false,
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("plan fills the middle with the thread pane and no right region", () => {
|
|
37
|
+
// Assert
|
|
38
|
+
expect(planLayout()).toEqual({ threads: true, rightSidebar: "off", zoomChanges: false });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("the first-run default lands on the inbox with the diff zoomed", () => {
|
|
42
|
+
// Assert
|
|
43
|
+
expect(defaultLayout()).toEqual({ threads: true, rightSidebar: "changes", zoomChanges: true });
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("layoutFromPanes", () => {
|
|
48
|
+
test("the Changes editor wins the right region when open", () => {
|
|
49
|
+
// Assert
|
|
50
|
+
expect(
|
|
51
|
+
layoutFromPanes({ threads: true, changesOpen: true, projectOpen: true, zoomChanges: true }),
|
|
52
|
+
).toEqual({ threads: true, rightSidebar: "changes", zoomChanges: true });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("the Project tree holds the right region when the Changes editor is closed", () => {
|
|
56
|
+
// Assert
|
|
57
|
+
expect(
|
|
58
|
+
layoutFromPanes({
|
|
59
|
+
threads: false,
|
|
60
|
+
changesOpen: false,
|
|
61
|
+
projectOpen: true,
|
|
62
|
+
zoomChanges: false,
|
|
63
|
+
}),
|
|
64
|
+
).toEqual({ threads: false, rightSidebar: "project", zoomChanges: false });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("the right region is off when neither pane is open", () => {
|
|
68
|
+
// Assert
|
|
69
|
+
expect(
|
|
70
|
+
layoutFromPanes({
|
|
71
|
+
threads: true,
|
|
72
|
+
changesOpen: false,
|
|
73
|
+
projectOpen: false,
|
|
74
|
+
zoomChanges: false,
|
|
75
|
+
}),
|
|
76
|
+
).toEqual({ threads: true, rightSidebar: "off", zoomChanges: false });
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pane composition a launch opens with: whether the Threads sidebar is open,
|
|
3
|
+
* what the right region shows, and whether the Changes editor is zoomed. Each
|
|
4
|
+
* command picks a layout through a factory; a bare launch restores the last one
|
|
5
|
+
* the user left (persisted in config), falling back to the default.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface LaunchLayout {
|
|
9
|
+
/** The Threads sidebar is open. */
|
|
10
|
+
threads: boolean;
|
|
11
|
+
/** The right region: the Changes editor, the Project tree, or closed. */
|
|
12
|
+
rightSidebar: "changes" | "project" | "off";
|
|
13
|
+
/** The Changes editor is zoomed - the Thread pane hides so the diff fills the middle. */
|
|
14
|
+
zoomChanges: boolean;
|
|
15
|
+
/** Which Project sidebar tab opens while the Changes editor stays visible. */
|
|
16
|
+
projectMode?: "changes" | "tree";
|
|
17
|
+
/** Keyboard focus when opening a selected Thread. */
|
|
18
|
+
focusPane?: "threads" | "thread" | "changes" | "project";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** `cueloop diff`: the diff front and center. */
|
|
22
|
+
export function reviewLayout(): LaunchLayout {
|
|
23
|
+
return { threads: true, rightSidebar: "changes", zoomChanges: true };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** `cueloop pair`: browse project files beside the zoomed Changes editor. */
|
|
27
|
+
export function pairLayout(): LaunchLayout {
|
|
28
|
+
return {
|
|
29
|
+
threads: true,
|
|
30
|
+
rightSidebar: "changes",
|
|
31
|
+
zoomChanges: true,
|
|
32
|
+
projectMode: "tree",
|
|
33
|
+
focusPane: "project",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** `cueloop review <pr>`: PR context beside the Changes panel. */
|
|
38
|
+
export function pullRequestReviewLayout(): LaunchLayout {
|
|
39
|
+
return { threads: true, rightSidebar: "changes", zoomChanges: false };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** `cueloop plan` / `cueloop reply`: the thread pane fills the middle; no Changes panel. */
|
|
43
|
+
export function planLayout(): LaunchLayout {
|
|
44
|
+
return { threads: true, rightSidebar: "off", zoomChanges: false };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The first-run default, before any layout has been remembered: the inbox with the diff zoomed. */
|
|
48
|
+
export function defaultLayout(): LaunchLayout {
|
|
49
|
+
return { threads: true, rightSidebar: "changes", zoomChanges: true };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Read back the layout the panes currently sit in, to remember it for the next bare launch. */
|
|
53
|
+
export function layoutFromPanes(panes: {
|
|
54
|
+
threads: boolean;
|
|
55
|
+
changesOpen: boolean;
|
|
56
|
+
projectOpen: boolean;
|
|
57
|
+
zoomChanges: boolean;
|
|
58
|
+
}): LaunchLayout {
|
|
59
|
+
const rightSidebar = panes.changesOpen ? "changes" : panes.projectOpen ? "project" : "off";
|
|
60
|
+
|
|
61
|
+
return { threads: panes.threads, rightSidebar, zoomChanges: panes.zoomChanges };
|
|
62
|
+
}
|
package/src/mark-runs.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mark geometry for an annotated line of text: which stretches are marked
|
|
3
|
+
* (a discussion span, a held selection) or carry the caret cell, the greedy
|
|
4
|
+
* word wrap into visual lines, and the printable-key test that starts a
|
|
5
|
+
* comment. Renderer-free, shared by the plan thread view and the diff sheet.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { KeyEvent } from "@opentui/core";
|
|
9
|
+
import { wordRanges } from "./thread-selection";
|
|
10
|
+
|
|
11
|
+
export interface MarkRange {
|
|
12
|
+
start: number;
|
|
13
|
+
end: number;
|
|
14
|
+
/** The bare caret's single cell - a cursor, not yet a mark. */
|
|
15
|
+
caretOnly?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface Run {
|
|
19
|
+
text: string;
|
|
20
|
+
marked: boolean;
|
|
21
|
+
caretOnly: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Split a block's text into plain/marked runs from the union of ranges. */
|
|
25
|
+
export function runsFor(text: string, ranges: MarkRange[]): Run[] {
|
|
26
|
+
const cuts = new Set<number>([0, text.length]);
|
|
27
|
+
|
|
28
|
+
for (const range of ranges) {
|
|
29
|
+
cuts.add(Math.max(0, range.start));
|
|
30
|
+
cuts.add(Math.min(text.length, range.end));
|
|
31
|
+
}
|
|
32
|
+
const edges = [...cuts].toSorted((left, right) => left - right);
|
|
33
|
+
const runs: Run[] = [];
|
|
34
|
+
|
|
35
|
+
for (let index = 0; index < edges.length - 1; index++) {
|
|
36
|
+
const start = edges[index]!;
|
|
37
|
+
const end = edges[index + 1]!;
|
|
38
|
+
|
|
39
|
+
if (end <= start) continue;
|
|
40
|
+
const covering = ranges.filter((range) => range.start <= start && end <= range.end);
|
|
41
|
+
// the caret cell shows through a mark, so the head of a selection is visible
|
|
42
|
+
runs.push({
|
|
43
|
+
text: text.slice(start, end),
|
|
44
|
+
marked: covering.some((range) => range.caretOnly !== true),
|
|
45
|
+
caretOnly: covering.some((range) => range.caretOnly === true),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return runs.length > 0 ? runs : [{ text, marked: false, caretOnly: false }];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A visual line of a block: the char range of the block's text it shows. */
|
|
53
|
+
export interface VisualLine {
|
|
54
|
+
start: number;
|
|
55
|
+
end: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Greedy word wrap into visual-line char ranges, so a comment card can slot
|
|
60
|
+
* in directly below the line its mark ends on (mid-paragraph when needed).
|
|
61
|
+
*/
|
|
62
|
+
export function wrapLines(text: string, width: number): VisualLine[] {
|
|
63
|
+
const lines: VisualLine[] = [];
|
|
64
|
+
let segmentStart = 0;
|
|
65
|
+
|
|
66
|
+
// hard breaks first: every visual line must be exactly one terminal row,
|
|
67
|
+
// or hit-testing and the mark geometry drift from what is painted
|
|
68
|
+
for (const segment of text.split("\n")) {
|
|
69
|
+
const segmentEnd = segmentStart + segment.length;
|
|
70
|
+
|
|
71
|
+
if (width <= 0 || segment.length <= width) {
|
|
72
|
+
lines.push({ start: segmentStart, end: segmentEnd });
|
|
73
|
+
} else {
|
|
74
|
+
// greedy word wrap inside the segment; the first line keeps its
|
|
75
|
+
// leading indentation, continuation lines start at a word
|
|
76
|
+
let lineStart = segmentStart;
|
|
77
|
+
let lineEnd = segmentStart;
|
|
78
|
+
|
|
79
|
+
for (const word of wordRanges(segment)) {
|
|
80
|
+
const wordStart = segmentStart + word.start;
|
|
81
|
+
const wordEnd = segmentStart + word.end;
|
|
82
|
+
|
|
83
|
+
if (wordEnd - lineStart > width && lineEnd > lineStart) {
|
|
84
|
+
lines.push({ start: lineStart, end: lineEnd });
|
|
85
|
+
lineStart = wordStart;
|
|
86
|
+
}
|
|
87
|
+
// a token wider than the line (a long identifier, a URL) breaks at the cell edge, so
|
|
88
|
+
// every visual line stays one terminal row and nothing is clipped away
|
|
89
|
+
while (wordEnd - lineStart > width) {
|
|
90
|
+
lines.push({ start: lineStart, end: lineStart + width });
|
|
91
|
+
lineStart += width;
|
|
92
|
+
}
|
|
93
|
+
lineEnd = wordEnd;
|
|
94
|
+
}
|
|
95
|
+
lines.push({ start: lineStart, end: Math.max(lineEnd, lineStart) });
|
|
96
|
+
}
|
|
97
|
+
segmentStart = segmentEnd + 1;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return lines;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The block's mark ranges clipped to one visual line, in that line's coordinates. */
|
|
104
|
+
export function lineMarkRanges(ranges: MarkRange[], line: VisualLine): MarkRange[] {
|
|
105
|
+
return ranges
|
|
106
|
+
.map((range) => ({
|
|
107
|
+
start: Math.max(range.start, line.start) - line.start,
|
|
108
|
+
end: Math.min(range.end, line.end) - line.start,
|
|
109
|
+
caretOnly: range.caretOnly,
|
|
110
|
+
}))
|
|
111
|
+
.filter((range) => range.end > range.start);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Printable input that should reach a composer: one key, or a whole pasted run in one event. */
|
|
115
|
+
export function printableSequence(key: KeyEvent): string | null {
|
|
116
|
+
if (!key.sequence || key.ctrl || key.meta) return null;
|
|
117
|
+
// reject any run carrying a control byte, so escape sequences and \r\t\b never seed a draft
|
|
118
|
+
for (const character of key.sequence) if (character < " ") return null;
|
|
119
|
+
|
|
120
|
+
return key.sequence.length > 0 ? key.sequence : null;
|
|
121
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The native SyntaxStyle for the inline markdown editor: maps each markdown
|
|
3
|
+
* token group to a theme color and attribute set, and resolves the styleId the
|
|
4
|
+
* edit buffer needs for `addHighlightByCharRange`. Cached per theme, mirroring
|
|
5
|
+
* syntaxStyleFor. Syntax punctuation (the "marker" group) is dimmed rather than
|
|
6
|
+
* hidden, since the editor shows raw source.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { SyntaxStyle, type StyleDefinitionInput } from "@opentui/core";
|
|
10
|
+
import { type Theme } from "./theme";
|
|
11
|
+
import { MARKDOWN_HIGHLIGHT_GROUPS, type MarkdownHighlightGroup } from "./markdown-highlight";
|
|
12
|
+
|
|
13
|
+
/** A markdown SyntaxStyle plus the lookup from token group to its native styleId. */
|
|
14
|
+
export interface MarkdownEditorStyle {
|
|
15
|
+
style: SyntaxStyle;
|
|
16
|
+
styleIdFor: (group: MarkdownHighlightGroup) => number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function markdownGroupStyles(theme: Theme): Record<MarkdownHighlightGroup, StyleDefinitionInput> {
|
|
20
|
+
return {
|
|
21
|
+
heading: { bold: true },
|
|
22
|
+
marker: { fg: theme.textDim },
|
|
23
|
+
link: { fg: theme.blue, underline: true },
|
|
24
|
+
code: { fg: theme.textDim },
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const markdownEditorStyleCache = new WeakMap<Theme, MarkdownEditorStyle>();
|
|
29
|
+
|
|
30
|
+
/** Build (or reuse) the markdown editor's SyntaxStyle for a theme, with a group-to-styleId lookup. */
|
|
31
|
+
export function markdownEditorStyle(theme: Theme): MarkdownEditorStyle {
|
|
32
|
+
const cached = markdownEditorStyleCache.get(theme);
|
|
33
|
+
|
|
34
|
+
if (cached) return cached;
|
|
35
|
+
const groupStyles = markdownGroupStyles(theme);
|
|
36
|
+
const style = SyntaxStyle.fromStyles(groupStyles);
|
|
37
|
+
const styleIds = new Map<MarkdownHighlightGroup, number>();
|
|
38
|
+
|
|
39
|
+
for (const group of MARKDOWN_HIGHLIGHT_GROUPS) {
|
|
40
|
+
styleIds.set(group, style.getStyleId(group) ?? 0);
|
|
41
|
+
}
|
|
42
|
+
const entry: MarkdownEditorStyle = {
|
|
43
|
+
style,
|
|
44
|
+
styleIdFor: (group) => styleIds.get(group) ?? 0,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
markdownEditorStyleCache.set(theme, entry);
|
|
48
|
+
|
|
49
|
+
return entry;
|
|
50
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
layoutFrontmatterGrid,
|
|
4
|
+
layoutMarkdownTable,
|
|
5
|
+
parseFrontmatterRows,
|
|
6
|
+
parseMarkdownTable,
|
|
7
|
+
} from "./markdown-grid";
|
|
8
|
+
|
|
9
|
+
/** The plain text of a grid line, segments concatenated. */
|
|
10
|
+
function lineText(line: { text: string }[]): string {
|
|
11
|
+
return line.map((segment) => segment.text).join("");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("parseMarkdownTable", () => {
|
|
15
|
+
test("reads header cells, alignments, and body rows", () => {
|
|
16
|
+
const table = parseMarkdownTable("| Name | Size |\n| :--- | ---: |\n| a | 1 |\n| bb | 22 |");
|
|
17
|
+
|
|
18
|
+
expect(table.header).toEqual(["Name", "Size"]);
|
|
19
|
+
expect(table.aligns).toEqual(["left", "right"]);
|
|
20
|
+
expect(table.rows).toEqual([
|
|
21
|
+
["a", "1"],
|
|
22
|
+
["bb", "22"],
|
|
23
|
+
]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("center alignment reads from a :---: delimiter", () => {
|
|
27
|
+
expect(parseMarkdownTable("| a |\n| :---: |\n| x |").aligns).toEqual(["center"]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("an escaped pipe stays inside its cell instead of splitting the column", () => {
|
|
31
|
+
const table = parseMarkdownTable("| Name | Note |\n| --- | --- |\n| a | b \\| c |");
|
|
32
|
+
|
|
33
|
+
expect(table.rows).toEqual([["a", "b | c"]]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("a blank cell between border pipes survives the border strip", () => {
|
|
37
|
+
expect(parseMarkdownTable("| | b |\n| --- | --- |\n| x | y |").header).toEqual(["", "b"]);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("layoutMarkdownTable", () => {
|
|
42
|
+
test("bolds the header, rules under it, and right-aligns a numeric column", () => {
|
|
43
|
+
const lines = layoutMarkdownTable(
|
|
44
|
+
parseMarkdownTable("| Name | Size |\n| --- | ---: |\n| a | 1 |"),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
expect(lines[0]![0]!.bold).toBe(true);
|
|
48
|
+
expect(lineText(lines[0]!)).toBe("Name Size");
|
|
49
|
+
expect(lines[1]![0]!.dim).toBe(true);
|
|
50
|
+
expect(lineText(lines[1]!)).toBe("─".repeat("Name Size".length));
|
|
51
|
+
// "a" padded to the "Name" column, "1" right-aligned under "Size"
|
|
52
|
+
expect(lineText(lines[2]!)).toBe("a" + " ".repeat(8) + "1");
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("parseFrontmatterRows", () => {
|
|
57
|
+
test("splits top-level key: value pairs", () => {
|
|
58
|
+
expect(parseFrontmatterRows("title: Plan\nowner: me")).toEqual([
|
|
59
|
+
{ key: "title", value: "Plan" },
|
|
60
|
+
{ key: "owner", value: "me" },
|
|
61
|
+
]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("folds an indented continuation into the row above", () => {
|
|
65
|
+
expect(parseFrontmatterRows("tags:\n - one\n - two")).toEqual([
|
|
66
|
+
{ key: "tags", value: "- one\n- two" },
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("layoutFrontmatterGrid", () => {
|
|
72
|
+
test("draws a full border with a bold key and a rule between rows", () => {
|
|
73
|
+
const lines = layoutFrontmatterGrid(
|
|
74
|
+
[
|
|
75
|
+
{ key: "title", value: "Plan" },
|
|
76
|
+
{ key: "owner", value: "me" },
|
|
77
|
+
],
|
|
78
|
+
40,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
expect(lineText(lines[0]!).startsWith("┌")).toBe(true);
|
|
82
|
+
expect(lineText(lines.at(-1)!).startsWith("└")).toBe(true);
|
|
83
|
+
// the key cell is bold, the border pieces are dim
|
|
84
|
+
const firstRow = lines[1]!;
|
|
85
|
+
|
|
86
|
+
expect(firstRow.find((segment) => segment.text.includes("title"))!.bold).toBe(true);
|
|
87
|
+
expect(firstRow[0]!.dim).toBe(true);
|
|
88
|
+
// a divider separates the two rows
|
|
89
|
+
expect(lines.some((line) => lineText(line).startsWith("├"))).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("wraps a long value across cell lines, keeping the key on the first", () => {
|
|
93
|
+
const lines = layoutFrontmatterGrid([{ key: "desc", value: "one two three four five" }], 24);
|
|
94
|
+
const bodyLines = lines.filter((line) => lineText(line).startsWith("│"));
|
|
95
|
+
|
|
96
|
+
expect(bodyLines.length).toBeGreaterThan(1);
|
|
97
|
+
expect(bodyLines[0]!.some((segment) => segment.text.includes("desc"))).toBe(true);
|
|
98
|
+
// the continuation line keeps the key column blank
|
|
99
|
+
expect(bodyLines[1]!.some((segment) => segment.text.includes("desc"))).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
});
|