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.
- package/README.md +31 -11
- package/bunfig.toml +11 -0
- package/dist/client/index.html +16 -5
- package/dist/client/index.js +1393 -0
- package/dist/client/static/js/index-8t70jhyj.js +5149 -0
- package/dist/client/static/js/index-n4wkqj7t.css +1 -0
- package/index.html +54 -0
- package/package.json +9 -10
- package/public/apple-touch-icon.png +0 -0
- package/public/icon-192x192.png +0 -0
- package/public/icon.svg +60 -0
- package/public/manifest.webmanifest +38 -0
- package/src/cli/lib/commands/serve.js +9 -5
- package/src/cli/lib/runtime.js +18 -7
- package/src/client/components/common/code-editor/handler.test.ts +274 -0
- package/src/client/components/common/code-editor/handler.ts +175 -0
- package/src/client/components/common/code-editor/index.tsx +185 -220
- package/src/client/components/common/code-surface/index.tsx +109 -12
- package/src/client/components/common/lazy-panels.tsx +5 -1
- package/src/client/components/common/provider-icon/glyphs.ts +118 -0
- package/src/client/components/common/provider-icon/index.tsx +85 -0
- package/src/client/components/common/viewport.ts +5 -3
- package/src/client/components/layout/RightActivityBar.tsx +2 -1
- package/src/client/components/layout/desktop-layout/TopNavbar.tsx +21 -4
- package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +4 -0
- package/src/client/components/layout/desktop-layout/index.tsx +26 -4
- package/src/client/components/mobile/LayoutWrapper.tsx +16 -0
- package/src/client/components/mobile/RightSidebar.tsx +11 -4
- package/src/client/components/mobile/SessionSidebar.tsx +10 -0
- package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +0 -1
- package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +16 -1
- package/src/client/components/mobile/mobile-session-sidebar/List.tsx +3 -0
- package/src/client/components/settings/categories/ProviderSettings.tsx +45 -4
- package/src/client/components/settings/categories/UsageSettings.tsx +22 -7
- package/src/client/components/settings/categories/provider-settings/AddModelModal.tsx +322 -0
- package/src/client/components/settings/categories/provider-settings/AddProviderModal.tsx +152 -56
- package/src/client/components/settings/categories/provider-settings/AuthSection.tsx +52 -1
- package/src/client/components/settings/categories/provider-settings/DeleteProviderModal.tsx +133 -0
- package/src/client/components/settings/categories/provider-settings/DialectFields.tsx +136 -0
- package/src/client/components/settings/categories/provider-settings/EffortLadder.tsx +89 -0
- package/src/client/components/settings/categories/provider-settings/Header.tsx +2 -2
- package/src/client/components/settings/categories/provider-settings/LoginModal.tsx +2 -2
- package/src/client/components/settings/categories/provider-settings/ModelsList.tsx +22 -2
- package/src/client/components/settings/categories/provider-settings/PresetPicker.tsx +82 -0
- package/src/client/components/settings/categories/provider-settings/PriceFields.tsx +107 -0
- package/src/client/components/settings/categories/provider-settings/ReconnectModal.tsx +43 -22
- package/src/client/components/settings/categories/provider-settings/SidebarList.tsx +2 -2
- package/src/client/components/settings/categories/usage-settings/LimitsSection.tsx +115 -0
- package/src/client/components/settings/categories/usage-settings/ProviderCard.tsx +19 -26
- package/src/client/components/settings/categories/usage-settings/ProviderDetail.tsx +29 -22
- package/src/client/components/settings/categories/usage-settings/SidebarList.tsx +60 -48
- package/src/client/components/settings/categories/usage-settings/format.ts +36 -0
- package/src/client/components/settings/categories/usage-settings/providers.ts +52 -28
- package/src/client/components/workspace/browser-panel/AddressBar.tsx +1 -10
- package/src/client/components/workspace/browser-panel/Screencast.tsx +4 -5
- package/src/client/components/workspace/browser-panel/index.tsx +6 -6
- package/src/client/components/workspace/browser-toolbar/index.tsx +22 -12
- package/src/client/components/workspace/chat-timeline/AiMessageFooter.tsx +1 -1
- package/src/client/components/workspace/chat-timeline/GeneratingIndicator.tsx +1 -1
- package/src/client/components/workspace/chat-timeline/ToolCallCard.tsx +1 -1
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Todo.tsx +1 -1
- package/src/client/components/workspace/diff-panel/Toolbar.tsx +71 -38
- package/src/client/components/workspace/diff-panel/index.tsx +139 -53
- package/src/client/components/workspace/editor/CommandPalette.test.ts +242 -0
- package/src/client/components/workspace/editor/CommandPalette.tsx +138 -0
- package/src/client/components/workspace/editor/FindWidget.test.ts +261 -0
- package/src/client/components/workspace/editor/FindWidget.tsx +219 -0
- package/src/client/components/workspace/editor/Tabs.tsx +2 -2
- package/src/client/components/workspace/editor/Toolbar.tsx +129 -26
- package/src/client/components/workspace/editor/index.tsx +180 -62
- package/src/client/components/workspace/file-explorer/Modals.tsx +79 -2
- package/src/client/components/workspace/file-explorer/TreeItem.tsx +43 -113
- package/src/client/components/workspace/model-dropdown/Item.tsx +13 -23
- package/src/client/components/workspace/model-dropdown/Panel.tsx +1 -1
- package/src/client/components/workspace/model-dropdown/Section.tsx +13 -4
- package/src/client/components/workspace/model-dropdown/index.tsx +7 -3
- package/src/client/components/workspace/todo-panel/PhaseGroup.tsx +76 -0
- package/src/client/components/workspace/todo-panel/Row.tsx +69 -0
- package/src/client/components/workspace/todo-panel/index.tsx +161 -0
- package/src/client/components/workspace/usage-panel/ProviderSelect.tsx +10 -5
- package/src/client/components/workspace/usage-panel/index.tsx +36 -18
- package/src/client/data/samples/dialogue-session.ts +1 -1
- package/src/client/data/samples/sample-tools/other.ts +1 -1
- package/src/client/data/settings/provider.ts +141 -9
- package/src/client/hooks/editor/find-harness.test-util.ts +170 -0
- package/src/client/hooks/editor/scroll-container.test.ts +102 -0
- package/src/client/hooks/editor/scroll-container.ts +26 -0
- package/src/client/hooks/editor/use-code-window.ts +2 -20
- package/src/client/hooks/editor/use-editor-find-write.test.ts +170 -0
- package/src/client/hooks/editor/use-editor-find.test.ts +118 -0
- package/src/client/hooks/editor/use-editor-find.ts +321 -0
- package/src/client/hooks/editor/use-editor-history.test.ts +216 -0
- package/src/client/hooks/editor/use-editor-history.ts +134 -0
- package/src/client/hooks/editor/use-file-editor.ts +24 -27
- package/src/client/hooks/editor/use-occurrences.test.ts +236 -0
- package/src/client/hooks/editor/use-occurrences.ts +98 -0
- package/src/client/hooks/models/use-provider-names.ts +1 -1
- package/src/client/hooks/settings/providers/actions.ts +337 -0
- package/src/client/hooks/settings/providers/api.ts +69 -0
- package/src/client/hooks/settings/providers/index.ts +138 -0
- package/src/client/hooks/settings/providers/messages.ts +77 -0
- package/src/client/hooks/settings/useUsageReport.ts +8 -3
- package/src/client/hooks/ui/theme.ts +3 -3
- package/src/client/hooks/workspace/file-tabs.ts +62 -11
- package/src/client/hooks/workspace/file-tree-actions.test.ts +348 -0
- package/src/client/hooks/workspace/file-tree-actions.ts +250 -0
- package/src/client/hooks/workspace/session-todos.ts +102 -0
- package/src/client/stubs/elkjs.ts +6 -1
- package/src/client/tailwind.css +30 -0
- package/src/server/index.ts +20 -2
- package/src/server/lib/assets/font-css.server.ts +86 -0
- package/src/server/lib/assets/fonts.server.ts +117 -0
- package/src/server/lib/bundler/css.ts +116 -0
- package/src/server/lib/lifecycle/shell-route.ts +17 -0
- package/src/server/lib/models/provider-listing.server.ts +257 -0
- package/src/server/lib/models/provider-registry.server.test.ts +122 -0
- package/src/server/lib/models/provider-registry.server.ts +76 -25
- package/src/server/lib/omp/config/models-config.ts +11 -0
- package/src/server/lib/omp/config/models-validation.test.ts +137 -0
- package/src/server/lib/omp/config/models-validation.ts +111 -2
- package/src/server/lib/omp/config/provider-seeds.test.ts +167 -0
- package/src/server/lib/omp/config/provider-seeds.ts +176 -0
- package/src/server/lib/omp/config/providers.ts +94 -100
- package/src/server/lib/omp/core/auth-credentials.ts +45 -0
- package/src/server/lib/omp/session/todos.ts +100 -0
- package/src/server/lib/omp/session/{usage.ts → usage/aggregate.ts} +1 -118
- package/src/server/lib/omp/session/usage/shaping.ts +130 -0
- package/src/server/lib/usage/deepseek.ts +4 -5
- package/src/server/lib/usage/kenari.ts +7 -6
- package/src/server/lib/usage/omp-usage.server.test.ts +126 -0
- package/src/server/lib/usage/omp-usage.server.ts +194 -0
- package/src/server/lib/usage/provider-key.ts +81 -3
- package/src/server/lib/usage/providers.server.test.ts +47 -0
- package/src/server/lib/usage/providers.server.ts +208 -0
- package/src/server/lib/usage/vendor-balance.server.test.ts +104 -0
- package/src/server/lib/usage/vendor-balance.server.ts +284 -0
- package/src/server/plugins/shell.server.ts +56 -0
- package/src/server/plugins/ssr.ts +24 -36
- package/src/server/plugins/static.test.ts +26 -29
- package/src/server/plugins/static.ts +40 -19
- package/src/server/routes/fs/action-create.test.ts +90 -0
- package/src/server/routes/fs/action.ts +23 -0
- package/src/server/routes/omp/index.ts +2 -0
- package/src/server/routes/omp/session-todos.ts +80 -0
- package/src/server/routes/settings/index.ts +2 -0
- package/src/server/routes/settings/provider-model.ts +107 -0
- package/src/server/routes/settings/provider-models.ts +65 -202
- package/src/server/routes/settings/providers.ts +22 -7
- package/src/server/routes/settings/usage.ts +125 -45
- package/src/server/routes/telemetry/tokens.ts +2 -1
- package/src/shared/lib/chat/{todo-parser.ts → todo/parser.ts} +17 -0
- package/src/shared/lib/chat/todo/progress.ts +97 -0
- package/src/shared/lib/chat/todo/snapshot.test.ts +156 -0
- package/src/shared/lib/chat/todo/snapshot.ts +215 -0
- package/src/shared/lib/code/editor/commands.test.ts +155 -0
- package/src/shared/lib/code/editor/commands.ts +260 -0
- package/src/shared/lib/code/editor/edits.test.ts +100 -0
- package/src/shared/lib/code/editor/edits.ts +122 -0
- package/src/shared/lib/code/editor/find-status.test.ts +50 -0
- package/src/shared/lib/code/editor/find-status.ts +36 -0
- package/src/shared/lib/code/editor/find.test.ts +203 -0
- package/src/shared/lib/code/editor/find.ts +224 -0
- package/src/{client/components/common/code-editor → shared/lib/code/editor}/history.test.ts +1 -1
- package/src/shared/lib/code/editor/keymap.test.ts +109 -0
- package/src/shared/lib/code/editor/keymap.ts +151 -0
- package/src/shared/lib/code/editor/layers.test.ts +77 -0
- package/src/shared/lib/code/editor/layers.ts +67 -0
- package/src/shared/lib/code/editor/line-edits.test.ts +131 -0
- package/src/shared/lib/code/editor/line-edits.ts +159 -0
- package/src/shared/lib/code/editor/lines.test.ts +81 -0
- package/src/shared/lib/code/editor/lines.ts +83 -0
- package/src/shared/lib/code/editor/marks.test.ts +86 -0
- package/src/shared/lib/code/editor/marks.ts +106 -0
- package/src/shared/lib/code/editor/occurrences.test.ts +117 -0
- package/src/shared/lib/code/editor/occurrences.ts +111 -0
- package/src/shared/lib/code/editor/palette.test.ts +91 -0
- package/src/shared/lib/code/editor/palette.ts +92 -0
- package/src/shared/lib/code/editor/reset-css.test.ts +35 -0
- package/src/shared/lib/code/editor/reset-css.ts +32 -0
- package/src/shared/lib/code/editor/text-commands.test.ts +178 -0
- package/src/shared/lib/code/editor/text-commands.ts +154 -0
- package/src/shared/lib/code/syntax-highlight.test.ts +75 -0
- package/src/shared/lib/code/syntax-highlight.ts +29 -17
- package/src/shared/lib/code/windowed-highlight.ts +52 -1
- package/src/shared/lib/fs/diff-parser.test.ts +116 -0
- package/src/shared/lib/fs/diff-parser.ts +86 -33
- package/src/shared/lib/models/{provider-connection.ts → provider/connection.ts} +37 -0
- package/src/shared/lib/models/provider/dialect.ts +179 -0
- package/src/shared/lib/models/provider/glyph.test.ts +116 -0
- package/src/shared/lib/models/provider/glyph.ts +200 -0
- package/src/shared/lib/models/{provider-models.ts → provider/models.ts} +91 -6
- package/src/shared/lib/models/provider/presets.test.ts +172 -0
- package/src/shared/lib/models/provider/presets.ts +83 -0
- package/src/shared/lib/ui/key-binding.test.ts +115 -0
- package/src/shared/lib/ui/key-binding.ts +143 -0
- package/src/shared/lib/util/platform.test.ts +87 -0
- package/src/shared/lib/util/platform.ts +30 -0
- package/src/shared/lib/workspace/file-tab-id.ts +29 -0
- package/src/shared/lib/workspace/keymap.test.ts +82 -0
- package/src/shared/lib/workspace/keymap.ts +30 -0
- package/src/shared/lib/workspace/refresh-cadence.ts +9 -0
- package/src/shared/lib/workspace/right-panels.ts +8 -0
- package/src/shared/lib/workspace/session-state/store.ts +0 -1
- package/src/shared/types/index.ts +1 -0
- package/src/shared/types/settings/provider.ts +81 -0
- package/src/shared/types/settings/usage.ts +88 -14
- package/src/shared/types/todo.ts +108 -0
- package/tsconfig.json +3 -1
- package/dist/client/static/css/index.5f630421b7.css +0 -2
- package/dist/client/static/font/KaTeX_AMS-Regular.a79f1c3119.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Caligraphic-Bold.ec17d13264.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Caligraphic-Regular.55fac25845.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Fraktur-Bold.d42a5579b0.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Fraktur-Regular.d3c882a649.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Main-Bold.c3fb5ac22f.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Main-BoldItalic.6f2bb1dff2.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Main-Italic.8916142bec.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Main-Regular.0462f03bdf.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Math-BoldItalic.572d331f69.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Math-Italic.f28c23acad.woff2 +0 -0
- package/dist/client/static/font/KaTeX_SansSerif-Bold.8c5b5494b6.woff2 +0 -0
- package/dist/client/static/font/KaTeX_SansSerif-Italic.3b1e59b3ba.woff2 +0 -0
- package/dist/client/static/font/KaTeX_SansSerif-Regular.ba21ed5f84.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Script-Regular.03e9641d6f.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Size1-Regular.eae34984b3.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Size2-Regular.5916a24fa3.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Size4-Regular.10d95fd3a2.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Typewriter-Regular.a8709e3622.woff2 +0 -0
- package/dist/client/static/font/fira-code-latin-400-normal.2655b10c55.woff2 +0 -0
- package/dist/client/static/font/fira-code-latin-500-normal.92a991f204.woff2 +0 -0
- package/dist/client/static/font/fira-code-latin-600-normal.43788eb7f1.woff2 +0 -0
- package/dist/client/static/font/fira-code-latin-700-normal.2667215d33.woff2 +0 -0
- package/dist/client/static/js/77.0d81c74cf0.js +0 -139
- package/dist/client/static/js/77.0d81c74cf0.js.LICENSE.txt +0 -8
- package/dist/client/static/js/async/0.8e59f2eb5b.js +0 -1
- package/dist/client/static/js/async/0k.5852e6b4fa.js +0 -1
- package/dist/client/static/js/async/0w.9ed36d5860.js +0 -1
- package/dist/client/static/js/async/1.f387c12b63.js +0 -1
- package/dist/client/static/js/async/12.d091f91c99.js +0 -1
- package/dist/client/static/js/async/13.8f810a8a6c.js +0 -1
- package/dist/client/static/js/async/19.6fe909d55a.js +0 -160
- package/dist/client/static/js/async/1b.23f1141a38.js +0 -1
- package/dist/client/static/js/async/1k.973e8e21fc.js +0 -2
- package/dist/client/static/js/async/1k.973e8e21fc.js.LICENSE.txt +0 -11
- package/dist/client/static/js/async/1p.0d5cddda96.js +0 -1
- package/dist/client/static/js/async/2.b589f1733b.js +0 -1
- package/dist/client/static/js/async/2v.96165101de.js +0 -1
- package/dist/client/static/js/async/3.0cacc08423.js +0 -1
- package/dist/client/static/js/async/37.b28b2f61cd.js +0 -1
- package/dist/client/static/js/async/3d.5085fb913f.js +0 -1
- package/dist/client/static/js/async/3j.357c80ff38.js +0 -2
- package/dist/client/static/js/async/3j.357c80ff38.js.LICENSE.txt +0 -11
- package/dist/client/static/js/async/3p.bcf32a0983.js +0 -1
- package/dist/client/static/js/async/3r.3a69e98ee0.js +0 -64
- package/dist/client/static/js/async/3u.1228460e2a.js +0 -1
- package/dist/client/static/js/async/3y.3530b824ad.js +0 -198
- package/dist/client/static/js/async/3y.3530b824ad.js.LICENSE.txt +0 -5
- package/dist/client/static/js/async/4.9442eeb472.js +0 -1
- package/dist/client/static/js/async/4e.678285ca55.js +0 -1
- package/dist/client/static/js/async/4k.33a0c7eeb4.js +0 -1
- package/dist/client/static/js/async/4m.928a1dde85.js +0 -31
- package/dist/client/static/js/async/4m.928a1dde85.js.LICENSE.txt +0 -11
- package/dist/client/static/js/async/4q.aad20dab25.js +0 -1
- package/dist/client/static/js/async/5.ebaf8b89cc.js +0 -36
- package/dist/client/static/js/async/51.95bcae98af.js +0 -2
- package/dist/client/static/js/async/51.95bcae98af.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/5b.d88304bc13.js +0 -1
- package/dist/client/static/js/async/5j.d6407c4248.js +0 -287
- package/dist/client/static/js/async/5o.88316d4460.js +0 -62
- package/dist/client/static/js/async/6.f034c9b528.js +0 -1
- package/dist/client/static/js/async/66.5dfa29f957.js +0 -93
- package/dist/client/static/js/async/6o.5b1a06a382.js +0 -1
- package/dist/client/static/js/async/7.e4a9288dd0.js +0 -1
- package/dist/client/static/js/async/73.308dff4da8.js +0 -1
- package/dist/client/static/js/async/7e.a1cc9bfd88.js +0 -1
- package/dist/client/static/js/async/7g.a7814350c3.js +0 -1
- package/dist/client/static/js/async/7l.fadbbc0583.js +0 -1
- package/dist/client/static/js/async/8.5067439d8c.js +0 -1
- package/dist/client/static/js/async/8j.ea110663fb.js +0 -1
- package/dist/client/static/js/async/a.bce4542392.js +0 -1
- package/dist/client/static/js/async/aa.65d3852576.js +0 -1
- package/dist/client/static/js/async/ar.9303e5f2e8.js +0 -2
- package/dist/client/static/js/async/ar.9303e5f2e8.js.LICENSE.txt +0 -11
- package/dist/client/static/js/async/az.d656062b0f.js +0 -1
- package/dist/client/static/js/async/b.b4f6f06515.js +0 -1
- package/dist/client/static/js/async/bi.09688ba75c.js +0 -1
- package/dist/client/static/js/async/bq.586bc738d1.js +0 -1
- package/dist/client/static/js/async/c.797d933882.js +0 -1
- package/dist/client/static/js/async/c0.0af1e640d3.js +0 -24
- package/dist/client/static/js/async/ca.8a52cfd447.js +0 -1
- package/dist/client/static/js/async/cf.d34fff5d67.js +0 -1
- package/dist/client/static/js/async/ct.b74928f7a3.js +0 -1
- package/dist/client/static/js/async/cx.3cb0242a3a.js +0 -226
- package/dist/client/static/js/async/d.a5fc8fe1b3.js +0 -1
- package/dist/client/static/js/async/da.f62c70f6c8.js +0 -1
- package/dist/client/static/js/async/dp.1725d6432c.js +0 -1
- package/dist/client/static/js/async/e.93214d622b.js +0 -79
- package/dist/client/static/js/async/f.ab50467719.js +0 -19
- package/dist/client/static/js/async/f.ab50467719.js.LICENSE.txt +0 -12
- package/dist/client/static/js/async/f7.46971e9d6d.js +0 -2
- package/dist/client/static/js/async/f7.46971e9d6d.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/f8.bbf0dbae70.js +0 -1
- package/dist/client/static/js/async/g.cb81980498.js +0 -2
- package/dist/client/static/js/async/g.cb81980498.js.LICENSE.txt +0 -11
- package/dist/client/static/js/async/h.4e790471e4.js +0 -27
- package/dist/client/static/js/async/h1.393f066d09.js +0 -1
- package/dist/client/static/js/async/h3.216e9798ab.js +0 -148
- package/dist/client/static/js/async/hd.0da931b622.js +0 -1
- package/dist/client/static/js/async/hk.ba7fb04d41.js +0 -1
- package/dist/client/static/js/async/hn.b645041e00.js +0 -1
- package/dist/client/static/js/async/ho.475a5e1ae4.js +0 -1
- package/dist/client/static/js/async/hp.e9fe2e395a.js +0 -2
- package/dist/client/static/js/async/hp.e9fe2e395a.js.LICENSE.txt +0 -17
- package/dist/client/static/js/async/hs.e7c59d8090.js +0 -41
- package/dist/client/static/js/async/i.7c148e8440.js +0 -1
- package/dist/client/static/js/async/id.573df20f29.js +0 -1
- package/dist/client/static/js/async/j.ab925882d8.js +0 -1
- package/dist/client/static/js/async/jd.63886a1f09.js +0 -1
- package/dist/client/static/js/async/jg.d86ebd4013.js +0 -86
- package/dist/client/static/js/async/jl.874d352278.js +0 -1
- package/dist/client/static/js/async/jll.11313b455a.js +0 -1
- package/dist/client/static/js/async/js.67182759ef.js +0 -1
- package/dist/client/static/js/async/jsc.23b70d6f40.js +0 -1
- package/dist/client/static/js/async/jse.334588ff5d.js +0 -1
- package/dist/client/static/js/async/jz.72eb929106.js +0 -77
- package/dist/client/static/js/async/k.3210286401.js +0 -217
- package/dist/client/static/js/async/ka.c1990ecaa3.js +0 -115
- package/dist/client/static/js/async/katex-vendor.de6a87c308.js +0 -1
- package/dist/client/static/js/async/ku.29c8a59d0c.js +0 -1
- package/dist/client/static/js/async/ky.8dab1dade0.js +0 -1
- package/dist/client/static/js/async/l.12dd34e1be.js +0 -1
- package/dist/client/static/js/async/l9.949710adaf.js +0 -83
- package/dist/client/static/js/async/l9.949710adaf.js.LICENSE.txt +0 -13
- package/dist/client/static/js/async/ls.9d9a01026c.js +0 -104
- package/dist/client/static/js/async/m.eb2e6752e2.js +0 -28
- package/dist/client/static/js/async/m.eb2e6752e2.js.LICENSE.txt +0 -11
- package/dist/client/static/js/async/m3.d2b05f9604.js +0 -2
- package/dist/client/static/js/async/m3.d2b05f9604.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/ma.2c261b04dd.js +0 -2
- package/dist/client/static/js/async/ma.2c261b04dd.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/me.8ace414f80.js +0 -23
- package/dist/client/static/js/async/ms.98960af4eb.js +0 -67
- package/dist/client/static/js/async/ms.98960af4eb.js.LICENSE.txt +0 -12
- package/dist/client/static/js/async/mt.0352e976d8.js +0 -1
- package/dist/client/static/js/async/n.a9f1c51701.js +0 -1
- package/dist/client/static/js/async/na.a36cd532d6.js +0 -1
- package/dist/client/static/js/async/nk.fc7e58325e.js +0 -1
- package/dist/client/static/js/async/o.36353dfb88.js +0 -1
- package/dist/client/static/js/async/oc.b73042e8bd.js +0 -8
- package/dist/client/static/js/async/og.8eb577893a.js +0 -193
- package/dist/client/static/js/async/p.48ecfcd14e.js +0 -1
- package/dist/client/static/js/async/px.fc6877924b.js +0 -1
- package/dist/client/static/js/async/q.22588845a9.js +0 -21
- package/dist/client/static/js/async/q.22588845a9.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/q5.97dc1481b6.js +0 -286
- package/dist/client/static/js/async/qs.2a9ddb3c72.js +0 -1
- package/dist/client/static/js/async/r.51010ad87c.js +0 -1
- package/dist/client/static/js/async/rd.cb6bf495bb.js +0 -2
- package/dist/client/static/js/async/rd.cb6bf495bb.js.LICENSE.txt +0 -4
- package/dist/client/static/js/async/s.0f395cdda5.js +0 -94
- package/dist/client/static/js/async/t.4dca0e2ba8.js +0 -1
- package/dist/client/static/js/async/t9.d44f57d518.js +0 -1
- package/dist/client/static/js/async/ta.973534bbec.js +0 -1
- package/dist/client/static/js/async/tr.e878658140.js +0 -309
- package/dist/client/static/js/async/u.0871ac5b22.js +0 -1
- package/dist/client/static/js/async/ud.3065bfee31.js +0 -2
- package/dist/client/static/js/async/ud.3065bfee31.js.LICENSE.txt +0 -4
- package/dist/client/static/js/async/up.ea990da0ff.js +0 -32
- package/dist/client/static/js/async/uw.8890abbabc.js +0 -1
- package/dist/client/static/js/async/v.7d28d3c241.js +0 -34
- package/dist/client/static/js/async/vp.9b13bf8610.js +0 -1
- package/dist/client/static/js/async/vt.42416b707d.js +0 -1
- package/dist/client/static/js/async/w.08ac210f60.js +0 -1
- package/dist/client/static/js/async/wt.ad92393a90.js +0 -1
- package/dist/client/static/js/async/x.563f72ca6b.js +0 -1
- package/dist/client/static/js/async/x0.475516418f.js +0 -1
- package/dist/client/static/js/async/x1.da8687159b.js +0 -90
- package/dist/client/static/js/async/xe.da8dcc2015.js +0 -2
- package/dist/client/static/js/async/xe.da8dcc2015.js.LICENSE.txt +0 -11
- package/dist/client/static/js/async/xl.a3c562a58c.js +0 -1
- package/dist/client/static/js/async/xn.6510c0abe2.js +0 -2
- package/dist/client/static/js/async/xn.6510c0abe2.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/xn1.83a7784fe1.js +0 -10
- package/dist/client/static/js/async/xn1.83a7784fe1.js.LICENSE.txt +0 -11
- package/dist/client/static/js/async/xnl.feb0021e50.js +0 -2
- package/dist/client/static/js/async/xnl.feb0021e50.js.LICENSE.txt +0 -12
- package/dist/client/static/js/async/y.1a9ed7a5d8.js +0 -1
- package/dist/client/static/js/async/y6.43c57defe4.js +0 -1
- package/dist/client/static/js/async/yb.8c1342401d.js +0 -1
- package/dist/client/static/js/async/ybq.4491f2f2dc.js +0 -1
- package/dist/client/static/js/async/yg.35bebd6d0c.js +0 -149
- package/dist/client/static/js/async/yq.917ddd7ba2.js +0 -1
- package/dist/client/static/js/async/ys.00687cf82d.js +0 -38
- package/dist/client/static/js/async/yv.d99d829136.js +0 -1
- package/dist/client/static/js/async/z.56f61a4374.js +0 -1
- package/dist/client/static/js/async/zi.5fdb07b86a.js +0 -24
- package/dist/client/static/js/async/zj.654f37e8ff.js +0 -1
- package/dist/client/static/js/async/zn.966d6f1cc6.js +0 -1
- package/dist/client/static/js/async/zo.26213b40c0.js +0 -1
- package/dist/client/static/js/async/zx.e9f4105e00.js +0 -1
- package/dist/client/static/js/index.e6a257402a.js +0 -38
- package/dist/client/static/js/index.e6a257402a.js.LICENSE.txt +0 -4
- package/dist/client/static/js/markdown-vendor.71e43278b3.js +0 -144
- package/dist/client/static/js/shiki-vendor.cb8452ddcd.js +0 -1
- package/src/client/components/settings/categories/provider-settings/Icons.tsx +0 -35
- package/src/client/hooks/settings/providers.ts +0 -326
- package/src/server/plugins/dev-assets.ts +0 -49
- package/src/shared/lib/code/editor-utils.ts +0 -16
- package/src/shared/lib/models/provider-presets.ts +0 -29
- /package/dist/client/{apple-touch-icon.png → static/apple-touch-icon-dskx5sr3.png} +0 -0
- /package/dist/client/{icon-192x192.png → static/icon-192x192-6gn5cr2f.png} +0 -0
- /package/dist/client/{icon.svg → static/icon-d07z9dtz.svg} +0 -0
- /package/dist/client/{manifest.webmanifest → static/manifest-bnyy75kt.webmanifest} +0 -0
- /package/{dist/client → public}/icon-512x512.png +0 -0
- /package/{dist/client → public}/icon-maskable-512x512.png +0 -0
- /package/{dist/client → public}/sw.js +0 -0
- /package/src/{client/components/common/code-editor → shared/lib/code/editor}/history.ts +0 -0
- /package/src/shared/lib/models/{provider-cleanup.ts → provider/cleanup.ts} +0 -0
- /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
|
+
});
|