@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,65 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * `isSpawnArrival` / `isOwnSpawn` — the two questions a session-id change asks.
8
+ *
9
+ * Both are pure so every ordering can be stated directly. The orderings are the
10
+ * point: the browser is where they were found (three CI specs at once showed an
11
+ * answer on screen and then an empty timeline), but reproducing a child that
12
+ * answers before the URL flips is not something a browser spec can be asked to
13
+ * do on demand.
14
+ */
15
+
16
+ import { describe, expect, test } from 'bun:test';
17
+ import { isOwnSpawn, isSpawnArrival } from '@/client/hooks/chat/timeline/session-switch';
18
+
19
+ describe('isSpawnArrival', () => {
20
+ test('the id the send path recorded is an arrival, whatever the previous id was', () => {
21
+ // A chat opened from a WORKSPACE: no `?sessionId=` at all, so the previous
22
+ // id is null and the URL moves straight to the child's UUID. This is the
23
+ // shape that used to clear a finished turn.
24
+ expect(
25
+ isSpawnArrival({ sessionId: 'uuid-1', previousSessionId: null, adoptedSessionId: 'uuid-1' }),
26
+ ).toBe(true);
27
+ // A pending chat, mid-run or after it: same fact.
28
+ expect(
29
+ isSpawnArrival({ sessionId: 'uuid-1', previousSessionId: 'new-9', adoptedSessionId: 'uuid-1' }),
30
+ ).toBe(true);
31
+ });
32
+
33
+ test('`new-…` → UUID is still an arrival when the record was already consumed', () => {
34
+ // The pending store hands the adoption to the session-state provider once,
35
+ // so the URL's own shape is the only evidence left.
36
+ expect(
37
+ isSpawnArrival({ sessionId: 'uuid-1', previousSessionId: 'new-9', adoptedSessionId: null }),
38
+ ).toBe(true);
39
+ });
40
+
41
+ test('opening another session is not an arrival', () => {
42
+ expect(
43
+ isSpawnArrival({ sessionId: 'uuid-2', previousSessionId: 'uuid-1', adoptedSessionId: 'uuid-1' }),
44
+ ).toBe(false);
45
+ });
46
+
47
+ test('a null or unchanged id is not an arrival', () => {
48
+ expect(isSpawnArrival({ sessionId: null, previousSessionId: 'uuid-1', adoptedSessionId: null })).toBe(false);
49
+ expect(
50
+ isSpawnArrival({ sessionId: 'uuid-1', previousSessionId: 'uuid-1', adoptedSessionId: 'uuid-1' }),
51
+ ).toBe(true);
52
+ });
53
+ });
54
+
55
+ describe('isOwnSpawn', () => {
56
+ test('a pending id and the adopted UUID are both own spawns', () => {
57
+ expect(isOwnSpawn('new-9', null)).toBe(true);
58
+ expect(isOwnSpawn('uuid-1', 'uuid-1')).toBe(true);
59
+ });
60
+
61
+ test('a session this page did not spawn is not one', () => {
62
+ expect(isOwnSpawn('uuid-2', 'uuid-1')).toBe(false);
63
+ expect(isOwnSpawn(null, 'uuid-1')).toBe(false);
64
+ });
65
+ });
@@ -0,0 +1,63 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Which rows a session-switch keeps, and which a committed payload may replace.
8
+ *
9
+ * Split out of `session-load.ts` for the file-size ceiling, and it is one
10
+ * subject rather than an arbitrary cut: both functions answer "does this arrival
11
+ * belong to the session the timeline is already showing?".
12
+ *
13
+ * The distinction they encode was paid for twice, in opposite directions.
14
+ *
15
+ * - **A spawn's arrival is not a session switch.** The optimistic bubbles are
16
+ * the operator's own turn and the row the stream is filling; clearing them
17
+ * deleted a turn that had already rendered. Two shapes reach here and only one
18
+ * was ever recognised: `new-…` → UUID, and a chat opened from a WORKSPACE
19
+ * (`?folderId=` and no `?sessionId=`), whose previous id is `null` and whose
20
+ * URL moves straight to the child's UUID. The second was keyed on the
21
+ * generating flag, so a FAST child — the scripted one answers in milliseconds
22
+ * — finished before the URL flipped and the reset ran anyway. Measured in CI
23
+ * on three specs at once: the answer was on screen, then the timeline was
24
+ * empty. The id the send path recorded is the fact that survives either
25
+ * ordering.
26
+ * - **A committed payload is a lagging snapshot** while a run this page started
27
+ * owns the rows, and an own spawn's transcript may legitimately be empty (omp
28
+ * buffers its writes until the first assistant message settles). Either way it
29
+ * must not empty the timeline.
30
+ */
31
+
32
+ /** The ids a session-switch decision needs, all of them refs. */
33
+ export interface SessionSwitchFacts {
34
+ sessionId: string | null;
35
+ previousSessionId: string | null;
36
+ /** The id a spawn answered with, recorded by the send path. */
37
+ adoptedSessionId: string | null;
38
+ }
39
+
40
+ /**
41
+ * Whether this change is a spawn's arrival rather than the user opening another
42
+ * session — in which case the optimistic rows stay.
43
+ */
44
+ export function isSpawnArrival({ sessionId, previousSessionId, adoptedSessionId }: SessionSwitchFacts): boolean {
45
+ if (!sessionId) return false;
46
+ // The recorded id is checked FIRST and covers both orderings: it is written
47
+ // before the URL moves, so a child that answered mid-run and one that answered
48
+ // after the run ended are the same fact here.
49
+ if (adoptedSessionId === sessionId) return true;
50
+ // `new-…` → UUID. Kept as a fallback for a spawn whose record was already
51
+ // consumed (the pending store hands the adoption to the session-state
52
+ // provider once), where the URL's own shape is the only evidence left.
53
+ return previousSessionId?.startsWith('new-') === true && !sessionId.startsWith('new-');
54
+ }
55
+
56
+ /**
57
+ * Whether `sessionId` is one THIS PAGE spawned, so an empty committed payload
58
+ * means "not written yet" rather than "nothing happened".
59
+ */
60
+ export function isOwnSpawn(sessionId: string | null, adoptedSessionId: string | null): boolean {
61
+ if (!sessionId) return false;
62
+ return sessionId.startsWith('new-') || adoptedSessionId === sessionId;
63
+ }
@@ -0,0 +1,182 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The deferred-highlight hook, and the idle scheduler under it.
8
+ *
9
+ * The property that matters is the one the user feels: the FIRST render after a
10
+ * tap must not run the tokenizer. A card that appeared only after 220–300 ms of
11
+ * grammar work (measured at 4× CPU; 730–1484 ms at 20×) is the delay this
12
+ * exists to remove, and a hook that highlighted synchronously "sometimes" would
13
+ * pass a markup assertion and fail the point.
14
+ *
15
+ * So the scheduler is injectable here — the same seam the warm-up and the
16
+ * grammar loader use — and the assertions are about WHEN the compute ran, not
17
+ * only about what it produced. No wall-clock waits: the fallback path is driven
18
+ * by fake timers.
19
+ */
20
+
21
+ import { afterAll, afterEach, beforeAll, describe, expect, test, vi } from 'bun:test';
22
+ import { Window } from 'happy-dom';
23
+ import { h, render } from 'preact';
24
+ import { act } from 'preact/test-utils';
25
+
26
+ import { useDeferredHighlight } from '@/client/hooks/code/deferred-highlight';
27
+ import { scheduleIdle } from '@/shared/lib/code/idle';
28
+ import { clearHighlightMemo, highlightMemoSize } from '@/shared/lib/code/highlight-memo';
29
+ import { resetTokenizerWarmup, warmSyntaxTokenizers } from '@/shared/lib/code/highlighter-warmup';
30
+
31
+ const DOM_GLOBALS = ['window', 'document', 'navigator', 'Node', 'Element', 'HTMLElement', 'Event', 'MouseEvent'] as const;
32
+ const nativeGlobals: Partial<Record<(typeof DOM_GLOBALS)[number], unknown>> = {};
33
+ let container: HTMLElement;
34
+
35
+ beforeAll(() => {
36
+ const win = new Window({ url: 'http://localhost' });
37
+ const target = globalThis as unknown as Record<string, unknown>;
38
+ for (const key of DOM_GLOBALS) {
39
+ nativeGlobals[key] = target[key];
40
+ target[key] = (win as unknown as Record<string, unknown>)[key];
41
+ }
42
+ container = document.createElement('div');
43
+ document.body.appendChild(container);
44
+ });
45
+
46
+ afterAll(() => {
47
+ const target = globalThis as unknown as Record<string, unknown>;
48
+ for (const key of DOM_GLOBALS) {
49
+ if (nativeGlobals[key] === undefined) delete target[key];
50
+ else target[key] = nativeGlobals[key];
51
+ }
52
+ });
53
+
54
+ afterEach(() => {
55
+ clearHighlightMemo();
56
+ resetTokenizerWarmup();
57
+ });
58
+
59
+ describe('scheduleIdle', () => {
60
+ test('a runtime without requestIdleCallback still runs the work', () => {
61
+ const host = globalThis as unknown as Record<string, unknown>;
62
+ const saved = host.requestIdleCallback;
63
+ delete host.requestIdleCallback;
64
+ vi.useFakeTimers();
65
+ try {
66
+ let runs = 0;
67
+ scheduleIdle(() => { runs += 1; });
68
+ vi.advanceTimersByTime(1);
69
+ expect(runs).toBe(1);
70
+ } finally {
71
+ vi.useRealTimers();
72
+ if (saved !== undefined) host.requestIdleCallback = saved;
73
+ }
74
+ });
75
+
76
+ test('cancelling before the slice fires skips the work', () => {
77
+ const host = globalThis as unknown as Record<string, unknown>;
78
+ const saved = host.requestIdleCallback;
79
+ delete host.requestIdleCallback;
80
+ vi.useFakeTimers();
81
+ try {
82
+ let runs = 0;
83
+ const cancel = scheduleIdle(() => { runs += 1; });
84
+ cancel();
85
+ vi.advanceTimersByTime(1);
86
+ expect(runs).toBe(0);
87
+ } finally {
88
+ vi.useRealTimers();
89
+ if (saved !== undefined) host.requestIdleCallback = saved;
90
+ }
91
+ });
92
+ });
93
+
94
+ describe('warmSyntaxTokenizers', () => {
95
+ test('tokenizes one sample per language, one slice at a time', () => {
96
+ const calls: Array<{ language: string; code: string }> = [];
97
+ const queue: Array<() => void> = [];
98
+ warmSyntaxTokenizers({
99
+ languages: ['typescript', 'bash'],
100
+ ready: () => true,
101
+ highlight: (code, language) => calls.push({ language, code }),
102
+ schedule: (run) => {
103
+ queue.push(run);
104
+ return () => {};
105
+ },
106
+ });
107
+
108
+ // One slice is armed, not the whole set: a slow grammar must not become the
109
+ // long task this exists to prevent.
110
+ expect(queue).toHaveLength(1);
111
+ act(() => queue.shift()!());
112
+ expect(calls.map((c) => c.language)).toEqual(['typescript']);
113
+ expect(queue).toHaveLength(1);
114
+ act(() => queue.shift()!());
115
+ expect(calls.map((c) => c.language)).toEqual(['typescript', 'bash']);
116
+ });
117
+
118
+ test('is a no-op the second time, so a re-evaluated entry cannot re-warm', () => {
119
+ let calls = 0;
120
+ const options = {
121
+ languages: ['typescript'],
122
+ ready: () => true,
123
+ highlight: () => { calls += 1; },
124
+ schedule: (run: () => void) => { run(); return () => {}; },
125
+ };
126
+ warmSyntaxTokenizers(options);
127
+ warmSyntaxTokenizers(options);
128
+ expect(calls).toBe(1);
129
+ });
130
+
131
+ test('a grammar that has not landed is retried rather than skipped', () => {
132
+ const calls: string[] = [];
133
+ let ready = false;
134
+ const queue: Array<() => void> = [];
135
+ warmSyntaxTokenizers({
136
+ languages: ['typescript'],
137
+ ready: () => ready,
138
+ highlight: (_code, language) => calls.push(language),
139
+ schedule: (run) => {
140
+ queue.push(run);
141
+ return () => {};
142
+ },
143
+ });
144
+
145
+ act(() => queue.shift()!());
146
+ expect(calls).toEqual([]);
147
+ ready = true;
148
+ act(() => queue.shift()!());
149
+ expect(calls).toEqual(['typescript']);
150
+ });
151
+ });
152
+
153
+ describe('the deferred render', () => {
154
+ test('draws the escaped source without tokenizing on the mounting tick', async () => {
155
+ let renders = 0;
156
+ function Card({ code }: { code: string }) {
157
+ const html = useDeferredHighlight(code, 'typescript');
158
+ renders += 1;
159
+ return h('pre', { dangerouslySetInnerHTML: { __html: html } });
160
+ }
161
+
162
+ await act(async () => {
163
+ render(h(Card, { code: 'const value = 1;' }), container);
164
+ });
165
+
166
+ // Nothing has been tokenized on this tick — the tap's own task is clean.
167
+ expect(highlightMemoSize()).toBe(0);
168
+ expect(renders).toBeGreaterThan(0);
169
+ // What it DID draw is the escaped source, not an empty box.
170
+ expect(container.textContent).toContain('const value = 1;');
171
+ });
172
+
173
+ test('empty input renders nothing and caches nothing', async () => {
174
+ function Card() {
175
+ return h('pre', { dangerouslySetInnerHTML: { __html: useDeferredHighlight('', 'typescript') } });
176
+ }
177
+ await act(async () => {
178
+ render(h(Card, {}), container);
179
+ });
180
+ expect(highlightMemoSize()).toBe(0);
181
+ });
182
+ });
@@ -0,0 +1,129 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Syntax-highlighted markup that never blocks the interaction that asked for it.
8
+ *
9
+ * A tool card renders its code the moment it is expanded, and the tokenizer used
10
+ * to run inside that render: measured on a throttled phone (4× CPU), a `read`
11
+ * card's expand was a 220–300 ms long task of which ~75% was the Shiki grammar
12
+ * engine, and at 20× the same card blocked for 730–1.484 ms. The card was on
13
+ * screen the whole time — it just could not paint until the last line had been
14
+ * tokenized.
15
+ *
16
+ * So the first render draws the text, escaped and correct, and the tokenized
17
+ * markup replaces it once the main thread is free (`scheduleIdle`). The two
18
+ * carry the same characters, so the swap costs colour and no layout.
19
+ *
20
+ * Three properties make that honest:
21
+ *
22
+ * - **A cached block renders highlighted on the FIRST paint.** `highlight-memo`
23
+ * is checked during render, so reopening a card the reader already opened —
24
+ * the common case while paging a transcript — has no plain-text frame at all.
25
+ * - **A stale block is never shown.** The value is tagged with the input it was
26
+ * computed from; a filter keystroke in the search panel therefore shows the
27
+ * new text, never the previous query's markup.
28
+ * - **Nothing is computed for empty input.** Call sites that gate a branch pass
29
+ * `''`, which is how `Edit` stops tokenizing the two panels it is not drawing.
30
+ */
31
+
32
+ import { useEffect, useMemo, useState } from 'preact/hooks';
33
+ import { escapeCode } from '@/shared/lib/code/highlighter';
34
+ import {
35
+ cachedHighlightBlock,
36
+ cachedHighlightBlockLines,
37
+ highlightBlock,
38
+ highlightBlockLines,
39
+ } from '@/shared/lib/code/highlight-memo';
40
+ import { scheduleIdle } from '@/shared/lib/code/idle';
41
+ import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
42
+
43
+ interface Deferred<T> {
44
+ /** Identity of the input; a value computed for another input is not shown. */
45
+ key: object;
46
+ hasContent: boolean;
47
+ cached: T | undefined;
48
+ plain: T;
49
+ compute: () => T;
50
+ }
51
+
52
+ function useDeferred<T>(spec: Deferred<T>): T {
53
+ // A grammar arriving is what turns a plain fallback into a real highlight, so
54
+ // the effect re-runs on it even though the input did not move.
55
+ const syntaxReady = useSyntaxReady();
56
+ const [state, setState] = useState<{ key: object; value: T }>(() => ({
57
+ key: spec.key,
58
+ value: spec.cached ?? spec.plain,
59
+ }));
60
+
61
+ /** Adopt a value only when it is a change, so a cached block does not cost an
62
+ * extra render on every mount. */
63
+ const adopt = (key: object, value: T): void => {
64
+ setState((previous) => (previous.key === key && previous.value === value ? previous : { key, value }));
65
+ };
66
+
67
+ useEffect(() => {
68
+ if (!spec.hasContent) {
69
+ adopt(spec.key, spec.plain);
70
+ return;
71
+ }
72
+ if (spec.cached !== undefined) {
73
+ adopt(spec.key, spec.cached);
74
+ return;
75
+ }
76
+ let cancelled = false;
77
+ const cancel = scheduleIdle(() => {
78
+ if (cancelled) return;
79
+ let value: T;
80
+ try {
81
+ value = spec.compute();
82
+ } catch {
83
+ // A grammar that cannot tokenize this input leaves the escaped text,
84
+ // which is what the first paint already drew.
85
+ value = spec.plain;
86
+ }
87
+ if (!cancelled) adopt(spec.key, value);
88
+ });
89
+ return () => {
90
+ cancelled = true;
91
+ cancel();
92
+ };
93
+ // `spec` is rebuilt every render; its KEY (the input) and the grammar's
94
+ // readiness are what decide the work.
95
+ // eslint-disable-next-line react-hooks/exhaustive-deps
96
+ }, [spec.key, syntaxReady]);
97
+
98
+ if (state.key === spec.key) return state.value;
99
+ return spec.cached ?? spec.plain;
100
+ }
101
+
102
+ /** Tokenized markup for one block, deferred off the render that asked for it. */
103
+ export function useDeferredHighlight(code: string, language: string): string {
104
+ const key = useMemo(() => ({ code, language }), [code, language]);
105
+ const plain = useMemo(() => escapeCode(code), [code]);
106
+ return useDeferred<string>({
107
+ key,
108
+ hasContent: Boolean(code),
109
+ cached: code ? cachedHighlightBlock(code, language) : undefined,
110
+ plain,
111
+ compute: () => highlightBlock(code, language),
112
+ });
113
+ }
114
+
115
+ /** Tokenized markup per line, deferred the same way. */
116
+ export function useDeferredHighlightLines(code: string, language: string): string[] {
117
+ const key = useMemo(() => ({ code, language }), [code, language]);
118
+ const plain = useMemo(
119
+ () => (code ? code.split('\n').map(escapeCode) : []),
120
+ [code],
121
+ );
122
+ return useDeferred<string[]>({
123
+ key,
124
+ hasContent: Boolean(code),
125
+ cached: code ? cachedHighlightBlockLines(code, language) : undefined,
126
+ plain,
127
+ compute: () => highlightBlockLines(code, language),
128
+ });
129
+ }
@@ -14,7 +14,7 @@
14
14
  */
