@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,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code highlighting tokens: the theme's named colors projected onto tree-sitter
|
|
3
|
+
* capture names. One group -> color map is the single source of truth, feeding
|
|
4
|
+
* both the native code renderable (via a cached SyntaxStyle per theme) and the
|
|
5
|
+
* diff sheet's per-row spans. Unknown languages simply render unstyled -
|
|
6
|
+
* highlighting is an enhancement, never a blocker.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
SyntaxStyle,
|
|
11
|
+
infoStringToFiletype,
|
|
12
|
+
extensionToFiletype,
|
|
13
|
+
basenameToFiletype,
|
|
14
|
+
} from "@opentui/core";
|
|
15
|
+
import type { Theme } from "../theme";
|
|
16
|
+
|
|
17
|
+
/** Markdown fence info -> tree-sitter filetype, with the aliases we accept. */
|
|
18
|
+
export function filetypeFor(language?: string): string | undefined {
|
|
19
|
+
const info = (language ?? "").toLowerCase();
|
|
20
|
+
|
|
21
|
+
if (!info) return undefined;
|
|
22
|
+
const aliases: SyntaxAliases = {
|
|
23
|
+
ts: "typescript",
|
|
24
|
+
js: "javascript",
|
|
25
|
+
py: "python",
|
|
26
|
+
rb: "ruby",
|
|
27
|
+
rs: "rust",
|
|
28
|
+
sh: "bash",
|
|
29
|
+
shell: "bash",
|
|
30
|
+
zsh: "bash",
|
|
31
|
+
yml: "yaml",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return infoStringToFiletype(aliases[info] ?? info) ?? aliases[info] ?? info;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A diff/code file path -> tree-sitter filetype, by basename then extension. */
|
|
38
|
+
export function filetypeForPath(path: string): string | undefined {
|
|
39
|
+
const basename = path.split("/").pop() ?? path;
|
|
40
|
+
const byBasename = basenameToFiletype.get(basename);
|
|
41
|
+
|
|
42
|
+
if (byBasename) return byBasename;
|
|
43
|
+
const dotIndex = basename.lastIndexOf(".");
|
|
44
|
+
|
|
45
|
+
if (dotIndex <= 0) return undefined;
|
|
46
|
+
|
|
47
|
+
return extensionToFiletype.get(basename.slice(dotIndex + 1).toLowerCase());
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface SyntaxAliases {
|
|
51
|
+
[alias: string]: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface SyntaxGroupStyle {
|
|
55
|
+
fg: string;
|
|
56
|
+
italic?: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface SyntaxGroupStyles {
|
|
60
|
+
[group: string]: SyntaxGroupStyle;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The one group -> style map, keyed by tree-sitter capture name. */
|
|
64
|
+
function syntaxGroupStyles(theme: Theme): SyntaxGroupStyles {
|
|
65
|
+
return {
|
|
66
|
+
default: { fg: theme.textMuted },
|
|
67
|
+
comment: { fg: theme.textDim, italic: true },
|
|
68
|
+
string: { fg: theme.green },
|
|
69
|
+
number: { fg: theme.blue },
|
|
70
|
+
constant: { fg: theme.blue },
|
|
71
|
+
boolean: { fg: theme.blue },
|
|
72
|
+
keyword: { fg: theme.accent },
|
|
73
|
+
operator: { fg: theme.textDim },
|
|
74
|
+
punctuation: { fg: theme.textDim },
|
|
75
|
+
function: { fg: theme.text },
|
|
76
|
+
method: { fg: theme.text },
|
|
77
|
+
type: { fg: theme.blue },
|
|
78
|
+
constructor: { fg: theme.blue },
|
|
79
|
+
variable: { fg: theme.textMuted },
|
|
80
|
+
property: { fg: theme.textMuted },
|
|
81
|
+
parameter: { fg: theme.textMuted },
|
|
82
|
+
tag: { fg: theme.accent },
|
|
83
|
+
attribute: { fg: theme.blue },
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const syntaxStyleCache = new WeakMap<Theme, SyntaxStyle>();
|
|
88
|
+
|
|
89
|
+
export function syntaxStyleFor(theme: Theme): SyntaxStyle {
|
|
90
|
+
const cached = syntaxStyleCache.get(theme);
|
|
91
|
+
|
|
92
|
+
if (cached) return cached;
|
|
93
|
+
const style = SyntaxStyle.fromStyles(syntaxGroupStyles(theme));
|
|
94
|
+
|
|
95
|
+
syntaxStyleCache.set(theme, style);
|
|
96
|
+
|
|
97
|
+
return style;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** A one-group syntax style plus the styleId that paints a "/skill" reference in the accent color. */
|
|
101
|
+
export interface ReferenceStyle {
|
|
102
|
+
style: SyntaxStyle;
|
|
103
|
+
styleId: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const referenceStyleCache = new WeakMap<Theme, ReferenceStyle>();
|
|
107
|
+
|
|
108
|
+
export function referenceStyleFor(theme: Theme): ReferenceStyle {
|
|
109
|
+
const cached = referenceStyleCache.get(theme);
|
|
110
|
+
|
|
111
|
+
if (cached) return cached;
|
|
112
|
+
const entry: ReferenceStyle = {
|
|
113
|
+
style: SyntaxStyle.fromStyles({ reference: { fg: theme.accent } }),
|
|
114
|
+
styleId: 0,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
entry.styleId = entry.style.getStyleId("reference") ?? 0;
|
|
118
|
+
referenceStyleCache.set(theme, entry);
|
|
119
|
+
|
|
120
|
+
return entry;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The foreground for a tree-sitter capture group; undefined when unstyled. */
|
|
124
|
+
export function colorForSyntaxGroup(group: string, theme: Theme): string | undefined {
|
|
125
|
+
const styles = syntaxGroupStyles(theme);
|
|
126
|
+
const direct = styles[group];
|
|
127
|
+
|
|
128
|
+
if (direct) return direct.fg;
|
|
129
|
+
// capture names are dotted (e.g. "keyword.control"); match the broadest prefix
|
|
130
|
+
const base = group.split(".")[0]!;
|
|
131
|
+
|
|
132
|
+
return styles[base]?.fg;
|
|
133
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** The embedded terminal renderable runs a real PTY child through Ghostty's VT and paints its screen; a live `printf` child must show up in the OpenTUI frame. Skipped where no prebuilt libghostty-vt ships for the platform. */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, test } from "bun:test";
|
|
4
|
+
import React from "react";
|
|
5
|
+
import { testRender } from "@opentui/react/test-utils";
|
|
6
|
+
import { waitForText } from "../test-support";
|
|
7
|
+
import {
|
|
8
|
+
embeddedTerminalAvailable,
|
|
9
|
+
registerTerminalPane,
|
|
10
|
+
TerminalPaneRenderable,
|
|
11
|
+
} from "./terminal-pane";
|
|
12
|
+
|
|
13
|
+
registerTerminalPane();
|
|
14
|
+
|
|
15
|
+
describe("TerminalPaneRenderable", () => {
|
|
16
|
+
test.skipIf(!embeddedTerminalAvailable())(
|
|
17
|
+
"paints a live child process's output into the box",
|
|
18
|
+
async () => {
|
|
19
|
+
// Arrange / Act - a child that prints a known line
|
|
20
|
+
const setup = await testRender(
|
|
21
|
+
React.createElement("terminalPane", {
|
|
22
|
+
command: "/bin/sh",
|
|
23
|
+
args: ["-c", "printf 'HELLO-EMBED\\n'; sleep 5"],
|
|
24
|
+
style: { width: 40, height: 6 },
|
|
25
|
+
}),
|
|
26
|
+
{ width: 40, height: 6 },
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
// Assert - the child's output reaches the painted frame via Ghostty
|
|
30
|
+
await waitForText(setup, "HELLO-EMBED");
|
|
31
|
+
setup.renderer.destroy();
|
|
32
|
+
},
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
test("exposes availability + the renderable class", () => {
|
|
36
|
+
// Assert - the module loads and reports platform support honestly
|
|
37
|
+
expect([true, false]).toContain(embeddedTerminalAvailable());
|
|
38
|
+
expect(TerminalPaneRenderable).toBeDefined();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An OpenTUI renderable that runs a real child process (a shell, cc, pi, codex)
|
|
3
|
+
* on a PTY and paints its live screen into the box - the terminal-in-the-rail
|
|
4
|
+
* primitive. It wires the forkpty shim (child + tty, ../pty) to a Ghostty VT emulator
|
|
5
|
+
* (ghostty-terminal.ts) and blits the emulator's cell grid every frame via
|
|
6
|
+
* OptimizedBuffer.setCell. Register once with `registerTerminalPane`, then use
|
|
7
|
+
* `<terminalPane command="claude" ... />` in the OpenTUI React tree.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Renderable, RGBA, createTextAttributes, type RenderContext } from "@opentui/core";
|
|
11
|
+
import type { OptimizedBuffer } from "@opentui/core";
|
|
12
|
+
import type { RenderableOptions } from "@opentui/core";
|
|
13
|
+
import { extend } from "@opentui/react";
|
|
14
|
+
import { spawn, ptyAvailable, type IPty } from "../pty";
|
|
15
|
+
import {
|
|
16
|
+
loadGhosttyTerminals,
|
|
17
|
+
type GhosttyColor,
|
|
18
|
+
type GhosttyTerminal,
|
|
19
|
+
type GhosttyTerminalFactory,
|
|
20
|
+
} from "../ghostty-terminal";
|
|
21
|
+
|
|
22
|
+
/** One shared VT library load; null when this platform ships no prebuilt dylib. */
|
|
23
|
+
let ghosttyFactory: GhosttyTerminalFactory | null | undefined;
|
|
24
|
+
|
|
25
|
+
function factory(): GhosttyTerminalFactory | null {
|
|
26
|
+
if (ghosttyFactory === undefined) ghosttyFactory = loadGhosttyTerminals();
|
|
27
|
+
|
|
28
|
+
return ghosttyFactory;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Whether the embedded terminal can run here: both native shims ship for this
|
|
32
|
+
* platform (the Ghostty VT renderer and the forkpty PTY). */
|
|
33
|
+
export function embeddedTerminalAvailable(): boolean {
|
|
34
|
+
return factory() !== null && ptyAvailable();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Props for `<terminalPane>`: the child to run plus its cwd/env and a plan-context seed. */
|
|
38
|
+
export interface TerminalPaneOptions extends RenderableOptions {
|
|
39
|
+
/** The program to run, e.g. "claude" / "pi" / "codex" / a shell. */
|
|
40
|
+
command?: string;
|
|
41
|
+
args?: string[];
|
|
42
|
+
cwd?: string;
|
|
43
|
+
env?: Record<string, string>;
|
|
44
|
+
/** Text left unsubmitted in the child after it starts (plan-context seed). */
|
|
45
|
+
seedText?: string;
|
|
46
|
+
/** Fired when the child process exits. */
|
|
47
|
+
onExit?: (exitCode: number) => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function terminalEnvironment(env: NodeJS.ProcessEnv) {
|
|
51
|
+
return Object.fromEntries(
|
|
52
|
+
Object.entries(env).filter((entry): entry is [string, string] => entry[1] !== undefined),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const encoder = new TextEncoder();
|
|
57
|
+
const DEFAULT_FG = RGBA.fromInts(208, 208, 208, 255);
|
|
58
|
+
const TRANSPARENT = RGBA.fromValues(0, 0, 0, 0);
|
|
59
|
+
/** Glyph color under the block cursor (dark ink on the light cursor block). */
|
|
60
|
+
const CURSOR_INK = RGBA.fromInts(20, 20, 24, 255);
|
|
61
|
+
|
|
62
|
+
/** Runs a PTY child and paints its Ghostty-rendered screen into this box. */
|
|
63
|
+
export class TerminalPaneRenderable extends Renderable {
|
|
64
|
+
private pty: IPty | null = null;
|
|
65
|
+
private vt: GhosttyTerminal | null = null;
|
|
66
|
+
private cols = 0;
|
|
67
|
+
private rows = 0;
|
|
68
|
+
// The seed is held until the child's first output, so it lands in a ready
|
|
69
|
+
// prompt rather than racing the harness's startup (readline/TUI not up yet).
|
|
70
|
+
private pendingSeed: string | undefined;
|
|
71
|
+
private readonly opts: TerminalPaneOptions;
|
|
72
|
+
|
|
73
|
+
constructor(ctx: RenderContext, options: TerminalPaneOptions) {
|
|
74
|
+
super(ctx, options);
|
|
75
|
+
this.opts = options;
|
|
76
|
+
this.focusable = true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Start the child + emulator once real layout dimensions are known. */
|
|
80
|
+
private start(cols: number, rows: number): void {
|
|
81
|
+
const terminals = factory();
|
|
82
|
+
|
|
83
|
+
if (!terminals) return;
|
|
84
|
+
const vt = terminals.create(cols, rows);
|
|
85
|
+
|
|
86
|
+
if (!vt) return; // no emulator, no visible screen - do not spawn a blind child
|
|
87
|
+
this.vt = vt;
|
|
88
|
+
this.cols = cols;
|
|
89
|
+
this.rows = rows;
|
|
90
|
+
this.pendingSeed = this.opts.seedText;
|
|
91
|
+
this.pty = spawn(this.opts.command ?? process.env.SHELL ?? "/bin/sh", this.opts.args ?? [], {
|
|
92
|
+
name: "xterm-256color",
|
|
93
|
+
cols,
|
|
94
|
+
rows,
|
|
95
|
+
cwd: this.opts.cwd ?? process.cwd(),
|
|
96
|
+
env: this.opts.env ?? terminalEnvironment(process.env),
|
|
97
|
+
});
|
|
98
|
+
this.pty.onData((data) => {
|
|
99
|
+
// the shim streams a UTF-8-decoded string (split multibyte is handled); a
|
|
100
|
+
// rare non-UTF-8 byte arrives as U+FFFD - acceptable for agent TUIs.
|
|
101
|
+
this.vt?.write(encoder.encode(data));
|
|
102
|
+
if (this.pendingSeed !== undefined) {
|
|
103
|
+
this.pty?.write(this.pendingSeed);
|
|
104
|
+
this.pendingSeed = undefined;
|
|
105
|
+
}
|
|
106
|
+
this.requestRender();
|
|
107
|
+
});
|
|
108
|
+
this.pty.onExit(({ exitCode }) => this.opts.onExit?.(exitCode));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Send raw bytes to the child (a key's escape sequence, or pasted text). */
|
|
112
|
+
write(data: string): void {
|
|
113
|
+
this.pty?.write(data);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
protected onResize(width: number, height: number): void {
|
|
117
|
+
if (width <= 0 || height <= 0) return;
|
|
118
|
+
if (!this.pty) {
|
|
119
|
+
this.start(width, height);
|
|
120
|
+
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (width === this.cols && height === this.rows) return;
|
|
124
|
+
this.cols = width;
|
|
125
|
+
this.rows = height;
|
|
126
|
+
this.vt?.resize(width, height);
|
|
127
|
+
this.pty.resize(width, height);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
protected renderSelf(buffer: OptimizedBuffer): void {
|
|
131
|
+
if (!this.vt) return;
|
|
132
|
+
for (let y = 0; y < this.height; y++) {
|
|
133
|
+
for (let x = 0; x < this.width; x++) {
|
|
134
|
+
const cell = this.vt.readCell(x, y);
|
|
135
|
+
|
|
136
|
+
// Skip an empty normal-width cell; the buffer already starts blank.
|
|
137
|
+
if (cell && cell.width === 0 && cell.codepoint === 0) continue;
|
|
138
|
+
const char = cell && cell.codepoint ? String.fromCodePoint(cell.codepoint) : " ";
|
|
139
|
+
let fg = cell ? resolveColor(cell.fg, DEFAULT_FG) : DEFAULT_FG;
|
|
140
|
+
let bg = cell ? resolveColor(cell.bg, TRANSPARENT) : TRANSPARENT;
|
|
141
|
+
|
|
142
|
+
if (cell?.inverse) [fg, bg] = [bg, fg];
|
|
143
|
+
const attributes = cell
|
|
144
|
+
? createTextAttributes({
|
|
145
|
+
bold: cell.bold,
|
|
146
|
+
italic: cell.italic,
|
|
147
|
+
underline: cell.underline,
|
|
148
|
+
dim: cell.faint,
|
|
149
|
+
strikethrough: cell.strikethrough,
|
|
150
|
+
})
|
|
151
|
+
: 0;
|
|
152
|
+
|
|
153
|
+
buffer.setCell(this.x + x, this.y + y, char, fg, bg, attributes);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// cursor is active-area coords; equal to viewport at the bottom (the live
|
|
157
|
+
// typing case). It can drift if the child scrolls the viewport back.
|
|
158
|
+
const cursor = this.vt.readCursor();
|
|
159
|
+
|
|
160
|
+
if (cursor.visible && cursor.x < this.width && cursor.y < this.height) {
|
|
161
|
+
const under = this.vt.readCell(cursor.x, cursor.y);
|
|
162
|
+
const char = under && under.codepoint ? String.fromCodePoint(under.codepoint) : " ";
|
|
163
|
+
|
|
164
|
+
// a block cursor: paint the cell with fg/bg swapped
|
|
165
|
+
buffer.setCell(this.x + cursor.x, this.y + cursor.y, char, CURSOR_INK, DEFAULT_FG, 0);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Kill the child and free the VT. Idempotent - the detach path calls this
|
|
170
|
+
* directly because the React reconciler removes a child without destroying it. */
|
|
171
|
+
shutdown(): void {
|
|
172
|
+
this.pty?.kill();
|
|
173
|
+
this.vt?.free();
|
|
174
|
+
this.pty = null;
|
|
175
|
+
this.vt = null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
protected destroySelf(): void {
|
|
179
|
+
this.shutdown();
|
|
180
|
+
super.destroySelf();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Map a Ghostty cell color to an OpenTUI RGBA, using `fallback` for the terminal default. */
|
|
185
|
+
function resolveColor(color: GhosttyColor, fallback: RGBA): RGBA {
|
|
186
|
+
if (color.kind === "rgb") return RGBA.fromInts(color.r, color.g, color.b, 255);
|
|
187
|
+
if (color.kind === "palette") return RGBA.fromIndex(color.index);
|
|
188
|
+
|
|
189
|
+
return fallback;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let registered = false;
|
|
193
|
+
|
|
194
|
+
/** Register `<terminalPane>` with the OpenTUI React reconciler (idempotent). */
|
|
195
|
+
export function registerTerminalPane(): void {
|
|
196
|
+
if (registered) return;
|
|
197
|
+
registered = true;
|
|
198
|
+
extend({ terminalPane: TerminalPaneRenderable });
|
|
199
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK, type Theme } from "../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "./story";
|
|
4
|
+
import { ThemeProvider } from "./theme-context";
|
|
5
|
+
import { Card } from "./primitives/Card";
|
|
6
|
+
|
|
7
|
+
export const meta: StoryMeta = { title: "Foundations/Theme" };
|
|
8
|
+
|
|
9
|
+
/** A high-contrast variant to prove tokens flow through the provider. */
|
|
10
|
+
const HIGH_CONTRAST: Theme = { ...DARK, accent: "#00ffff", elevated: "#000000", border: "#ffffff" };
|
|
11
|
+
|
|
12
|
+
export const ContextDefault: Story = {
|
|
13
|
+
render: () => (
|
|
14
|
+
<Card title=" default dark theme " contentRows={1}>
|
|
15
|
+
<text>no provider needed</text>
|
|
16
|
+
</Card>
|
|
17
|
+
),
|
|
18
|
+
expectedColors: [DARK.elevated],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const ProviderSwap: Story = {
|
|
22
|
+
render: () => (
|
|
23
|
+
<ThemeProvider theme={HIGH_CONTRAST}>
|
|
24
|
+
<Card title=" provider theme " contentRows={1} borderColor={HIGH_CONTRAST.accent}>
|
|
25
|
+
<text>tokens come from the provider</text>
|
|
26
|
+
</Card>
|
|
27
|
+
</ThemeProvider>
|
|
28
|
+
),
|
|
29
|
+
expectedColors: ["#00ffff", "#000000"],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const PropOverride: Story = {
|
|
33
|
+
render: () => (
|
|
34
|
+
<ThemeProvider theme={DARK}>
|
|
35
|
+
<Card
|
|
36
|
+
title=" prop-overridden theme "
|
|
37
|
+
contentRows={1}
|
|
38
|
+
theme={HIGH_CONTRAST}
|
|
39
|
+
borderColor={HIGH_CONTRAST.accent}
|
|
40
|
+
>
|
|
41
|
+
<text>the theme prop beats the context</text>
|
|
42
|
+
</Card>
|
|
43
|
+
</ThemeProvider>
|
|
44
|
+
),
|
|
45
|
+
expectedColors: ["#00ffff"],
|
|
46
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider theming for the component system. The context default is the
|
|
3
|
+
* built-in dark theme, so embedding a component (or rendering a story) needs
|
|
4
|
+
* no provider at all. One ThemeProvider swap switches the live theme; every
|
|
5
|
+
* component also accepts an optional `theme` prop that overrides the context
|
|
6
|
+
* for explicit control. Components consume named tokens only - a hardcoded
|
|
7
|
+
* color inside components/ is a review rejection.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React, { createContext, useContext } from "react";
|
|
11
|
+
import { DARK, type Theme } from "../theme";
|
|
12
|
+
|
|
13
|
+
const ThemeContext = createContext<Theme>(DARK);
|
|
14
|
+
|
|
15
|
+
export function ThemeProvider({
|
|
16
|
+
theme,
|
|
17
|
+
children,
|
|
18
|
+
}: {
|
|
19
|
+
theme: Theme;
|
|
20
|
+
children: React.ReactNode;
|
|
21
|
+
}): React.ReactNode {
|
|
22
|
+
return <ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function useTheme(): Theme {
|
|
26
|
+
return useContext(ThemeContext);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Token resolution for a component: the explicit `theme` prop wins over the
|
|
31
|
+
* provider context, which defaults to the built-in dark theme.
|
|
32
|
+
*/
|
|
33
|
+
export function useComponentTheme(override?: Theme): Theme {
|
|
34
|
+
const contextTheme = useContext(ThemeContext);
|
|
35
|
+
|
|
36
|
+
return override ?? contextTheme;
|
|
37
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { Thread, WorkspaceKey } from "@cueloop/schema";
|
|
3
|
+
import { groupInbox, projectName } from "./thread-tree";
|
|
4
|
+
|
|
5
|
+
function session(id: string, title: string, rootCommit?: string, remote?: string): Thread {
|
|
6
|
+
const workspace: WorkspaceKey = { repoRoot: `/home/dev/${id}-checkout`, branch: "main" };
|
|
7
|
+
if (rootCommit !== undefined) workspace.rootCommit = rootCommit;
|
|
8
|
+
if (remote !== undefined) workspace.remote = remote;
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
schemaVersion: "1",
|
|
12
|
+
id,
|
|
13
|
+
workspace,
|
|
14
|
+
artifact: { type: "plan", content: `# ${title}\n`, meta: { title } },
|
|
15
|
+
revisions: [],
|
|
16
|
+
annotations: [],
|
|
17
|
+
message: null,
|
|
18
|
+
status: "pending",
|
|
19
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("projectName", () => {
|
|
24
|
+
test("prefers the remote basename and strips the .git suffix", () => {
|
|
25
|
+
// Arrange
|
|
26
|
+
const workspace: WorkspaceKey = {
|
|
27
|
+
repoRoot: "/tmp/whatever",
|
|
28
|
+
branch: "main",
|
|
29
|
+
remote: "git@github.com:acme/widget.git",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Act
|
|
33
|
+
const name = projectName(workspace);
|
|
34
|
+
|
|
35
|
+
// Assert
|
|
36
|
+
expect(name).toBe("widget");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("falls back to the working-tree folder name when there is no remote", () => {
|
|
40
|
+
// Arrange
|
|
41
|
+
const workspace: WorkspaceKey = { repoRoot: "/home/dev/cueloop", branch: "main" };
|
|
42
|
+
|
|
43
|
+
// Act
|
|
44
|
+
const name = projectName(workspace);
|
|
45
|
+
|
|
46
|
+
// Assert
|
|
47
|
+
expect(name).toBe("cueloop");
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("groupInbox", () => {
|
|
52
|
+
test("splits repo-bound threads into Projects and the rest into Threads", () => {
|
|
53
|
+
// Arrange
|
|
54
|
+
const sessions = [
|
|
55
|
+
session("a", "Read the repo", "root-1", "git@github.com:acme/widget.git"),
|
|
56
|
+
session("b", "A loose idea"),
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
// Act
|
|
60
|
+
const { rows, ordered } = groupInbox(sessions);
|
|
61
|
+
|
|
62
|
+
// Assert
|
|
63
|
+
const labels = rows.map((row) => `${row.kind}:${"label" in row ? row.label : row.id}`);
|
|
64
|
+
expect(labels).toEqual([
|
|
65
|
+
"section:Projects",
|
|
66
|
+
"project:widget",
|
|
67
|
+
"thread:a",
|
|
68
|
+
"section:Threads",
|
|
69
|
+
"thread:b",
|
|
70
|
+
]);
|
|
71
|
+
expect(ordered.map((s) => s.id)).toEqual(["a", "b"]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("collapses threads sharing a root commit into one project", () => {
|
|
75
|
+
// Arrange
|
|
76
|
+
const sessions = [
|
|
77
|
+
session("a", "First", "root-1", "git@github.com:acme/widget.git"),
|
|
78
|
+
session("b", "Second", "root-1", "git@github.com:acme/widget.git"),
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
// Act
|
|
82
|
+
const { rows, ordered } = groupInbox(sessions);
|
|
83
|
+
|
|
84
|
+
// Assert
|
|
85
|
+
const projectRows = rows.filter((row) => row.kind === "project");
|
|
86
|
+
expect(projectRows).toHaveLength(1);
|
|
87
|
+
expect(ordered).toHaveLength(2);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("selectionIndex on each thread matches its position in ordered", () => {
|
|
91
|
+
// Arrange
|
|
92
|
+
const sessions = [session("a", "One", "root-1"), session("b", "Two")];
|
|
93
|
+
|
|
94
|
+
// Act
|
|
95
|
+
const { rows, ordered } = groupInbox(sessions);
|
|
96
|
+
|
|
97
|
+
// Assert
|
|
98
|
+
for (const row of rows) {
|
|
99
|
+
if (row.kind === "thread") expect(ordered[row.selectionIndex]!.id).toBe(row.id);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("omits the Projects section when nothing is repo-bound", () => {
|
|
104
|
+
// Arrange
|
|
105
|
+
const sessions = [session("a", "Loose one"), session("b", "Loose two")];
|
|
106
|
+
|
|
107
|
+
// Act
|
|
108
|
+
const { rows } = groupInbox(sessions);
|
|
109
|
+
|
|
110
|
+
// Assert
|
|
111
|
+
expect(rows.some((row) => row.kind === "section" && row.label === "Projects")).toBe(false);
|
|
112
|
+
expect(rows.some((row) => row.kind === "section" && row.label === "Threads")).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("a pinned thread lifts into a Starred section at the top, out of its normal group", () => {
|
|
116
|
+
// Arrange - one repo-bound thread, one standalone; pin the repo-bound one
|
|
117
|
+
const sessions = [session("a", "Pinned one", "root-1"), session("b", "Loose two")];
|
|
118
|
+
|
|
119
|
+
// Act
|
|
120
|
+
const { rows, ordered } = groupInbox(sessions, new Set(["a"]));
|
|
121
|
+
|
|
122
|
+
// Assert - Starred is the first section and holds thread a; a is gone from Projects
|
|
123
|
+
const sections = rows
|
|
124
|
+
.filter((row) => row.kind === "section")
|
|
125
|
+
.map((row) => (row.kind === "section" ? row.label : ""));
|
|
126
|
+
|
|
127
|
+
expect(sections[0]).toBe("Starred");
|
|
128
|
+
expect(sections).not.toContain("Projects"); // its only member was pinned away
|
|
129
|
+
expect(ordered[0]!.id).toBe("a"); // the cursor walks pinned threads first
|
|
130
|
+
const pinnedThreadIds = rows.filter((row) => row.kind === "thread").map((row) => row.id);
|
|
131
|
+
|
|
132
|
+
expect(pinnedThreadIds).toEqual(["a", "b"]);
|
|
133
|
+
});
|
|
134
|
+
});
|