@cueloop/client 0.1.0-alpha.8 → 0.1.0-alpha.81

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.
Files changed (307) hide show
  1. package/native/build-pty.sh +51 -0
  2. package/native/build.sh +82 -0
  3. package/native/darwin-arm64/libcuelooppty.dylib +0 -0
  4. package/native/darwin-arm64/libcueloopvt.dylib +0 -0
  5. package/native/src/pty.zig +193 -0
  6. package/native/src/shim.zig +114 -0
  7. package/package.json +40 -32
  8. package/src/App.appearance.test.tsx +53 -0
  9. package/src/App.autoclose.test.tsx +77 -51
  10. package/src/App.diff.test.tsx +176 -45
  11. package/src/App.export.test.tsx +58 -41
  12. package/src/App.focus.test.ts +89 -0
  13. package/src/App.identity.test.tsx +78 -0
  14. package/src/App.plan-render.test.tsx +209 -0
  15. package/src/App.plan-review.test.tsx +428 -0
  16. package/src/App.prototype-markdown.test.tsx +92 -0
  17. package/src/App.prototype.test.tsx +176 -0
  18. package/src/App.readonly.test.tsx +129 -67
  19. package/src/App.regression.test.tsx +94 -0
  20. package/src/App.rendering.test.tsx +61 -35
  21. package/src/App.share-dialog.test.tsx +172 -0
  22. package/src/App.submit-confirm.test.tsx +222 -0
  23. package/src/App.test.tsx +333 -131
  24. package/src/App.toast.test.tsx +98 -0
  25. package/src/App.tsx +1338 -608
  26. package/src/App.visual.test.tsx +57 -0
  27. package/src/App.walk.test.tsx +299 -0
  28. package/src/App.workbench.test.tsx +701 -0
  29. package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
  30. package/src/annotation-palette.ts +62 -0
  31. package/src/ansi-screen.test.ts +28 -0
  32. package/src/ansi-screen.ts +72 -0
  33. package/src/app-key-state.ts +61 -0
  34. package/src/app-screens.tsx +574 -0
  35. package/src/app-view-model.ts +162 -0
  36. package/src/attribution.test.ts +31 -0
  37. package/src/attribution.ts +42 -0
  38. package/src/clipboard.test.ts +23 -0
  39. package/src/clipboard.ts +33 -0
  40. package/src/components/AnnotatableFileView.stories.tsx +42 -0
  41. package/src/components/AnnotatableFileView.test.tsx +102 -0
  42. package/src/components/AnnotatableFileView.tsx +134 -0
  43. package/src/components/AnnotationCards.stories.tsx +86 -0
  44. package/src/components/AnnotationCards.tsx +341 -0
  45. package/src/components/AnsiScreen.stories.tsx +12 -0
  46. package/src/components/AnsiScreen.tsx +28 -0
  47. package/src/components/AppHeader.stories.tsx +50 -0
  48. package/src/components/AppHeader.tsx +83 -0
  49. package/src/components/AppShell.brand.test.tsx +58 -0
  50. package/src/components/AppShell.stories.tsx +89 -0
  51. package/src/components/AppShell.tsx +329 -0
  52. package/src/components/AppShell.zoom.test.tsx +135 -0
  53. package/src/components/BareWorkbenchFileView.stories.tsx +27 -0
  54. package/src/components/BareWorkbenchFileView.tsx +64 -0
  55. package/src/components/Breadcrumb.stories.tsx +19 -0
  56. package/src/components/Breadcrumb.tsx +45 -0
  57. package/src/components/ChangesColumn.stories.tsx +44 -0
  58. package/src/components/ChangesColumn.tsx +199 -0
  59. package/src/components/ChangesFileTree.test.tsx +61 -0
  60. package/src/components/CodeBlock.stories.tsx +42 -0
  61. package/src/components/CodeBlock.tsx +84 -0
  62. package/src/components/CompletionOverlay.stories.tsx +37 -0
  63. package/src/components/CompletionOverlay.tsx +77 -0
  64. package/src/components/ConfirmCard.stories.tsx +37 -0
  65. package/src/components/ConfirmCard.tsx +153 -0
  66. package/src/components/ConfirmDialog.stories.tsx +21 -0
  67. package/src/components/ConfirmDialog.tsx +72 -0
  68. package/src/components/DiffContentView.scroll.test.tsx +121 -0
  69. package/src/components/DiffContentView.split.test.tsx +140 -0
  70. package/src/components/DiffContentView.stories.tsx +80 -0
  71. package/src/components/DiffContentView.syntax.test.tsx +82 -0
  72. package/src/components/DiffContentView.tsx +876 -0
  73. package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
  74. package/src/components/DiscussionMarkerRail.tsx +186 -0
  75. package/src/components/EditorGrid.stories.tsx +59 -0
  76. package/src/components/EditorGrid.tabs.test.tsx +118 -0
  77. package/src/components/EditorGrid.tsx +426 -0
  78. package/src/components/EditorGrid.zoom.test.tsx +68 -0
  79. package/src/components/FileContentsView.stories.tsx +16 -0
  80. package/src/components/FileContentsView.test.tsx +29 -0
  81. package/src/components/FileContentsView.tsx +98 -0
  82. package/src/components/GridTabContent.stories.tsx +59 -0
  83. package/src/components/GridTabContent.tsx +182 -0
  84. package/src/components/HandleChipsInput.stories.tsx +24 -0
  85. package/src/components/HandleChipsInput.test.tsx +35 -0
  86. package/src/components/HandleChipsInput.tsx +145 -0
  87. package/src/components/KeybindsDialog.stories.tsx +31 -0
  88. package/src/components/KeybindsDialog.tsx +103 -0
  89. package/src/components/MarkdownGridBlock.stories.tsx +33 -0
  90. package/src/components/MarkdownGridBlock.tsx +72 -0
  91. package/src/components/MarkdownThreadEditor.stories.tsx +15 -0
  92. package/src/components/MarkdownThreadEditor.test.tsx +98 -0
  93. package/src/components/MarkdownThreadEditor.tsx +162 -0
  94. package/src/components/MenuBar.stories.tsx +28 -0
  95. package/src/components/MenuBar.tsx +44 -0
  96. package/src/components/NavModeHint.stories.tsx +40 -0
  97. package/src/components/NavModeHint.tsx +31 -0
  98. package/src/components/OverlayScrollbar.stories.tsx +33 -0
  99. package/src/components/OverlayScrollbar.test.tsx +139 -0
  100. package/src/components/OverlayScrollbar.tsx +129 -0
  101. package/src/components/PaneGrid.stories.tsx +84 -0
  102. package/src/components/PaneGrid.tsx +128 -0
  103. package/src/components/PanelColumn.stories.tsx +25 -0
  104. package/src/components/PanelColumn.tsx +98 -0
  105. package/src/components/ProjectTreeView.stories.tsx +25 -0
  106. package/src/components/ProjectTreeView.test.tsx +59 -0
  107. package/src/components/ProjectTreeView.tsx +111 -0
  108. package/src/components/PromptDialog.stories.tsx +36 -0
  109. package/src/components/PromptDialog.test.tsx +61 -0
  110. package/src/components/PromptDialog.tsx +99 -0
  111. package/src/components/PrototypeContentView.stories.tsx +19 -0
  112. package/src/components/PrototypeContentView.tsx +446 -0
  113. package/src/components/RootOverlay.stories.tsx +45 -0
  114. package/src/components/RootOverlay.tsx +48 -0
  115. package/src/components/ScrollArea.stories.tsx +20 -0
  116. package/src/components/ScrollArea.tsx +44 -0
  117. package/src/components/SettingsDialog.stories.tsx +117 -0
  118. package/src/components/SettingsDialog.tsx +186 -0
  119. package/src/components/SettingsRows.stories.tsx +18 -0
  120. package/src/components/SettingsRows.tsx +112 -0
  121. package/src/components/ShareDialog.stories.tsx +121 -0
  122. package/src/components/ShareDialog.tsx +552 -0
  123. package/src/components/ShellHeader.stories.tsx +50 -0
  124. package/src/components/ShellHeader.tsx +137 -0
  125. package/src/components/SlashComposer.stories.tsx +37 -0
  126. package/src/components/SlashComposer.tsx +87 -0
  127. package/src/components/ThreadFooter.stories.tsx +30 -0
  128. package/src/components/ThreadFooter.test.tsx +47 -0
  129. package/src/components/ThreadFooter.tsx +76 -0
  130. package/src/components/ThreadTree.stories.tsx +46 -0
  131. package/src/components/ThreadTree.tsx +255 -0
  132. package/src/components/ThreadView.selection.test.tsx +899 -0
  133. package/src/components/ThreadView.slash.test.tsx +146 -0
  134. package/src/components/ThreadView.stories.tsx +26 -0
  135. package/src/components/ThreadView.tsx +472 -0
  136. package/src/components/Toast.stories.tsx +19 -0
  137. package/src/components/Toast.tsx +52 -0
  138. package/src/components/Tooltip.stories.tsx +26 -0
  139. package/src/components/Tooltip.tsx +72 -0
  140. package/src/components/TreePane.stories.tsx +106 -0
  141. package/src/components/TreePane.tsx +157 -0
  142. package/src/components/WalkWizard.stories.tsx +100 -0
  143. package/src/components/WalkWizard.tsx +150 -0
  144. package/src/components/WelcomePlayground.stories.tsx +12 -0
  145. package/src/components/WelcomePlayground.test.tsx +47 -0
  146. package/src/components/WelcomePlayground.tsx +142 -0
  147. package/src/components/__snapshots__/stories.test.tsx.snap +2238 -0
  148. package/src/components/diff-content-view-layout.test.ts +198 -0
  149. package/src/components/diff-content-view-layout.ts +84 -0
  150. package/src/components/editor-grid.test.ts +86 -0
  151. package/src/components/editor-grid.ts +138 -0
  152. package/src/components/file-tree.test.ts +60 -0
  153. package/src/components/file-tree.ts +112 -0
  154. package/src/components/menu-control.stories.tsx +35 -0
  155. package/src/components/menu-control.tsx +43 -0
  156. package/src/components/pages/Workspace.stories.tsx +181 -0
  157. package/src/components/primitives/Button.stories.tsx +32 -0
  158. package/src/components/primitives/Button.tsx +56 -0
  159. package/src/components/primitives/Card.stories.tsx +25 -0
  160. package/src/components/primitives/Card.tsx +63 -0
  161. package/src/components/primitives/Dialog.stories.tsx +24 -0
  162. package/src/components/primitives/Dialog.test.tsx +37 -0
  163. package/src/components/primitives/Dialog.tsx +71 -0
  164. package/src/components/primitives/DialogActions.stories.tsx +20 -0
  165. package/src/components/primitives/DialogActions.tsx +39 -0
  166. package/src/components/primitives/IconButton.stories.tsx +36 -0
  167. package/src/components/primitives/IconButton.tsx +62 -0
  168. package/src/components/primitives/StatusBar.stories.tsx +11 -0
  169. package/src/components/primitives/StatusBar.tsx +23 -0
  170. package/src/components/primitives/Switch.stories.tsx +17 -0
  171. package/src/components/primitives/Switch.tsx +32 -0
  172. package/src/components/primitives/Tabs.stories.tsx +29 -0
  173. package/src/components/primitives/Tabs.tsx +102 -0
  174. package/src/components/primitives/Toolbar.stories.tsx +17 -0
  175. package/src/components/primitives/Toolbar.tsx +14 -0
  176. package/src/components/primitives/Tree.stories.tsx +124 -0
  177. package/src/components/primitives/Tree.tsx +100 -0
  178. package/src/components/primitives/frame.test.ts +12 -0
  179. package/src/components/primitives/frame.ts +17 -0
  180. package/src/components/primitives/icons.ts +61 -0
  181. package/src/components/primitives/tree-model.test.ts +99 -0
  182. package/src/components/primitives/tree-model.ts +137 -0
  183. package/src/components/quick-actions-editor.stories.tsx +58 -0
  184. package/src/components/quick-actions-editor.test.tsx +117 -0
  185. package/src/components/quick-actions-editor.tsx +226 -0
  186. package/src/components/share-dialog-store.ts +58 -0
  187. package/src/components/stories-app.tsx +193 -0
  188. package/src/components/stories.test.tsx +74 -0
  189. package/src/components/story-fixtures.ts +102 -0
  190. package/src/components/story-tree.test.ts +108 -0
  191. package/src/components/story.ts +159 -0
  192. package/src/components/syntax-highlight.ts +133 -0
  193. package/src/components/terminal-pane.test.tsx +40 -0
  194. package/src/components/terminal-pane.ts +199 -0
  195. package/src/components/theme-context.stories.tsx +46 -0
  196. package/src/components/theme-context.tsx +37 -0
  197. package/src/components/thread-tree.test.ts +134 -0
  198. package/src/components/thread-tree.ts +109 -0
  199. package/src/components/truncate-text.ts +7 -0
  200. package/src/components/truncate-title.test.ts +31 -0
  201. package/src/components/truncate-title.ts +15 -0
  202. package/src/config-identity.test.ts +78 -0
  203. package/src/config.test.ts +517 -33
  204. package/src/config.ts +467 -57
  205. package/src/diff-fold.test.ts +78 -0
  206. package/src/diff-fold.ts +126 -0
  207. package/src/diff-hunk-curate.test.ts +315 -0
  208. package/src/diff-hunk-curate.ts +153 -0
  209. package/src/diff-intraline.test.ts +238 -0
  210. package/src/diff-intraline.ts +236 -0
  211. package/src/diff-syntax.test.ts +112 -0
  212. package/src/diff-syntax.ts +179 -0
  213. package/src/discussions.ts +79 -0
  214. package/src/editor.test.ts +149 -0
  215. package/src/editor.ts +139 -9
  216. package/src/ghostty-terminal.test.ts +85 -0
  217. package/src/ghostty-terminal.ts +199 -0
  218. package/src/github-identity.test.ts +58 -0
  219. package/src/github-identity.ts +41 -0
  220. package/src/herdr.test.ts +9 -34
  221. package/src/herdr.ts +25 -21
  222. package/src/index.ts +13 -0
  223. package/src/integrations.test.ts +41 -0
  224. package/src/integrations.ts +32 -0
  225. package/src/intent-dispatch.test.ts +723 -0
  226. package/src/intent-dispatch.ts +692 -0
  227. package/src/key-bindings.test.ts +156 -0
  228. package/src/key-bindings.ts +459 -0
  229. package/src/keymap.test.ts +461 -113
  230. package/src/keymap.ts +321 -79
  231. package/src/kitty-image.test.ts +66 -0
  232. package/src/kitty-image.ts +138 -0
  233. package/src/launch-layout.test.ts +52 -0
  234. package/src/launch-layout.ts +42 -0
  235. package/src/mark-runs.ts +121 -0
  236. package/src/markdown-editor-syntax-style.ts +50 -0
  237. package/src/markdown-grid.test.ts +101 -0
  238. package/src/markdown-grid.ts +203 -0
  239. package/src/markdown-highlight.test.ts +61 -0
  240. package/src/markdown-highlight.ts +122 -0
  241. package/src/pasted-image.test.ts +67 -0
  242. package/src/pasted-image.ts +60 -0
  243. package/src/perf/otlp-trace.test.ts +116 -0
  244. package/src/perf/otlp-trace.ts +130 -0
  245. package/src/perf/perf-report.ts +17 -0
  246. package/src/perf/perf-timings.test.ts +18 -0
  247. package/src/perf/perf-timings.ts +38 -0
  248. package/src/prototype-browser.test.ts +64 -0
  249. package/src/prototype-browser.ts +329 -0
  250. package/src/prototype-pixels.tsx +35 -0
  251. package/src/pty.test.ts +81 -0
  252. package/src/pty.ts +245 -0
  253. package/src/ready-signal.test.ts +85 -0
  254. package/src/ready-signal.ts +52 -0
  255. package/src/run.ts +78 -13
  256. package/src/serve.test.ts +24 -12
  257. package/src/serve.ts +52 -10
  258. package/src/share.ts +237 -0
  259. package/src/skills.test.ts +74 -0
  260. package/src/skills.ts +75 -0
  261. package/src/slash-palette.test.ts +103 -0
  262. package/src/slash-palette.ts +129 -0
  263. package/src/split-diff.test.ts +110 -0
  264. package/src/split-diff.ts +149 -0
  265. package/src/test-support.test.ts +264 -0
  266. package/src/test-support.ts +369 -0
  267. package/src/theme-presets.test.ts +51 -0
  268. package/src/theme-presets.ts +214 -0
  269. package/src/theme.ts +84 -19
  270. package/src/thread-chords.test.ts +79 -0
  271. package/src/thread-chords.ts +163 -0
  272. package/src/thread-controller.curate.test.ts +450 -0
  273. package/src/thread-controller.derivation.test.ts +62 -0
  274. package/src/thread-controller.live-diff.test.ts +322 -0
  275. package/src/thread-controller.optimistic.test.ts +69 -0
  276. package/src/thread-controller.share.test.ts +561 -0
  277. package/src/thread-controller.switch-cache.test.ts +176 -0
  278. package/src/thread-controller.tree.test.ts +261 -0
  279. package/src/thread-controller.ts +1893 -0
  280. package/src/thread-selection.test.ts +210 -0
  281. package/src/thread-selection.ts +243 -0
  282. package/src/tree-view.test.ts +128 -0
  283. package/src/tree-view.ts +148 -0
  284. package/src/use-annotation-surface.tsx +915 -0
  285. package/src/use-changes-workbench.ts +190 -0
  286. package/src/use-frame-measure.ts +41 -0
  287. package/src/use-remember-layout.ts +31 -0
  288. package/src/use-settings-dialog.tsx +320 -0
  289. package/src/use-terminal-virtualizer.ts +233 -0
  290. package/src/use-thread-body-editing.tsx +58 -0
  291. package/src/version.ts +5 -0
  292. package/src/view-diff.test.ts +137 -9
  293. package/src/view-diff.ts +269 -23
  294. package/src/view-plan.regression.test.ts +60 -0
  295. package/src/view-plan.test.ts +456 -0
  296. package/src/view-plan.ts +719 -0
  297. package/src/visual/contrast.test.ts +35 -0
  298. package/src/visual/contrast.ts +33 -0
  299. package/src/walk.test.ts +121 -0
  300. package/src/walk.ts +74 -0
  301. package/src/workbench-snapshot.test.ts +81 -0
  302. package/src/workbench-snapshot.ts +29 -0
  303. package/src/session-controller.ts +0 -345
  304. package/src/syntax.test.ts +0 -41
  305. package/src/syntax.ts +0 -88
  306. package/src/view.test.ts +0 -157
  307. package/src/view.ts +0 -312
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Groups the pending inbox into Projects and Threads for the sidebar. Threads
3
+ * whose workspace carries a rootCommit gather under their project (keyed by that
4
+ * commit, so moving or re-cloning the repo never forks it); the rest are
5
+ * standalone Threads. The flat `ordered` sequence is what the inbox cursor
6
+ * walks, so the keyboard grammar keeps a single index across the two groups.
7
+ */
8
+
9
+ import type { Thread, WorkspaceKey } from "@cueloop/schema";
10
+
11
+ /** A repo's display name: the remote basename when present, else the working-tree folder name. */
12
+ export function projectName(workspace: WorkspaceKey): string {
13
+ const { remote, repoRoot } = workspace;
14
+
15
+ if (remote !== undefined) {
16
+ const cleaned = remote.replace(/\.git$/, "").replace(/\/+$/, "");
17
+ const base = cleaned
18
+ .split(/[/:]/)
19
+ .filter((part) => part.length > 0)
20
+ .at(-1);
21
+ if (base !== undefined) return base;
22
+ }
23
+
24
+ const dir = repoRoot
25
+ .replace(/\/+$/, "")
26
+ .split("/")
27
+ .filter((part) => part.length > 0)
28
+ .at(-1);
29
+
30
+ return dir ?? repoRoot;
31
+ }
32
+
33
+ export type InboxRow =
34
+ | { kind: "section"; id: string; label: string }
35
+ | { kind: "project"; id: string; label: string }
36
+ | { kind: "thread"; id: string; session: Thread; selectionIndex: number; depth: number };
37
+
38
+ export interface GroupedInbox {
39
+ rows: InboxRow[];
40
+ /** Threads in display order; the inbox cursor indexes this. */
41
+ ordered: Thread[];
42
+ }
43
+
44
+ function threadTitle(session: Thread): string {
45
+ return session.artifact.meta.title ?? session.id;
46
+ }
47
+
48
+ interface ProjectGroup {
49
+ name: string;
50
+ sessions: Thread[];
51
+ }
52
+
53
+ /** Group pending sessions into Pinned, then Projects (by root commit), then standalone Threads. */
54
+ export function groupInbox(
55
+ sessions: readonly Thread[],
56
+ pinnedIds?: ReadonlySet<string>,
57
+ ): GroupedInbox {
58
+ const pinned: Thread[] = [];
59
+ const projects = new Map<string, ProjectGroup>();
60
+ const standalone: Thread[] = [];
61
+
62
+ for (const session of sessions) {
63
+ if (pinnedIds?.has(session.id) === true) {
64
+ pinned.push(session);
65
+ continue;
66
+ }
67
+
68
+ const key = session.workspace.rootCommit;
69
+ if (key === undefined) {
70
+ standalone.push(session);
71
+ continue;
72
+ }
73
+
74
+ const existing = projects.get(key);
75
+ if (existing !== undefined) existing.sessions.push(session);
76
+ else projects.set(key, { name: projectName(session.workspace), sessions: [session] });
77
+ }
78
+
79
+ const rows: InboxRow[] = [];
80
+ const ordered: Thread[] = [];
81
+ const pushThread = (session: Thread, depth: number): void => {
82
+ rows.push({ kind: "thread", id: session.id, session, selectionIndex: ordered.length, depth });
83
+ ordered.push(session);
84
+ };
85
+
86
+ if (pinned.length > 0) {
87
+ rows.push({ kind: "section", id: "section:pinned", label: "Starred" });
88
+ for (const session of pinned) pushThread(session, 1);
89
+ }
90
+
91
+ const projectEntries = [...projects.entries()].sort((a, b) => a[1].name.localeCompare(b[1].name));
92
+ if (projectEntries.length > 0) {
93
+ rows.push({ kind: "section", id: "section:projects", label: "Projects" });
94
+ for (const [key, project] of projectEntries) {
95
+ rows.push({ kind: "project", id: `project:${key}`, label: project.name });
96
+ // a project's threads cascade one level under its name
97
+ for (const session of project.sessions) pushThread(session, 2);
98
+ }
99
+ }
100
+
101
+ if (standalone.length > 0) {
102
+ rows.push({ kind: "section", id: "section:threads", label: "Threads" });
103
+ for (const session of standalone) pushThread(session, 1);
104
+ }
105
+
106
+ return { rows, ordered };
107
+ }
108
+
109
+ export { threadTitle };
@@ -0,0 +1,7 @@
1
+ /** Truncate text to one line of at most maxLength characters, ellipsis-terminated. */
2
+
3
+ export function truncateToSingleLine(value: string, maxLength: number): string {
4
+ const oneLine = value.replace(/\n/g, " ");
5
+
6
+ return oneLine.length > maxLength ? oneLine.slice(0, maxLength - 1) + "…" : oneLine;
7
+ }
@@ -0,0 +1,31 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { truncateTitle } from "./truncate-title";
3
+
4
+ describe("truncateTitle", () => {
5
+ test("a title that fits is returned unchanged", () => {
6
+ expect(truncateTitle("short", 20)).toBe("short");
7
+ });
8
+
9
+ test("a long title is clipped to the width with a trailing ellipsis", () => {
10
+ const clipped = truncateTitle("Review the accent change in the theme", 20);
11
+
12
+ expect(clipped).toHaveLength(20);
13
+ expect(clipped.endsWith("…")).toBe(true);
14
+ expect(clipped).toBe("Review the accent c…");
15
+ });
16
+
17
+ test("newlines collapse to spaces so the row never wraps", () => {
18
+ expect(truncateTitle("line one\nline two", 40)).toBe("line one line two");
19
+ });
20
+
21
+ test("a zero width yields an empty string", () => {
22
+ expect(truncateTitle("anything", 0)).toBe("");
23
+ });
24
+
25
+ test("clips by code point, never splitting an astral character into a replacement glyph", () => {
26
+ const clipped = truncateTitle("🚀🚀🚀🚀🚀", 3);
27
+
28
+ expect(clipped).toBe("🚀🚀…");
29
+ expect(clipped).not.toContain("�");
30
+ });
31
+ });
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Fits a thread title on one line: unchanged when it fits, otherwise clipped to
3
+ * an ellipsis. Slices by code point, not UTF-16 unit, so an emoji or other
4
+ * astral character is never split into a replacement glyph.
5
+ */
6
+ export function truncateTitle(title: string, maxWidth: number): string {
7
+ const oneLine = title.replace(/\n/g, " ");
8
+
9
+ if (maxWidth <= 0) return "";
10
+ const characters = [...oneLine];
11
+
12
+ if (characters.length <= maxWidth) return oneLine;
13
+
14
+ return `${characters.slice(0, Math.max(0, maxWidth - 1)).join("")}…`;
15
+ }
@@ -0,0 +1,78 @@
1
+ import { expect, test } from "bun:test";
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { loadConfig, persistIdentity } from "./config";
6
+
7
+ function withTempConfig(run: (path: string) => void): void {
8
+ const directory = mkdtempSync(join(tmpdir(), "cueloop-config-"));
9
+
10
+ try {
11
+ run(join(directory, "config.toml"));
12
+ } finally {
13
+ rmSync(directory, { recursive: true, force: true });
14
+ }
15
+ }
16
+
17
+ test("defaults identity to a typed provider with no name", () => {
18
+ withTempConfig((path) => {
19
+ expect(loadConfig({ userConfigPath: path }).identity).toEqual({ provider: "typed" });
20
+ });
21
+ });
22
+
23
+ test("persists and reloads a GitHub-synced identity", () => {
24
+ withTempConfig((path) => {
25
+ persistIdentity({ name: "Markus M", provider: "github" }, path);
26
+
27
+ expect(loadConfig({ userConfigPath: path }).identity).toEqual({
28
+ name: "Markus M",
29
+ provider: "github",
30
+ });
31
+ });
32
+ });
33
+
34
+ test("rewrites the identity block in place on a re-sync", () => {
35
+ withTempConfig((path) => {
36
+ persistIdentity({ name: "typed name", provider: "typed" }, path);
37
+ persistIdentity({ name: "markus", provider: "github" }, path);
38
+
39
+ expect(loadConfig({ userConfigPath: path }).identity).toEqual({
40
+ name: "markus",
41
+ provider: "github",
42
+ });
43
+ });
44
+ });
45
+
46
+ test("a display name containing a bracket survives a re-sync without corrupting the config", () => {
47
+ withTempConfig((path) => {
48
+ persistIdentity({ name: "Alice [Smith]", provider: "typed" }, path);
49
+ persistIdentity({ name: "Bob", provider: "github" }, path);
50
+
51
+ expect(loadConfig({ userConfigPath: path }).identity).toEqual({
52
+ name: "Bob",
53
+ provider: "github",
54
+ });
55
+ });
56
+ });
57
+
58
+ test("repository config cannot forge the reviewer identity", () => {
59
+ withTempConfig((userPath) => {
60
+ persistIdentity({ name: "Me", provider: "typed" }, userPath);
61
+ const repoRoot = mkdtempSync(join(tmpdir(), "cueloop-repo-"));
62
+
63
+ try {
64
+ mkdirSync(join(repoRoot, ".cueloop"), { recursive: true });
65
+ writeFileSync(
66
+ join(repoRoot, ".cueloop", "config.toml"),
67
+ '[identity]\nname = "Forged"\nprovider = "github"\n',
68
+ );
69
+
70
+ expect(loadConfig({ userConfigPath: userPath, repoRoot }).identity).toEqual({
71
+ name: "Me",
72
+ provider: "typed",
73
+ });
74
+ } finally {
75
+ rmSync(repoRoot, { recursive: true, force: true });
76
+ }
77
+ });
78
+ });