@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
@@ -0,0 +1,111 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The code surface's type and chrome, in CodeMirror's own theme format.
8
+ *
9
+ * Type is INHERITED, not restated: the host element carries the font family,
10
+ * size and line box (the zoom control and the editor-font setting both write
11
+ * there), and `.cm-editor` inherits them. That is what keeps a zoom step a pure
12
+ * CSS change — a theme extension carrying a pixel size would have to be
13
+ * reconfigured on every step, and every reconfigure re-creates the editor's
14
+ * plugins (including the tokenizer).
15
+ *
16
+ * Colours are read through `--editor-*` custom properties with the app's
17
+ * palette as the FALLBACK:
18
+ *
19
+ * - `auto` (the default) writes none of them, so every declaration resolves to
20
+ * `--theme-*` and an app theme flip repaints the editor with zero JS — the
21
+ * same property the Shiki decorations rely on.
22
+ * - An explicit editor theme writes them on the surface's own host element
23
+ * (`editorThemeVars`), so the editor paints Dracula while the console around
24
+ * it stays on the app palette. Because the values arrive as CSS, switching
25
+ * between two editor themes is also a repaint: nothing here is reconfigured.
26
+ *
27
+ * The one rule that cannot be a variable is the TOKEN colour. Shiki emits both
28
+ * palettes on every token, and which one paints is decided by the app's variant
29
+ * attribute — except an explicit editor theme tokenizes with ITSELF in both
30
+ * slots, so the two custom properties carry identical values and either one is
31
+ * correct. That is why the dark rule below is safe to state unconditionally.
32
+ */
33
+
34
+ import type { Extension } from '@codemirror/state';
35
+ import { EditorView } from '@codemirror/view';
36
+
37
+ /** The surface's own CSS. Everything here is structural, never per-layout. */
38
+ export function surfaceTheme(): Extension {
39
+ return EditorView.theme({
40
+ '&': {
41
+ height: '100%',
42
+ backgroundColor: 'var(--editor-bg, transparent)',
43
+ color: 'var(--editor-fg, var(--theme-ink))',
44
+ fontSize: 'inherit',
45
+ fontFamily: 'inherit',
46
+ lineHeight: 'inherit',
47
+ },
48
+ '&.cm-focused': { outline: 'none' },
49
+ '.cm-scroller': {
50
+ overflow: 'auto',
51
+ fontFamily: 'inherit',
52
+ fontSize: 'inherit',
53
+ lineHeight: 'inherit',
54
+ },
55
+ '.cm-content': {
56
+ // CM6 reads these two back as the document's padding, which is why the
57
+ // breathing room belongs here and not on a wrapper: a scroll container's
58
+ // own padding is not part of its scrollable overflow.
59
+ paddingTop: 'var(--surface-pad, 16px)',
60
+ paddingBottom: 'var(--surface-pad, 16px)',
61
+ caretColor: 'var(--editor-cursor, var(--theme-ink))',
62
+ },
63
+ '.cm-line': { padding: '0 var(--surface-pad, 16px)' },
64
+ // The gutter is CM6's own (lineNumbers()), styled to the panel's band. An
65
+ // explicit editor theme paints it in the theme's own surface, which is what
66
+ // VS Code does for the 46 themes that ship no `editorGutter.background`.
67
+ '.cm-gutters': {
68
+ backgroundColor: 'var(--editor-bg, var(--theme-canvas))',
69
+ color: 'var(--editor-gutter-fg, color-mix(in srgb, var(--theme-ink) 30%, transparent))',
70
+ border: 'none',
71
+ borderRight: '1px solid var(--editor-border, color-mix(in srgb, var(--theme-ink) 10%, transparent))',
72
+ fontFamily: 'inherit',
73
+ fontSize: 'inherit',
74
+ lineHeight: 'inherit',
75
+ },
76
+ '.cm-lineNumbers .cm-gutterElement': {
77
+ padding: '0 12px 0 16px',
78
+ minWidth: '1.5rem',
79
+ textAlign: 'right',
80
+ },
81
+ // The selection is drawn by `drawSelection()`; the native one is left to
82
+ // the browser when the editor is not focused.
83
+ '.cm-selectionBackground, &.cm-focused .cm-selectionBackground': {
84
+ backgroundColor: 'var(--editor-selection, color-mix(in srgb, var(--theme-ink) 20%, transparent))',
85
+ },
86
+ '.cm-cursor, .cm-dropCursor': {
87
+ borderLeftColor: 'var(--editor-cursor, var(--theme-ink))',
88
+ },
89
+ // A find match, painted by the decoration field (`marks.ts`). The geometry
90
+ // rules are the ones the old markup layer needed: horizontal edges only, or
91
+ // a match spanning several tokens paints a seam between them.
92
+ '.cm-content .find-match': {
93
+ color: 'inherit',
94
+ backgroundColor: 'var(--editor-find, color-mix(in srgb, var(--theme-warning) 32%, transparent))',
95
+ },
96
+ '.cm-content .find-match[data-find-current]': {
97
+ backgroundColor:
98
+ 'var(--editor-find-current, color-mix(in srgb, var(--theme-warning) 55%, transparent))',
99
+ borderTop: '1px solid color-mix(in srgb, var(--theme-warning) 75%, transparent)',
100
+ borderBottom: '1px solid color-mix(in srgb, var(--theme-warning) 75%, transparent)',
101
+ },
102
+ // Shiki tokens. The colour itself comes from the custom properties the
103
+ // decoration carries; `tailwind.css` holds the same pair for every other
104
+ // surface, and this block is the editor's copy of it. The `&`-less form
105
+ // below is CM6's base-class prefix, and the second rule is why an app theme
106
+ // flip repaints tokens with no re-tokenize.
107
+ '.cm-content .shiki-tok': { color: 'var(--shiki-light)' },
108
+ 'html[data-theme-variant="dark"] & .cm-content .shiki-tok': { color: 'var(--shiki-dark)' },
109
+ '.cm-placeholder': { color: 'color-mix(in srgb, var(--editor-fg, var(--theme-ink)) 35%, transparent)' },
110
+ });
111
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The two width-mode buttons, driven through the REAL session-state provider.
8
+ *
9
+ * Three properties only a mount can see, and each fails silently:
10
+ *
11
+ * - The two toolbars draw the SAME state. `useSessionState` hands every caller
12
+ * a private copy, so a version reading it would leave the diff toolbar
13
+ * showing `normal` after the editor toolbar entered compact view — two
14
+ * buttons disagreeing about one layout.
15
+ * - Each button reports `aria-pressed` for its own mode and nothing else, so
16
+ * the pair reads as a mode selection rather than as two unrelated switches.
17
+ * - Clicking the ACTIVE mode returns to `normal`, which is the only way back to
18
+ * a layout with the chat on screen without hunting for the other button.
19
+ *
20
+ * Rendered with `h()` against happy-dom, over a stubbed `fetch` that answers
21
+ * the session-state blob (the provider loads it before anything is ready).
22
+ */
23
+
24
+ import { afterAll, afterEach, beforeAll, describe, expect, test } from 'bun:test';
25
+ import { Window } from 'happy-dom';
26
+ import { h, render } from 'preact';
27
+ import { act } from 'preact/test-utils';
28
+ import { EditorViewButtons } from '@/client/components/common/editor-view-buttons';
29
+ import { SessionStateProvider } from '@/client/components/common/session-state-provider/index';
30
+ import { forgetSession } from '@/shared/lib/workspace/session-state/store';
31
+ import { installDomGlobals, restoreDomGlobals } from '@/test-support/pristine-globals';
32
+
33
+ const SESSION = 'new-editor-view-buttons';
34
+ /** The runner's own fetch, reached through `Bun` so a leaked stub cannot be mistaken for it. */
35
+ const nativeFetch = Bun.fetch;
36
+
37
+ let container: HTMLElement;
38
+
39
+ beforeAll(() => {
40
+ installDomGlobals(new Window({ url: 'http://localhost' }));
41
+ });
42
+
43
+ afterEach(() => {
44
+ if (container) {
45
+ render(null, container);
46
+ container.remove();
47
+ }
48
+ forgetSession(SESSION);
49
+ (globalThis as unknown as Record<string, unknown>).fetch = nativeFetch;
50
+ });
51
+
52
+ afterAll(() => {
53
+ restoreDomGlobals();
54
+ });
55
+
56
+ /**
57
+ * Mount TWO button pairs under one provider — the editor toolbar's and the diff
58
+ * toolbar's — because "they share one slot" is the claim, and a single pair
59
+ * could pass with private copies.
60
+ */
61
+ async function mount(stored: Record<string, unknown> = {}): Promise<void> {
62
+ const target = globalThis as unknown as Record<string, unknown>;
63
+ target.fetch = async () => {
64
+ const json = (body: unknown) =>
65
+ new Response(JSON.stringify(body), { status: 200, headers: { 'content-type': 'application/json' } });
66
+ return json({ sessionId: SESSION, state: stored });
67
+ };
68
+
69
+ container = document.createElement('div');
70
+ document.body.appendChild(container);
71
+ await act(async () => {
72
+ render(
73
+ h(SessionStateProvider, {
74
+ sessionId: SESSION,
75
+ children: h(
76
+ 'div',
77
+ null,
78
+ h('div', { id: 'editor-toolbar' }, h(EditorViewButtons, { size: 14 })),
79
+ h('div', { id: 'diff-toolbar' }, h(EditorViewButtons, { size: 13 })),
80
+ ),
81
+ }),
82
+ container,
83
+ );
84
+ });
85
+ // The provider's blob load settles across a tick.
86
+ for (let i = 0; i < 10; i += 1) await act(async () => {});
87
+ }
88
+
89
+ /** The named button inside one of the two toolbar hosts. */
90
+ function button(toolbar: 'editor-toolbar' | 'diff-toolbar', name: string): HTMLButtonElement {
91
+ const host = container.querySelector(`#${toolbar}`);
92
+ const found = [...(host?.querySelectorAll('button') ?? [])].find(
93
+ (el) => el.getAttribute('aria-label') === name,
94
+ );
95
+ if (!found) throw new Error(`no "${name}" button in #${toolbar}`);
96
+ return found as HTMLButtonElement;
97
+ }
98
+
99
+ async function click(el: HTMLButtonElement): Promise<void> {
100
+ await act(async () => {
101
+ el.dispatchEvent(new window.MouseEvent('click', { bubbles: true }) as never);
102
+ });
103
+ }
104
+
105
+ describe('EditorViewButtons', () => {
106
+ test('both toolbars offer both modes, and neither is pressed by default', async () => {
107
+ await mount();
108
+
109
+ for (const toolbar of ['editor-toolbar', 'diff-toolbar'] as const) {
110
+ expect(button(toolbar, 'Compact view').getAttribute('aria-pressed')).toBe('false');
111
+ expect(button(toolbar, 'Full view').getAttribute('aria-pressed')).toBe('false');
112
+ }
113
+ });
114
+
115
+ test('a click enters the mode, and the OTHER toolbar shows the same state', async () => {
116
+ await mount();
117
+ await click(button('editor-toolbar', 'Compact view'));
118
+
119
+ expect(button('editor-toolbar', 'Compact view').getAttribute('aria-pressed')).toBe('true');
120
+ expect(button('diff-toolbar', 'Compact view').getAttribute('aria-pressed')).toBe('true');
121
+ expect(button('diff-toolbar', 'Full view').getAttribute('aria-pressed')).toBe('false');
122
+ });
123
+
124
+ test('the two modes are exclusive: entering one clears the other', async () => {
125
+ await mount();
126
+ await click(button('editor-toolbar', 'Compact view'));
127
+ await click(button('diff-toolbar', 'Full view'));
128
+
129
+ expect(button('editor-toolbar', 'Compact view').getAttribute('aria-pressed')).toBe('false');
130
+ expect(button('editor-toolbar', 'Full view').getAttribute('aria-pressed')).toBe('true');
131
+ });
132
+
133
+ test('clicking the active mode returns to the ordinary layout', async () => {
134
+ await mount();
135
+ await click(button('editor-toolbar', 'Full view'));
136
+ await click(button('editor-toolbar', 'Full view'));
137
+
138
+ expect(button('editor-toolbar', 'Full view').getAttribute('aria-pressed')).toBe('false');
139
+ expect(button('editor-toolbar', 'Compact view').getAttribute('aria-pressed')).toBe('false');
140
+ });
141
+
142
+ test('a mode stored for this session is restored on mount', async () => {
143
+ await mount({ 'layout.editorViewMode': 'full' });
144
+
145
+ expect(button('editor-toolbar', 'Full view').getAttribute('aria-pressed')).toBe('true');
146
+ expect(button('diff-toolbar', 'Full view').getAttribute('aria-pressed')).toBe('true');
147
+ });
148
+ });
@@ -0,0 +1,109 @@
1
+ import { MessageSquareOff, PanelLeftClose } from 'lucide-preact';
2
+ import type { LucideIcon } from 'lucide-preact';
3
+ import { useSharedSessionState } from '@/client/hooks/workspace/session-state';
4
+ import type { ToolbarOverflowAction } from '@/client/components/common/toolbar-row';
5
+ import {
6
+ DEFAULT_EDITOR_VIEW_MODE,
7
+ EDITOR_VIEW_MODE_KEY,
8
+ isEditorViewMode,
9
+ toggledEditorViewMode,
10
+ type EditorViewMode,
11
+ type EditorViewModeTarget,
12
+ } from '@/shared/lib/workspace/editor-view-mode';
13
+
14
+ /**
15
+ * The two width modes, as data.
16
+ *
17
+ * Each is a row so the buttons, their labels and their icons cannot drift
18
+ * apart: the label states exactly which columns disappear, because "Compact"
19
+ * and "Full" are words that could mean anything, and the icons are DIFFERENT
20
+ * SHAPES rather than two spellings of the same one — the third width control
21
+ * beside them (full SCREEN, which covers the right panel too) keeps its own
22
+ * `Maximize` glyph.
23
+ */
24
+ export const EDITOR_VIEW_MODES: Array<{
25
+ target: EditorViewModeTarget;
26
+ label: string;
27
+ hint: string;
28
+ Icon: LucideIcon;
29
+ }> = [
30
+ {
31
+ target: 'compact',
32
+ label: 'Compact view',
33
+ hint: 'Editor takes the chat width — chat hidden, sidebar and right panel kept',
34
+ Icon: MessageSquareOff,
35
+ },
36
+ {
37
+ target: 'full',
38
+ label: 'Full view',
39
+ hint: 'Editor takes the chat and sidebar width — chat and sidebar hidden, right panel kept',
40
+ Icon: PanelLeftClose,
41
+ },
42
+ ];
43
+
44
+ /** The mode in force, and the setter the buttons share. */
45
+ export function useEditorViewMode(): { mode: EditorViewMode; setMode: (next: EditorViewMode) => void } {
46
+ const [stored, setMode] = useSharedSessionState<EditorViewMode>(EDITOR_VIEW_MODE_KEY, DEFAULT_EDITOR_VIEW_MODE);
47
+ return { mode: isEditorViewMode(stored) ? stored : DEFAULT_EDITOR_VIEW_MODE, setMode };
48
+ }
49
+
50
+ /**
51
+ * The same two modes as `⋯` menu rows, for a toolbar too narrow to draw them.
52
+ *
53
+ * A hook rather than a plain function because the menu row has to CLICK the same
54
+ * slot the buttons write, and a row built from a stale mode would enter the mode
55
+ * already in force (which the toggle reads as "leave it").
56
+ */
57
+ export function useEditorViewActions(): ToolbarOverflowAction[] {
58
+ const { mode, setMode } = useEditorViewMode();
59
+ return EDITOR_VIEW_MODES.map(({ target, label, Icon }) => ({
60
+ // A menu row has no hover title, so the sentence that explains the mode is
61
+ // part of what the row says rather than lost with the button's tooltip.
62
+ label,
63
+ icon: Icon,
64
+ active: mode === target,
65
+ onSelect: () => setMode(toggledEditorViewMode(mode, target)),
66
+ }));
67
+ }
68
+
69
+ interface EditorViewButtonsProps {
70
+ /** Glyph size: the editor toolbar draws 14px, the diff toolbar 13px. */
71
+ size?: number;
72
+ }
73
+
74
+ /**
75
+ * The two width-mode buttons, drawn by BOTH the editor and the diff toolbar.
76
+ *
77
+ * One component because the two toolbars are the same surface on two tab kinds:
78
+ * a mode entered from the diff and a mode entered from a file must be the same
79
+ * mode, and a second copy of these rules is how the diff ends up offering a
80
+ * width the layout does not implement. The state is the shared session slot
81
+ * (see `useEditorViewLayout`) so the layout collapses the columns on the same
82
+ * render.
83
+ */
84
+ export function EditorViewButtons({ size = 14 }: EditorViewButtonsProps) {
85
+ const { mode, setMode } = useEditorViewMode();
86
+
87
+ return (
88
+ <>
89
+ {EDITOR_VIEW_MODES.map(({ target, label, hint, Icon }) => {
90
+ const active = mode === target;
91
+ return (
92
+ <button
93
+ key={target}
94
+ type="button"
95
+ onClick={() => setMode(toggledEditorViewMode(mode, target))}
96
+ className={`flex items-center justify-center p-1.5 rounded transition-colors cursor-pointer flex-shrink-0 ${
97
+ active ? 'bg-ink/10 text-ink' : 'text-ink/50 hover:text-ink hover:bg-ink/5'
98
+ }`}
99
+ title={hint}
100
+ aria-label={label}
101
+ aria-pressed={active}
102
+ >
103
+ <Icon size={size} />
104
+ </button>
105
+ );
106
+ })}
107
+ </>
108
+ );
109
+ }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The shell update's dialog: confirm, install, report.
8
+ *
9
+ * ## Why the message lives HERE and not in the footer
10
+ *
11
+ * The footer is a row of controls, one of them 24px wide. A sentence like "Kabmo
12
+ * 0.3.2 installed — restarting" pushed into that row pushed the controls around
13
+ * and read as a status line for the SIDEBAR rather than for the update the user
14
+ * just asked for. The control therefore stays a control, and everything a run has
15
+ * to say — the confirmation before it, the progress during it, the shell's own
16
+ * refusal after it — is a dialog.
17
+ *
18
+ * ## The confirmation is not ceremony
19
+ *
20
+ * The click replaces the app the user is looking at and restarts it. That is not
21
+ * something a 24px glyph should do on a single press with no words, which is why
22
+ * the first thing the dialog says is what will happen and what will be lost
23
+ * (nothing but the window: the servers and the session are the console's own
24
+ * state, and they survive).
25
+ *
26
+ * ## Every phase reports the shell's own words
27
+ *
28
+ * `done` and `error` print the sentence the SHELL sent. Nothing here invents
29
+ * "Update failed": a shell that verified the download and declined knows why, and
30
+ * that reason is the only thing a user can act on.
31
+ */
32
+
33
+ import { AlertCircle, Check, Download, Loader2 } from 'lucide-preact';
34
+ import { Modal } from '@/client/components/common/Modal';
35
+ import {
36
+ shellDownloadLabel,
37
+ type ShellDownload,
38
+ } from '@/client/hooks/ui/updates/shell-download';
39
+ import type { ShellUpdatePhase } from '@/client/hooks/ui/updates/shell-update';
40
+
41
+ /**
42
+ * What the dialog is showing.
43
+ *
44
+ * `confirm` is the dialog's own phase, not the hook's: the hook's `idle` means "no
45
+ * run has happened", and the dialog is only ever open on a run the user is about to
46
+ * start or one that already ran.
47
+ */
48
+ export type ShellUpdateModalPhase = ShellUpdatePhase | 'confirm';
49
+
50
+ interface ShellUpdateModalProps {
51
+ download: ShellDownload;
52
+ phase: ShellUpdateModalPhase;
53
+ message: string | null;
54
+ onConfirm: () => void;
55
+ onClose: () => void;
56
+ }
57
+
58
+ /** What this platform actually does with the artifact, in the platform's terms. */
59
+ function installNote(download: ShellDownload): string {
60
+ switch (download.platform) {
61
+ case 'macos':
62
+ return 'Kabmo downloads the new build, checks its signature and replaces itself. The window reopens on the new version.';
63
+ case 'windows':
64
+ return 'Kabmo downloads the new installer and runs it silently. The window closes and reopens on the new version.';
65
+ case 'linux':
66
+ return 'Kabmo downloads the new AppImage over the one it is running from and restarts. A package install (.deb / .rpm) cannot replace itself — that build is offered the release page instead.';
67
+ default:
68
+ return 'Kabmo installs the new build and restarts.';
69
+ }
70
+ }
71
+
72
+ const SECONDARY =
73
+ 'rounded-md border border-ink/20 px-3 py-1.5 text-[11px] font-medium text-ink transition-colors hover:border-ink/40 cursor-pointer';
74
+ const PRIMARY =
75
+ 'inline-flex items-center gap-1.5 rounded-md bg-ink px-3 py-1.5 text-[11px] font-medium text-paper transition-opacity hover:opacity-90 disabled:opacity-60 disabled:cursor-not-allowed cursor-pointer';
76
+
77
+ export function ShellUpdateModal({ download, phase, message, onConfirm, onClose }: ShellUpdateModalProps) {
78
+ const label = shellDownloadLabel(download);
79
+
80
+ return (
81
+ <Modal
82
+ onClose={onClose}
83
+ maxWidthClass="max-w-sm"
84
+ header={<h2 className="text-sm font-semibold text-ink">{label}</h2>}
85
+ footer={
86
+ <div className="flex items-center justify-end gap-2 px-5 pb-4">
87
+ {phase === 'confirm' && (
88
+ <>
89
+ <button type="button" className={SECONDARY} onClick={onClose}>
90
+ Cancel
91
+ </button>
92
+ <button type="button" className={PRIMARY} onClick={onConfirm}>
93
+ <Download size={13} />
94
+ <span>Install update</span>
95
+ </button>
96
+ </>
97
+ )}
98
+
99
+ {phase === 'working' && (
100
+ <button type="button" className={PRIMARY} disabled>
101
+ <Loader2 size={13} className="animate-spin motion-reduce:animate-none" />
102
+ <span>Installing…</span>
103
+ </button>
104
+ )}
105
+
106
+ {phase === 'done' && (
107
+ <button type="button" className={PRIMARY} onClick={onClose}>
108
+ <Check size={13} />
109
+ <span>Close</span>
110
+ </button>
111
+ )}
112
+
113
+ {phase === 'error' && (
114
+ <>
115
+ <button type="button" className={SECONDARY} onClick={onClose}>
116
+ Close
117
+ </button>
118
+ {/* Retry re-runs the SAME offer: the release is still the one the
119
+ console read, and a failure here is the shell's download or its
120
+ verification, not a stale version. */}
121
+ <button type="button" className={PRIMARY} onClick={onConfirm}>
122
+ <Download size={13} />
123
+ <span>Try again</span>
124
+ </button>
125
+ </>
126
+ )}
127
+ </div>
128
+ }
129
+ >
130
+ <div className="px-5 py-4 space-y-3 text-[11px] leading-5 text-ink/70">
131
+ {phase === 'confirm' && (
132
+ <>
133
+ <p>{installNote(download)}</p>
134
+ <p className="text-ink/50">
135
+ Version {download.latest} · the console keeps running; only this window restarts.
136
+ </p>
137
+ </>
138
+ )}
139
+
140
+ {phase === 'working' && (
141
+ <p className="flex items-center gap-2 text-ink">
142
+ <Loader2 size={14} className="animate-spin motion-reduce:animate-none" />
143
+ <span>Downloading and verifying the new build…</span>
144
+ </p>
145
+ )}
146
+
147
+ {(phase === 'done' || phase === 'error') && (
148
+ <p
149
+ role="status"
150
+ className={`flex items-start gap-2 ${phase === 'error' ? 'text-error' : 'text-ink'}`}
151
+ >
152
+ {phase === 'error' ? (
153
+ <AlertCircle size={14} className="mt-0.5 shrink-0" />
154
+ ) : (
155
+ <Check size={14} className="mt-0.5 shrink-0" />
156
+ )}
157
+ <span>{message ?? (phase === 'error' ? 'The update failed.' : 'Update installed.')}</span>
158
+ </p>
159
+ )}
160
+ </div>
161
+ </Modal>
162
+ );
163
+ }