@cueloop/client 0.1.0-alpha.7 → 0.1.0-alpha.70
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 +39 -32
- package/src/App.appearance.test.tsx +53 -0
- package/src/App.autoclose.test.tsx +76 -50
- package/src/App.diff.test.tsx +156 -42
- package/src/App.export.test.tsx +58 -40
- package/src/App.plan-review.test.tsx +363 -0
- package/src/App.prototype-markdown.test.tsx +92 -0
- package/src/App.prototype.test.tsx +176 -0
- package/src/App.readonly.test.tsx +128 -67
- package/src/App.rendering.test.tsx +61 -35
- package/src/App.submit-confirm.test.tsx +191 -0
- package/src/App.test.tsx +310 -114
- package/src/App.toast.test.tsx +89 -0
- package/src/App.tsx +1039 -799
- package/src/App.walk.test.tsx +298 -0
- package/src/App.workbench.test.tsx +461 -0
- package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
- package/src/annotation-palette.ts +61 -0
- package/src/app-key-state.ts +61 -0
- package/src/app-screens.tsx +432 -0
- package/src/app-view-model.ts +159 -0
- package/src/attribution.test.ts +31 -0
- package/src/attribution.ts +42 -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 +126 -0
- package/src/components/AnnotationCards.stories.tsx +87 -0
- package/src/components/AnnotationCards.tsx +303 -0
- package/src/components/AppHeader.stories.tsx +50 -0
- package/src/components/AppHeader.tsx +83 -0
- package/src/components/AppShell.stories.tsx +89 -0
- package/src/components/AppShell.tsx +286 -0
- package/src/components/AppShell.zoom.test.tsx +94 -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 +39 -0
- package/src/components/ChangesColumn.tsx +171 -0
- package/src/components/CodeBlock.stories.tsx +42 -0
- package/src/components/CodeBlock.tsx +84 -0
- package/src/components/CompletionOverlay.stories.tsx +23 -0
- package/src/components/CompletionOverlay.tsx +60 -0
- package/src/components/ConfirmCard.stories.tsx +35 -0
- package/src/components/ConfirmCard.tsx +128 -0
- package/src/components/ConfirmDialog.stories.tsx +21 -0
- package/src/components/ConfirmDialog.tsx +66 -0
- package/src/components/DiffContentView.scroll.test.tsx +121 -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 +859 -0
- package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
- package/src/components/DiscussionMarkerRail.tsx +181 -0
- package/src/components/EditorGrid.stories.tsx +59 -0
- package/src/components/EditorGrid.tabs.test.tsx +118 -0
- package/src/components/EditorGrid.tsx +368 -0
- package/src/components/EditorGrid.zoom.test.tsx +68 -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/KeybindsDialog.stories.tsx +31 -0
- package/src/components/KeybindsDialog.tsx +103 -0
- package/src/components/MenuBar.stories.tsx +28 -0
- package/src/components/MenuBar.tsx +44 -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 +95 -0
- package/src/components/ProjectTreeView.stories.tsx +25 -0
- package/src/components/ProjectTreeView.test.tsx +32 -0
- package/src/components/ProjectTreeView.tsx +83 -0
- package/src/components/PromptDialog.stories.tsx +36 -0
- package/src/components/PromptDialog.tsx +87 -0
- package/src/components/PrototypeContentView.stories.tsx +19 -0
- package/src/components/PrototypeContentView.tsx +446 -0
- package/src/components/RootOverlay.stories.tsx +44 -0
- package/src/components/RootOverlay.tsx +35 -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 +195 -0
- package/src/components/SettingsRows.stories.tsx +18 -0
- package/src/components/SettingsRows.tsx +112 -0
- package/src/components/ShellHeader.stories.tsx +50 -0
- package/src/components/ShellHeader.tsx +137 -0
- package/src/components/ThreadFooter.stories.tsx +30 -0
- package/src/components/ThreadFooter.test.tsx +31 -0
- package/src/components/ThreadFooter.tsx +54 -0
- package/src/components/ThreadTree.stories.tsx +46 -0
- package/src/components/ThreadTree.tsx +219 -0
- package/src/components/ThreadView.selection.test.tsx +812 -0
- package/src/components/ThreadView.slash.test.tsx +158 -0
- package/src/components/ThreadView.stories.tsx +26 -0
- package/src/components/ThreadView.tsx +330 -0
- package/src/components/ThreadsSidebar.stories.tsx +36 -0
- package/src/components/ThreadsSidebar.tsx +71 -0
- package/src/components/Toast.stories.tsx +12 -0
- package/src/components/Toast.tsx +45 -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 +152 -0
- package/src/components/WelcomePlayground.stories.tsx +12 -0
- package/src/components/WelcomePlayground.test.tsx +47 -0
- package/src/components/WelcomePlayground.tsx +140 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +1845 -0
- package/src/components/agent-launcher.stories.tsx +26 -0
- package/src/components/agent-launcher.test.tsx +50 -0
- package/src/components/agent-launcher.tsx +149 -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/fade-title.test.ts +49 -0
- package/src/components/fade-title.ts +61 -0
- package/src/components/file-tree.test.ts +60 -0
- package/src/components/file-tree.ts +112 -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 +52 -0
- package/src/components/primitives/Card.stories.tsx +25 -0
- package/src/components/primitives/Card.tsx +60 -0
- package/src/components/primitives/Dialog.stories.tsx +24 -0
- package/src/components/primitives/Dialog.tsx +66 -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/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 +96 -0
- package/src/components/primitives/frame.test.ts +12 -0
- package/src/components/primitives/frame.ts +14 -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 +54 -0
- package/src/components/quick-actions-editor.test.tsx +49 -0
- package/src/components/quick-actions-editor.tsx +150 -0
- package/src/components/session-tree.test.ts +134 -0
- package/src/components/session-tree.ts +108 -0
- package/src/components/stories-app.tsx +184 -0
- package/src/components/stories.test.tsx +66 -0
- package/src/components/story-fixtures.ts +102 -0
- package/src/components/story-tree.test.ts +100 -0
- package/src/components/story.ts +142 -0
- package/src/components/syntax-highlight.ts +110 -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/truncate-text.ts +7 -0
- package/src/config.test.ts +461 -31
- package/src/config.ts +389 -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 +165 -0
- package/src/diff-intraline.ts +146 -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/herdr.test.ts +9 -34
- package/src/herdr.ts +25 -21
- package/src/index.ts +11 -0
- package/src/integrations.test.ts +41 -0
- package/src/integrations.ts +32 -0
- package/src/intent-dispatch.test.ts +698 -0
- package/src/intent-dispatch.ts +700 -0
- package/src/key-bindings.test.ts +156 -0
- package/src/key-bindings.ts +465 -0
- package/src/keymap.test.ts +611 -0
- package/src/keymap.ts +410 -0
- package/src/kitty-image.test.ts +66 -0
- package/src/kitty-image.ts +138 -0
- package/src/mark-runs.ts +120 -0
- package/src/prototype-browser.test.ts +56 -0
- package/src/prototype-browser.ts +307 -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 +30 -9
- package/src/serve.test.ts +24 -12
- package/src/serve.ts +20 -9
- package/src/share.ts +215 -0
- package/src/skills.test.ts +74 -0
- package/src/skills.ts +68 -0
- package/src/slash-palette.test.ts +54 -0
- package/src/slash-palette.ts +104 -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 +355 -0
- package/src/theme-presets.test.ts +51 -0
- package/src/theme-presets.ts +214 -0
- package/src/theme.ts +84 -19
- package/src/thread-chords.test.ts +170 -0
- package/src/thread-chords.ts +169 -0
- package/src/thread-controller.curate.test.ts +438 -0
- package/src/thread-controller.live-diff.test.ts +199 -0
- package/src/thread-controller.share.test.ts +472 -0
- package/src/thread-controller.tree.test.ts +255 -0
- package/src/thread-controller.ts +1509 -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 +148 -0
- package/src/use-annotation-surface.tsx +851 -0
- package/src/use-changes-workbench.ts +151 -0
- package/src/use-frame-measure.ts +38 -0
- package/src/use-settings-dialog.tsx +274 -0
- package/src/use-terminal-virtualizer.ts +233 -0
- package/src/version.ts +5 -0
- package/src/view-diff.test.ts +66 -9
- package/src/view-diff.ts +231 -23
- package/src/view-plan.regression.test.ts +60 -0
- package/src/view-plan.test.ts +406 -0
- package/src/view-plan.ts +570 -0
- package/src/walk.test.ts +121 -0
- package/src/walk.ts +74 -0
- 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 -312
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import { ScrollArea } from "./components/ScrollArea";
|
|
2
|
+
import React, { useEffect, useState, type Dispatch, type SetStateAction } from "react";
|
|
3
|
+
import type { DiffFileContents, Thread, VerdictKind } from "@cueloop/schema";
|
|
4
|
+
import { returnPaneFor } from "@cueloop/schema";
|
|
5
|
+
import type { Theme } from "./theme";
|
|
6
|
+
import type { QuickAction } from "./config";
|
|
7
|
+
import type { Mode, TreeAsk } from "./intent-dispatch";
|
|
8
|
+
import type { Intent } from "./keymap";
|
|
9
|
+
import type { ReviewController, ToastState } from "./thread-controller";
|
|
10
|
+
import { noteForFile } from "./walk";
|
|
11
|
+
import type { WalkFile } from "./walk";
|
|
12
|
+
import type { CheatsheetSection } from "./key-bindings";
|
|
13
|
+
import type { SettingsCategory, SettingsValues } from "./components/SettingsDialog";
|
|
14
|
+
import type { SettingsNav } from "./use-settings-dialog";
|
|
15
|
+
import { CLIENT_VERSION } from "./version";
|
|
16
|
+
import { ThemeProvider } from "./components/theme-context";
|
|
17
|
+
import type { InboxRow } from "./components/session-tree";
|
|
18
|
+
import { SettingsDialog } from "./components/SettingsDialog";
|
|
19
|
+
import { CompletionOverlay } from "./components/CompletionOverlay";
|
|
20
|
+
import { ThreadTree } from "./components/ThreadTree";
|
|
21
|
+
import { WelcomePlayground } from "./components/WelcomePlayground";
|
|
22
|
+
import { AppShell, type ProjectPanelMode } from "./components/AppShell";
|
|
23
|
+
import { EditorGrid } from "./components/EditorGrid";
|
|
24
|
+
import { ProjectTreeView } from "./components/ProjectTreeView";
|
|
25
|
+
import { ChangesFileTree } from "./components/ChangesColumn";
|
|
26
|
+
import { BareWorkbenchFileView } from "./components/BareWorkbenchFileView";
|
|
27
|
+
import { useChangesWorkbench } from "./use-changes-workbench";
|
|
28
|
+
import { ConfirmDialog } from "./components/ConfirmDialog";
|
|
29
|
+
import { PromptDialog } from "./components/PromptDialog";
|
|
30
|
+
import { WalkWizard } from "./components/WalkWizard";
|
|
31
|
+
import { Toast } from "./components/Toast";
|
|
32
|
+
|
|
33
|
+
export function ErrorScreen({ error, theme }: { error: string; theme: Theme }): React.ReactNode {
|
|
34
|
+
return (
|
|
35
|
+
<ThemeProvider theme={theme}>
|
|
36
|
+
<text fg={theme.red}>cueloop: {error}</text>
|
|
37
|
+
</ThemeProvider>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function ConnectingScreen({ theme }: { theme: Theme }): React.ReactNode {
|
|
42
|
+
return (
|
|
43
|
+
<ThemeProvider theme={theme}>
|
|
44
|
+
<text fg={theme.textDim}>connecting to the daemon…</text>
|
|
45
|
+
</ThemeProvider>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function MenuChrome(props: {
|
|
50
|
+
menuDialog: "keybinds" | "settings" | null;
|
|
51
|
+
theme: Theme;
|
|
52
|
+
keybindsSections: CheatsheetSection[];
|
|
53
|
+
settingsCategories: SettingsCategory[];
|
|
54
|
+
settingsValues: SettingsValues;
|
|
55
|
+
settingsNav: SettingsNav;
|
|
56
|
+
onCategorySelect: (categoryId: string) => void;
|
|
57
|
+
cycleSetting: (rowKey: string) => void;
|
|
58
|
+
}): React.ReactNode {
|
|
59
|
+
const {
|
|
60
|
+
menuDialog,
|
|
61
|
+
theme,
|
|
62
|
+
keybindsSections,
|
|
63
|
+
settingsCategories,
|
|
64
|
+
settingsValues,
|
|
65
|
+
settingsNav,
|
|
66
|
+
onCategorySelect,
|
|
67
|
+
cycleSetting,
|
|
68
|
+
} = props;
|
|
69
|
+
|
|
70
|
+
// the gear opens the settings dialog directly; Keybinds is a leaf in its tree nav
|
|
71
|
+
if (menuDialog !== "settings") return null;
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<SettingsDialog
|
|
75
|
+
isOpen
|
|
76
|
+
version={CLIENT_VERSION}
|
|
77
|
+
keybindsSections={keybindsSections}
|
|
78
|
+
categories={settingsCategories}
|
|
79
|
+
values={settingsValues}
|
|
80
|
+
activeCategoryId={settingsNav.categoryId}
|
|
81
|
+
activeRowIndex={settingsNav.rowIndex}
|
|
82
|
+
activeZone={settingsNav.zone}
|
|
83
|
+
onCategorySelect={onCategorySelect}
|
|
84
|
+
onRowActivate={(row) => cycleSetting(row.key)}
|
|
85
|
+
theme={theme}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The welcome shell's Project pane: the launch repo's changed files in changes mode, its full tree otherwise. */
|
|
91
|
+
function WelcomeProjectPanel({
|
|
92
|
+
mode,
|
|
93
|
+
controller,
|
|
94
|
+
onOpenFile,
|
|
95
|
+
theme,
|
|
96
|
+
}: {
|
|
97
|
+
mode: ProjectPanelMode;
|
|
98
|
+
controller: ReviewController;
|
|
99
|
+
onOpenFile: (path: string) => void;
|
|
100
|
+
theme: Theme;
|
|
101
|
+
}): React.ReactNode {
|
|
102
|
+
const [changes, setChanges] = useState<readonly DiffFileContents[]>([]);
|
|
103
|
+
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
let alive = true;
|
|
106
|
+
|
|
107
|
+
void controller.repoChanges().then(
|
|
108
|
+
(files) => {
|
|
109
|
+
if (alive) setChanges(files);
|
|
110
|
+
},
|
|
111
|
+
() => {
|
|
112
|
+
if (alive) setChanges([]);
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
return () => {
|
|
117
|
+
alive = false;
|
|
118
|
+
};
|
|
119
|
+
}, [controller]);
|
|
120
|
+
|
|
121
|
+
if (mode === "changes") {
|
|
122
|
+
return <ChangesFileTree files={changes} onSelectFile={onOpenFile} theme={theme} />;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<ProjectTreeView
|
|
127
|
+
loadFiles={() => controller.repoFiles()}
|
|
128
|
+
onSelectFile={onOpenFile}
|
|
129
|
+
theme={theme}
|
|
130
|
+
/>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The shell with no thread open: the same header and Projects/Threads sidebar as
|
|
136
|
+
* the thread view, and a disposable Welcome tab in the center. There is no
|
|
137
|
+
* separate inbox screen - opening the app lands here, and picking a thread swaps
|
|
138
|
+
* the center for it. Closing the Welcome tab leaves a bare "select a thread" hint.
|
|
139
|
+
*/
|
|
140
|
+
export function NoThreadShell(props: {
|
|
141
|
+
rows: InboxRow[];
|
|
142
|
+
inboxCursor: number;
|
|
143
|
+
mode: Mode;
|
|
144
|
+
theme: Theme;
|
|
145
|
+
controller: ReviewController;
|
|
146
|
+
setMode: Dispatch<SetStateAction<Mode>>;
|
|
147
|
+
menuChrome: React.ReactNode;
|
|
148
|
+
onOpenMenu: () => void;
|
|
149
|
+
/** Shared with the thread view, so picking a thread preserves the sidebar. */
|
|
150
|
+
sidebarOpen: boolean;
|
|
151
|
+
onToggleSidebar: () => void;
|
|
152
|
+
pinnedIds: ReadonlySet<string>;
|
|
153
|
+
onPin: (id: string) => void;
|
|
154
|
+
onRename: (id: string, title: string) => void;
|
|
155
|
+
/** Drives the "/" quick actions and skills in the welcome playground's composer. */
|
|
156
|
+
quickActions: QuickAction[];
|
|
157
|
+
/** Reports the welcome composer's open state, so the shell suspends its inbox keys while typing. */
|
|
158
|
+
onWelcomeComposingChange: (composing: boolean) => void;
|
|
159
|
+
}): React.ReactNode {
|
|
160
|
+
const {
|
|
161
|
+
rows,
|
|
162
|
+
inboxCursor,
|
|
163
|
+
mode,
|
|
164
|
+
theme,
|
|
165
|
+
controller,
|
|
166
|
+
setMode,
|
|
167
|
+
menuChrome,
|
|
168
|
+
onOpenMenu,
|
|
169
|
+
sidebarOpen,
|
|
170
|
+
onToggleSidebar,
|
|
171
|
+
pinnedIds,
|
|
172
|
+
onPin,
|
|
173
|
+
onRename,
|
|
174
|
+
quickActions,
|
|
175
|
+
onWelcomeComposingChange,
|
|
176
|
+
} = props;
|
|
177
|
+
const confirming = mode.type === "confirmDelete" ? mode : null;
|
|
178
|
+
// The bare-launch shell is the same four panes as a thread: the Thread pane waits in its empty state
|
|
179
|
+
// and a disposable Welcome tab rides in the Changes editor until a thread or diff is opened.
|
|
180
|
+
const workbench = useChangesWorkbench({ seed: "welcome" });
|
|
181
|
+
const openRepoFile = (path: string): void => workbench.openFile(path, "contents");
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<ThemeProvider theme={theme}>
|
|
185
|
+
<AppShell
|
|
186
|
+
sidebarOpen={sidebarOpen}
|
|
187
|
+
onToggleSidebar={onToggleSidebar}
|
|
188
|
+
onOpenMenu={onOpenMenu}
|
|
189
|
+
threadsPanel={
|
|
190
|
+
<ScrollArea>
|
|
191
|
+
<ThreadTree
|
|
192
|
+
rows={rows}
|
|
193
|
+
cursor={inboxCursor}
|
|
194
|
+
pinnedIds={pinnedIds}
|
|
195
|
+
width={30}
|
|
196
|
+
onSelect={(id) => controller.open(id)}
|
|
197
|
+
onRequestDelete={(id, title) =>
|
|
198
|
+
setMode({ type: "confirmDelete", sessionId: id, title })
|
|
199
|
+
}
|
|
200
|
+
onPin={onPin}
|
|
201
|
+
onRename={onRename}
|
|
202
|
+
theme={theme}
|
|
203
|
+
/>
|
|
204
|
+
</ScrollArea>
|
|
205
|
+
}
|
|
206
|
+
threadTitle=""
|
|
207
|
+
threadPanel={
|
|
208
|
+
<box
|
|
209
|
+
style={{
|
|
210
|
+
flexGrow: 1,
|
|
211
|
+
flexDirection: "column",
|
|
212
|
+
alignItems: "center",
|
|
213
|
+
justifyContent: "center",
|
|
214
|
+
}}
|
|
215
|
+
>
|
|
216
|
+
<text fg={theme.textDim}>{rows.length === 0 ? "no threads" : "Select a thread"}</text>
|
|
217
|
+
</box>
|
|
218
|
+
}
|
|
219
|
+
changesOpen={workbench.changesOpen}
|
|
220
|
+
projectOpen={workbench.projectOpen}
|
|
221
|
+
onToggleChanges={workbench.toggleChanges}
|
|
222
|
+
onToggleProject={workbench.toggleProject}
|
|
223
|
+
onToggleRight={workbench.toggleRight}
|
|
224
|
+
projectMode={workbench.projectMode}
|
|
225
|
+
zoomHideThread={workbench.zoomed}
|
|
226
|
+
changesPanel={
|
|
227
|
+
<EditorGrid
|
|
228
|
+
tree={workbench.grid}
|
|
229
|
+
focusedGroupId={workbench.activeGroup}
|
|
230
|
+
onFocusGroup={workbench.focusGroup}
|
|
231
|
+
onActivateTab={workbench.activate}
|
|
232
|
+
onCloseTab={workbench.close}
|
|
233
|
+
onSplit={workbench.split}
|
|
234
|
+
onZoom={workbench.toggleZoom}
|
|
235
|
+
zoomed={workbench.zoomed}
|
|
236
|
+
renderTab={(tab) =>
|
|
237
|
+
tab.kind === "welcome" ? (
|
|
238
|
+
<WelcomePlayground
|
|
239
|
+
version={CLIENT_VERSION}
|
|
240
|
+
quickActions={quickActions}
|
|
241
|
+
onComposingChange={onWelcomeComposingChange}
|
|
242
|
+
theme={theme}
|
|
243
|
+
/>
|
|
244
|
+
) : (
|
|
245
|
+
<BareWorkbenchFileView
|
|
246
|
+
path={tab.path ?? ""}
|
|
247
|
+
controller={controller}
|
|
248
|
+
quickActions={quickActions}
|
|
249
|
+
onComposingChange={onWelcomeComposingChange}
|
|
250
|
+
onExit={() => {}}
|
|
251
|
+
theme={theme}
|
|
252
|
+
/>
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
theme={theme}
|
|
256
|
+
/>
|
|
257
|
+
}
|
|
258
|
+
projectPanel={
|
|
259
|
+
<WelcomeProjectPanel
|
|
260
|
+
mode={workbench.projectMode}
|
|
261
|
+
controller={controller}
|
|
262
|
+
onOpenFile={openRepoFile}
|
|
263
|
+
theme={theme}
|
|
264
|
+
/>
|
|
265
|
+
}
|
|
266
|
+
theme={theme}
|
|
267
|
+
>
|
|
268
|
+
{menuChrome}
|
|
269
|
+
<ConfirmDialog
|
|
270
|
+
isOpen={confirming !== null}
|
|
271
|
+
title=" Delete plan "
|
|
272
|
+
message={
|
|
273
|
+
confirming ? `Delete "${confirming.title}"? This removes the plan and its review.` : ""
|
|
274
|
+
}
|
|
275
|
+
onConfirm={() => {
|
|
276
|
+
if (confirming) controller.deleteSession(confirming.sessionId);
|
|
277
|
+
setMode({ type: "normal" });
|
|
278
|
+
}}
|
|
279
|
+
onCancel={() => setMode({ type: "normal" })}
|
|
280
|
+
theme={theme}
|
|
281
|
+
/>
|
|
282
|
+
{mode.type === "renameThread" ? (
|
|
283
|
+
<PromptDialog
|
|
284
|
+
isOpen
|
|
285
|
+
title=" rename thread "
|
|
286
|
+
label="new title for this thread:"
|
|
287
|
+
value={mode.text}
|
|
288
|
+
placeholder="a short title"
|
|
289
|
+
onInput={(text) => setMode({ ...mode, text })}
|
|
290
|
+
theme={theme}
|
|
291
|
+
/>
|
|
292
|
+
) : null}
|
|
293
|
+
</AppShell>
|
|
294
|
+
</ThemeProvider>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function CompletionScreen(props: {
|
|
299
|
+
theme: Theme;
|
|
300
|
+
session: Thread;
|
|
301
|
+
verdict: VerdictKind;
|
|
302
|
+
completion: { phase: "prompt" } | { phase: "counting"; remaining: number };
|
|
303
|
+
status: string;
|
|
304
|
+
}): React.ReactNode {
|
|
305
|
+
const { theme, session, verdict, completion, status } = props;
|
|
306
|
+
|
|
307
|
+
return (
|
|
308
|
+
<ThemeProvider theme={theme}>
|
|
309
|
+
<CompletionOverlay
|
|
310
|
+
verdict={verdict}
|
|
311
|
+
completion={completion}
|
|
312
|
+
status={status}
|
|
313
|
+
returnsTo={
|
|
314
|
+
returnPaneFor(session.artifact.meta.herdrPane)
|
|
315
|
+
? (session.artifact.meta.agent ?? "the agent")
|
|
316
|
+
: undefined
|
|
317
|
+
}
|
|
318
|
+
/>
|
|
319
|
+
</ThemeProvider>
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** The words each tree prompt uses; enter with an empty summary moves back without one. */
|
|
324
|
+
const TREE_PROMPTS: Record<TreeAsk, { title: string; label: string; placeholder: string }> = {
|
|
325
|
+
branch: { title: " Branch ", label: "Name for the new branch:", placeholder: "alt" },
|
|
326
|
+
label: {
|
|
327
|
+
title: " Checkpoint ",
|
|
328
|
+
label: "Name for this checkpoint:",
|
|
329
|
+
placeholder: "before the rewrite",
|
|
330
|
+
},
|
|
331
|
+
navigate: {
|
|
332
|
+
title: " Move back ",
|
|
333
|
+
label: "Summary of what you leave behind (optional):",
|
|
334
|
+
placeholder: "tried a shorter plan",
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
export function TrailingOverlays(props: {
|
|
339
|
+
walking: boolean;
|
|
340
|
+
walk: { index: number } | null;
|
|
341
|
+
walkFileList: WalkFile[];
|
|
342
|
+
viewedPaths: Set<string>;
|
|
343
|
+
session: Thread;
|
|
344
|
+
terminalWidth: number;
|
|
345
|
+
theme: Theme;
|
|
346
|
+
mode: Mode;
|
|
347
|
+
toast: ToastState | null;
|
|
348
|
+
setMode: Dispatch<SetStateAction<Mode>>;
|
|
349
|
+
dispatch: (intent: Intent) => void;
|
|
350
|
+
}): React.ReactNode {
|
|
351
|
+
const {
|
|
352
|
+
walking,
|
|
353
|
+
walk,
|
|
354
|
+
walkFileList,
|
|
355
|
+
viewedPaths,
|
|
356
|
+
session,
|
|
357
|
+
terminalWidth,
|
|
358
|
+
theme,
|
|
359
|
+
mode,
|
|
360
|
+
toast,
|
|
361
|
+
setMode,
|
|
362
|
+
dispatch,
|
|
363
|
+
} = props;
|
|
364
|
+
|
|
365
|
+
return (
|
|
366
|
+
<>
|
|
367
|
+
{walking && walk !== null ? (
|
|
368
|
+
<WalkWizard
|
|
369
|
+
files={walkFileList}
|
|
370
|
+
index={walk.index}
|
|
371
|
+
viewedPaths={viewedPaths}
|
|
372
|
+
note={
|
|
373
|
+
walkFileList[walk.index] !== undefined
|
|
374
|
+
? noteForFile(session.annotations, walkFileList[walk.index]!.path)
|
|
375
|
+
: undefined
|
|
376
|
+
}
|
|
377
|
+
terminalWidth={terminalWidth}
|
|
378
|
+
onSubmitRequest={() => {
|
|
379
|
+
dispatch({ type: "walkLeave" });
|
|
380
|
+
dispatch({ type: "openSubmit" });
|
|
381
|
+
}}
|
|
382
|
+
onBack={() => dispatch({ type: "walkBack" })}
|
|
383
|
+
/>
|
|
384
|
+
) : null}
|
|
385
|
+
{mode.type === "rename" ? (
|
|
386
|
+
<PromptDialog
|
|
387
|
+
isOpen
|
|
388
|
+
title=" Rename author "
|
|
389
|
+
label="Display name for this collaborator:"
|
|
390
|
+
value={mode.text}
|
|
391
|
+
placeholder="their name"
|
|
392
|
+
onInput={(text) => setMode({ ...mode, text })}
|
|
393
|
+
theme={theme}
|
|
394
|
+
/>
|
|
395
|
+
) : null}
|
|
396
|
+
{mode.type === "renameThread" ? (
|
|
397
|
+
<PromptDialog
|
|
398
|
+
isOpen
|
|
399
|
+
title=" rename thread "
|
|
400
|
+
label="new title for this thread:"
|
|
401
|
+
value={mode.text}
|
|
402
|
+
placeholder="a short title"
|
|
403
|
+
onInput={(text) => setMode({ ...mode, text })}
|
|
404
|
+
theme={theme}
|
|
405
|
+
/>
|
|
406
|
+
) : null}
|
|
407
|
+
{mode.type === "nameSelf" ? (
|
|
408
|
+
<PromptDialog
|
|
409
|
+
isOpen
|
|
410
|
+
title=" Welcome "
|
|
411
|
+
label="Your name (optional) - it attributes the notes you leave:"
|
|
412
|
+
value={mode.text}
|
|
413
|
+
placeholder="your name"
|
|
414
|
+
onInput={(text) => setMode({ ...mode, text })}
|
|
415
|
+
theme={theme}
|
|
416
|
+
/>
|
|
417
|
+
) : null}
|
|
418
|
+
{mode.type === "treePrompt" ? (
|
|
419
|
+
<PromptDialog
|
|
420
|
+
isOpen
|
|
421
|
+
title={TREE_PROMPTS[mode.ask].title}
|
|
422
|
+
label={TREE_PROMPTS[mode.ask].label}
|
|
423
|
+
value={mode.text}
|
|
424
|
+
placeholder={TREE_PROMPTS[mode.ask].placeholder}
|
|
425
|
+
onInput={(text) => setMode({ ...mode, text })}
|
|
426
|
+
theme={theme}
|
|
427
|
+
/>
|
|
428
|
+
) : null}
|
|
429
|
+
{toast ? <Toast title={toast.title} body={toast.body} theme={theme} /> : null}
|
|
430
|
+
</>
|
|
431
|
+
);
|
|
432
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { Dispatch, MutableRefObject, SetStateAction } from "react";
|
|
2
|
+
import type { Thread, VerdictKind } from "@cueloop/schema";
|
|
3
|
+
import type { Mode } from "./intent-dispatch";
|
|
4
|
+
import type { Intent, KeyState } from "./keymap";
|
|
5
|
+
import type { Completion } from "./thread-controller";
|
|
6
|
+
import type { WalkFile } from "./walk";
|
|
7
|
+
import { viewedCount } from "./walk";
|
|
8
|
+
import type { ConfirmCardProps } from "./components/ConfirmCard";
|
|
9
|
+
import type { BreadcrumbItem } from "./components/Breadcrumb";
|
|
10
|
+
|
|
11
|
+
export function computeRoleCapabilities(
|
|
12
|
+
readOnly: boolean,
|
|
13
|
+
role: "owner" | "observer" | "collaborator",
|
|
14
|
+
) {
|
|
15
|
+
const observer = readOnly || role === "observer";
|
|
16
|
+
|
|
17
|
+
return { observer, isOwner: !observer && role === "owner" };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function deriveReviewFlags(session: Thread | null) {
|
|
21
|
+
return {
|
|
22
|
+
isDiff: session?.artifact.type === "diff",
|
|
23
|
+
isPrototype: session?.artifact.type === "prototype",
|
|
24
|
+
resolved: session?.status === "resolved",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isWalking(isDiff: boolean, walk: { index: number } | null): boolean {
|
|
29
|
+
return isDiff && walk !== null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A prototype renders as a kitty pixel mockup only in the opt-in experimental mode:
|
|
34
|
+
* the flag is on and the artifact carries an HTML entry. Otherwise it is the default
|
|
35
|
+
* markdown design doc, rendered through the thread/markdown path.
|
|
36
|
+
*/
|
|
37
|
+
export function isPixelPrototypeMode(
|
|
38
|
+
isPrototype: boolean,
|
|
39
|
+
prototypePixels: boolean,
|
|
40
|
+
prototypePath: string | undefined,
|
|
41
|
+
): boolean {
|
|
42
|
+
return isPrototype && prototypePixels && Boolean(prototypePath);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function resolveOverlay(
|
|
46
|
+
mode: Mode,
|
|
47
|
+
completionPhase: Completion["phase"],
|
|
48
|
+
walking: boolean,
|
|
49
|
+
): KeyState["overlay"] {
|
|
50
|
+
if (mode.type === "compose" || mode.type === "railEdit") return "compose";
|
|
51
|
+
if (mode.type === "submit") return "submit";
|
|
52
|
+
if (mode.type === "confirmDelete") return "confirm";
|
|
53
|
+
if (
|
|
54
|
+
mode.type === "rename" ||
|
|
55
|
+
mode.type === "renameThread" ||
|
|
56
|
+
mode.type === "nameSelf" ||
|
|
57
|
+
mode.type === "treePrompt"
|
|
58
|
+
)
|
|
59
|
+
return "prompt";
|
|
60
|
+
if (mode.type === "spanActions") return "spanActions";
|
|
61
|
+
if (completionPhase === "prompt") return "completion-prompt";
|
|
62
|
+
if (completionPhase === "counting") return "completion-counting";
|
|
63
|
+
if (walking) return "walk";
|
|
64
|
+
|
|
65
|
+
return "none";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function isCompletionOverlayPhase(
|
|
69
|
+
completion: Completion,
|
|
70
|
+
): completion is { phase: "prompt" } | { phase: "counting"; remaining: number } {
|
|
71
|
+
return completion.phase === "prompt" || completion.phase === "counting";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function buildHeaderItems(params: {
|
|
75
|
+
session: Thread;
|
|
76
|
+
resolved: boolean;
|
|
77
|
+
observer: boolean;
|
|
78
|
+
role: "owner" | "observer" | "collaborator";
|
|
79
|
+
}): BreadcrumbItem[] {
|
|
80
|
+
const { session, resolved, observer, role } = params;
|
|
81
|
+
|
|
82
|
+
return [
|
|
83
|
+
{ label: "cueloop", tone: "accent" },
|
|
84
|
+
...(resolved
|
|
85
|
+
? [
|
|
86
|
+
{
|
|
87
|
+
label: `resolved: ${session.verdict!.kind.replace("_", " ")}`,
|
|
88
|
+
tone: "green" as const,
|
|
89
|
+
},
|
|
90
|
+
]
|
|
91
|
+
: []),
|
|
92
|
+
...(observer ? [{ label: "observer", tone: "dim" as const }] : []),
|
|
93
|
+
...(role === "collaborator"
|
|
94
|
+
? [{ label: "shared · your notes save as you go", tone: "dim" as const }]
|
|
95
|
+
: []),
|
|
96
|
+
{
|
|
97
|
+
label: `${session.artifact.meta.title ?? session.artifact.meta.planPath ?? session.id} · rev ${session.revisions.length}`,
|
|
98
|
+
tone: "dim",
|
|
99
|
+
},
|
|
100
|
+
{ label: `submitted by ${session.artifact.meta.agent ?? "unknown"}`, tone: "dim" },
|
|
101
|
+
];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function buildRenderFlags(params: {
|
|
105
|
+
session: Thread;
|
|
106
|
+
isOwner: boolean;
|
|
107
|
+
isDiff: boolean;
|
|
108
|
+
isPixelPrototype: boolean;
|
|
109
|
+
resolved: boolean;
|
|
110
|
+
menuDialog: "keybinds" | "settings" | null;
|
|
111
|
+
resolvedIds: Set<string>;
|
|
112
|
+
}) {
|
|
113
|
+
const { session, isOwner, isDiff, isPixelPrototype, resolved, menuDialog, resolvedIds } = params;
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
showOwnerActions: isOwner && !isDiff && !resolved,
|
|
117
|
+
prototypeCanComment: isOwner && !resolved,
|
|
118
|
+
chromeHidden: menuDialog !== null,
|
|
119
|
+
prototypePath: session.artifact.meta.prototypePath ?? "",
|
|
120
|
+
// a markdown prototype interleaves resolved cards like a plan; only the pixel mockup opts out
|
|
121
|
+
railResolvedIds: isDiff || isPixelPrototype ? null : resolvedIds,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
interface DraftHandlerDeps {
|
|
126
|
+
liveInput: MutableRefObject<string>;
|
|
127
|
+
setMode: Dispatch<SetStateAction<Mode>>;
|
|
128
|
+
dispatch: (intent: Intent) => void;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function buildSubmitConfirmState(
|
|
132
|
+
deps: DraftHandlerDeps & {
|
|
133
|
+
mode: Mode;
|
|
134
|
+
isDiff: boolean;
|
|
135
|
+
session: Thread;
|
|
136
|
+
walkFileList: WalkFile[];
|
|
137
|
+
viewedPaths: Set<string>;
|
|
138
|
+
},
|
|
139
|
+
): Omit<ConfirmCardProps, "theme"> | null {
|
|
140
|
+
const { mode, isDiff, session, walkFileList, viewedPaths, liveInput, setMode, dispatch } = deps;
|
|
141
|
+
|
|
142
|
+
if (mode.type !== "submit") return null;
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
verdict: mode.verdict,
|
|
146
|
+
summary: mode.summary,
|
|
147
|
+
viewedSummary:
|
|
148
|
+
isDiff && session.viewedPaths !== undefined
|
|
149
|
+
? `${viewedCount(walkFileList, viewedPaths)}/${walkFileList.length} files viewed`
|
|
150
|
+
: undefined,
|
|
151
|
+
onInput: (summary: string) => {
|
|
152
|
+
liveInput.current = summary;
|
|
153
|
+
setMode({ ...mode, summary });
|
|
154
|
+
},
|
|
155
|
+
onSelectVerdict: (verdict: VerdictKind) => setMode({ ...mode, verdict }),
|
|
156
|
+
onSubmit: () => dispatch({ type: "submitVerdict" }),
|
|
157
|
+
onCancel: () => dispatch({ type: "closeOverlay" }),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { Identity } from "@cueloop/schema";
|
|
3
|
+
import { ANONYMOUS_LABEL, resolveDisplayName, shortHandle } from "./attribution";
|
|
4
|
+
|
|
5
|
+
describe(shortHandle, () => {
|
|
6
|
+
test("shortens an ssh fingerprint to a stable handle", () => {
|
|
7
|
+
// Arrange / Act / Assert
|
|
8
|
+
expect(shortHandle("SHA256:1a2b3c4d5e6f")).toBe("1a2b3c4d");
|
|
9
|
+
expect(shortHandle("SHA256:1a2b3c4d5e6f")).toBe(shortHandle("SHA256:1a2b3c4d5e6f"));
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe(resolveDisplayName, () => {
|
|
14
|
+
const named: Identity[] = [{ id: "SHA256:abc", provider: "ssh", name: "Al" }];
|
|
15
|
+
const unnamed: Identity[] = [{ id: "SHA256:abc", provider: "ssh" }];
|
|
16
|
+
|
|
17
|
+
test("a local rename override wins over everything", () => {
|
|
18
|
+
// Arrange / Act
|
|
19
|
+
const label = resolveDisplayName("SHA256:abc", named, { "SHA256:abc": "Alex" });
|
|
20
|
+
|
|
21
|
+
// Assert
|
|
22
|
+
expect(label).toBe("Alex");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("falls back to name, then anonymous, then the short handle", () => {
|
|
26
|
+
// Arrange / Act / Assert
|
|
27
|
+
expect(resolveDisplayName("SHA256:abc", named, {})).toBe("Al");
|
|
28
|
+
expect(resolveDisplayName("SHA256:abc", unnamed, {})).toBe(ANONYMOUS_LABEL);
|
|
29
|
+
expect(resolveDisplayName("SHA256:1a2b3c4d5e", [], {})).toBe("1a2b3c4d");
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attribution labels for annotations. An annotation's `author` is an identity
|
|
3
|
+
* id (an SSH fingerprint today). The rail resolves it against the share's
|
|
4
|
+
* participant registry and the planner's local rename overrides; ADR 0006.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Identity } from "@cueloop/schema";
|
|
8
|
+
|
|
9
|
+
const FINGERPRINT_PREFIX = "SHA256:";
|
|
10
|
+
|
|
11
|
+
/** Shown for a known participant who chose not to give a name. */
|
|
12
|
+
export const ANONYMOUS_LABEL = "-- anonymous --";
|
|
13
|
+
|
|
14
|
+
/** A short, stable handle from a raw author id when no name is known. */
|
|
15
|
+
export function shortHandle(authorId: string): string {
|
|
16
|
+
const base = authorId.startsWith(FINGERPRINT_PREFIX)
|
|
17
|
+
? authorId.slice(FINGERPRINT_PREFIX.length)
|
|
18
|
+
: authorId;
|
|
19
|
+
|
|
20
|
+
return base.slice(0, 8) || authorId;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The rail label for an author id: a local rename wins, then the collaborator's
|
|
25
|
+
* own name, then anonymous for a known-but-unnamed participant, then a short
|
|
26
|
+
* handle for an id we hold no identity for.
|
|
27
|
+
*/
|
|
28
|
+
export function resolveDisplayName(
|
|
29
|
+
authorId: string,
|
|
30
|
+
participants: Identity[] | undefined,
|
|
31
|
+
overrides: Record<string, string>,
|
|
32
|
+
): string {
|
|
33
|
+
const override = overrides[authorId];
|
|
34
|
+
|
|
35
|
+
if (override) return override;
|
|
36
|
+
const identity = participants?.find((participant) => participant.id === authorId);
|
|
37
|
+
|
|
38
|
+
if (identity?.name) return identity.name;
|
|
39
|
+
if (identity) return ANONYMOUS_LABEL;
|
|
40
|
+
|
|
41
|
+
return shortHandle(authorId);
|
|
42
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { clipboardCommands } from "./clipboard";
|
|
3
|
+
|
|
4
|
+
describe(clipboardCommands, () => {
|
|
5
|
+
test("uses pbcopy on macOS", () => {
|
|
6
|
+
// Assert
|
|
7
|
+
expect(clipboardCommands("darwin")).toEqual([["pbcopy"]]);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("uses clip.exe on Windows", () => {
|
|
11
|
+
// Assert
|
|
12
|
+
expect(clipboardCommands("win32")).toEqual([["clip.exe"]]);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("prefers wayland, then X11 tools on Linux", () => {
|
|
16
|
+
// Act
|
|
17
|
+
const commands = clipboardCommands("linux");
|
|
18
|
+
|
|
19
|
+
// Assert
|
|
20
|
+
expect(commands[0]).toEqual(["wl-copy"]);
|
|
21
|
+
expect(commands.map((command) => command[0])).toEqual(["wl-copy", "xclip", "xsel"]);
|
|
22
|
+
});
|
|
23
|
+
});
|