15
15
 
16
16
  import type { EditorCommand } from '@/shared/lib/code/editor/keymap';
17
- import type { FindOptions } from '@/shared/lib/code/editor/find';
17
+ import type { FindOptions } from '@/shared/lib/code/editor/find-scan';
18
18
 
19
19
  export interface UseEditorFindOptions {
20
20
  /** The buffer on screen. */
@@ -0,0 +1,206 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The editor-theme setting, and the two rules that make it safe to offer.
8
+ *
9
+ * A theme is a MODULE that is fetched on selection, so there are two failure
10
+ * modes a unit test can pin without a browser:
11
+ *
12
+ * 1. **The module has to be registered before Shiki can tokenize with it.**
13
+ * `loadTheme` is idempotent, and a second surface selecting the same theme
14
+ * must share the first fetch rather than start a second one.
15
+ * 2. **`auto` must not register anything**, and an id this build does not ship
16
+ * must resolve to no theme rather than leave the editor half-painted.
17
+ *
18
+ * Driven through happy-dom because the hook subscribes to a window event — the
19
+ * same shape the editor FONT uses, and the reason an already-open editor hears
20
+ * about a change at all (`appSettings` is a boot-time prop).
21
+ */
22
+
23
+ import { afterAll, afterEach, beforeAll, describe, expect, test } from 'bun:test';
24
+ import { Window } from 'happy-dom';
25
+ import { h, render } from 'preact';
26
+ import { act } from 'preact/test-utils';
27
+
28
+ import {
29
+ EDITOR_THEME_CHANGED_EVENT,
30
+ currentEditorThemeId,
31
+ useEditorTheme,
32
+ } from '@/client/hooks/editor/theme';
33
+ import {
34
+ isEditorThemeReady,
35
+ loadEditorTheme,
36
+ onEditorThemeReady,
37
+ resetEditorThemeLoader,
38
+ } from '@/shared/lib/code/editor/theme-loader';
39
+ import { EDITOR_AUTO_THEME_ID, resolveEditorTheme } from '@/shared/lib/code/editor/themes';
40
+ import { bootSyntax, onLanguageReady, requestLanguage } from '@/shared/lib/code/highlighter';
41
+ import { primeKabmoSettings } from '@/shared/lib/settings/client';
42
+
43
+ const DOM_GLOBALS = [
44
+ 'window', 'document', 'navigator', 'HTMLElement', 'Element', 'Node', 'Event', 'CustomEvent',
45
+ 'getComputedStyle', 'MutationObserver', 'requestAnimationFrame', 'cancelAnimationFrame',
46
+ ] as const;
47
+
48
+ /** The runner's own globals, put back once this file's DOM work is done. */
49
+ const native: Partial<Record<(typeof DOM_GLOBALS)[number], unknown>> = {};
50
+
51
+ /** happy-dom's `CustomEvent`, so a dispatch is an instance its EventTarget accepts. */
52
+ let Custom: typeof CustomEvent;
53
+
54
+ async function bootTokenizer(): Promise<void> {
55
+ // The highlighter only boots where `window` exists, and the Bun runner has
56
+ // none. Stub it for the boot alone, then hand the global to happy-dom — a
57
+ // `window` left pointing at `globalThis` would make the hook's
58
+ // `dispatchEvent` the runner's, which rejects a happy-dom CustomEvent.
59
+ const target = globalThis as unknown as Record<string, unknown>;
60
+ const had = 'window' in target ? target.window : undefined;
61
+ target.window = globalThis;
62
+ await bootSyntax();
63
+ if (had === undefined) delete target.window;
64
+ else target.window = had;
65
+ const { promise, resolve } = Promise.withResolvers<void>();
66
+ const settle = () => {
67
+ if (requestLanguage('typescript')) {
68
+ off();
69
+ resolve();
70
+ }
71
+ };
72
+ const off = onLanguageReady(settle);
73
+ settle();
74
+ await promise;
75
+ }
76
+
77
+ beforeAll(async () => {
78
+ const win = new Window({ url: 'http://localhost' });
79
+ const target = globalThis as unknown as Record<string, unknown>;
80
+ for (const key of DOM_GLOBALS) {
81
+ if (!(key in native)) native[key] = target[key];
82
+ const value = (win as unknown as Record<string, unknown>)[key];
83
+ if (value !== undefined) target[key] = value;
84
+ }
85
+ Custom = win.CustomEvent as unknown as typeof CustomEvent;
86
+ await bootTokenizer();
87
+ resetEditorThemeLoader();
88
+ });
89
+
90
+ afterEach(() => {
91
+ primeKabmoSettings({});
92
+ document.body.innerHTML = '';
93
+ });
94
+
95
+ afterAll(() => {
96
+ const target = globalThis as unknown as Record<string, unknown>;
97
+ for (const key of DOM_GLOBALS) {
98
+ if (native[key] === undefined) delete target[key];
99
+ else target[key] = native[key];
100
+ }
101
+ });
102
+
103
+ /** A probe that renders exactly what the hook says the surface should paint. */
104
+ function ThemeProbe() {
105
+ const theme = useEditorTheme();
106
+ return h(
107
+ 'div',
108
+ { id: 'probe' },
109
+ `${theme.theme?.id ?? 'auto'}|${theme.vars['--editor-bg'] ?? 'none'}|${theme.themes.light}`,
110
+ );
111
+ }
112
+
113
+ async function mountProbe(): Promise<HTMLElement> {
114
+ const host = document.createElement('div');
115
+ document.body.appendChild(host);
116
+ await act(async () => {
117
+ render(h(ThemeProbe, {}), host);
118
+ });
119
+ return host.querySelector('#probe') as HTMLElement;
120
+ }
121
+
122
+ describe('currentEditorThemeId', () => {
123
+ test('reads the stored value and falls back to auto', () => {
124
+ primeKabmoSettings({});
125
+ expect(currentEditorThemeId()).toBe(EDITOR_AUTO_THEME_ID);
126
+
127
+ primeKabmoSettings({ kabmo_settings: { editorTheme: 'nord' } });
128
+ expect(currentEditorThemeId()).toBe('nord');
129
+ });
130
+ });
131
+
132
+ describe('theme module loading', () => {
133
+ test('auto needs no module, and an unknown id is a no-op', async () => {
134
+ resetEditorThemeLoader();
135
+ await loadEditorTheme(EDITOR_AUTO_THEME_ID);
136
+ expect(isEditorThemeReady(EDITOR_AUTO_THEME_ID)).toBe(true);
137
+
138
+ await loadEditorTheme('no-such-theme');
139
+ expect(isEditorThemeReady('no-such-theme')).toBe(false);
140
+ });
141
+
142
+ test('a shipped theme registers with the highlighter and announces itself', async () => {
143
+ resetEditorThemeLoader();
144
+ const seen: string[] = [];
145
+ const off = onEditorThemeReady((id) => seen.push(id));
146
+ try {
147
+ expect(isEditorThemeReady('nord')).toBe(false);
148
+ await loadEditorTheme('nord');
149
+ expect(isEditorThemeReady('nord')).toBe(true);
150
+ expect(seen).toEqual(['nord']);
151
+ } finally {
152
+ off();
153
+ }
154
+ });
155
+
156
+ test('two requests for one theme share a single registration', async () => {
157
+ resetEditorThemeLoader();
158
+ let announcements = 0;
159
+ const off = onEditorThemeReady(() => announcements++);
160
+ try {
161
+ await Promise.all([loadEditorTheme('dracula'), loadEditorTheme('dracula')]);
162
+ expect(announcements).toBe(1);
163
+ // Already registered: a third call resolves without announcing again.
164
+ await loadEditorTheme('dracula');
165
+ expect(announcements).toBe(1);
166
+ } finally {
167
+ off();
168
+ }
169
+ });
170
+ });
171
+
172
+ describe('useEditorTheme', () => {
173
+ test('auto renders no variables and the app’s dual pair', async () => {
174
+ primeKabmoSettings({});
175
+ const probe = await mountProbe();
176
+ expect(probe.textContent).toBe('auto|none|one-light');
177
+ });
178
+
179
+ test('an explicit theme renders its own surface and names itself in both slots', async () => {
180
+ primeKabmoSettings({ kabmo_settings: { editorTheme: 'dracula' } });
181
+ const probe = await mountProbe();
182
+ const theme = resolveEditorTheme('dracula');
183
+ expect(theme).not.toBeNull();
184
+ expect(probe.textContent).toBe(`dracula|${theme!.bg}|dracula`);
185
+ });
186
+
187
+ test('a write reaches an ALREADY-MOUNTED editor without a reload', async () => {
188
+ primeKabmoSettings({});
189
+ const probe = await mountProbe();
190
+ expect(probe.textContent).toBe('auto|none|one-light');
191
+
192
+ // Exactly what the settings modal dispatches on a change.
193
+ await act(async () => {
194
+ window.dispatchEvent(new Custom(EDITOR_THEME_CHANGED_EVENT, { detail: 'nord' }));
195
+ });
196
+ const nord = resolveEditorTheme('nord');
197
+ expect(probe.textContent).toBe(`nord|${nord!.bg}|nord`);
198
+
199
+ // And back to auto, which must clear the variables again rather than leave
200
+ // the previous theme's surface behind.
201
+ await act(async () => {
202
+ window.dispatchEvent(new Custom(EDITOR_THEME_CHANGED_EVENT, { detail: EDITOR_AUTO_THEME_ID }));
203
+ });
204
+ expect(probe.textContent).toBe('auto|none|one-light');
205
+ });
206
+ });