@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,4 +1,20 @@
1
- import { AlertCircle, Check, Copy, Download, Eye, EyeOff, Maximize, Minimize, Save, Search, WrapText, ZoomIn, ZoomOut } from 'lucide-preact';
1
+ import {
2
+ AlertCircle,
3
+ Check,
4
+ Copy,
5
+ Download,
6
+ Eye,
7
+ EyeOff,
8
+ Maximize,
9
+ Minimize,
10
+ Save,
11
+ Search,
12
+ WrapText,
13
+ ZoomIn,
14
+ ZoomOut,
15
+ } from 'lucide-preact';
16
+ import { EditorViewButtons, useEditorViewActions } from '@/client/components/common/editor-view-buttons';
17
+ import { ToolbarRow, type ToolbarCluster } from '@/client/components/common/toolbar-row';
2
18
 
3
19
  export type EditorSaveStatus = 'idle' | 'saving' | 'saved' | 'error' | 'needs-permission';
4
20
 
@@ -94,146 +110,227 @@ export function EditorToolbar({
94
110
  ? 'Save failed — the file on disk was not changed'
95
111
  : 'Save file (Ctrl+S)';
96
112
 
97
- return (
98
- // `@container`: the path below hides on a NARROW PANEL, not a narrow
99
- // viewport. At the editor panel's 320px floor the action strip is 273px of
100
- // `flex-shrink-0`, which left the path a measured 23px — a truncated
101
- // fragment of a filename is not information, and the tab above already
102
- // carries it.
103
- <div className="@container flex items-center justify-between px-3 py-2 border-b border-ink/10 bg-paper flex-shrink-0">
104
- {/* Left status */}
105
- <div className="hidden @[420px]:flex items-center space-x-2 text-xs font-mono text-ink/40 min-w-0">
106
- <span className="truncate max-w-[300px]" title={path}>{path}</span>
107
- </div>
113
+ const saveButton = (
114
+ <button
115
+ type="button"
116
+ onClick={onSave}
117
+ disabled={saveDisabled || saveStatus === 'saving'}
118
+ className={`${ACTION_CLASS} hover:bg-ink/5`}
119
+ title={saveTitle}
120
+ aria-label={saveTitle}
121
+ >
122
+ {saveStatus === 'saving' ? (
123
+ <Save size={14} className="text-amber-500 animate-pulse" />
124
+ ) : saveStatus === 'saved' ? (
125
+ <Check size={14} className="text-success" />
126
+ ) : saveStatus === 'error' ? (
127
+ <AlertCircle size={14} className="text-error" />
128
+ ) : saveStatus === 'needs-permission' ? (
129
+ // Not an error: the buffer is fine and one click grants access.
130
+ <Save size={14} className="text-warning" />
131
+ ) : (
132
+ <Save size={14} className="hover:text-ink" />
133
+ )}
134
+ </button>
135
+ );
108
136
 
109
- {/* Right actions. `ml-auto` keeps them right-aligned once the path block
110
- above is hidden, which `justify-between` alone would not do (a single
111
- child sits at the start). */}
112
- <div className="flex items-center space-x-1 text-ink/40 flex-shrink-0 ml-auto">
113
- {!byteViewer && (
114
- <div className="flex items-center pr-2 mr-1 border-r border-ink/10">
115
- <button
116
- type="button"
117
- onClick={onSave}
118
- disabled={saveDisabled || saveStatus === 'saving'}
119
- className={`${ACTION_CLASS} hover:bg-ink/5`}
120
- title={saveTitle}
121
- aria-label={saveTitle}
122
- >
123
- {saveStatus === 'saving' ? (
124
- <Save size={14} className="text-amber-500 animate-pulse" />
125
- ) : saveStatus === 'saved' ? (
126
- <Check size={14} className="text-success" />
127
- ) : saveStatus === 'error' ? (
128
- <AlertCircle size={14} className="text-error" />
129
- ) : saveStatus === 'needs-permission' ? (
130
- // Not an error: the buffer is fine and one click grants access.
131
- <Save size={14} className="text-warning" />
132
- ) : (
133
- <Save size={14} className="hover:text-ink" />
134
- )}
135
- </button>
136
- </div>
137
- )}
137
+ const copyLabel = copied ? 'Copied' : byteViewer ? 'Copy image' : 'Copy content';
138
+ const copyButton = (
139
+ <button
140
+ type="button"
141
+ onClick={onCopy}
142
+ className={`${ACTION_CLASS} hover:bg-ink/5 hover:text-ink`}
143
+ title={copyLabel}
144
+ aria-label={copyLabel}
145
+ >
146
+ {copied ? <Check size={14} className="text-success" /> : <Copy size={14} />}
147
+ </button>
148
+ );
138
149
 
139
- {hasPreview && (
140
- <div className="flex items-center pr-2 mr-1 border-r border-ink/10">
141
- <button
142
- type="button"
143
- onClick={onTogglePreview}
144
- className={`${ACTION_CLASS} hover:bg-ink/5 ${isPreview ? 'text-ink' : 'hover:text-ink'}`}
145
- title={isPreview ? 'Show source' : 'Show preview'}
146
- aria-label={isPreview ? 'Show source' : 'Show preview'}
147
- aria-pressed={isPreview}
148
- >
149
- {isPreview ? <EyeOff size={14} /> : <Eye size={14} />}
150
- </button>
151
- </div>
152
- )}
150
+ const downloadButton = (
151
+ <button
152
+ type="button"
153
+ onClick={onDownload}
154
+ className={`${ACTION_CLASS} hover:bg-ink/5 hover:text-ink`}
155
+ title="Download file"
156
+ aria-label="Download file"
157
+ >
158
+ <Download size={14} />
159
+ </button>
160
+ );
153
161
 
154
- {!byteViewer && (
155
- <>
156
- <div className="flex items-center pr-2 mr-1 border-r border-ink/10">
157
- <button
158
- type="button"
159
- onClick={onToggleFind}
160
- className={`${ACTION_CLASS} hover:bg-ink/5 ${findOpen ? 'text-ink' : 'hover:text-ink'}`}
161
- title="Find and replace (Ctrl+F)"
162
- aria-label="Find and replace"
163
- aria-pressed={findOpen}
164
- >
165
- <Search size={14} />
166
- </button>
167
- </div>
162
+ const maximizeLabel = isMaximized ? 'Exit full screen' : 'Full screen';
163
+ const maximizeButton = (
164
+ <button
165
+ type="button"
166
+ onClick={onToggleMaximize}
167
+ className={`${ACTION_CLASS} hover:bg-ink/5 hover:text-ink ${isMaximized ? 'text-ink' : ''}`}
168
+ title={maximizeLabel}
169
+ aria-label={maximizeLabel}
170
+ aria-pressed={isMaximized}
171
+ >
172
+ {isMaximized ? <Minimize size={14} /> : <Maximize size={14} />}
173
+ </button>
174
+ );
168
175
 
169
- <div className="flex items-center pr-2 mr-1 border-r border-ink/10">
170
- <button
171
- type="button"
172
- onClick={onToggleWordWrap}
173
- className={`${ACTION_CLASS} hover:bg-ink/5 ${wordWrap ? 'text-ink' : 'hover:text-ink'}`}
174
- title={wordWrap ? 'Word wrap on' : 'Word wrap off'}
175
- aria-label={wordWrap ? 'Word wrap on' : 'Word wrap off'}
176
- aria-pressed={wordWrap}
177
- >
178
- <WrapText size={14} />
179
- </button>
180
- </div>
176
+ const viewModeActions = useEditorViewActions();
177
+ const findLabel = 'Find and replace';
178
+ const wordWrapLabel = wordWrap ? 'Word wrap on' : 'Word wrap off';
179
+ const previewLabel = isPreview ? 'Show source' : 'Show preview';
181
180
 
182
- <div className="flex items-center pr-2 mr-1 border-r border-ink/10">
183
- <button
184
- type="button"
185
- onClick={onZoomOut}
186
- className={`${ACTION_CLASS} hover:bg-ink/5 hover:text-ink`}
187
- title="Decrease font size"
188
- aria-label="Decrease font size"
189
- >
190
- <ZoomOut size={14} />
191
- </button>
192
- <button
193
- type="button"
194
- onClick={onZoomIn}
195
- className={`${ACTION_CLASS} hover:bg-ink/5 hover:text-ink`}
196
- title="Increase font size"
197
- aria-label="Increase font size"
198
- >
199
- <ZoomIn size={14} />
200
- </button>
201
- </div>
202
- </>
203
- )}
181
+ /**
182
+ * The controls, as clusters that hide as a unit.
183
+ *
184
+ * ORDER is the hiding order, most important first: the row keeps the width
185
+ * modes and full screen (the controls this panel is FOR), then copy/download,
186
+ * and the reading aids (find, wrap, zoom, preview) move into the `⋯` menu on a
187
+ * narrow panel. Grouping is the existing one — zoom is a pair, copy/download is
188
+ * a pair, the modes are a pair — because splitting a pair leaves half of it on
189
+ * the row.
190
+ */
191
+ const clusters: ToolbarCluster[] = [];
204
192
 
205
- <div className="flex items-center pr-2 mr-1 border-r border-ink/10">
193
+ if (!byteViewer) {
194
+ clusters.push({
195
+ id: 'save',
196
+ // The primary action of the panel: a menu row must never be the only way
197
+ // to save a file.
198
+ pinned: true,
199
+ actions: [{ label: 'Save file', icon: Save, onSelect: onSave, disabled: saveDisabled }],
200
+ children: saveButton,
201
+ });
202
+ }
203
+ if (hasPreview) {
204
+ clusters.push({
205
+ id: 'preview',
206
+ actions: [{ label: previewLabel, icon: isPreview ? EyeOff : Eye, onSelect: onTogglePreview, active: isPreview }],
207
+ children: (
208
+ <button
209
+ type="button"
210
+ onClick={onTogglePreview}
211
+ className={`${ACTION_CLASS} hover:bg-ink/5 ${isPreview ? 'text-ink' : 'hover:text-ink'}`}
212
+ title={previewLabel}
213
+ aria-label={previewLabel}
214
+ aria-pressed={isPreview}
215
+ >
216
+ {isPreview ? <EyeOff size={14} /> : <Eye size={14} />}
217
+ </button>
218
+ ),
219
+ });
220
+ }
221
+ if (!byteViewer) {
222
+ clusters.push({
223
+ id: 'find',
224
+ actions: [{ label: findLabel, icon: Search, onSelect: onToggleFind, active: findOpen }],
225
+ children: (
226
+ <button
227
+ type="button"
228
+ onClick={onToggleFind}
229
+ className={`${ACTION_CLASS} hover:bg-ink/5 ${findOpen ? 'text-ink' : 'hover:text-ink'}`}
230
+ title={`${findLabel} (Ctrl+F)`}
231
+ aria-label={findLabel}
232
+ aria-pressed={findOpen}
233
+ >
234
+ <Search size={14} />
235
+ </button>
236
+ ),
237
+ });
238
+ clusters.push({
239
+ id: 'wrap',
240
+ actions: [{ label: wordWrapLabel, icon: WrapText, onSelect: onToggleWordWrap, active: wordWrap }],
241
+ children: (
242
+ <button
243
+ type="button"
244
+ onClick={onToggleWordWrap}
245
+ className={`${ACTION_CLASS} hover:bg-ink/5 ${wordWrap ? 'text-ink' : 'hover:text-ink'}`}
246
+ title={wordWrapLabel}
247
+ aria-label={wordWrapLabel}
248
+ aria-pressed={wordWrap}
249
+ >
250
+ <WrapText size={14} />
251
+ </button>
252
+ ),
253
+ });
254
+ clusters.push({
255
+ id: 'zoom',
256
+ actions: [
257
+ { label: 'Decrease font size', icon: ZoomOut, onSelect: onZoomOut },
258
+ { label: 'Increase font size', icon: ZoomIn, onSelect: onZoomIn },
259
+ ],
260
+ children: (
261
+ <>
206
262
  <button
207
263
  type="button"
208
- onClick={onCopy}
264
+ onClick={onZoomOut}
209
265
  className={`${ACTION_CLASS} hover:bg-ink/5 hover:text-ink`}
210
- title={copied ? 'Copied' : byteViewer ? 'Copy image' : 'Copy content'}
211
- aria-label={copied ? 'Copied' : byteViewer ? 'Copy image' : 'Copy content'}
266
+ title="Decrease font size"
267
+ aria-label="Decrease font size"
212
268
  >
213
- {copied ? <Check size={14} className="text-success" /> : <Copy size={14} />}
269
+ <ZoomOut size={14} />
214
270
  </button>
215
271
  <button
216
272
  type="button"
217
- onClick={onDownload}
273
+ onClick={onZoomIn}
218
274
  className={`${ACTION_CLASS} hover:bg-ink/5 hover:text-ink`}
219
- title="Download file"
220
- aria-label="Download file"
275
+ title="Increase font size"
276
+ aria-label="Increase font size"
221
277
  >
222
- <Download size={14} />
278
+ <ZoomIn size={14} />
223
279
  </button>
224
- </div>
280
+ </>
281
+ ),
282
+ });
283
+ }
225
284
 
226
- <button
227
- type="button"
228
- onClick={onToggleMaximize}
229
- className={`${ACTION_CLASS} hover:bg-ink/5 hover:text-ink ${isMaximized ? 'text-ink' : ''}`}
230
- title={isMaximized ? 'Exit full screen' : 'Full screen'}
231
- aria-label={isMaximized ? 'Exit full screen' : 'Full screen'}
232
- aria-pressed={isMaximized}
233
- >
234
- {isMaximized ? <Minimize size={14} /> : <Maximize size={14} />}
235
- </button>
285
+ clusters.push({
286
+ id: 'copy',
287
+ actions: [
288
+ { label: copyLabel, icon: Copy, onSelect: onCopy },
289
+ { label: 'Download file', icon: Download, onSelect: onDownload },
290
+ ],
291
+ children: (
292
+ <>
293
+ {copyButton}
294
+ {downloadButton}
295
+ </>
296
+ ),
297
+ });
298
+
299
+ // The width modes and full SCREEN are the controls this panel exists for, so
300
+ // they are the LAST clusters — the tail is what the menu takes. Three controls
301
+ // about one axis, three different glyphs: compact collapses the chat (a
302
+ // crossed-out message), full collapses the chat and the sidebar (a closing
303
+ // panel edge), and full screen covers the right panel too (a maximise frame).
304
+ clusters.push({
305
+ id: 'view-modes',
306
+ // The two width modes are why this toolbar grew these buttons, and a mode
307
+ // reachable only from a menu is a mode half the readers never find.
308
+ pinned: true,
309
+ actions: viewModeActions,
310
+ children: <EditorViewButtons size={14} />,
311
+ });
312
+ clusters.push({
313
+ id: 'maximize',
314
+ pinned: true,
315
+ actions: [{ label: maximizeLabel, icon: isMaximized ? Minimize : Maximize, onSelect: onToggleMaximize, active: isMaximized }],
316
+ children: maximizeButton,
317
+ });
318
+
319
+ return (
320
+ // `@container`: the path below hides on a NARROW PANEL, not a narrow
321
+ // viewport. At the editor panel's 320px floor the path would be a truncated
322
+ // fragment of a filename — not information, and the tab above already
323
+ // carries it.
324
+ <div className="@container flex items-center justify-between gap-2 px-3 py-2 border-b border-ink/10 bg-paper flex-shrink-0">
325
+ {/* Left status */}
326
+ <div className="hidden @[420px]:flex items-center space-x-2 text-xs font-mono text-ink/40 min-w-0">
327
+ <span className="truncate max-w-[300px]" title={path}>{path}</span>
236
328
  </div>
329
+
330
+ {/* One row, never two: what does not fit moves into `⋯` (see
331
+ `ToolbarRow`). A wrapped toolbar pushed the document down by its own
332
+ height and read as a second toolbar. */}
333
+ <ToolbarRow clusters={clusters} className="text-ink/40 ml-auto" />
237
334
  </div>
238
335
  );
239
336
  }
@@ -5,7 +5,7 @@ import { EditorTabs } from '@/client/components/workspace/editor/Tabs';
5
5
  import { EDITOR_KEY_BINDINGS, isFindBarCommand, type EditorCommand } from '@/shared/lib/code/editor/keymap';
6
6
  import { EDITOR_DEFAULT_FONT_SIZE } from '@/shared/lib/code/editor/typography';
7
7
  import { useEditorFontStack } from '@/client/hooks/editor/font';
8
- import type { TextRange } from '@/shared/lib/code/editor/commands';
8
+ import { useEditorTheme } from '@/client/hooks/editor/theme';
9
9
  import type { CodeSurfaceHandle } from '@/client/components/common/code-surface';
10
10
  import { resolveBinding } from '@/shared/lib/ui/key-binding';
11
11
  import { useActiveEditorPanel } from '@/client/components/workspace/plugin-panel/resolve';
@@ -62,12 +62,11 @@ export function Editor({
62
62
  const [zoomLevel, setZoomLevel] = useSessionState<number>('editor.zoomLevel', EDITOR_DEFAULT_FONT_SIZE);
63
63
  const [isMaximized, setIsMaximized] = useState(false);
64
64
  const [wordWrap, setWordWrap] = useSessionState<boolean>('editor.wordWrap', true);
65
- /** Extra ⌘D ranges; the surface paints them and the editor edits them by replication. */
66
- const [occurrences, setOccurrences] = useState<readonly TextRange[]>([]);
67
65
  const [paletteOpen, setPaletteOpen] = useState(false);
68
66
  const { sessionId } = useSessionStateContext();
69
67
  const surfaceRef = useRef<CodeSurfaceHandle | null>(null);
70
68
  const editorFontFamily = useEditorFontStack();
69
+ const editorTheme = useEditorTheme();
71
70
 
72
71
  // A deleted file keeps its tab (its buffer is still on screen), but the tab
73
72
  // strip must say so. Only a plain FILE tab is probed: a diff tab's path names
@@ -216,10 +215,10 @@ export function Editor({
216
215
  * Editor shortcuts, on the CAPTURE phase of the panel.
217
216
  *
218
217
  * Capture is what makes them work while focus is inside the document: the
219
- * `<textarea>` has its own handler (Tab, undo, Escape-to-blur) and would
220
- * otherwise see these first. Only the commands the PANEL owns are consumed
221
- * here — the editor's own keymap runs in the textarea, where it has the
222
- * buffer and the caret in hand.
218
+ * contenteditable has CodeMirror's own handlers (the generated keymap, and
219
+ * `defaultKeymap` behind it) and would otherwise see these first. Only the
220
+ * commands the PANEL owns are consumed here — the editor's own keymap runs in
221
+ * the document, where it has the buffer and the caret in hand.
223
222
  *
224
223
  * The panel is the scope, not the window: another panel's text field keeps
225
224
  * its own ⌘F, and the composer is untouched.
@@ -312,12 +311,11 @@ export function Editor({
312
311
  setIsMaximized={setIsMaximized}
313
312
  zoomLevel={zoomLevel}
314
313
  setZoomLevel={setZoomLevel}
315
- occurrences={occurrences}
316
- setOccurrences={setOccurrences}
317
314
  paletteOpen={paletteOpen}
318
315
  setPaletteOpen={setPaletteOpen}
319
316
  runCommand={runCommand}
320
317
  editorFontFamily={editorFontFamily}
318
+ editorTheme={editorTheme}
321
319
  togglePreview={togglePreview}
322
320
  revealActive={Boolean(searchReveal)}
323
321
  missing={activeFileMissing}
@@ -32,6 +32,7 @@ import { SessionStateProvider } from '@/client/components/common/session-state-p
32
32
  import { forgetSession } from '@/shared/lib/workspace/session-state/store';
33
33
  import { installDomGlobals, restoreDomGlobals } from '@/test-support/pristine-globals';
34
34
  import { publishEditorDocument, resetEditorDocument, type EditorDocument } from '@/client/lib/editor-document';
35
+ import { MAX_OUTLINE_ROWS } from '@/shared/lib/code/outline/rows';
35
36
  import { getLineReveal, setLineReveal } from '@/client/lib/line-reveal';
36
37
 
37
38
  const SESSION = 'new-outline-section';
@@ -215,4 +216,21 @@ describe('OutlineSection', () => {
215
216
  expect(reveal().getAttribute('data-expanded')).toBe('true');
216
217
  expect(toggle().getAttribute('aria-expanded')).toBe('true');
217
218
  });
219
+
220
+ // The cap is what keeps a large file's outline from becoming a DOM: 63 618
221
+ // declarations mounted 446 308 nodes and charged 953 ms to every keystroke
222
+ // (measured), where the editor's own cost is 34 ms.
223
+ test('a large outline renders only the first rows, and says how many it is not showing', async () => {
224
+ owner = {};
225
+ const many = Array.from({ length: MAX_OUTLINE_ROWS + 200 }, (_, i) => `export function fn${i}() {}`).join('\n');
226
+ publishEditorDocument(owner, { ...READY, path: 'src/many.ts', content: many });
227
+ await mount();
228
+ await expand();
229
+
230
+ expect(rowLabels()).toHaveLength(MAX_OUTLINE_ROWS);
231
+ // The count badge shows the window against the total, so the number on the
232
+ // toggle cannot be read as "this file has 500 symbols".
233
+ expect(toggle().textContent).toContain(`${MAX_OUTLINE_ROWS}/700`);
234
+ expect(container.textContent).toContain('Showing the first 500 of 700 symbols.');
235
+ });
218
236
  });
@@ -35,6 +35,7 @@ import { setLineReveal } from '@/client/lib/line-reveal';
35
35
  import { useSessionState } from '@/client/hooks/workspace/session-state';
36
36
  import { useOutlineView } from '@/client/hooks/editor/use-outline-view';
37
37
  import type { OutlineSymbol } from '@/shared/lib/code/outline/types';
38
+ import { outlineTruncatedNotice } from '@/shared/lib/code/outline/rows';
38
39
 
39
40
  export function OutlineSection() {
40
41
  const [expanded, setExpanded] = useSessionState<boolean>('files.outlineExpanded', false);
@@ -44,11 +45,12 @@ export function OutlineSection() {
44
45
  // Recomputed from the LIVE buffer, so an edit moves the rows with it. The
45
46
  // scan is a depth-aware line walk (see `outline/extract.ts`): measured at
46
47
  // 0.1–2.2 ms on this repository's largest sources.
47
- const { symbols, notice } = useOutlineView({
48
+ const { symbols, total, truncated, notice } = useOutlineView({
48
49
  content: current.content,
49
50
  language: current.language,
50
51
  state: current.state,
51
52
  note: current.note,
53
+ path: current.path,
52
54
  });
53
55
 
54
56
  // A diff tab HIDES the section rather than explaining that it has none. The
@@ -91,9 +93,9 @@ export function OutlineSection() {
91
93
  <span className="w-3 text-center font-mono" aria-hidden="true">{expanded ? '▾' : '▸'}</span>
92
94
  <ListTree size={12} />
93
95
  <span>Outline</span>
94
- {symbols.length > 0 && (
96
+ {total > 0 && (
95
97
  <span className="font-normal ml-1 border border-ink/20 rounded-full px-1.5 text-[9px] text-ink/50">
96
- {symbols.length}
98
+ {truncated ? `${symbols.length}/${total.toLocaleString()}` : total}
97
99
  </span>
98
100
  )}
99
101
  </button>
@@ -125,6 +127,11 @@ export function OutlineSection() {
125
127
  <span className="flex-shrink-0 font-mono text-[9px] text-ink/35">{symbol.line}</span>
126
128
  </button>
127
129
  ))}
130
+ {truncated && (
131
+ <div className="px-1 py-1 text-[10px] italic text-ink/40">
132
+ {outlineTruncatedNotice(total)}
133
+ </div>
134
+ )}
128
135
  </div>
129
136
  )}
130
137
  </div>
@@ -6,7 +6,7 @@ import { SearchResults } from '@/client/components/workspace/search-panel/Result
6
6
  import { setSearchReveal } from '@/client/lib/search-reveal';
7
7
  import { groupSearchResults, MAX_SEARCH_RESULTS } from '@/shared/lib/fs/search-list';
8
8
  import type { SearchResultItem } from '@/shared/types/fs';
9
- import type { FindOptions } from '@/shared/lib/code/editor/find';
9
+ import type { FindOptions } from '@/shared/lib/code/editor/find-scan';
10
10
 
11
11
  interface SearchPanelBodyProps {
12
12
  className: string;
@@ -0,0 +1,118 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The New worktree dialog, rendered.
8
+ *
9
+ * Three rules here are the ones a reader would otherwise discover in a browser
10
+ * and nowhere else:
11
+ *
12
+ * - the generated branch is in the FIRST paint (before any effect flushes),
13
+ * because the window between paint and the effect is exactly when a user
14
+ * types — measured: a field filled with `main` was replaced by the generated
15
+ * name and the dialog created `wt/calm-iguana` instead of reporting that
16
+ * `main` is taken;
17
+ * - the folder follows the branch, so it is never named after a previous one;
18
+ * - a closed dialog renders nothing at all, so no field is focusable while it
19
+ * is shut.
20
+ */
21
+
22
+ import { afterAll, afterEach, beforeAll, describe, expect, test } from 'bun:test';
23
+ import { Window } from 'happy-dom';
24
+ import { h, render } from 'preact';
25
+ import { act } from 'preact/test-utils';
26
+ import { installDomGlobals, restoreDomGlobals } from '@/test-support/pristine-globals';
27
+ import { NewWorktreeModal } from '@/client/components/workspace/worktree-modal/index';
28
+
29
+ const nativeFetch = Bun.fetch;
30
+ const mounted: HTMLElement[] = [];
31
+
32
+ beforeAll(() => {
33
+ installDomGlobals(new Window({ url: 'http://localhost' }));
34
+ });
35
+
36
+ afterEach(() => {
37
+ for (const host of mounted.splice(0)) {
38
+ render(null, host);
39
+ host.remove();
40
+ }
41
+ (globalThis as unknown as Record<string, unknown>).fetch = nativeFetch;
42
+ });
43
+
44
+ afterAll(() => {
45
+ restoreDomGlobals();
46
+ });
47
+
48
+ /**
49
+ * Mount, with the first render FLUSHED. Preact batches a state update into a
50
+ * microtask, so a `useEffect` that seeds a field has not run when `render`
51
+ * returns — reading the inputs then reads empty ones, which is a property of the
52
+ * test harness and not of the dialog.
53
+ */
54
+ async function mount(node: h.JSX.Element): Promise<HTMLElement> {
55
+ const host = document.createElement('div');
56
+ document.body.appendChild(host);
57
+ await act(async () => {
58
+ render(node, host);
59
+ });
60
+ mounted.push(host);
61
+ return host;
62
+ }
63
+
64
+ function textOf(host: HTMLElement): string {
65
+ return (host.textContent ?? '').replace(/\s+/g, ' ').trim();
66
+ }
67
+
68
+ describe('NewWorktreeModal', () => {
69
+ test('opens on a generated branch, with the folder following it', async () => {
70
+ const host = await mount(
71
+ h(NewWorktreeModal, { isOpen: true, onClose: () => {}, root: '/repo', folderId: 1, folderName: 'kabmo', onCreated: () => {} }),
72
+ );
73
+
74
+ const inputs = [...host.querySelectorAll('input')] as HTMLInputElement[];
75
+ const branch = inputs[0]?.value ?? '';
76
+ const folderName = inputs[2]?.value ?? '';
77
+ expect(branch).toMatch(/^wt\/[a-z]+-[a-z]+$/);
78
+ // The folder is the branch without our namespace, so it is never `wt-x`.
79
+ expect(folderName).toBe(branch.replace(/^wt\//, ''));
80
+ expect(folderName.startsWith('wt-')).toBe(false);
81
+ });
82
+
83
+ test('the generated branch is in the FIRST paint, before any effect flushes', () => {
84
+ const host = document.createElement('div');
85
+ document.body.appendChild(host);
86
+ // Deliberately NOT `mount`: no `act`, so no effect has run. A field seeded
87
+ // by an effect reads empty here, and the browser's own window between paint
88
+ // and the effect flush is exactly when a user types into it.
89
+ render(
90
+ h(NewWorktreeModal, { isOpen: true, onClose: () => {}, root: '/repo', folderId: 1, folderName: 'kabmo', onCreated: () => {} }),
91
+ host,
92
+ );
93
+ mounted.push(host);
94
+
95
+ const inputs = [...host.querySelectorAll('input')] as HTMLInputElement[];
96
+ const branch = inputs[0]?.value ?? '';
97
+ expect(branch).toMatch(/^wt\/[a-z]+-[a-z]+$/);
98
+ // One generated name for both fields: deriving the folder from a SECOND
99
+ // call would show a folder the branch does not have.
100
+ expect(inputs[2]?.value ?? '').toBe(branch.replace(/^wt\//, ''));
101
+ });
102
+
103
+ test('states what happens to uncommitted changes, and on which project', async () => {
104
+ const host = await mount(
105
+ h(NewWorktreeModal, { isOpen: true, onClose: () => {}, root: '/repo', folderId: 1, folderName: 'kabmo', onCreated: () => {} }),
106
+ );
107
+ const text = textOf(host);
108
+ expect(text).toContain('Uncommitted changes stay in');
109
+ expect(text).toContain('kabmo');
110
+ });
111
+
112
+ test('renders nothing while closed', async () => {
113
+ const host = await mount(
114
+ h(NewWorktreeModal, { isOpen: false, onClose: () => {}, root: '/repo', folderId: 1, onCreated: () => {} }),
115
+ );
116
+ expect(host.textContent).toBe('');
117
+ });
118
+ });