ompchamber 2.0.2 → 3.1.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 (419) hide show
  1. package/README.md +31 -11
  2. package/bunfig.toml +11 -0
  3. package/dist/client/index.html +16 -5
  4. package/dist/client/index.js +1393 -0
  5. package/dist/client/static/js/index-8t70jhyj.js +5149 -0
  6. package/dist/client/static/js/index-n4wkqj7t.css +1 -0
  7. package/index.html +54 -0
  8. package/package.json +9 -10
  9. package/public/apple-touch-icon.png +0 -0
  10. package/public/icon-192x192.png +0 -0
  11. package/public/icon.svg +60 -0
  12. package/public/manifest.webmanifest +38 -0
  13. package/src/cli/lib/commands/serve.js +9 -5
  14. package/src/cli/lib/runtime.js +18 -7
  15. package/src/client/components/common/code-editor/handler.test.ts +274 -0
  16. package/src/client/components/common/code-editor/handler.ts +175 -0
  17. package/src/client/components/common/code-editor/index.tsx +185 -220
  18. package/src/client/components/common/code-surface/index.tsx +109 -12
  19. package/src/client/components/common/lazy-panels.tsx +5 -1
  20. package/src/client/components/common/provider-icon/glyphs.ts +118 -0
  21. package/src/client/components/common/provider-icon/index.tsx +85 -0
  22. package/src/client/components/common/viewport.ts +5 -3
  23. package/src/client/components/layout/RightActivityBar.tsx +2 -1
  24. package/src/client/components/layout/desktop-layout/TopNavbar.tsx +21 -4
  25. package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +4 -0
  26. package/src/client/components/layout/desktop-layout/index.tsx +26 -4
  27. package/src/client/components/mobile/LayoutWrapper.tsx +16 -0
  28. package/src/client/components/mobile/RightSidebar.tsx +11 -4
  29. package/src/client/components/mobile/SessionSidebar.tsx +10 -0
  30. package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +0 -1
  31. package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +16 -1
  32. package/src/client/components/mobile/mobile-session-sidebar/List.tsx +3 -0
  33. package/src/client/components/settings/categories/ProviderSettings.tsx +45 -4
  34. package/src/client/components/settings/categories/UsageSettings.tsx +22 -7
  35. package/src/client/components/settings/categories/provider-settings/AddModelModal.tsx +322 -0
  36. package/src/client/components/settings/categories/provider-settings/AddProviderModal.tsx +152 -56
  37. package/src/client/components/settings/categories/provider-settings/AuthSection.tsx +52 -1
  38. package/src/client/components/settings/categories/provider-settings/DeleteProviderModal.tsx +133 -0
  39. package/src/client/components/settings/categories/provider-settings/DialectFields.tsx +136 -0
  40. package/src/client/components/settings/categories/provider-settings/EffortLadder.tsx +89 -0
  41. package/src/client/components/settings/categories/provider-settings/Header.tsx +2 -2
  42. package/src/client/components/settings/categories/provider-settings/LoginModal.tsx +2 -2
  43. package/src/client/components/settings/categories/provider-settings/ModelsList.tsx +22 -2
  44. package/src/client/components/settings/categories/provider-settings/PresetPicker.tsx +82 -0
  45. package/src/client/components/settings/categories/provider-settings/PriceFields.tsx +107 -0
  46. package/src/client/components/settings/categories/provider-settings/ReconnectModal.tsx +43 -22
  47. package/src/client/components/settings/categories/provider-settings/SidebarList.tsx +2 -2
  48. package/src/client/components/settings/categories/usage-settings/LimitsSection.tsx +115 -0
  49. package/src/client/components/settings/categories/usage-settings/ProviderCard.tsx +19 -26
  50. package/src/client/components/settings/categories/usage-settings/ProviderDetail.tsx +29 -22
  51. package/src/client/components/settings/categories/usage-settings/SidebarList.tsx +60 -48
  52. package/src/client/components/settings/categories/usage-settings/format.ts +36 -0
  53. package/src/client/components/settings/categories/usage-settings/providers.ts +52 -28
  54. package/src/client/components/workspace/browser-panel/AddressBar.tsx +1 -10
  55. package/src/client/components/workspace/browser-panel/Screencast.tsx +4 -5
  56. package/src/client/components/workspace/browser-panel/index.tsx +6 -6
  57. package/src/client/components/workspace/browser-toolbar/index.tsx +22 -12
  58. package/src/client/components/workspace/chat-timeline/AiMessageFooter.tsx +1 -1
  59. package/src/client/components/workspace/chat-timeline/GeneratingIndicator.tsx +1 -1
  60. package/src/client/components/workspace/chat-timeline/ToolCallCard.tsx +1 -1
  61. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Todo.tsx +1 -1
  62. package/src/client/components/workspace/diff-panel/Toolbar.tsx +71 -38
  63. package/src/client/components/workspace/diff-panel/index.tsx +139 -53
  64. package/src/client/components/workspace/editor/CommandPalette.test.ts +242 -0
  65. package/src/client/components/workspace/editor/CommandPalette.tsx +138 -0
  66. package/src/client/components/workspace/editor/FindWidget.test.ts +261 -0
  67. package/src/client/components/workspace/editor/FindWidget.tsx +219 -0
  68. package/src/client/components/workspace/editor/Tabs.tsx +2 -2
  69. package/src/client/components/workspace/editor/Toolbar.tsx +129 -26
  70. package/src/client/components/workspace/editor/index.tsx +180 -62
  71. package/src/client/components/workspace/file-explorer/Modals.tsx +79 -2
  72. package/src/client/components/workspace/file-explorer/TreeItem.tsx +43 -113
  73. package/src/client/components/workspace/model-dropdown/Item.tsx +13 -23
  74. package/src/client/components/workspace/model-dropdown/Panel.tsx +1 -1
  75. package/src/client/components/workspace/model-dropdown/Section.tsx +13 -4
  76. package/src/client/components/workspace/model-dropdown/index.tsx +7 -3
  77. package/src/client/components/workspace/todo-panel/PhaseGroup.tsx +76 -0
  78. package/src/client/components/workspace/todo-panel/Row.tsx +69 -0
  79. package/src/client/components/workspace/todo-panel/index.tsx +161 -0
  80. package/src/client/components/workspace/usage-panel/ProviderSelect.tsx +10 -5
  81. package/src/client/components/workspace/usage-panel/index.tsx +36 -18
  82. package/src/client/data/samples/dialogue-session.ts +1 -1
  83. package/src/client/data/samples/sample-tools/other.ts +1 -1
  84. package/src/client/data/settings/provider.ts +141 -9
  85. package/src/client/hooks/editor/find-harness.test-util.ts +170 -0
  86. package/src/client/hooks/editor/scroll-container.test.ts +102 -0
  87. package/src/client/hooks/editor/scroll-container.ts +26 -0
  88. package/src/client/hooks/editor/use-code-window.ts +2 -20
  89. package/src/client/hooks/editor/use-editor-find-write.test.ts +170 -0
  90. package/src/client/hooks/editor/use-editor-find.test.ts +118 -0
  91. package/src/client/hooks/editor/use-editor-find.ts +321 -0
  92. package/src/client/hooks/editor/use-editor-history.test.ts +216 -0
  93. package/src/client/hooks/editor/use-editor-history.ts +134 -0
  94. package/src/client/hooks/editor/use-file-editor.ts +24 -27
  95. package/src/client/hooks/editor/use-occurrences.test.ts +236 -0
  96. package/src/client/hooks/editor/use-occurrences.ts +98 -0
  97. package/src/client/hooks/models/use-provider-names.ts +1 -1
  98. package/src/client/hooks/settings/providers/actions.ts +337 -0
  99. package/src/client/hooks/settings/providers/api.ts +69 -0
  100. package/src/client/hooks/settings/providers/index.ts +138 -0
  101. package/src/client/hooks/settings/providers/messages.ts +77 -0
  102. package/src/client/hooks/settings/useUsageReport.ts +8 -3
  103. package/src/client/hooks/ui/theme.ts +3 -3
  104. package/src/client/hooks/workspace/file-tabs.ts +62 -11
  105. package/src/client/hooks/workspace/file-tree-actions.test.ts +348 -0
  106. package/src/client/hooks/workspace/file-tree-actions.ts +250 -0
  107. package/src/client/hooks/workspace/session-todos.ts +102 -0
  108. package/src/client/stubs/elkjs.ts +6 -1
  109. package/src/client/tailwind.css +30 -0
  110. package/src/server/index.ts +20 -2
  111. package/src/server/lib/assets/font-css.server.ts +86 -0
  112. package/src/server/lib/assets/fonts.server.ts +117 -0
  113. package/src/server/lib/bundler/css.ts +116 -0
  114. package/src/server/lib/lifecycle/shell-route.ts +17 -0
  115. package/src/server/lib/models/provider-listing.server.ts +257 -0
  116. package/src/server/lib/models/provider-registry.server.test.ts +122 -0
  117. package/src/server/lib/models/provider-registry.server.ts +76 -25
  118. package/src/server/lib/omp/config/models-config.ts +11 -0
  119. package/src/server/lib/omp/config/models-validation.test.ts +137 -0
  120. package/src/server/lib/omp/config/models-validation.ts +111 -2
  121. package/src/server/lib/omp/config/provider-seeds.test.ts +167 -0
  122. package/src/server/lib/omp/config/provider-seeds.ts +176 -0
  123. package/src/server/lib/omp/config/providers.ts +94 -100
  124. package/src/server/lib/omp/core/auth-credentials.ts +45 -0
  125. package/src/server/lib/omp/session/todos.ts +100 -0
  126. package/src/server/lib/omp/session/{usage.ts → usage/aggregate.ts} +1 -118
  127. package/src/server/lib/omp/session/usage/shaping.ts +130 -0
  128. package/src/server/lib/usage/deepseek.ts +4 -5
  129. package/src/server/lib/usage/kenari.ts +7 -6
  130. package/src/server/lib/usage/omp-usage.server.test.ts +126 -0
  131. package/src/server/lib/usage/omp-usage.server.ts +194 -0
  132. package/src/server/lib/usage/provider-key.ts +81 -3
  133. package/src/server/lib/usage/providers.server.test.ts +47 -0
  134. package/src/server/lib/usage/providers.server.ts +208 -0
  135. package/src/server/lib/usage/vendor-balance.server.test.ts +104 -0
  136. package/src/server/lib/usage/vendor-balance.server.ts +284 -0
  137. package/src/server/plugins/shell.server.ts +56 -0
  138. package/src/server/plugins/ssr.ts +24 -36
  139. package/src/server/plugins/static.test.ts +26 -29
  140. package/src/server/plugins/static.ts +40 -19
  141. package/src/server/routes/fs/action-create.test.ts +90 -0
  142. package/src/server/routes/fs/action.ts +23 -0
  143. package/src/server/routes/omp/index.ts +2 -0
  144. package/src/server/routes/omp/session-todos.ts +80 -0
  145. package/src/server/routes/settings/index.ts +2 -0
  146. package/src/server/routes/settings/provider-model.ts +107 -0
  147. package/src/server/routes/settings/provider-models.ts +65 -202
  148. package/src/server/routes/settings/providers.ts +22 -7
  149. package/src/server/routes/settings/usage.ts +125 -45
  150. package/src/server/routes/telemetry/tokens.ts +2 -1
  151. package/src/shared/lib/chat/{todo-parser.ts → todo/parser.ts} +17 -0
  152. package/src/shared/lib/chat/todo/progress.ts +97 -0
  153. package/src/shared/lib/chat/todo/snapshot.test.ts +156 -0
  154. package/src/shared/lib/chat/todo/snapshot.ts +215 -0
  155. package/src/shared/lib/code/editor/commands.test.ts +155 -0
  156. package/src/shared/lib/code/editor/commands.ts +260 -0
  157. package/src/shared/lib/code/editor/edits.test.ts +100 -0
  158. package/src/shared/lib/code/editor/edits.ts +122 -0
  159. package/src/shared/lib/code/editor/find-status.test.ts +50 -0
  160. package/src/shared/lib/code/editor/find-status.ts +36 -0
  161. package/src/shared/lib/code/editor/find.test.ts +203 -0
  162. package/src/shared/lib/code/editor/find.ts +224 -0
  163. package/src/{client/components/common/code-editor → shared/lib/code/editor}/history.test.ts +1 -1
  164. package/src/shared/lib/code/editor/keymap.test.ts +109 -0
  165. package/src/shared/lib/code/editor/keymap.ts +151 -0
  166. package/src/shared/lib/code/editor/layers.test.ts +77 -0
  167. package/src/shared/lib/code/editor/layers.ts +67 -0
  168. package/src/shared/lib/code/editor/line-edits.test.ts +131 -0
  169. package/src/shared/lib/code/editor/line-edits.ts +159 -0
  170. package/src/shared/lib/code/editor/lines.test.ts +81 -0
  171. package/src/shared/lib/code/editor/lines.ts +83 -0
  172. package/src/shared/lib/code/editor/marks.test.ts +86 -0
  173. package/src/shared/lib/code/editor/marks.ts +106 -0
  174. package/src/shared/lib/code/editor/occurrences.test.ts +117 -0
  175. package/src/shared/lib/code/editor/occurrences.ts +111 -0
  176. package/src/shared/lib/code/editor/palette.test.ts +91 -0
  177. package/src/shared/lib/code/editor/palette.ts +92 -0
  178. package/src/shared/lib/code/editor/reset-css.test.ts +35 -0
  179. package/src/shared/lib/code/editor/reset-css.ts +32 -0
  180. package/src/shared/lib/code/editor/text-commands.test.ts +178 -0
  181. package/src/shared/lib/code/editor/text-commands.ts +154 -0
  182. package/src/shared/lib/code/syntax-highlight.test.ts +75 -0
  183. package/src/shared/lib/code/syntax-highlight.ts +29 -17
  184. package/src/shared/lib/code/windowed-highlight.ts +52 -1
  185. package/src/shared/lib/fs/diff-parser.test.ts +116 -0
  186. package/src/shared/lib/fs/diff-parser.ts +86 -33
  187. package/src/shared/lib/models/{provider-connection.ts → provider/connection.ts} +37 -0
  188. package/src/shared/lib/models/provider/dialect.ts +179 -0
  189. package/src/shared/lib/models/provider/glyph.test.ts +116 -0
  190. package/src/shared/lib/models/provider/glyph.ts +200 -0
  191. package/src/shared/lib/models/{provider-models.ts → provider/models.ts} +91 -6
  192. package/src/shared/lib/models/provider/presets.test.ts +172 -0
  193. package/src/shared/lib/models/provider/presets.ts +83 -0
  194. package/src/shared/lib/ui/key-binding.test.ts +115 -0
  195. package/src/shared/lib/ui/key-binding.ts +143 -0
  196. package/src/shared/lib/util/platform.test.ts +87 -0
  197. package/src/shared/lib/util/platform.ts +30 -0
  198. package/src/shared/lib/workspace/file-tab-id.ts +29 -0
  199. package/src/shared/lib/workspace/keymap.test.ts +82 -0
  200. package/src/shared/lib/workspace/keymap.ts +30 -0
  201. package/src/shared/lib/workspace/refresh-cadence.ts +9 -0
  202. package/src/shared/lib/workspace/right-panels.ts +8 -0
  203. package/src/shared/lib/workspace/session-state/store.ts +0 -1
  204. package/src/shared/types/index.ts +1 -0
  205. package/src/shared/types/settings/provider.ts +81 -0
  206. package/src/shared/types/settings/usage.ts +88 -14
  207. package/src/shared/types/todo.ts +108 -0
  208. package/tsconfig.json +3 -1
  209. package/dist/client/static/css/index.5f630421b7.css +0 -2
  210. package/dist/client/static/font/KaTeX_AMS-Regular.a79f1c3119.woff2 +0 -0
  211. package/dist/client/static/font/KaTeX_Caligraphic-Bold.ec17d13264.woff2 +0 -0
  212. package/dist/client/static/font/KaTeX_Caligraphic-Regular.55fac25845.woff2 +0 -0
  213. package/dist/client/static/font/KaTeX_Fraktur-Bold.d42a5579b0.woff2 +0 -0
  214. package/dist/client/static/font/KaTeX_Fraktur-Regular.d3c882a649.woff2 +0 -0
  215. package/dist/client/static/font/KaTeX_Main-Bold.c3fb5ac22f.woff2 +0 -0
  216. package/dist/client/static/font/KaTeX_Main-BoldItalic.6f2bb1dff2.woff2 +0 -0
  217. package/dist/client/static/font/KaTeX_Main-Italic.8916142bec.woff2 +0 -0
  218. package/dist/client/static/font/KaTeX_Main-Regular.0462f03bdf.woff2 +0 -0
  219. package/dist/client/static/font/KaTeX_Math-BoldItalic.572d331f69.woff2 +0 -0
  220. package/dist/client/static/font/KaTeX_Math-Italic.f28c23acad.woff2 +0 -0
  221. package/dist/client/static/font/KaTeX_SansSerif-Bold.8c5b5494b6.woff2 +0 -0
  222. package/dist/client/static/font/KaTeX_SansSerif-Italic.3b1e59b3ba.woff2 +0 -0
  223. package/dist/client/static/font/KaTeX_SansSerif-Regular.ba21ed5f84.woff2 +0 -0
  224. package/dist/client/static/font/KaTeX_Script-Regular.03e9641d6f.woff2 +0 -0
  225. package/dist/client/static/font/KaTeX_Size1-Regular.eae34984b3.woff2 +0 -0
  226. package/dist/client/static/font/KaTeX_Size2-Regular.5916a24fa3.woff2 +0 -0
  227. package/dist/client/static/font/KaTeX_Size4-Regular.10d95fd3a2.woff2 +0 -0
  228. package/dist/client/static/font/KaTeX_Typewriter-Regular.a8709e3622.woff2 +0 -0
  229. package/dist/client/static/font/fira-code-latin-400-normal.2655b10c55.woff2 +0 -0
  230. package/dist/client/static/font/fira-code-latin-500-normal.92a991f204.woff2 +0 -0
  231. package/dist/client/static/font/fira-code-latin-600-normal.43788eb7f1.woff2 +0 -0
  232. package/dist/client/static/font/fira-code-latin-700-normal.2667215d33.woff2 +0 -0
  233. package/dist/client/static/js/77.0d81c74cf0.js +0 -139
  234. package/dist/client/static/js/77.0d81c74cf0.js.LICENSE.txt +0 -8
  235. package/dist/client/static/js/async/0.8e59f2eb5b.js +0 -1
  236. package/dist/client/static/js/async/0k.5852e6b4fa.js +0 -1
  237. package/dist/client/static/js/async/0w.9ed36d5860.js +0 -1
  238. package/dist/client/static/js/async/1.f387c12b63.js +0 -1
  239. package/dist/client/static/js/async/12.d091f91c99.js +0 -1
  240. package/dist/client/static/js/async/13.8f810a8a6c.js +0 -1
  241. package/dist/client/static/js/async/19.6fe909d55a.js +0 -160
  242. package/dist/client/static/js/async/1b.23f1141a38.js +0 -1
  243. package/dist/client/static/js/async/1k.973e8e21fc.js +0 -2
  244. package/dist/client/static/js/async/1k.973e8e21fc.js.LICENSE.txt +0 -11
  245. package/dist/client/static/js/async/1p.0d5cddda96.js +0 -1
  246. package/dist/client/static/js/async/2.b589f1733b.js +0 -1
  247. package/dist/client/static/js/async/2v.96165101de.js +0 -1
  248. package/dist/client/static/js/async/3.0cacc08423.js +0 -1
  249. package/dist/client/static/js/async/37.b28b2f61cd.js +0 -1
  250. package/dist/client/static/js/async/3d.5085fb913f.js +0 -1
  251. package/dist/client/static/js/async/3j.357c80ff38.js +0 -2
  252. package/dist/client/static/js/async/3j.357c80ff38.js.LICENSE.txt +0 -11
  253. package/dist/client/static/js/async/3p.bcf32a0983.js +0 -1
  254. package/dist/client/static/js/async/3r.3a69e98ee0.js +0 -64
  255. package/dist/client/static/js/async/3u.1228460e2a.js +0 -1
  256. package/dist/client/static/js/async/3y.3530b824ad.js +0 -198
  257. package/dist/client/static/js/async/3y.3530b824ad.js.LICENSE.txt +0 -5
  258. package/dist/client/static/js/async/4.9442eeb472.js +0 -1
  259. package/dist/client/static/js/async/4e.678285ca55.js +0 -1
  260. package/dist/client/static/js/async/4k.33a0c7eeb4.js +0 -1
  261. package/dist/client/static/js/async/4m.928a1dde85.js +0 -31
  262. package/dist/client/static/js/async/4m.928a1dde85.js.LICENSE.txt +0 -11
  263. package/dist/client/static/js/async/4q.aad20dab25.js +0 -1
  264. package/dist/client/static/js/async/5.ebaf8b89cc.js +0 -36
  265. package/dist/client/static/js/async/51.95bcae98af.js +0 -2
  266. package/dist/client/static/js/async/51.95bcae98af.js.LICENSE.txt +0 -6
  267. package/dist/client/static/js/async/5b.d88304bc13.js +0 -1
  268. package/dist/client/static/js/async/5j.d6407c4248.js +0 -287
  269. package/dist/client/static/js/async/5o.88316d4460.js +0 -62
  270. package/dist/client/static/js/async/6.f034c9b528.js +0 -1
  271. package/dist/client/static/js/async/66.5dfa29f957.js +0 -93
  272. package/dist/client/static/js/async/6o.5b1a06a382.js +0 -1
  273. package/dist/client/static/js/async/7.e4a9288dd0.js +0 -1
  274. package/dist/client/static/js/async/73.308dff4da8.js +0 -1
  275. package/dist/client/static/js/async/7e.a1cc9bfd88.js +0 -1
  276. package/dist/client/static/js/async/7g.a7814350c3.js +0 -1
  277. package/dist/client/static/js/async/7l.fadbbc0583.js +0 -1
  278. package/dist/client/static/js/async/8.5067439d8c.js +0 -1
  279. package/dist/client/static/js/async/8j.ea110663fb.js +0 -1
  280. package/dist/client/static/js/async/a.bce4542392.js +0 -1
  281. package/dist/client/static/js/async/aa.65d3852576.js +0 -1
  282. package/dist/client/static/js/async/ar.9303e5f2e8.js +0 -2
  283. package/dist/client/static/js/async/ar.9303e5f2e8.js.LICENSE.txt +0 -11
  284. package/dist/client/static/js/async/az.d656062b0f.js +0 -1
  285. package/dist/client/static/js/async/b.b4f6f06515.js +0 -1
  286. package/dist/client/static/js/async/bi.09688ba75c.js +0 -1
  287. package/dist/client/static/js/async/bq.586bc738d1.js +0 -1
  288. package/dist/client/static/js/async/c.797d933882.js +0 -1
  289. package/dist/client/static/js/async/c0.0af1e640d3.js +0 -24
  290. package/dist/client/static/js/async/ca.8a52cfd447.js +0 -1
  291. package/dist/client/static/js/async/cf.d34fff5d67.js +0 -1
  292. package/dist/client/static/js/async/ct.b74928f7a3.js +0 -1
  293. package/dist/client/static/js/async/cx.3cb0242a3a.js +0 -226
  294. package/dist/client/static/js/async/d.a5fc8fe1b3.js +0 -1
  295. package/dist/client/static/js/async/da.f62c70f6c8.js +0 -1
  296. package/dist/client/static/js/async/dp.1725d6432c.js +0 -1
  297. package/dist/client/static/js/async/e.93214d622b.js +0 -79
  298. package/dist/client/static/js/async/f.ab50467719.js +0 -19
  299. package/dist/client/static/js/async/f.ab50467719.js.LICENSE.txt +0 -12
  300. package/dist/client/static/js/async/f7.46971e9d6d.js +0 -2
  301. package/dist/client/static/js/async/f7.46971e9d6d.js.LICENSE.txt +0 -6
  302. package/dist/client/static/js/async/f8.bbf0dbae70.js +0 -1
  303. package/dist/client/static/js/async/g.cb81980498.js +0 -2
  304. package/dist/client/static/js/async/g.cb81980498.js.LICENSE.txt +0 -11
  305. package/dist/client/static/js/async/h.4e790471e4.js +0 -27
  306. package/dist/client/static/js/async/h1.393f066d09.js +0 -1
  307. package/dist/client/static/js/async/h3.216e9798ab.js +0 -148
  308. package/dist/client/static/js/async/hd.0da931b622.js +0 -1
  309. package/dist/client/static/js/async/hk.ba7fb04d41.js +0 -1
  310. package/dist/client/static/js/async/hn.b645041e00.js +0 -1
  311. package/dist/client/static/js/async/ho.475a5e1ae4.js +0 -1
  312. package/dist/client/static/js/async/hp.e9fe2e395a.js +0 -2
  313. package/dist/client/static/js/async/hp.e9fe2e395a.js.LICENSE.txt +0 -17
  314. package/dist/client/static/js/async/hs.e7c59d8090.js +0 -41
  315. package/dist/client/static/js/async/i.7c148e8440.js +0 -1
  316. package/dist/client/static/js/async/id.573df20f29.js +0 -1
  317. package/dist/client/static/js/async/j.ab925882d8.js +0 -1
  318. package/dist/client/static/js/async/jd.63886a1f09.js +0 -1
  319. package/dist/client/static/js/async/jg.d86ebd4013.js +0 -86
  320. package/dist/client/static/js/async/jl.874d352278.js +0 -1
  321. package/dist/client/static/js/async/jll.11313b455a.js +0 -1
  322. package/dist/client/static/js/async/js.67182759ef.js +0 -1
  323. package/dist/client/static/js/async/jsc.23b70d6f40.js +0 -1
  324. package/dist/client/static/js/async/jse.334588ff5d.js +0 -1
  325. package/dist/client/static/js/async/jz.72eb929106.js +0 -77
  326. package/dist/client/static/js/async/k.3210286401.js +0 -217
  327. package/dist/client/static/js/async/ka.c1990ecaa3.js +0 -115
  328. package/dist/client/static/js/async/katex-vendor.de6a87c308.js +0 -1
  329. package/dist/client/static/js/async/ku.29c8a59d0c.js +0 -1
  330. package/dist/client/static/js/async/ky.8dab1dade0.js +0 -1
  331. package/dist/client/static/js/async/l.12dd34e1be.js +0 -1
  332. package/dist/client/static/js/async/l9.949710adaf.js +0 -83
  333. package/dist/client/static/js/async/l9.949710adaf.js.LICENSE.txt +0 -13
  334. package/dist/client/static/js/async/ls.9d9a01026c.js +0 -104
  335. package/dist/client/static/js/async/m.eb2e6752e2.js +0 -28
  336. package/dist/client/static/js/async/m.eb2e6752e2.js.LICENSE.txt +0 -11
  337. package/dist/client/static/js/async/m3.d2b05f9604.js +0 -2
  338. package/dist/client/static/js/async/m3.d2b05f9604.js.LICENSE.txt +0 -6
  339. package/dist/client/static/js/async/ma.2c261b04dd.js +0 -2
  340. package/dist/client/static/js/async/ma.2c261b04dd.js.LICENSE.txt +0 -6
  341. package/dist/client/static/js/async/me.8ace414f80.js +0 -23
  342. package/dist/client/static/js/async/ms.98960af4eb.js +0 -67
  343. package/dist/client/static/js/async/ms.98960af4eb.js.LICENSE.txt +0 -12
  344. package/dist/client/static/js/async/mt.0352e976d8.js +0 -1
  345. package/dist/client/static/js/async/n.a9f1c51701.js +0 -1
  346. package/dist/client/static/js/async/na.a36cd532d6.js +0 -1
  347. package/dist/client/static/js/async/nk.fc7e58325e.js +0 -1
  348. package/dist/client/static/js/async/o.36353dfb88.js +0 -1
  349. package/dist/client/static/js/async/oc.b73042e8bd.js +0 -8
  350. package/dist/client/static/js/async/og.8eb577893a.js +0 -193
  351. package/dist/client/static/js/async/p.48ecfcd14e.js +0 -1
  352. package/dist/client/static/js/async/px.fc6877924b.js +0 -1
  353. package/dist/client/static/js/async/q.22588845a9.js +0 -21
  354. package/dist/client/static/js/async/q.22588845a9.js.LICENSE.txt +0 -6
  355. package/dist/client/static/js/async/q5.97dc1481b6.js +0 -286
  356. package/dist/client/static/js/async/qs.2a9ddb3c72.js +0 -1
  357. package/dist/client/static/js/async/r.51010ad87c.js +0 -1
  358. package/dist/client/static/js/async/rd.cb6bf495bb.js +0 -2
  359. package/dist/client/static/js/async/rd.cb6bf495bb.js.LICENSE.txt +0 -4
  360. package/dist/client/static/js/async/s.0f395cdda5.js +0 -94
  361. package/dist/client/static/js/async/t.4dca0e2ba8.js +0 -1
  362. package/dist/client/static/js/async/t9.d44f57d518.js +0 -1
  363. package/dist/client/static/js/async/ta.973534bbec.js +0 -1
  364. package/dist/client/static/js/async/tr.e878658140.js +0 -309
  365. package/dist/client/static/js/async/u.0871ac5b22.js +0 -1
  366. package/dist/client/static/js/async/ud.3065bfee31.js +0 -2
  367. package/dist/client/static/js/async/ud.3065bfee31.js.LICENSE.txt +0 -4
  368. package/dist/client/static/js/async/up.ea990da0ff.js +0 -32
  369. package/dist/client/static/js/async/uw.8890abbabc.js +0 -1
  370. package/dist/client/static/js/async/v.7d28d3c241.js +0 -34
  371. package/dist/client/static/js/async/vp.9b13bf8610.js +0 -1
  372. package/dist/client/static/js/async/vt.42416b707d.js +0 -1
  373. package/dist/client/static/js/async/w.08ac210f60.js +0 -1
  374. package/dist/client/static/js/async/wt.ad92393a90.js +0 -1
  375. package/dist/client/static/js/async/x.563f72ca6b.js +0 -1
  376. package/dist/client/static/js/async/x0.475516418f.js +0 -1
  377. package/dist/client/static/js/async/x1.da8687159b.js +0 -90
  378. package/dist/client/static/js/async/xe.da8dcc2015.js +0 -2
  379. package/dist/client/static/js/async/xe.da8dcc2015.js.LICENSE.txt +0 -11
  380. package/dist/client/static/js/async/xl.a3c562a58c.js +0 -1
  381. package/dist/client/static/js/async/xn.6510c0abe2.js +0 -2
  382. package/dist/client/static/js/async/xn.6510c0abe2.js.LICENSE.txt +0 -6
  383. package/dist/client/static/js/async/xn1.83a7784fe1.js +0 -10
  384. package/dist/client/static/js/async/xn1.83a7784fe1.js.LICENSE.txt +0 -11
  385. package/dist/client/static/js/async/xnl.feb0021e50.js +0 -2
  386. package/dist/client/static/js/async/xnl.feb0021e50.js.LICENSE.txt +0 -12
  387. package/dist/client/static/js/async/y.1a9ed7a5d8.js +0 -1
  388. package/dist/client/static/js/async/y6.43c57defe4.js +0 -1
  389. package/dist/client/static/js/async/yb.8c1342401d.js +0 -1
  390. package/dist/client/static/js/async/ybq.4491f2f2dc.js +0 -1
  391. package/dist/client/static/js/async/yg.35bebd6d0c.js +0 -149
  392. package/dist/client/static/js/async/yq.917ddd7ba2.js +0 -1
  393. package/dist/client/static/js/async/ys.00687cf82d.js +0 -38
  394. package/dist/client/static/js/async/yv.d99d829136.js +0 -1
  395. package/dist/client/static/js/async/z.56f61a4374.js +0 -1
  396. package/dist/client/static/js/async/zi.5fdb07b86a.js +0 -24
  397. package/dist/client/static/js/async/zj.654f37e8ff.js +0 -1
  398. package/dist/client/static/js/async/zn.966d6f1cc6.js +0 -1
  399. package/dist/client/static/js/async/zo.26213b40c0.js +0 -1
  400. package/dist/client/static/js/async/zx.e9f4105e00.js +0 -1
  401. package/dist/client/static/js/index.e6a257402a.js +0 -38
  402. package/dist/client/static/js/index.e6a257402a.js.LICENSE.txt +0 -4
  403. package/dist/client/static/js/markdown-vendor.71e43278b3.js +0 -144
  404. package/dist/client/static/js/shiki-vendor.cb8452ddcd.js +0 -1
  405. package/src/client/components/settings/categories/provider-settings/Icons.tsx +0 -35
  406. package/src/client/hooks/settings/providers.ts +0 -326
  407. package/src/server/plugins/dev-assets.ts +0 -49
  408. package/src/shared/lib/code/editor-utils.ts +0 -16
  409. package/src/shared/lib/models/provider-presets.ts +0 -29
  410. /package/dist/client/{apple-touch-icon.png → static/apple-touch-icon-dskx5sr3.png} +0 -0
  411. /package/dist/client/{icon-192x192.png → static/icon-192x192-6gn5cr2f.png} +0 -0
  412. /package/dist/client/{icon.svg → static/icon-d07z9dtz.svg} +0 -0
  413. /package/dist/client/{manifest.webmanifest → static/manifest-bnyy75kt.webmanifest} +0 -0
  414. /package/{dist/client → public}/icon-512x512.png +0 -0
  415. /package/{dist/client → public}/icon-maskable-512x512.png +0 -0
  416. /package/{dist/client → public}/sw.js +0 -0
  417. /package/src/{client/components/common/code-editor → shared/lib/code/editor}/history.ts +0 -0
  418. /package/src/shared/lib/models/{provider-cleanup.ts → provider/cleanup.ts} +0 -0
  419. /package/src/shared/lib/models/{provider-label.ts → provider/label.ts} +0 -0
