@kabmo-labs/kabmo 0.8.0 → 0.9.0

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 (254) hide show
  1. package/README.md +7 -0
  2. package/dist/client/index.html +1 -1
  3. package/dist/client/index.js +314 -314
  4. package/dist/client/static/js/{index-qwpvd9fv.js → index-bpptfkz2.js} +922 -892
  5. package/dist/client/static/js/index-k315p1e8.css +1 -0
  6. package/package.json +9 -1
  7. package/src/client/components/common/code-surface/documents.test.ts +120 -0
  8. package/src/client/components/common/code-surface/documents.ts +201 -0
  9. package/src/client/components/common/code-surface/extensions.test.ts +110 -0
  10. package/src/client/components/common/code-surface/extensions.ts +162 -0
  11. package/src/client/components/common/code-surface/handle.ts +102 -0
  12. package/src/client/components/common/code-surface/index.tsx +253 -314
  13. package/src/client/components/common/code-surface/keymap.test.ts +133 -0
  14. package/src/client/components/common/code-surface/keymap.ts +267 -0
  15. package/src/client/components/common/code-surface/marks.ts +74 -0
  16. package/src/client/components/common/code-surface/shiki.ts +234 -0
  17. package/src/client/components/common/code-surface/sync.test.ts +185 -0
  18. package/src/client/components/common/code-surface/sync.ts +58 -0
  19. package/src/client/components/common/code-surface/theme.test.ts +202 -0
  20. package/src/client/components/common/code-surface/theme.ts +111 -0
  21. package/src/client/components/common/editor-view-buttons/index.test.tsx +148 -0
  22. package/src/client/components/common/editor-view-buttons/index.tsx +109 -0
  23. package/src/client/components/common/shell-update-button/Modal.tsx +163 -0
  24. package/src/client/components/common/shell-update-button/index.test.tsx +251 -0
  25. package/src/client/components/common/shell-update-button/index.tsx +125 -0
  26. package/src/client/components/common/toolbar-row/index.test.tsx +166 -0
  27. package/src/client/components/common/toolbar-row/index.tsx +189 -0
  28. package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +41 -78
  29. package/src/client/components/layout/desktop-layout/index.tsx +47 -67
  30. package/src/client/components/layout/desktop-layout/resizer/index.tsx +6 -3
  31. package/src/client/components/layout/session-sidebar/Footer.tsx +8 -15
  32. package/src/client/components/mobile/LayoutWrapper.tsx +1 -0
  33. package/src/client/components/mobile/mobile-right-sidebar/FullDiff.tsx +3 -0
  34. package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +63 -38
  35. package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.test.tsx +137 -0
  36. package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.tsx +14 -3
  37. package/src/client/components/mobile/mobile-session-sidebar/Footer.tsx +9 -21
  38. package/src/client/components/settings/Modal.tsx +8 -0
  39. package/src/client/components/settings/categories/AppearanceSettings.tsx +3 -0
  40. package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.test.tsx +167 -0
  41. package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.tsx +184 -0
  42. package/src/client/components/settings/categories/project-settings/WorktreeSettings.tsx +8 -2
  43. package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +16 -8
  44. package/src/client/components/workspace/chat-timeline/SystemNotice.tsx +9 -4
  45. package/src/client/components/workspace/chat-timeline/TaskResultContent.tsx +9 -7
  46. package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.test.tsx +132 -0
  47. package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.tsx +120 -0
  48. package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.test.tsx +319 -0
  49. package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.tsx +285 -0
  50. package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/index.test.ts +2 -0
  51. package/src/client/components/workspace/chat-timeline/tool-renderers/hashline-patch/index.tsx +34 -29
  52. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/AstEdit.tsx +11 -9
  53. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Bash.tsx +10 -7
  54. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Edit.tsx +25 -8
  55. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.tsx +8 -11
  56. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.test.ts +114 -0
  57. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.tsx +111 -0
  58. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Read.tsx +7 -7
  59. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/SearchTool.tsx +9 -59
  60. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ArtifactBody.tsx +2 -7
  61. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.test.ts +27 -28
  62. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.tsx +39 -26
  63. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ExcerptCode.tsx +8 -8
  64. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/HighlightedCode.tsx +35 -0
  65. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/JsonCodeBlock.tsx +4 -4
  66. package/src/client/components/workspace/diff-panel/MergeDiffView.tsx +179 -0
  67. package/src/client/components/workspace/diff-panel/ThemedMergeDiffView.tsx +24 -0
  68. package/src/client/components/workspace/diff-panel/Toolbar.tsx +224 -186
  69. package/src/client/components/workspace/diff-panel/ToolbarSegments.tsx +98 -0
  70. package/src/client/components/workspace/diff-panel/index.tsx +76 -145
  71. package/src/client/components/workspace/diff-panel/merge.ts +315 -0
  72. package/src/client/components/workspace/editor/Body.tsx +4 -7
  73. package/src/client/components/workspace/editor/CommandPalette.tsx +2 -2
  74. package/src/client/components/workspace/editor/FileDocument.tsx +58 -63
  75. package/src/client/components/workspace/editor/FilePane.tsx +4 -7
  76. package/src/client/components/workspace/editor/FindWidget.tsx +7 -2
  77. package/src/client/components/workspace/editor/Toolbar.tsx +220 -123
  78. package/src/client/components/workspace/editor/index.tsx +7 -9
  79. package/src/client/components/workspace/file-explorer/OutlineSection.test.tsx +18 -0
  80. package/src/client/components/workspace/file-explorer/OutlineSection.tsx +10 -3
  81. package/src/client/components/workspace/search-panel/Body.tsx +1 -1
  82. package/src/client/components/workspace/worktree-modal/index.test.tsx +118 -0
  83. package/src/client/hooks/chat/omp/reconnect.test.ts +338 -0
  84. package/src/client/hooks/chat/omp/reconnect.ts +187 -0
  85. package/src/client/hooks/chat/omp/run-liveness.test.ts +71 -0
  86. package/src/client/hooks/chat/omp/run-liveness.ts +65 -0
  87. package/src/client/hooks/chat/omp/session-list.activity.test.ts +114 -0
  88. package/src/client/hooks/chat/omp/session-list.test-util.ts +164 -0
  89. package/src/client/hooks/chat/omp/session-list.test.ts +30 -150
  90. package/src/client/hooks/chat/omp/session-list.tsx +69 -9
  91. package/src/client/hooks/chat/timeline/generating.ts +19 -2
  92. package/src/client/hooks/chat/timeline/index.ts +19 -24
  93. package/src/client/hooks/chat/timeline/lost-run.test.ts +329 -0
  94. package/src/client/hooks/chat/timeline/lost-run.ts +177 -0
  95. package/src/client/hooks/chat/timeline/session-load.test-util.ts +205 -0
  96. package/src/client/hooks/chat/timeline/session-load.test.ts +61 -162
  97. package/src/client/hooks/chat/timeline/session-load.ts +69 -37
  98. package/src/client/hooks/chat/timeline/session-switch.test.ts +65 -0
  99. package/src/client/hooks/chat/timeline/session-switch.ts +63 -0
  100. package/src/client/hooks/code/deferred-highlight.test.ts +182 -0
  101. package/src/client/hooks/code/deferred-highlight.ts +129 -0
  102. package/src/client/hooks/editor/find-state.ts +1 -1
  103. package/src/client/hooks/editor/theme.test.ts +206 -0
  104. package/src/client/hooks/editor/theme.ts +82 -0
  105. package/src/client/hooks/editor/use-editor-drop.test.ts +101 -0
  106. package/src/client/hooks/editor/use-editor-drop.ts +67 -14
  107. package/src/client/hooks/editor/use-editor-find.ts +7 -7
  108. package/src/client/hooks/editor/use-outline-view.test.ts +182 -0
  109. package/src/client/hooks/editor/use-outline-view.ts +94 -7
  110. package/src/client/hooks/editor/use-scroll-restore.test.ts +55 -2
  111. package/src/client/hooks/editor/use-scroll-restore.ts +124 -4
  112. package/src/client/hooks/editor/use-search-reveal.ts +1 -1
  113. package/src/client/hooks/ui/settings-modal.ts +56 -0
  114. package/src/client/hooks/ui/updates/shell-download.test.ts +2 -0
  115. package/src/client/hooks/ui/updates/shell-download.ts +55 -14
  116. package/src/client/hooks/ui/updates/shell-offer.test.ts +26 -0
  117. package/src/client/hooks/ui/updates/shell-update.test.ts +140 -0
  118. package/src/client/hooks/ui/updates/shell-update.ts +119 -0
  119. package/src/client/hooks/workspace/diff-file.ts +29 -10
  120. package/src/client/hooks/workspace/diff-git-actions.ts +147 -0
  121. package/src/client/hooks/workspace/editor-view.ts +61 -0
  122. package/src/client/hooks/workspace/file-tabs.test.ts +62 -5
  123. package/src/client/hooks/workspace/file-tabs.ts +24 -11
  124. package/src/client/hooks/workspace/navbar-session-actions.ts +72 -0
  125. package/src/client/hooks/workspace/worktree/index.ts +20 -5
  126. package/src/client/lib/desktop/bridge.ts +45 -0
  127. package/src/client/lib/search-reveal.ts +1 -1
  128. package/src/client/main.tsx +5 -1
  129. package/src/client/tailwind.css +37 -8
  130. package/src/server/lib/auth/guard.test.ts +28 -0
  131. package/src/server/lib/auth/guard.ts +10 -0
  132. package/src/server/lib/fs/worktree/create.ts +2 -3
  133. package/src/server/lib/fs/worktree/list.ts +13 -0
  134. package/src/server/lib/fs/worktree/remove.ts +3 -3
  135. package/src/server/lib/lifecycle/claim-port.test.ts +156 -0
  136. package/src/server/lib/lifecycle/port-guard.test.ts +20 -164
  137. package/src/server/lib/lifecycle/port-guard.ts +39 -7
  138. package/src/server/lib/lifecycle/probe-health.test.ts +166 -0
  139. package/src/server/lib/lifecycle/probe.test.ts +77 -164
  140. package/src/server/lib/lifecycle/probe.ts +121 -13
  141. package/src/server/lib/omp/rpc/frame-fold-signals.test.ts +112 -0
  142. package/src/server/lib/omp/rpc/frame-fold.ts +10 -5
  143. package/src/server/lib/omp/session/sidebar-activity.server.test.ts +162 -0
  144. package/src/server/lib/omp/session/sidebar-activity.server.ts +70 -0
  145. package/src/server/lib/omp/session/telemetry/compute.ts +13 -1
  146. package/src/server/lib/omp/session/telemetry.test.ts +33 -0
  147. package/src/server/lib/realtime/signals.server.ts +10 -0
  148. package/src/server/lib/realtime/topics/catalog.server.ts +4 -1
  149. package/src/server/lib/realtime/topics.server.ts +16 -0
  150. package/src/server/lib/updates/shell-assets.ts +94 -3
  151. package/src/server/lib/updates/shell-cache.ts +105 -0
  152. package/src/server/lib/updates/shell-pages.test.ts +33 -2
  153. package/src/server/lib/updates/shell-pages.ts +17 -5
  154. package/src/server/lib/updates/shell-release-desktop.test.ts +166 -0
  155. package/src/server/lib/updates/shell-release.test.ts +10 -27
  156. package/src/server/lib/updates/shell-release.ts +107 -111
  157. package/src/server/routes/fs/git-merge.test.ts +11 -1
  158. package/src/server/routes/fs/read.test.ts +24 -7
  159. package/src/server/routes/fs/scope.test.ts +15 -10
  160. package/src/server/routes/fs/scope.ts +3 -14
  161. package/src/server/routes/updates/android.ts +6 -1
  162. package/src/server/routes/updates/desktop.ts +29 -7
  163. package/src/server/routes/updates/ios.ts +5 -1
  164. package/src/server/routes/worktrees/list.ts +11 -1
  165. package/src/server/routes/worktrees/routes.test.ts +17 -0
  166. package/src/shared/lib/chat/timeline/omp-callbacks.test.ts +36 -0
  167. package/src/shared/lib/chat/timeline/omp-callbacks.ts +18 -0
  168. package/src/shared/lib/chat/timeline/run-signals.test.ts +2 -0
  169. package/src/shared/lib/code/editor/comment-syntax.test.ts +37 -0
  170. package/src/shared/lib/code/editor/comment-syntax.ts +55 -0
  171. package/src/shared/lib/code/editor/find-replace.test.ts +134 -0
  172. package/src/shared/lib/code/editor/{find.ts → find-replace.ts} +23 -107
  173. package/src/shared/lib/code/editor/{find.test.ts → find-scan.test.ts} +19 -73
  174. package/src/shared/lib/code/editor/find-scan.ts +177 -0
  175. package/src/shared/lib/code/editor/keymap.ts +3 -3
  176. package/src/shared/lib/code/editor/lines.test.ts +7 -55
  177. package/src/shared/lib/code/editor/lines.ts +8 -48
  178. package/src/shared/lib/code/editor/marks.test.ts +4 -5
  179. package/src/shared/lib/code/editor/marks.ts +13 -25
  180. package/src/shared/lib/code/editor/theme-loader.ts +97 -0
  181. package/src/shared/lib/code/editor/themes.generated.ts +156 -0
  182. package/src/shared/lib/code/editor/themes.test.ts +165 -0
  183. package/src/shared/lib/code/editor/themes.ts +165 -0
  184. package/src/shared/lib/code/highlight-memo.test.ts +103 -0
  185. package/src/shared/lib/code/highlight-memo.ts +149 -0
  186. package/src/shared/lib/code/highlighter-warmup.ts +112 -0
  187. package/src/shared/lib/code/highlighter.ts +3 -1
  188. package/src/shared/lib/code/idle.ts +37 -0
  189. package/src/shared/lib/code/outline/rows.test.ts +96 -0
  190. package/src/shared/lib/code/outline/rows.ts +94 -0
  191. package/src/shared/lib/code/syntax-highlight.test.ts +1 -79
  192. package/src/shared/lib/code/syntax-highlight.ts +2 -3
  193. package/src/shared/lib/fs/file-kind-classify.test.ts +25 -11
  194. package/src/shared/lib/fs/file-kind.ts +47 -13
  195. package/src/shared/lib/fs/file-url.test.ts +52 -0
  196. package/src/shared/lib/fs/file-url.ts +37 -0
  197. package/src/shared/lib/fs/patch-sides.test.ts +114 -0
  198. package/src/shared/lib/fs/patch-sides.ts +139 -0
  199. package/src/shared/lib/realtime/protocol.ts +16 -0
  200. package/src/shared/lib/ui/toolbar-overflow.test.ts +83 -0
  201. package/src/shared/lib/ui/toolbar-overflow.ts +86 -0
  202. package/src/shared/lib/updates/shell-arch.ts +54 -0
  203. package/src/shared/lib/updates/shell-ua.ts +4 -4
  204. package/src/shared/lib/workspace/editor-view-mode.test.ts +70 -0
  205. package/src/shared/lib/workspace/editor-view-mode.ts +48 -0
  206. package/src/shared/lib/workspace/panel-widths.test.ts +30 -0
  207. package/src/shared/lib/workspace/panel-widths.ts +25 -5
  208. package/src/shared/lib/workspace/session-state/cache.ts +159 -0
  209. package/src/shared/lib/workspace/session-state/harness.test-util.ts +55 -0
  210. package/src/shared/lib/workspace/session-state/merge.ts +100 -0
  211. package/src/shared/lib/workspace/session-state/store-load.test.ts +206 -0
  212. package/src/shared/lib/workspace/session-state/store.test.ts +17 -139
  213. package/src/shared/lib/workspace/session-state/store.ts +92 -130
  214. package/src/shared/lib/workspace/workspace-panel-widths.ts +124 -0
  215. package/src/shared/types/realtime.ts +29 -0
  216. package/src/shared/types/settings/state.ts +10 -0
  217. package/src/test-support/loopback.ts +183 -0
  218. package/dist/client/static/js/index-gtbpewr7.css +0 -1
  219. package/src/client/components/common/code-editor/handler.test.ts +0 -274
  220. package/src/client/components/common/code-editor/handler.ts +0 -175
  221. package/src/client/components/common/code-editor/index.tsx +0 -314
  222. package/src/client/components/common/code-surface/measure.ts +0 -148
  223. package/src/client/components/workspace/chat-timeline/target-picker/index.test.tsx +0 -227
  224. package/src/client/components/workspace/chat-timeline/target-picker/index.tsx +0 -260
  225. package/src/client/components/workspace/diff-panel/SplitView.tsx +0 -209
  226. package/src/client/components/workspace/diff-panel/UnifiedView.tsx +0 -150
  227. package/src/client/hooks/editor/scroll-container.test.ts +0 -102
  228. package/src/client/hooks/editor/scroll-container.ts +0 -26
  229. package/src/client/hooks/editor/use-code-window.ts +0 -100
  230. package/src/client/hooks/editor/use-editor-history.test.ts +0 -216
  231. package/src/client/hooks/editor/use-editor-history.ts +0 -134
  232. package/src/client/hooks/editor/use-occurrences.test.ts +0 -236
  233. package/src/client/hooks/editor/use-occurrences.ts +0 -98
  234. package/src/shared/lib/code/editor/commands.test.ts +0 -155
  235. package/src/shared/lib/code/editor/commands.ts +0 -260
  236. package/src/shared/lib/code/editor/edits.test.ts +0 -100
  237. package/src/shared/lib/code/editor/edits.ts +0 -122
  238. package/src/shared/lib/code/editor/history.test.ts +0 -118
  239. package/src/shared/lib/code/editor/history.ts +0 -101
  240. package/src/shared/lib/code/editor/layers.test.ts +0 -77
  241. package/src/shared/lib/code/editor/layers.ts +0 -67
  242. package/src/shared/lib/code/editor/line-edits.test.ts +0 -131
  243. package/src/shared/lib/code/editor/line-edits.ts +0 -159
  244. package/src/shared/lib/code/editor/occurrences.test.ts +0 -117
  245. package/src/shared/lib/code/editor/occurrences.ts +0 -111
  246. package/src/shared/lib/code/editor/reset-css.test.ts +0 -35
  247. package/src/shared/lib/code/editor/reset-css.ts +0 -32
  248. package/src/shared/lib/code/editor/text-commands.test.ts +0 -178
  249. package/src/shared/lib/code/editor/text-commands.ts +0 -154
  250. package/src/shared/lib/code/lazy-window.test.ts +0 -161
  251. package/src/shared/lib/code/lazy-window.ts +0 -156
  252. package/src/shared/lib/code/windowed-highlight.ts +0 -114
  253. package/src/shared/lib/fs/diff-parser.test.ts +0 -116
  254. package/src/shared/lib/fs/diff-parser.ts +0 -213