@@ -0,0 +1,97 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Display-side helpers for a todo list: the one-line progress summary, the
8
+ * per-task visual state, and the "which task is the agent on" lookup the panel
9
+ * highlights.
10
+ *
11
+ * Everything here is presentational — the parse itself lives in `snapshot.ts`.
12
+ * The status → presentation mapping is a table rather than a chain of `if`s
13
+ * because there are five statuses and three consumers (icon, label, class
14
+ * list), and a missing case in any one of them is how a `blocked` task ends up
15
+ * rendered as a plain pending one.
16
+ */
17
+
18
+ import type { TodoItem, TodoPhase, TodoProgress, TodoStatus } from '@/shared/types/todo';
19
+
20
+ export interface TodoStatusVisual {
21
+ /** Short label for the row's status pill. */
22
+ label: string;
23
+ /** Tailwind text color for the icon and label. */
24
+ tone: string;
25
+ /** True when the row is a live (in-progress) task — highlighted, not dimmed. */
26
+ active: boolean;
27
+ /** True when the task is out of the way (completed / abandoned). */
28
+ closed: boolean;
29
+ }
30
+
31
+ const STATUS_VISUALS: Record<TodoStatus, TodoStatusVisual> = {
32
+ pending: { label: 'Pending', tone: 'text-ink/45', active: false, closed: false },
33
+ in_progress: { label: 'In progress', tone: 'text-ink', active: true, closed: false },
34
+ completed: { label: 'Completed', tone: 'text-success', active: false, closed: true },
35
+ abandoned: { label: 'Abandoned', tone: 'text-error/70', active: false, closed: true },
36
+ blocked: { label: 'Blocked', tone: 'text-warning', active: false, closed: false },
37
+ };
38
+
39
+ export function todoStatusVisual(status: TodoStatus): TodoStatusVisual {
40
+ return STATUS_VISUALS[status] ?? STATUS_VISUALS.pending;
41
+ }
42
+
43
+ /**
44
+ * One-line summary of a snapshot: `4/11 done · 1 in progress · 2 blocked`.
45
+ *
46
+ * Counts CLOSED (completed + abandoned) against the total, matching omp's own
47
+ * HUD counter — an abandoned task is deliberately not coming back, so leaving
48
+ * it in the denominator is what makes a list read as permanently stuck.
49
+ */
50
+ export function todoProgressLabel(progress: TodoProgress): string {
51
+ if (progress.total === 0) return 'No tasks';
52
+ const parts = [`${progress.closed}/${progress.total} done`];
53
+ if (progress.inProgress > 0) parts.push(`${progress.inProgress} in progress`);
54
+ if (progress.blocked > 0) parts.push(`${progress.blocked} blocked`);
55
+ if (progress.abandoned > 0) parts.push(`${progress.abandoned} abandoned`);
56
+ return parts.join(' · ');
57
+ }
58
+
59
+ /** Percentage of the list that is closed, rounded to a whole number. */
60
+ export function todoProgressPercent(progress: TodoProgress): number {
61
+ if (progress.total === 0) return 0;
62
+ return Math.round((progress.closed / progress.total) * 100);
63
+ }
64
+
65
+ /**
66
+ * The row the panel marks as current: the in-progress task, else the first
67
+ * pending one. Identified by phase index + task index rather than by content,
68
+ * because two phases may legitimately hold tasks with identical text (omp
69
+ * itself keys its own task lookup by content, and that ambiguity is its
70
+ * problem, not one to inherit here).
71
+ */
72
+ export function currentTaskLocation(phases: readonly TodoPhase[]): { phase: number; task: number } | null {
73
+ for (let phase = 0; phase < phases.length; phase++) {
74
+ const tasks = phases[phase].tasks;
75
+ for (let task = 0; task < tasks.length; task++) {
76
+ if (tasks[task].status === 'in_progress') return { phase, task };
77
+ }
78
+ }
79
+ for (let phase = 0; phase < phases.length; phase++) {
80
+ const tasks = phases[phase].tasks;
81
+ for (let task = 0; task < tasks.length; task++) {
82
+ if (tasks[task].status === 'pending') return { phase, task };
83
+ }
84
+ }
85
+ return null;
86
+ }
87
+
88
+ /** Tasks of one phase that are closed, for the phase header's `n/m` badge. */
89
+ export function closedTaskCount(phase: TodoPhase): number {
90
+ return phase.tasks.filter((task) => task.status === 'completed' || task.status === 'abandoned').length;
91
+ }
92
+
93
+ /** Blocker note of a task, trimmed, or undefined when there is nothing to show. */
94
+ export function taskNote(task: TodoItem): string | undefined {
95
+ const note = task.blocker?.trim() || task.details?.trim();
96
+ return note ? note : undefined;
97
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ import { describe, expect, test } from 'bun:test';
7
+
8
+ import { latestTodoSnapshot, todoProgress } from '@/shared/lib/chat/todo/snapshot';
9
+ import type { TodoPhase, TodoStatus } from '@/shared/types/todo';
10
+
11
+ /**
12
+ * The reader has to match oh-my-pi's own branch rehydration, and every rule it
13
+ * implements was verified against a real session written by omp 18.3.0:
14
+ *
15
+ * - a `todo` toolResult carries `message.details.phases`;
16
+ * - `op:"view"` results carry phases but commit nothing (omp skips them);
17
+ * - a `user_todo_edit` custom entry carries `data.phases` and is durable;
18
+ * - the list is read off the ACTIVE BRANCH (leaf → root via `parentId`), so a
19
+ * rewound fork must not resurface.
20
+ *
21
+ * The failure mode of getting any of these wrong is a panel that quietly shows
22
+ * a stale plan, which no other surface would contradict.
23
+ */
24
+
25
+ const phases = (...tasks: Array<[string, TodoStatus]>): TodoPhase[] => [
26
+ { name: 'Phase', tasks: tasks.map(([content, status]) => ({ content, status })) },
27
+ ];
28
+
29
+ function entry(id: string, parentId: string | null, body: Record<string, unknown>) {
30
+ return { id, parentId, timestamp: '2026-09-25T00:00:00.000Z', ...body };
31
+ }
32
+
33
+ function todoResult(id: string, parentId: string | null, details: Record<string, unknown>) {
34
+ return entry(id, parentId, {
35
+ type: 'message',
36
+ message: { role: 'toolResult', toolName: 'todo', content: [{ type: 'text', text: '' }], details },
37
+ });
38
+ }
39
+
40
+ describe('latestTodoSnapshot', () => {
41
+ test('reads the deepest committed snapshot on the active branch', () => {
42
+ const entries = [
43
+ todoResult('a1', null, { op: 'init', phases: phases(['one', 'pending'], ['two', 'pending']) }),
44
+ todoResult('a2', 'a1', { op: 'done', phases: phases(['one', 'completed'], ['two', 'in_progress']) }),
45
+ ];
46
+
47
+ const snapshot = latestTodoSnapshot(entries);
48
+
49
+ expect(snapshot?.sourceEntryId).toBe('a2');
50
+ expect(snapshot?.source).toBe('toolResult');
51
+ expect(snapshot?.op).toBe('done');
52
+ expect(snapshot?.phases[0].tasks[0].status).toBe('completed');
53
+ });
54
+
55
+ test('skips a `view` result — it reports the list without committing it', () => {
56
+ const entries = [
57
+ todoResult('a1', null, { op: 'init', phases: phases(['one', 'completed']) }),
58
+ // A view result can be arbitrarily stale (it is echoed, not committed).
59
+ todoResult('a2', 'a1', { op: 'view', phases: phases(['one', 'pending']) }),
60
+ ];
61
+
62
+ expect(latestTodoSnapshot(entries)?.sourceEntryId).toBe('a1');
63
+ });
64
+
65
+ test('skips an errored result', () => {
66
+ const entries = [
67
+ todoResult('a1', null, { op: 'init', phases: phases(['one', 'pending']) }),
68
+ {
69
+ ...todoResult('a2', 'a1', { op: 'done', phases: phases(['one', 'completed']) }),
70
+ message: {
71
+ role: 'toolResult',
72
+ toolName: 'todo',
73
+ isError: true,
74
+ details: { op: 'done', phases: phases(['one', 'completed']) },
75
+ },
76
+ },
77
+ ];
78
+
79
+ expect(latestTodoSnapshot(entries)?.sourceEntryId).toBe('a1');
80
+ });
81
+
82
+ test('a `user_todo_edit` custom entry outranks an older tool result', () => {
83
+ const entries = [
84
+ todoResult('a1', null, { op: 'init', phases: phases(['one', 'pending']) }),
85
+ entry('a2', 'a1', {
86
+ type: 'custom',
87
+ customType: 'user_todo_edit',
88
+ data: { phases: phases(['one', 'abandoned']) },
89
+ }),
90
+ ];
91
+
92
+ const snapshot = latestTodoSnapshot(entries);
93
+
94
+ expect(snapshot?.source).toBe('custom');
95
+ expect(snapshot?.phases[0].tasks[0].status).toBe('abandoned');
96
+ });
97
+
98
+ test('a snapshot on an abandoned branch is ignored (rewind)', () => {
99
+ // a2 was rewound past: the leaf is b2, whose parent chain never reaches a2.
100
+ const entries = [
101
+ todoResult('a1', null, { op: 'init', phases: phases(['old', 'in_progress']) }),
102
+ todoResult('a2', 'a1', { op: 'done', phases: phases(['old', 'completed']) }),
103
+ todoResult('b1', 'a1', { op: 'append', phases: phases(['new', 'pending']) }),
104
+ todoResult('b2', 'b1', { op: 'start', phases: phases(['new', 'in_progress']) }),
105
+ ];
106
+
107
+ const snapshot = latestTodoSnapshot(entries);
108
+
109
+ expect(snapshot?.sourceEntryId).toBe('b2');
110
+ expect(snapshot?.phases[0].tasks[0].content).toBe('new');
111
+ });
112
+
113
+ test('returns null when the transcript holds no todo state', () => {
114
+ const entries = [
115
+ entry('a1', null, { type: 'message', message: { role: 'user', content: 'hello' } }),
116
+ todoResult('a2', 'a1', { op: 'view', phases: phases(['one', 'pending']) }),
117
+ ];
118
+
119
+ expect(latestTodoSnapshot(entries)).toBeNull();
120
+ });
121
+
122
+ test('rejects a malformed snapshot rather than rendering a half list', () => {
123
+ const entries = [
124
+ todoResult('a1', null, { op: 'init', phases: [{ name: 'Phase', tasks: [{ content: 'x', status: 'nonsense' }] }] }),
125
+ ];
126
+
127
+ expect(latestTodoSnapshot(entries)).toBeNull();
128
+ });
129
+
130
+ test('carries storage and timestamp when the entry records them', () => {
131
+ const entries = [todoResult('a1', null, { op: 'init', phases: phases(['one', 'pending']), storage: 'memory' })];
132
+
133
+ const snapshot = latestTodoSnapshot(entries);
134
+
135
+ expect(snapshot?.storage).toBe('memory');
136
+ expect(snapshot?.updatedAt).toBe('2026-09-25T00:00:00.000Z');
137
+ });
138
+ });
139
+
140
+ describe('todoProgress', () => {
141
+ test('counts closed as completed plus abandoned', () => {
142
+ const progress = todoProgress(
143
+ phases(['a', 'completed'], ['b', 'abandoned'], ['c', 'in_progress'], ['d', 'blocked'], ['e', 'pending']),
144
+ );
145
+
146
+ expect(progress).toEqual({
147
+ total: 5,
148
+ completed: 1,
149
+ closed: 2,
150
+ inProgress: 1,
151
+ pending: 1,
152
+ blocked: 1,
153
+ abandoned: 1,
154
+ });
155
+ });
156
+ });
@@ -0,0 +1,215 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Parse oh-my-pi's persisted `todo` state out of a session transcript.
8
+ *
9
+ * omp keeps the authoritative list in its own session file, not in an
10
+ * in-memory HUD: the `todo` tool commits a snapshot to the entry it appends,
11
+ * and omp reads the deepest snapshot back off the ACTIVE BRANCH when it
12
+ * rehydrates (`getLatestTodoPhasesFromEntries` → `syncTodoPhasesFromBranch`).
13
+ * This module performs the same read, so the chamber's panel shows what the
14
+ * agent itself would resume with.
15
+ *
16
+ * Two rules are load-bearing and each was verified against omp 18.3.0:
17
+ *
18
+ * - **The active branch, not file order.** A session file is a tree: every
19
+ * entry carries `parentId`, and a rewind/fork leaves entries on the file
20
+ * that the current leaf no longer descends from. omp reads `getBranch()`
21
+ * (leaf → root), so file order would resurrect a todo list the agent had
22
+ * already rewound past.
23
+ * - **A `view` snapshot is not a snapshot.** `todo op=view` returns the
24
+ * current list without committing it; a `view` result's phases may be
25
+ * stale, so omp skips those results entirely and so does this reader.
26
+ * Errored results are skipped for the same reason.
27
+ */
28
+
29
+ import { isRecord } from '@/shared/lib/util/guards';
30
+ import type {
31
+ TodoItem,
32
+ TodoOperation,
33
+ TodoPhase,
34
+ TodoProgress,
35
+ TodoSnapshot,
36
+ } from '@/shared/types/todo';
37
+
38
+ const TODO_STATUSES: Record<string, true> = {
39
+ pending: true,
40
+ in_progress: true,
41
+ completed: true,
42
+ abandoned: true,
43
+ blocked: true,
44
+ };
45
+
46
+ /** Custom-entry type omp writes for a durable, non-tool todo mutation. */
47
+ const USER_TODO_EDIT_CUSTOM_TYPE = 'user_todo_edit';
48
+
49
+ /** Whether an unknown value is one omp-persisted phase. */
50
+ export function isTodoPhase(value: unknown): value is TodoPhase {
51
+ if (!isRecord(value) || typeof value.name !== 'string' || !Array.isArray(value.tasks)) return false;
52
+ return value.tasks.every((task) => {
53
+ if (!isRecord(task) || typeof task.content !== 'string') return false;
54
+ return typeof task.status === 'string' && TODO_STATUSES[task.status] === true;
55
+ });
56
+ }
57
+
58
+ /** Narrow an unknown array to a validated phase list, or undefined. */
59
+ function toPhases(value: unknown): TodoPhase[] | undefined {
60
+ if (!Array.isArray(value) || !value.every(isTodoPhase)) return undefined;
61
+ return value as TodoPhase[];
62
+ }
63
+
64
+ /** One JSONL entry as this reader needs it. */
65
+ interface TodoEntryLike {
66
+ id?: unknown;
67
+ parentId?: unknown;
68
+ timestamp?: unknown;
69
+ type?: unknown;
70
+ customType?: unknown;
71
+ data?: unknown;
72
+ message?: unknown;
73
+ }
74
+
75
+ /**
76
+ * The phases an entry commits, or undefined when it commits none.
77
+ *
78
+ * Mirrors omp's own `aVn`: a `user_todo_edit` custom entry carries `data.phases`
79
+ * (already-normalized), and a `todo` toolResult carries `details.phases` unless
80
+ * it errored or was a pure `view` read.
81
+ */
82
+ function committedPhases(entry: TodoEntryLike): TodoPhase[] | undefined {
83
+ if (entry.type === 'custom' && entry.customType === USER_TODO_EDIT_CUSTOM_TYPE) {
84
+ const data = isRecord(entry.data) ? entry.data : undefined;
85
+ return toPhases(data?.phases);
86
+ }
87
+ if (entry.type !== 'message') return undefined;
88
+ const message = isRecord(entry.message) ? entry.message : undefined;
89
+ if (!message || message.role !== 'toolResult' || message.toolName !== 'todo') return undefined;
90
+ if (message.isError === true) return undefined;
91
+ const details = isRecord(message.details) ? message.details : undefined;
92
+ if (details?.op === 'view') return undefined;
93
+ return toPhases(details?.phases);
94
+ }
95
+
96
+ /** The chain of entry ids from the session's leaf back to the root. */
97
+ function activeBranchIds(entries: TodoEntryLike[]): Set<string> {
98
+ const parentOf = new Map<string, string | null>();
99
+ for (const entry of entries) {
100
+ if (typeof entry.id !== 'string') continue;
101
+ parentOf.set(entry.id, typeof entry.parentId === 'string' ? entry.parentId : null);
102
+ }
103
+ const branch = new Set<string>();
104
+ // The leaf is the last entry in file order: omp appends as it advances, so
105
+ // the newest entry is where the conversation currently stands.
106
+ let cursor: string | null | undefined = undefined;
107
+ for (let i = entries.length - 1; i >= 0; i--) {
108
+ const id = entries[i]?.id;
109
+ if (typeof id === 'string') {
110
+ cursor = id;
111
+ break;
112
+ }
113
+ }
114
+ while (typeof cursor === 'string' && !branch.has(cursor)) {
115
+ branch.add(cursor);
116
+ cursor = parentOf.get(cursor) ?? null;
117
+ }
118
+ return branch;
119
+ }
120
+
121
+ /**
122
+ * The deepest committed todo snapshot on the active branch, or null when this
123
+ * transcript holds none. The returned `sourceEntryId` is the entry a later
124
+ * snapshot has to beat — omp's own identity for the list.
125
+ */
126
+ export function latestTodoSnapshot(entries: readonly unknown[]): TodoSnapshot | null {
127
+ const typed = entries as TodoEntryLike[];
128
+ const branch = activeBranchIds(typed);
129
+
130
+ for (let i = typed.length - 1; i >= 0; i--) {
131
+ const entry = typed[i];
132
+ const id = entry?.id;
133
+ // An entry with no id cannot be placed on the branch; a legacy file may
134
+ // still carry usable snapshots, so it is read but never claims identity.
135
+ if (typeof id === 'string' && !branch.has(id)) continue;
136
+ const phases = committedPhases(entry);
137
+ if (!phases) continue;
138
+ return {
139
+ phases,
140
+ sourceEntryId: typeof id === 'string' ? id : '',
141
+ source: entry.type === 'custom' ? 'custom' : 'toolResult',
142
+ ...(typeof entry.timestamp === 'string' ? { updatedAt: entry.timestamp } : {}),
143
+ ...readOp(entry),
144
+ };
145
+ }
146
+ return null;
147
+ }
148
+
149
+ /** Op + storage recorded on the snapshot entry, when it carries them. */
150
+ function readOp(entry: TodoEntryLike): { op?: TodoOperation; storage?: 'session' | 'memory' } {
151
+ const details =
152
+ entry.type === 'message'
153
+ ? (isRecord(entry.message) ? (isRecord(entry.message.details) ? entry.message.details : undefined) : undefined)
154
+ : undefined;
155
+ if (!details) return {};
156
+ const op = typeof details.op === 'string' ? (details.op as TodoOperation) : undefined;
157
+ const storage = details.storage === 'session' || details.storage === 'memory' ? details.storage : undefined;
158
+ return { ...(op ? { op } : {}), ...(storage ? { storage } : {}) };
159
+ }
160
+
161
+ /** Counter roll-up over every task in a snapshot. */
162
+ export function todoProgress(phases: readonly TodoPhase[]): TodoProgress {
163
+ const progress: TodoProgress = {
164
+ total: 0,
165
+ completed: 0,
166
+ closed: 0,
167
+ inProgress: 0,
168
+ pending: 0,
169
+ blocked: 0,
170
+ abandoned: 0,
171
+ };
172
+ for (const phase of phases) {
173
+ for (const task of phase.tasks) {
174
+ progress.total += 1;
175
+ switch (task.status) {
176
+ case 'completed':
177
+ progress.completed += 1;
178
+ progress.closed += 1;
179
+ break;
180
+ case 'abandoned':
181
+ progress.abandoned += 1;
182
+ progress.closed += 1;
183
+ break;
184
+ case 'in_progress':
185
+ progress.inProgress += 1;
186
+ break;
187
+ case 'blocked':
188
+ progress.blocked += 1;
189
+ break;
190
+ default:
191
+ progress.pending += 1;
192
+ break;
193
+ }
194
+ }
195
+ }
196
+ return progress;
197
+ }
198
+
199
+ /**
200
+ * The task the agent is on right now: an `in_progress` one if any, else the
201
+ * first `pending`. omp picks the same way for its own "next actionable" row.
202
+ */
203
+ export function nextActionableTask(phases: readonly TodoPhase[]): TodoItem | undefined {
204
+ for (const phase of phases) {
205
+ for (const task of phase.tasks) {
206
+ if (task.status === 'in_progress') return task;
207
+ }
208
+ }
209
+ for (const phase of phases) {
210
+ for (const task of phase.tasks) {
211
+ if (task.status === 'pending') return task;
212
+ }
213
+ }
214
+ return undefined;
215
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ import { describe, expect, test } from 'bun:test';
7
+
8
+ import {
9
+ commentSyntaxFor,
10
+ diffEdit,
11
+ nextOccurrence,
12
+ occurrenceRanges,
13
+ replicateEdit,
14
+ toggleLineComment,
15
+ wordRangeAt,
16
+ } from '@/shared/lib/code/editor/commands';
17
+
18
+ describe('wordRangeAt', () => {
19
+ test('selects the word the caret sits inside or at the end of', () => {
20
+ expect(wordRangeAt('const alpha = 1', 8)).toEqual({ start: 6, end: 11 });
21
+ // A caret just after the word still selects it — that is where ⌘D is
22
+ // pressed when the user has just typed the identifier.
23
+ expect(wordRangeAt('const alpha = 1', 11)).toEqual({ start: 6, end: 11 });
24
+ expect(wordRangeAt('const alpha = 1', 6)).toEqual({ start: 6, end: 11 });
25
+ });
26
+
27
+ test('treats `$` and `_` as word characters', () => {
28
+ expect(wordRangeAt('$foo_bar baz', 3)).toEqual({ start: 0, end: 8 });
29
+ });
30
+
31
+ test('a caret at the edge of a word takes that word, whitespace alone takes none', () => {
32
+ // Offset 5 in `alpha beta` is the space right after `alpha` — the position
33
+ // a caret sits at after typing the word, which is where ⌘D is pressed.
34
+ expect(wordRangeAt('alpha beta', 5)).toEqual({ start: 0, end: 5 });
35
+ // Whitespace with no word on either side: nothing to select.
36
+ expect(wordRangeAt(' foo', 0)).toBeNull();
37
+ expect(wordRangeAt('', 0)).toBeNull();
38
+ });
39
+ });
40
+
41
+ describe('occurrenceRanges and nextOccurrence', () => {
42
+ test('finds every occurrence in document order', () => {
43
+ expect(occurrenceRanges('alpha beta alpha', 'alpha')).toEqual([
44
+ { start: 0, end: 5 },
45
+ { start: 11, end: 16 },
46
+ ]);
47
+ });
48
+
49
+ test('the next occurrence is the first one after the selection, then it wraps', () => {
50
+ const text = 'a a a';
51
+ const first = occurrenceRanges(text, 'a')[0];
52
+ expect(nextOccurrence(text, 'a', [first])).toEqual({ start: 2, end: 3 });
53
+ const all = occurrenceRanges(text, 'a');
54
+ expect(nextOccurrence(text, 'a', all)).toBeNull();
55
+ });
56
+
57
+ test('skips ranges that are already selected', () => {
58
+ const all = occurrenceRanges('a a a', 'a');
59
+ expect(nextOccurrence('a a a', 'a', [all[0], all[2]])).toEqual({ start: 2, end: 3 });
60
+ });
61
+ });
62
+
63
+ describe('diffEdit', () => {
64
+ test('recovers an insertion from two buffers', () => {
65
+ expect(diffEdit('abc', 'abXc')).toEqual({ start: 2, end: 2, inserted: 'X' });
66
+ });
67
+
68
+ test('recovers a deletion', () => {
69
+ expect(diffEdit('abXc', 'abc')).toEqual({ start: 2, end: 3, inserted: '' });
70
+ });
71
+
72
+ test('recovers a replacement that swallowed a selection', () => {
73
+ // `abc` selected and typed over with `Z`: the diff is one replacement, not
74
+ // a deletion followed by an insertion.
75
+ expect(diffEdit('hello world', 'hello Z')).toEqual({ start: 6, end: 11, inserted: 'Z' });
76
+ });
77
+
78
+ test('a limit pins the reported region over the caret, not wherever the diff prefers', () => {
79
+ // On repetitive text a prefix/suffix diff is ambiguous — `aaXa` could be read
80
+ // as an insertion at offset 2 (where the diff finds it) or at offset 0 (where
81
+ // the caret was). The limit wins, and the diff stays exact either way.
82
+ const diff = diffEdit('aaa', 'aaXa', { start: 0, end: 0 });
83
+
84
+ expect(diff.start).toBeLessThanOrEqual(0);
85
+ expect(diff.end).toBeGreaterThanOrEqual(0);
86
+ expect(diff.start).toBe(0);
87
+ // Applying the reported edit must reproduce the buffer, whichever shape the
88
+ // limit forced: that is the property the caller depends on.
89
+ expect('aaa'.slice(0, diff.start) + diff.inserted + 'aaa'.slice(diff.end)).toBe('aaXa');
90
+ });
91
+ });
92
+
93
+ describe('replicateEdit', () => {
94
+ test('applies one insertion at every range, back to front', () => {
95
+ // Ranges 0..1 and 2..3 of `ab` — inserting `X` at both must not let the
96
+ // first insertion shift the second range's offsets.
97
+ const result = replicateEdit('abcd', [{ start: 0, end: 0 }, { start: 2, end: 2 }], 'X');
98
+
99
+ expect(result.value).toBe('XabXcd');
100
+ expect(result.carets).toEqual([1, 4]);
101
+ });
102
+
103
+ test('replaces the selected text at each range', () => {
104
+ const result = replicateEdit('foo bar foo', [{ start: 0, end: 3 }, { start: 8, end: 11 }], 'qux');
105
+
106
+ expect(result.value).toBe('qux bar qux');
107
+ expect(result.carets).toEqual([3, 11]);
108
+ });
109
+
110
+ test('ranges given out of order are still applied in document order', () => {
111
+ const result = replicateEdit('abcd', [{ start: 2, end: 2 }, { start: 0, end: 0 }], 'X');
112
+
113
+ expect(result.value).toBe('XabXcd');
114
+ });
115
+ });
116
+
117
+ describe('toggleLineComment', () => {
118
+ test('comments the selected lines at their first non-space character', () => {
119
+ const result = toggleLineComment(' one\n two', { start: 0, end: 11 }, { line: '//' });
120
+
121
+ expect(result.value).toBe(' // one\n // two');
122
+ });
123
+
124
+ test('uncomments when every touched line is already commented', () => {
125
+ const result = toggleLineComment('// one\n// two', { start: 0, end: 13 }, { line: '//' });
126
+
127
+ expect(result.value).toBe('one\ntwo');
128
+ });
129
+
130
+ test('a mixed selection comments every line rather than half-uncommenting', () => {
131
+ const result = toggleLineComment('// one\ntwo', { start: 0, end: 11 }, { line: '//' });
132
+
133
+ expect(result.value).toBe('// // one\n// two');
134
+ });
135
+
136
+ test('only the lines the selection touches are affected', () => {
137
+ const result = toggleLineComment('one\ntwo\nthree', { start: 4, end: 7 }, { line: '//' });
138
+
139
+ expect(result.value).toBe('one\n// two\nthree');
140
+ });
141
+
142
+ test('a block-only language wraps the selection once', () => {
143
+ const result = toggleLineComment('color: red;', { start: 0, end: 11 }, commentSyntaxFor('css'));
144
+
145
+ expect(result.value).toBe('/* color: red; */');
146
+ });
147
+
148
+ test('the hash languages use their own token', () => {
149
+ expect(commentSyntaxFor('python')).toEqual({ line: '#' });
150
+ expect(commentSyntaxFor('yaml')).toEqual({ line: '#' });
151
+ expect(commentSyntaxFor('sql')).toEqual({ line: '--' });
152
+ // The default is the C-family token, which every other grammar here uses.
153
+ expect(commentSyntaxFor('typescript')).toEqual({ line: '//' });
154
+ });
155
+ });