@@ -1,260 +0,0 @@
1
- /**
2
- * @license
3
- * SPDX-License-Identifier: Apache-2.0
4
- */
5
-
6
- /**
7
- * Where a chat that has not started will run: the workspace, or one of the
8
- * workspace's worktrees.
9
- *
10
- * One control rather than two, because both answers are the same question. The
11
- * menu is grouped exactly as OpenChamber's draft target is — the project root
12
- * first, then the two ways to make a worktree, then the worktrees that already
13
- * exist — and creating one here means the user does not have to leave the draft
14
- * to get it.
15
- *
16
- * The pick is `{folderId, worktreePath}`: choosing a folder CLEARS the worktree
17
- * (the groups are mutually exclusive), while choosing a worktree keeps its
18
- * folder, so the row the composer draws can always name both.
19
- */
20
-
21
- import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
22
- import { Check, ChevronDown, Folder, GitBranch } from 'lucide-preact';
23
- import { useOnClickOutside } from '@/client/hooks/ui/on-click-outside';
24
- import { useWorktrees } from '@/client/hooks/workspace/worktree/index';
25
- import type { SpawnTarget } from '@/client/hooks/chat/timeline/spawn-target';
26
- import { NewWorktreeModal } from '@/client/components/workspace/worktree-modal/index';
27
- import { createWorktree } from '@/client/hooks/workspace/worktree/index';
28
- import { lastPathSegment } from '@/shared/lib/fs/paths';
29
- import type { WorkspaceFolderData } from '@/shared/types';
30
-
31
- export interface TargetPickerProps {
32
- folders: WorkspaceFolderData[];
33
- selectedFolderId: number | null;
34
- setSelectedFolderId: (id: number | null) => void;
35
- target: SpawnTarget | null;
36
- setTarget: (next: SpawnTarget | null) => void;
37
- /**
38
- * Reports a completed create/removal to the caller (a toast). Optional: the
39
- * picker owns the flows, so a caller that only wants the pick needs nothing.
40
- */
41
- onReport?: (message: string) => void;
42
- }
43
-
44
- /** The trigger's words: the folder, plus the worktree when one is picked. */
45
- export function targetLabel(
46
- folders: WorkspaceFolderData[],
47
- selectedFolderId: number | null,
48
- target: SpawnTarget | null,
49
- ): string {
50
- const folder = folders.find((candidate) => String(candidate.id) === String(selectedFolderId));
51
- const name = folder?.name ?? 'Select Workspace Context';
52
- const worktree = lastPathSegment(target?.worktreePath);
53
- return worktree ? `${name} · ${worktree}` : name;
54
- }
55
-
56
- export function TargetPicker({
57
- folders,
58
- selectedFolderId,
59
- setSelectedFolderId,
60
- target,
61
- setTarget,
62
- onReport,
63
- }: TargetPickerProps) {
64
- const [open, setOpen] = useState(false);
65
- const [creating, setCreating] = useState(false);
66
- const [dialogFolder, setDialogFolder] = useState<WorkspaceFolderData | null>(null);
67
- const containerRef = useRef<HTMLDivElement>(null);
68
- useOnClickOutside(containerRef, () => setOpen(false));
69
-
70
- const selectedFolder = folders.find((candidate) => String(candidate.id) === String(selectedFolderId)) ?? null;
71
- const scopeRoot = selectedFolder?.project_path ?? null;
72
-
73
- // The list is read only while the menu is open: it costs a `git status` per
74
- // worktree, and a closed picker must never pay for it.
75
- const { worktrees, error, refresh } = useWorktrees(
76
- { root: scopeRoot, folderId: selectedFolder?.id ?? null },
77
- open && Boolean(scopeRoot),
78
- );
79
- useEffect(() => {
80
- if (open) void refresh();
81
- }, [open, refresh]);
82
-
83
- const list = useMemo(() => worktrees.filter((worktree) => !worktree.isPrimary), [worktrees]);
84
-
85
- const pickFolder = (folder: WorkspaceFolderData) => {
86
- setSelectedFolderId(folder.id);
87
- // A folder and a worktree are alternative answers, not a pair.
88
- setTarget(null);
89
- setOpen(false);
90
- };
91
-
92
- const pickWorktree = (folder: WorkspaceFolderData, worktreePath: string) => {
93
- setSelectedFolderId(folder.id);
94
- setTarget({ folderId: Number(folder.id), worktreePath });
95
- setOpen(false);
96
- };
97
-
98
- /**
99
- * Quick worktree: a generated branch and folder, created and selected without
100
- * leaving the draft. Reported rather than silent — the create takes a moment
101
- * and a pick that changed with no word reads as a mis-click.
102
- */
103
- const quickWorktree = async (folder: WorkspaceFolderData) => {
104
- if (!folder.project_path) return;
105
- setCreating(true);
106
- try {
107
- const created = await createWorktree(
108
- { root: folder.project_path, folderId: folder.id },
109
- { mode: 'new' },
110
- );
111
- pickWorktree(folder, created.path);
112
- onReport?.(`Worktree ${created.branch || lastPathSegment(created.path)} created`);
113
- } catch (error) {
114
- onReport?.(error instanceof Error ? error.message : String(error));
115
- } finally {
116
- setCreating(false);
117
- }
118
- };
119
-
120
- return (
121
- <div className="relative shrink-0" ref={containerRef}>
122
- <button
123
- type="button"
124
- onClick={() => setOpen((value) => !value)}
125
- aria-haspopup="menu"
126
- aria-expanded={open}
127
- className="group flex items-center space-x-1.5 px-1.5 py-1 rounded-md text-xs font-medium text-ink/75 hover:text-ink hover:bg-ink/5 focus-visible:bg-ink/5 transition-all outline-none cursor-pointer select-none"
128
- >
129
- {target?.worktreePath ? (
130
- <GitBranch size={13} className="text-ink/50 group-hover:text-ink/80 transition-colors flex-shrink-0" />
131
- ) : (
132
- <Folder size={13} className="text-ink/50 group-hover:text-ink/80 transition-colors flex-shrink-0" />
133
- )}
134
- <span className="flex items-center space-x-1 max-w-[260px] truncate tracking-tight">
135
- <span>{targetLabel(folders, selectedFolderId, target)}</span>
136
- </span>
137
- <ChevronDown
138
- size={13}
139
- className={`text-ink/40 group-hover:text-ink/70 flex-shrink-0 transition-transform duration-150 ${open ? 'rotate-180 text-ink/70' : ''}`}
140
- />
141
- </button>
142
-
143
- {open && (
144
- <div
145
- role="menu"
146
- className="absolute bottom-full left-0 mb-1.5 w-72 bg-paper border border-ink/10 rounded-lg shadow-lg z-50 flex flex-col overflow-hidden text-xs py-1"
147
- >
148
- <div className="px-3 py-1.5 text-[10px] uppercase tracking-wider text-ink/40 font-semibold border-b border-ink/5">
149
- Project root
150
- </div>
151
- <div className="max-h-56 scrollbar-overlay-container py-1">
152
- {folders.length === 0 ? (
153
- <div className="px-3 py-2 text-ink/40 italic">No workspaces available</div>
154
- ) : (
155
- folders.map((folder) => (
156
- <button
157
- key={folder.id}
158
- type="button"
159
- role="menuitemradio"
160
- aria-checked={selectedFolderId === folder.id && !target?.worktreePath}
161
- onClick={() => pickFolder(folder)}
162
- className={`w-full text-left px-3 py-1.5 flex items-center justify-between transition-colors cursor-pointer ${
163
- selectedFolderId === folder.id && !target?.worktreePath
164
- ? 'bg-ink/10 text-ink font-semibold'
165
- : 'text-ink/80 hover:bg-ink/5 hover:text-ink'
166
- }`}
167
- >
168
- <span className="flex items-center space-x-2 truncate pr-2">
169
- <Folder size={12} className={selectedFolderId === folder.id ? 'text-ink flex-shrink-0' : 'text-ink/50 flex-shrink-0'} />
170
- <span className="truncate font-medium">{folder.name}</span>
171
- </span>
172
- {selectedFolderId === folder.id && !target?.worktreePath && <Check size={12} className="text-ink flex-shrink-0" />}
173
- </button>
174
- ))
175
- )}
176
- </div>
177
-
178
- {/* Making one comes before the list, so a long list never hides it. */}
179
- {selectedFolder && scopeRoot && (
180
- <>
181
- <div className="border-t border-ink/5 px-3 py-1.5 text-[10px] uppercase tracking-wider text-ink/40 font-semibold">
182
- Worktrees
183
- </div>
184
- <button
185
- type="button"
186
- role="menuitem"
187
- disabled={creating}
188
- onClick={() => void quickWorktree(selectedFolder)}
189
- className="w-full text-left px-3 py-1.5 flex items-center gap-2 text-ink/80 hover:bg-ink/5 hover:text-ink disabled:opacity-50"
190
- >
191
- <span aria-hidden="true">⚡</span>
192
- {creating ? 'Creating…' : 'Quick worktree'}
193
- </button>
194
- <button
195
- type="button"
196
- role="menuitem"
197
- onClick={() => {
198
- setOpen(false);
199
- setDialogFolder(selectedFolder);
200
- }}
201
- className="w-full text-left px-3 py-1.5 flex items-center gap-2 text-ink/80 hover:bg-ink/5 hover:text-ink"
202
- >
203
- <GitBranch size={12} className="text-ink/50" />
204
- New worktree…
205
- </button>
206
- <div className="max-h-56 scrollbar-overlay-container py-1">
207
- {list.length === 0 ? (
208
- <div className="px-3 py-2 text-ink/40 italic">
209
- {error ? 'Worktrees could not be read' : 'No worktrees yet'}
210
- </div>
211
- ) : (
212
- list.map((worktree) => (
213
- <button
214
- key={worktree.path}
215
- type="button"
216
- role="menuitemradio"
217
- aria-checked={target?.worktreePath === worktree.path}
218
- onClick={() => pickWorktree(selectedFolder, worktree.path)}
219
- title={worktree.path}
220
- className={`w-full text-left px-3 py-1.5 flex items-center justify-between transition-colors cursor-pointer ${
221
- target?.worktreePath === worktree.path
222
- ? 'bg-ink/10 text-ink font-semibold'
223
- : 'text-ink/80 hover:bg-ink/5 hover:text-ink'
224
- }`}
225
- >
226
- <span className="flex items-center gap-2 truncate pr-2">
227
- <GitBranch size={12} className="text-ink/50 flex-shrink-0" />
228
- <span className="truncate font-medium">{worktree.branch || 'detached'}</span>
229
- {worktree.missing && <span className="text-warning shrink-0">missing</span>}
230
- {worktree.dirty > 0 && (
231
- <span className="text-ink/40 shrink-0" title={`${worktree.dirty} uncommitted files`}>
232
- ●{worktree.dirty}
233
- </span>
234
- )}
235
- </span>
236
- {target?.worktreePath === worktree.path && <Check size={12} className="text-ink flex-shrink-0" />}
237
- </button>
238
- ))
239
- )}
240
- </div>
241
- </>
242
- )}
243
- </div>
244
- )}
245
-
246
- <NewWorktreeModal
247
- isOpen={dialogFolder !== null}
248
- onClose={() => setDialogFolder(null)}
249
- root={dialogFolder?.project_path ?? null}
250
- folderId={dialogFolder ? Number(dialogFolder.id) : null}
251
- folderName={dialogFolder?.name}
252
- onCreated={(created) => {
253
- if (!dialogFolder) return;
254
- pickWorktree(dialogFolder, created.path);
255
- onReport?.(`Worktree ${created.branch} created`);
256
- }}
257
- />
258
- </div>
259
- );
260
- }
@@ -1,209 +0,0 @@
1
- import { useMemo } from 'preact/hooks';
2
- import type { SplitDiffRow } from '@/shared/lib/fs/diff-parser';
3
- import { highlightLines } from '@/shared/lib/code/syntax-highlight';
4
- import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
5
- import { useScrollRestore } from '@/client/hooks/editor/use-scroll-restore';
6
-
7
- interface SplitViewProps {
8
- rows: SplitDiffRow[];
9
- language: string;
10
- /** Wrap long lines instead of scrolling horizontally. */
11
- wordWrap: boolean;
12
- /**
13
- * The diff tab these rows belong to, so its scroll offset is remembered — or
14
- * null for a caller with no tab of its own (a diff inside a chat tool card,
15
- * where the timeline owns the scrolling).
16
- */
17
- tabKey: string | null;
18
- }
19
-
20
- export function SplitView({ rows, language, wordWrap, tabKey }: SplitViewProps) {
21
- const syntaxReady = useSyntaxReady();
22
- // Its own offset: split rows and unified lines are different heights, and
23
- // switching shape must not carry one view's position onto the other's rows.
24
- const scroll = useScrollRestore({ tabKey, surface: 'split' });
25
-
26
- const htmlByIndex = useMemo(() => {
27
- const left = new Array<string>(rows.length).fill('');
28
- const right = new Array<string>(rows.length).fill('');
29
- let start = -1;
30
- const flush = (end: number) => {
31
- if (start < 0) return;
32
- const group = rows.slice(start, end);
33
- const leftHtml = highlightLines(group.map((r) => r.left?.text ?? '').join('\n'), language);
34
- const rightHtml = highlightLines(group.map((r) => r.right?.text ?? '').join('\n'), language);
35
- for (let i = start; i < end; i++) {
36
- left[i] = leftHtml[i - start] ?? '';
37
- right[i] = rightHtml[i - start] ?? '';
38
- }
39
- start = -1;
40
- };
41
- rows.forEach((row, idx) => {
42
- if (row.isMeta) flush(idx);
43
- else if (start < 0) start = idx;
44
- });
45
- flush(rows.length);
46
- return { left, right };
47
- }, [rows, language, syntaxReady]);
48
-
49
- if (rows.length === 0) {
50
- return (
51
- <div className="flex-1 flex items-center justify-center text-ink/40 font-mono text-xs p-8">
52
- No differences between working file and target.
53
- </div>
54
- );
55
- }
56
-
57
- return (
58
- <div
59
- ref={scroll.ref}
60
- onScroll={scroll.onScroll}
61
- data-testid="diff-scroller"
62
- className="w-full h-full overflow-auto font-mono text-xs select-text bg-paper text-ink code-surface"
63
- >
64
- {/* The room under the last row belongs to the content, not the scroller:
65
- a scroll container's own bottom padding is not part of its scrollable
66
- overflow, so the padding has to sit inside it to have any effect. */}
67
- <div className={`pb-4 ${wordWrap ? 'w-full' : 'w-max min-w-full min-w-[700px]'}`}>
68
- <div className="grid grid-cols-2 sticky top-0 z-10 bg-canvas border-b border-ink/10 text-[11px] text-ink/70 font-sans select-none shadow-xs">
69
- <div className="px-3 py-1 font-medium border-r border-ink/10 flex items-center justify-between">
70
- <span>Original (HEAD / Base)</span>
71
- </div>
72
- <div className="px-3 py-1 font-medium flex items-center justify-between">
73
- <span>Modified (Working Tree)</span>
74
- </div>
75
- </div>
76
-
77
- {/* Wrap needs `table-fixed`: auto layout sizes a column by its widest
78
- cell's UNAMBIGUOUS minimum (`overflow-wrap: break-word` does not
79
- lower it), so a long line would widen the table and scroll instead of
80
- wrapping. Fixed layout takes its widths from the FIRST row — which
81
- here is the `colSpan={4}` meta row, so every column came out equal
82
- (measured 99px) and the line-number gutters grew to the width of a
83
- code column. The colgroup states them instead: two 48px gutters and
84
- the two content columns sharing the rest. */}
85
- <table className={`border-collapse ${wordWrap ? 'table-fixed w-full' : 'w-full'}`}>
86
- {wordWrap && (
87
- <colgroup>
88
- <col className="w-12" />
89
- <col />
90
- <col className="w-12" />
91
- <col />
92
- </colgroup>
93
- )}
94
- <tbody>
95
- {rows.map((row, idx) => {
96
- if (row.isMeta) {
97
- return (
98
- <tr key={idx} className="bg-canvas border-y border-ink/10 text-meta select-none">
99
- <td colSpan={4} className="py-1 px-3 text-[11px] font-semibold text-meta text-center font-mono">
100
- {row.metaText}
101
- </td>
102
- </tr>
103
- );
104
- }
105
-
106
- const left = row.left;
107
- const right = row.right;
108
-
109
- const isLeftDel = left?.type === 'del';
110
- const isRightAdd = right?.type === 'add';
111
-
112
- return (
113
- <tr key={idx} className="border-b border-ink/[0.04]">
114
- {/* Left Line Number */}
115
- <td
116
- className={`w-12 py-0.5 px-2 text-right text-[10px] select-none border-r border-ink/10 font-mono ${
117
- wordWrap ? 'align-top' : ''
118
- } ${
119
- isLeftDel
120
- ? 'bg-error/15 text-error font-semibold'
121
- : left
122
- ? 'bg-canvas/50 text-ink/40'
123
- : 'bg-canvas/80 text-transparent'
124
- }`}
125
- >
126
- {left?.lineNumber || ''}
127
- </td>
128
-
129
- {/* Left Content (Original) */}
130
- <td
131
- className={`py-0.5 px-2 border-r border-ink/10 text-ink ${
132
- wordWrap ? 'whitespace-pre-wrap break-words align-top' : 'min-w-[320px] whitespace-pre'
133
- } ${
134
- isLeftDel
135
- ? 'bg-error/10'
136
- : left
137
- ? 'hover:bg-ink/[0.02]'
138
- : 'bg-canvas/40 select-none'
139
- }`}
140
- >
141
- {left ? (
142
- <>
143
- <span className={`inline-block w-4 text-center select-none font-bold ${isLeftDel ? 'text-error' : 'opacity-0'}`}>
144
- {isLeftDel ? '-' : ' '}
145
- </span>
146
- <span
147
- className="shiki"
148
- dangerouslySetInnerHTML={{
149
- __html: left.text ? htmlByIndex.left[idx] || '&nbsp;' : '&nbsp;',
150
- }}
151
- />
152
- </>
153
- ) : (
154
- <span className="opacity-0">&nbsp;</span>
155
- )}
156
- </td>
157
-
158
- {/* Right Line Number */}
159
- <td
160
- className={`w-12 py-0.5 px-2 text-right text-[10px] select-none border-r border-ink/10 font-mono ${
161
- wordWrap ? 'align-top' : ''
162
- } ${
163
- isRightAdd
164
- ? 'bg-success/15 text-success font-semibold'
165
- : right
166
- ? 'bg-canvas/50 text-ink/40'
167
- : 'bg-canvas/80 text-transparent'
168
- }`}
169
- >
170
- {right?.lineNumber || ''}
171
- </td>
172
-
173
- {/* Right Content (Modified) */}
174
- <td
175
- className={`py-0.5 px-2 text-ink ${
176
- wordWrap ? 'whitespace-pre-wrap break-words align-top' : 'min-w-[320px] whitespace-pre'
177
- } ${
178
- isRightAdd
179
- ? 'bg-success/10'
180
- : right
181
- ? 'hover:bg-ink/[0.02]'
182
- : 'bg-canvas/40 select-none'
183
- }`}
184
- >
185
- {right ? (
186
- <>
187
- <span className={`inline-block w-4 text-center select-none font-bold ${isRightAdd ? 'text-success' : 'opacity-0'}`}>
188
- {isRightAdd ? '+' : ' '}
189
- </span>
190
- <span
191
- className="shiki"
192
- dangerouslySetInnerHTML={{
193
- __html: right.text ? htmlByIndex.right[idx] || '&nbsp;' : '&nbsp;',
194
- }}
195
- />
196
- </>
197
- ) : (
198
- <span className="opacity-0">&nbsp;</span>
199
- )}
200
- </td>
201
- </tr>
202
- );
203
- })}
204
- </tbody>
205
- </table>
206
- </div>
207
- </div>
208
- );
209
- }
@@ -1,150 +0,0 @@
1
- import { useMemo } from 'preact/hooks';
2
- import type { DiffLine } from '@/shared/lib/fs/diff-parser';
3
- import { highlightLines } from '@/shared/lib/code/syntax-highlight';
4
- import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
5
- import { useScrollRestore } from '@/client/hooks/editor/use-scroll-restore';
6
-
7
- interface UnifiedViewProps {
8
- lines: DiffLine[];
9
- language: string;
10
- /** Wrap long lines instead of scrolling horizontally. */
11
- wordWrap: boolean;
12
- /**
13
- * The diff tab these rows belong to, so its scroll offset is remembered — or
14
- * null for a caller with no tab of its own (a diff inside a chat tool card,
15
- * where the timeline owns the scrolling).
16
- */
17
- tabKey: string | null;
18
- }
19
-
20
- export function UnifiedView({ lines, language, wordWrap, tabKey }: UnifiedViewProps) {
21
- const syntaxReady = useSyntaxReady();
22
- // The unified and split shapes are different row counts and heights for the
23
- // same diff, so each keeps its own offset (see `ScrollSurface`).
24
- const scroll = useScrollRestore({ tabKey, surface: 'unified' });
25
-
26
- const htmlByIndex = useMemo(() => {
27
- const out = new Array<string>(lines.length).fill('');
28
- let start = -1;
29
- const flush = (end: number) => {
30
- if (start < 0) return;
31
- const html = highlightLines(lines.slice(start, end).map((l) => l.text).join('\n'), language);
32
- for (let i = start; i < end; i++) out[i] = html[i - start] ?? '';
33
- start = -1;
34
- };
35
- lines.forEach((line, idx) => {
36
- if (line.type === 'meta') flush(idx);
37
- else if (start < 0) start = idx;
38
- });
39
- flush(lines.length);
40
- return out;
41
- }, [lines, language, syntaxReady]);
42
-
43
- if (lines.length === 0) {
44
- return (
45
- <div className="flex-1 flex items-center justify-center text-ink/40 font-mono text-xs p-8">
46
- No differences between working file and target.
47
- </div>
48
- );
49
- }
50
-
51
- return (
52
- <div
53
- ref={scroll.ref}
54
- onScroll={scroll.onScroll}
55
- data-testid="diff-scroller"
56
- className="w-full h-full overflow-auto font-mono text-xs select-text bg-paper text-ink code-surface"
57
- >
58
- {/* The room under the last row belongs to the content, not the scroller:
59
- a scroll container's own bottom padding is not part of its scrollable
60
- overflow, so the padding has to sit inside it to have any effect. */}
61
- <div className={`pb-4 ${wordWrap ? 'w-full' : 'w-max min-w-full'}`}>
62
- {/* Wrap needs `table-fixed`: auto layout sizes a column by its widest
63
- cell's UNAMBIGUOUS minimum (`overflow-wrap: break-word` does not
64
- lower it), so a long line would still widen the table and scroll
65
- instead of wrapping. Fixed layout takes the width from the row's
66
- own box, which is what makes the cell's wrap rule effective. */}
67
- <table className={`border-collapse ${wordWrap ? 'table-fixed w-full' : 'w-max min-w-full'}`}>
68
- <tbody>
69
- {lines.map((line, idx) => {
70
- if (line.type === 'meta') {
71
- return (
72
- <tr key={idx} className="bg-canvas border-y border-ink/10 text-meta select-none">
73
- <td className="w-12 text-center py-1 px-2 text-[10px] opacity-60 border-r border-ink/10 font-mono">...</td>
74
- <td className="w-12 text-center py-1 px-2 text-[10px] opacity-60 border-r border-ink/10 font-mono">...</td>
75
- <td className="py-1 px-3 text-[11px] font-semibold text-meta font-mono tracking-tight">{line.text}</td>
76
- </tr>
77
- );
78
- }
79
-
80
- const isAdd = line.type === 'add';
81
- const isDel = line.type === 'del';
82
-
83
- return (
84
- <tr
85
- key={idx}
86
- className={`transition-colors group ${
87
- isAdd
88
- ? 'bg-success/10 hover:bg-success/15'
89
- : isDel
90
- ? 'bg-error/10 hover:bg-error/15'
91
- : 'hover:bg-ink/[0.03]'
92
- }`}
93
- >
94
- {/* Old line number. Top-aligned when the row wraps, or the
95
- number would float to the middle of a multi-line cell
96
- instead of naming its first line. */}
97
- <td
98
- className={`w-12 py-0.5 px-2 text-right text-[10px] select-none border-r border-ink/10 font-mono ${
99
- wordWrap ? 'align-top' : ''
100
- } ${
101
- isDel
102
- ? 'bg-error/15 text-error font-semibold'
103
- : isAdd
104
- ? 'bg-success/15 text-success/70'
105
- : 'bg-canvas/50 text-ink/40'
106
- }`}
107
- >
108
- {line.oldLineNumber || ''}
109
- </td>
110
-
111
- {/* New line number */}
112
- <td
113
- className={`w-12 py-0.5 px-2 text-right text-[10px] select-none border-r border-ink/10 font-mono ${
114
- wordWrap ? 'align-top' : ''
115
- } ${
116
- isAdd
117
- ? 'bg-success/15 text-success font-semibold'
118
- : isDel
119
- ? 'bg-error/15 text-error/70'
120
- : 'bg-canvas/50 text-ink/40'
121
- }`}
122
- >
123
- {line.newLineNumber || ''}
124
- </td>
125
-
126
- {/* Change prefix & code text */}
127
- <td className={`py-0.5 px-3 text-ink pr-6 ${wordWrap ? 'whitespace-pre-wrap break-words align-top' : 'whitespace-pre'}`}>
128
- <span
129
- className={`inline-block w-4 text-center select-none font-bold ${
130
- isAdd ? 'text-success' : isDel ? 'text-error' : 'opacity-0'
131
- }`}
132
- >
133
- {isAdd ? '+' : isDel ? '-' : ' '}
134
- </span>
135
- <span
136
- className="shiki"
137
- dangerouslySetInnerHTML={{
138
- __html: line.text ? htmlByIndex[idx] || '&nbsp;' : '&nbsp;',
139
- }}
140
- />
141
- </td>
142
- </tr>
143
- );
144
- })}
145
- </tbody>
146
- </table>
147
- </div>
148
- </div>
149
- );
150
- }