@kabmo-labs/kabmo 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/client/index.html +1 -1
- package/dist/client/index.js +403 -384
- package/dist/client/static/js/{index-0wvbca0b.js → index-bpptfkz2.js} +905 -875
- package/dist/client/static/js/index-k315p1e8.css +1 -0
- package/package.json +10 -1
- package/src/client/components/common/code-surface/documents.test.ts +120 -0
- package/src/client/components/common/code-surface/documents.ts +201 -0
- package/src/client/components/common/code-surface/extensions.test.ts +110 -0
- package/src/client/components/common/code-surface/extensions.ts +162 -0
- package/src/client/components/common/code-surface/handle.ts +102 -0
- package/src/client/components/common/code-surface/index.tsx +253 -314
- package/src/client/components/common/code-surface/keymap.test.ts +133 -0
- package/src/client/components/common/code-surface/keymap.ts +267 -0
- package/src/client/components/common/code-surface/marks.ts +74 -0
- package/src/client/components/common/code-surface/shiki.ts +234 -0
- package/src/client/components/common/code-surface/sync.test.ts +185 -0
- package/src/client/components/common/code-surface/sync.ts +58 -0
- package/src/client/components/common/code-surface/theme.test.ts +202 -0
- package/src/client/components/common/code-surface/theme.ts +111 -0
- package/src/client/components/common/editor-view-buttons/index.test.tsx +148 -0
- package/src/client/components/common/editor-view-buttons/index.tsx +109 -0
- package/src/client/components/common/header-controls/index.tsx +24 -1
- package/src/client/components/common/header-controls/shell-chrome.test.tsx +272 -0
- package/src/client/components/common/shell-update-button/Modal.tsx +163 -0
- package/src/client/components/common/shell-update-button/index.test.tsx +251 -0
- package/src/client/components/common/shell-update-button/index.tsx +125 -0
- package/src/client/components/common/toolbar-row/index.test.tsx +166 -0
- package/src/client/components/common/toolbar-row/index.tsx +189 -0
- package/src/client/components/common/workspace-options-menu/index.tsx +19 -1
- package/src/client/components/layout/desktop-layout/TopNavbar.tsx +5 -6
- package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +41 -78
- package/src/client/components/layout/desktop-layout/index.tsx +55 -69
- package/src/client/components/layout/desktop-layout/resizer/index.tsx +6 -3
- package/src/client/components/layout/session-sidebar/CategoryHeader.tsx +24 -0
- package/src/client/components/layout/session-sidebar/CategoryHeader.worktree.test.tsx +126 -0
- package/src/client/components/layout/session-sidebar/CategoryItem.tsx +4 -0
- package/src/client/components/layout/session-sidebar/Footer.tsx +8 -15
- package/src/client/components/layout/session-sidebar/SessionItem.tsx +12 -0
- package/src/client/components/layout/session-sidebar/SessionList.tsx +4 -0
- package/src/client/components/layout/session-sidebar/SessionsView.tsx +24 -0
- package/src/client/components/layout/session-sidebar/WorktreeBadge.test.ts +48 -0
- package/src/client/components/layout/session-sidebar/WorktreeBadge.tsx +55 -0
- package/src/client/components/mobile/LayoutWrapper.tsx +23 -16
- package/src/client/components/mobile/RightSidebar.tsx +21 -10
- package/src/client/components/mobile/SessionSidebar.tsx +8 -0
- package/src/client/components/mobile/header-titlebar.test.tsx +172 -0
- package/src/client/components/mobile/mobile-main-view/Header.tsx +10 -1
- package/src/client/components/mobile/mobile-right-sidebar/FullDiff.tsx +3 -0
- package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +63 -38
- package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.test.tsx +137 -0
- package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.tsx +14 -3
- package/src/client/components/mobile/mobile-session-sidebar/Footer.tsx +9 -21
- package/src/client/components/mobile/mobile-session-sidebar/Header.tsx +6 -2
- package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +16 -0
- package/src/client/components/mobile/mobile-session-sidebar/List.tsx +4 -0
- package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +5 -0
- package/src/client/components/mobile/mobile-session-sidebar/WorktreeDialog.tsx +49 -0
- package/src/client/components/mobile/worktree-session.ts +51 -0
- package/src/client/components/settings/Modal.tsx +8 -0
- package/src/client/components/settings/categories/AppearanceSettings.tsx +3 -0
- package/src/client/components/settings/categories/ProjectSettings.tsx +22 -8
- package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.test.tsx +167 -0
- package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.tsx +184 -0
- package/src/client/components/settings/categories/project-settings/WorktreeSettings.tsx +146 -0
- package/src/client/components/settings/categories/project-settings/worktree-row.test.ts +83 -0
- package/src/client/components/settings/categories/project-settings/worktree-row.ts +47 -0
- package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +36 -55
- package/src/client/components/workspace/chat-timeline/PendingChatView.tsx +8 -0
- package/src/client/components/workspace/chat-timeline/SystemNotice.tsx +9 -4
- package/src/client/components/workspace/chat-timeline/TaskResultContent.tsx +9 -7
- package/src/client/components/workspace/chat-timeline/index.tsx +5 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.test.tsx +132 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.tsx +120 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.test.tsx +319 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.tsx +285 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/index.test.ts +2 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/hashline-patch/index.tsx +34 -29
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/AstEdit.tsx +11 -9
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Bash.tsx +10 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Edit.tsx +25 -8
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.test.tsx +146 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.tsx +32 -17
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.test.ts +114 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.tsx +111 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Read.tsx +7 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/SearchTool.tsx +9 -59
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ArtifactBody.tsx +2 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.test.ts +27 -28
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.tsx +39 -26
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ExcerptCode.tsx +8 -8
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/HighlightedCode.tsx +35 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/JsonCodeBlock.tsx +4 -4
- package/src/client/components/workspace/diff-panel/MergeDiffView.tsx +179 -0
- package/src/client/components/workspace/diff-panel/ThemedMergeDiffView.tsx +24 -0
- package/src/client/components/workspace/diff-panel/Toolbar.tsx +224 -186
- package/src/client/components/workspace/diff-panel/ToolbarSegments.tsx +98 -0
- package/src/client/components/workspace/diff-panel/index.tsx +76 -145
- package/src/client/components/workspace/diff-panel/merge.ts +315 -0
- package/src/client/components/workspace/editor/Body.tsx +4 -7
- package/src/client/components/workspace/editor/CommandPalette.tsx +2 -2
- package/src/client/components/workspace/editor/FileDocument.tsx +58 -63
- package/src/client/components/workspace/editor/FilePane.tsx +4 -7
- package/src/client/components/workspace/editor/FindWidget.tsx +7 -2
- package/src/client/components/workspace/editor/Toolbar.tsx +220 -123
- package/src/client/components/workspace/editor/index.tsx +7 -9
- package/src/client/components/workspace/file-explorer/ContextMenu.tsx +2 -1
- package/src/client/components/workspace/file-explorer/Modals.tsx +40 -12
- package/src/client/components/workspace/file-explorer/OutlineSection.test.tsx +18 -0
- package/src/client/components/workspace/file-explorer/OutlineSection.tsx +10 -3
- package/src/client/components/workspace/file-explorer/RootMenu.tsx +14 -5
- package/src/client/components/workspace/file-explorer/Toolbar.tsx +74 -0
- package/src/client/components/workspace/file-explorer/TreeItem.tsx +38 -3
- package/src/client/components/workspace/file-explorer/filter.ts +47 -0
- package/src/client/components/workspace/file-explorer/index.tsx +77 -64
- package/src/client/components/workspace/file-explorer/local-edit.test.ts +135 -0
- package/src/client/components/workspace/search-panel/Body.tsx +1 -1
- package/src/client/components/workspace/worktree-modal/index.test.tsx +118 -0
- package/src/client/components/workspace/worktree-modal/index.tsx +261 -0
- package/src/client/hooks/chat/omp/reconnect.test.ts +338 -0
- package/src/client/hooks/chat/omp/reconnect.ts +187 -0
- package/src/client/hooks/chat/omp/run-liveness.test.ts +71 -0
- package/src/client/hooks/chat/omp/run-liveness.ts +65 -0
- package/src/client/hooks/chat/omp/session-list.activity.test.ts +114 -0
- package/src/client/hooks/chat/omp/session-list.test-util.ts +164 -0
- package/src/client/hooks/chat/omp/session-list.test.ts +30 -150
- package/src/client/hooks/chat/omp/session-list.tsx +69 -9
- package/src/client/hooks/chat/timeline/generating.ts +19 -2
- package/src/client/hooks/chat/timeline/index.ts +27 -28
- package/src/client/hooks/chat/timeline/lost-run.test.ts +329 -0
- package/src/client/hooks/chat/timeline/lost-run.ts +177 -0
- package/src/client/hooks/chat/timeline/send-execute.test.ts +2 -0
- package/src/client/hooks/chat/timeline/send-steer.test.ts +2 -0
- package/src/client/hooks/chat/timeline/send.test.ts +2 -0
- package/src/client/hooks/chat/timeline/send.ts +11 -1
- package/src/client/hooks/chat/timeline/session-load.test-util.ts +205 -0
- package/src/client/hooks/chat/timeline/session-load.test.ts +61 -162
- package/src/client/hooks/chat/timeline/session-load.ts +69 -37
- package/src/client/hooks/chat/timeline/session-switch.test.ts +65 -0
- package/src/client/hooks/chat/timeline/session-switch.ts +63 -0
- package/src/client/hooks/chat/timeline/spawn-target.ts +52 -0
- package/src/client/hooks/chat/timeline/tool-open.test.ts +74 -0
- package/src/client/hooks/chat/timeline/tool-open.ts +52 -8
- package/src/client/hooks/code/deferred-highlight.test.ts +182 -0
- package/src/client/hooks/code/deferred-highlight.ts +129 -0
- package/src/client/hooks/editor/find-state.ts +1 -1
- package/src/client/hooks/editor/theme.test.ts +206 -0
- package/src/client/hooks/editor/theme.ts +82 -0
- package/src/client/hooks/editor/use-editor-drop.test.ts +101 -0
- package/src/client/hooks/editor/use-editor-drop.ts +67 -14
- package/src/client/hooks/editor/use-editor-find.ts +7 -7
- package/src/client/hooks/editor/use-outline-view.test.ts +182 -0
- package/src/client/hooks/editor/use-outline-view.ts +94 -7
- package/src/client/hooks/editor/use-scroll-restore.test.ts +55 -2
- package/src/client/hooks/editor/use-scroll-restore.ts +124 -4
- package/src/client/hooks/editor/use-search-reveal.ts +1 -1
- package/src/client/hooks/ui/long-press.test.ts +174 -0
- package/src/client/hooks/ui/long-press.ts +112 -0
- package/src/client/hooks/ui/settings-modal.ts +56 -0
- package/src/client/hooks/ui/updates/shell-download.test.ts +75 -0
- package/src/client/hooks/ui/updates/shell-download.ts +153 -28
- package/src/client/hooks/ui/updates/shell-offer.test.ts +213 -0
- package/src/client/hooks/ui/updates/shell-update.test.ts +140 -0
- package/src/client/hooks/ui/updates/shell-update.ts +119 -0
- package/src/client/hooks/workspace/diff-file.ts +29 -10
- package/src/client/hooks/workspace/diff-git-actions.ts +147 -0
- package/src/client/hooks/workspace/editor-view.ts +61 -0
- package/src/client/hooks/workspace/file-create.ts +41 -11
- package/src/client/hooks/workspace/file-listing.ts +56 -1
- package/src/client/hooks/workspace/file-tabs.test.ts +62 -5
- package/src/client/hooks/workspace/file-tabs.ts +24 -11
- package/src/client/hooks/workspace/file-tree-actions.ts +37 -5
- package/src/client/hooks/workspace/navbar-session-actions.ts +72 -0
- package/src/client/hooks/workspace/worktree/index.ts +165 -0
- package/src/client/lib/desktop/bridge.ts +45 -0
- package/src/client/lib/search-reveal.ts +1 -1
- package/src/client/main.tsx +5 -1
- package/src/client/tailwind.css +56 -8
- package/src/server/index.ts +7 -2
- package/src/server/lib/auth/guard.test.ts +28 -0
- package/src/server/lib/auth/guard.ts +10 -0
- package/src/server/lib/bundler/titlebar-css.test.ts +42 -1
- package/src/server/lib/db/schema.ts +4 -0
- package/src/server/lib/db/worktree-registry.server.ts +91 -0
- package/src/server/lib/fs/canonical.ts +52 -0
- package/src/server/lib/fs/relative-path.ts +48 -0
- package/src/server/lib/fs/root-worktree.test.ts +116 -0
- package/src/server/lib/fs/root.ts +39 -10
- package/src/server/lib/fs/worktree/carry.ts +134 -0
- package/src/server/lib/fs/worktree/create.test.ts +249 -0
- package/src/server/lib/fs/worktree/create.ts +262 -0
- package/src/server/lib/fs/worktree/errors.ts +43 -0
- package/src/server/lib/fs/worktree/list.test.ts +207 -0
- package/src/server/lib/fs/worktree/list.ts +142 -0
- package/src/server/lib/fs/worktree/location.test.ts +206 -0
- package/src/server/lib/fs/worktree/location.ts +114 -0
- package/src/server/lib/fs/worktree/project-id.ts +53 -0
- package/src/server/lib/fs/worktree/remove.test.ts +242 -0
- package/src/server/lib/fs/worktree/remove.ts +152 -0
- package/src/server/lib/fs/worktree/settings.server.ts +56 -0
- package/src/server/lib/fs/worktree/topology.ts +142 -0
- package/src/server/lib/lifecycle/claim-port.test.ts +156 -0
- package/src/server/lib/lifecycle/port-guard.test.ts +20 -164
- package/src/server/lib/lifecycle/port-guard.ts +39 -7
- package/src/server/lib/lifecycle/probe-health.test.ts +166 -0
- package/src/server/lib/lifecycle/probe.test.ts +77 -164
- package/src/server/lib/lifecycle/probe.ts +121 -13
- package/src/server/lib/omp/rpc/frame-fold-signals.test.ts +112 -0
- package/src/server/lib/omp/rpc/frame-fold.ts +10 -5
- package/src/server/lib/omp/session/sidebar-activity.server.test.ts +162 -0
- package/src/server/lib/omp/session/sidebar-activity.server.ts +70 -0
- package/src/server/lib/omp/session/sidebar-data.canonical.test.ts +155 -0
- package/src/server/lib/omp/session/sidebar-data.server.ts +28 -1
- package/src/server/lib/omp/session/telemetry/compute.ts +13 -1
- package/src/server/lib/omp/session/telemetry.test.ts +33 -0
- package/src/server/lib/panels/local-edits.server.ts +33 -0
- package/src/server/lib/panels/sync.fixture.test-util.ts +92 -0
- package/src/server/lib/panels/sync.local-edits.test.ts +150 -0
- package/src/server/lib/panels/sync.server.test.ts +125 -147
- package/src/server/lib/panels/sync.server.ts +153 -48
- package/src/server/lib/realtime/signals.server.ts +10 -0
- package/src/server/lib/realtime/topics/catalog.server.ts +4 -1
- package/src/server/lib/realtime/topics.server.ts +16 -0
- package/src/server/lib/updates/shell-assets.test.ts +114 -0
- package/src/server/lib/updates/shell-assets.ts +200 -0
- package/src/server/lib/updates/shell-cache.ts +105 -0
- package/src/server/lib/updates/shell-pages.test.ts +187 -0
- package/src/server/lib/updates/shell-pages.ts +157 -0
- package/src/server/lib/updates/shell-release-desktop.test.ts +166 -0
- package/src/server/lib/updates/shell-release.test.ts +59 -71
- package/src/server/lib/updates/shell-release.ts +178 -198
- package/src/server/routes/fs/action-create.test.ts +18 -13
- package/src/server/routes/fs/action-nested.test.ts +184 -0
- package/src/server/routes/fs/action.ts +47 -16
- package/src/server/routes/fs/git-merge.test.ts +11 -1
- package/src/server/routes/fs/read.test.ts +24 -7
- package/src/server/routes/fs/scope.test.ts +15 -10
- package/src/server/routes/fs/scope.ts +3 -14
- package/src/server/routes/index.ts +2 -0
- package/src/server/routes/updates/android.ts +6 -1
- package/src/server/routes/updates/desktop.ts +59 -0
- package/src/server/routes/updates/index.ts +2 -0
- package/src/server/routes/updates/ios.ts +5 -1
- package/src/server/routes/worktrees/create.ts +138 -0
- package/src/server/routes/worktrees/index.ts +25 -0
- package/src/server/routes/worktrees/list.ts +134 -0
- package/src/server/routes/worktrees/remove.ts +102 -0
- package/src/server/routes/worktrees/routes.test.ts +326 -0
- package/src/server/routes/worktrees/shared.ts +90 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.test.ts +36 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.ts +18 -0
- package/src/shared/lib/chat/timeline/run-signals.test.ts +2 -0
- package/src/shared/lib/code/editor/comment-syntax.test.ts +37 -0
- package/src/shared/lib/code/editor/comment-syntax.ts +55 -0
- package/src/shared/lib/code/editor/find-replace.test.ts +134 -0
- package/src/shared/lib/code/editor/{find.ts → find-replace.ts} +23 -107
- package/src/shared/lib/code/editor/{find.test.ts → find-scan.test.ts} +19 -73
- package/src/shared/lib/code/editor/find-scan.ts +177 -0
- package/src/shared/lib/code/editor/keymap.ts +3 -3
- package/src/shared/lib/code/editor/lines.test.ts +7 -55
- package/src/shared/lib/code/editor/lines.ts +8 -48
- package/src/shared/lib/code/editor/marks.test.ts +4 -5
- package/src/shared/lib/code/editor/marks.ts +13 -25
- package/src/shared/lib/code/editor/theme-loader.ts +97 -0
- package/src/shared/lib/code/editor/themes.generated.ts +156 -0
- package/src/shared/lib/code/editor/themes.test.ts +165 -0
- package/src/shared/lib/code/editor/themes.ts +165 -0
- package/src/shared/lib/code/highlight-memo.test.ts +103 -0
- package/src/shared/lib/code/highlight-memo.ts +149 -0
- package/src/shared/lib/code/highlighter-warmup.ts +112 -0
- package/src/shared/lib/code/highlighter.ts +3 -1
- package/src/shared/lib/code/idle.ts +37 -0
- package/src/shared/lib/code/outline/rows.test.ts +96 -0
- package/src/shared/lib/code/outline/rows.ts +94 -0
- package/src/shared/lib/code/syntax-highlight.test.ts +1 -79
- package/src/shared/lib/code/syntax-highlight.ts +2 -3
- package/src/shared/lib/fs/file-kind-classify.test.ts +25 -11
- package/src/shared/lib/fs/file-kind.ts +47 -13
- package/src/shared/lib/fs/file-tree.test.ts +183 -0
- package/src/shared/lib/fs/file-tree.ts +159 -0
- package/src/shared/lib/fs/file-url.test.ts +52 -0
- package/src/shared/lib/fs/file-url.ts +37 -0
- package/src/shared/lib/fs/patch-sides.test.ts +114 -0
- package/src/shared/lib/fs/patch-sides.ts +139 -0
- package/src/shared/lib/fs/paths.test.ts +24 -1
- package/src/shared/lib/fs/paths.ts +17 -0
- package/src/shared/lib/fs/worktree/settings-keys.ts +18 -0
- package/src/shared/lib/omp/session/prewarm.ts +7 -0
- package/src/shared/lib/realtime/protocol.ts +16 -0
- package/src/shared/lib/ui/toolbar-overflow.test.ts +83 -0
- package/src/shared/lib/ui/toolbar-overflow.ts +86 -0
- package/src/shared/lib/updates/shell-arch.ts +54 -0
- package/src/shared/lib/updates/shell-ua.ts +24 -4
- package/src/shared/lib/workspace/active-project.ts +23 -0
- package/src/shared/lib/workspace/editor-view-mode.test.ts +70 -0
- package/src/shared/lib/workspace/editor-view-mode.ts +48 -0
- package/src/shared/lib/workspace/panel-widths.test.ts +30 -0
- package/src/shared/lib/workspace/panel-widths.ts +25 -5
- package/src/shared/lib/workspace/session-state/cache.ts +159 -0
- package/src/shared/lib/workspace/session-state/harness.test-util.ts +55 -0
- package/src/shared/lib/workspace/session-state/merge.ts +100 -0
- package/src/shared/lib/workspace/session-state/store-load.test.ts +206 -0
- package/src/shared/lib/workspace/session-state/store.test.ts +17 -139
- package/src/shared/lib/workspace/session-state/store.ts +92 -130
- package/src/shared/lib/workspace/workspace-modules.test.ts +49 -1
- package/src/shared/lib/workspace/workspace-panel-widths.ts +124 -0
- package/src/shared/lib/workspace/worktree/branch-name.test.ts +95 -0
- package/src/shared/lib/workspace/worktree/branch-name.ts +115 -0
- package/src/shared/lib/workspace/worktree/types.ts +104 -0
- package/src/shared/types/fs.ts +10 -0
- package/src/shared/types/realtime.ts +29 -0
- package/src/shared/types/settings/state.ts +10 -0
- package/src/shared/types/workspace.ts +14 -0
- package/src/test-support/file-explorer-disk.test-util.ts +76 -0
- package/src/test-support/file-explorer.test-util.ts +71 -40
- package/src/test-support/loopback.ts +183 -0
- package/dist/client/static/js/index-nzyjf92m.css +0 -1
- package/src/client/components/common/code-editor/handler.test.ts +0 -274
- package/src/client/components/common/code-editor/handler.ts +0 -175
- package/src/client/components/common/code-editor/index.tsx +0 -314
- package/src/client/components/common/code-surface/measure.ts +0 -148
- package/src/client/components/workspace/diff-panel/SplitView.tsx +0 -209
- package/src/client/components/workspace/diff-panel/UnifiedView.tsx +0 -150
- package/src/client/hooks/editor/scroll-container.test.ts +0 -102
- package/src/client/hooks/editor/scroll-container.ts +0 -26
- package/src/client/hooks/editor/use-code-window.ts +0 -100
- package/src/client/hooks/editor/use-editor-history.test.ts +0 -216
- package/src/client/hooks/editor/use-editor-history.ts +0 -134
- package/src/client/hooks/editor/use-occurrences.test.ts +0 -236
- package/src/client/hooks/editor/use-occurrences.ts +0 -98
- package/src/shared/lib/code/editor/commands.test.ts +0 -155
- package/src/shared/lib/code/editor/commands.ts +0 -260
- package/src/shared/lib/code/editor/edits.test.ts +0 -100
- package/src/shared/lib/code/editor/edits.ts +0 -122
- package/src/shared/lib/code/editor/history.test.ts +0 -118
- package/src/shared/lib/code/editor/history.ts +0 -101
- package/src/shared/lib/code/editor/layers.test.ts +0 -77
- package/src/shared/lib/code/editor/layers.ts +0 -67
- package/src/shared/lib/code/editor/line-edits.test.ts +0 -131
- package/src/shared/lib/code/editor/line-edits.ts +0 -159
- package/src/shared/lib/code/editor/occurrences.test.ts +0 -117
- package/src/shared/lib/code/editor/occurrences.ts +0 -111
- package/src/shared/lib/code/editor/reset-css.test.ts +0 -35
- package/src/shared/lib/code/editor/reset-css.ts +0 -32
- package/src/shared/lib/code/editor/text-commands.test.ts +0 -178
- package/src/shared/lib/code/editor/text-commands.ts +0 -154
- package/src/shared/lib/code/lazy-window.test.ts +0 -161
- package/src/shared/lib/code/lazy-window.ts +0 -156
- package/src/shared/lib/code/windowed-highlight.ts +0 -114
- package/src/shared/lib/fs/diff-parser.test.ts +0 -116
- package/src/shared/lib/fs/diff-parser.ts +0 -213
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `sidebar:activity` — the topic a tool call republishes.
|
|
8
|
+
*
|
|
9
|
+
* Its whole reason to exist is that the STRUCTURE must not be republished per
|
|
10
|
+
* tool call (216 KB over 731 sessions, measured), so the merge has to be exact:
|
|
11
|
+
* it may only ever move the two fields a run advances mid-turn, and only
|
|
12
|
+
* forwards. Each case below is a way that goes wrong.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { afterAll, afterEach, describe, expect, test } from 'bun:test';
|
|
16
|
+
|
|
17
|
+
import { restoreDomGlobals } from '@/test-support/pristine-globals';
|
|
18
|
+
import {
|
|
19
|
+
mountProvider,
|
|
20
|
+
structureBody,
|
|
21
|
+
teardownProvider,
|
|
22
|
+
} from '@/client/hooks/chat/omp/session-list.test-util';
|
|
23
|
+
|
|
24
|
+
afterEach(teardownProvider);
|
|
25
|
+
|
|
26
|
+
afterAll(() => {
|
|
27
|
+
restoreDomGlobals();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('SidebarDataProvider merges the activity topic', () => {
|
|
31
|
+
test('advances a running row past the stamp the structure shipped', async () => {
|
|
32
|
+
const harness = await mountProvider();
|
|
33
|
+
await harness.push(
|
|
34
|
+
{
|
|
35
|
+
folders: [{
|
|
36
|
+
id: 1, name: 'ws', isExpanded: true, hasMore: false, totalSessions: 1,
|
|
37
|
+
sessions: [{ id: 'a', folder_id: 1, title: 'A', updated_at: '2026-10-24T01:00:00.000Z' }],
|
|
38
|
+
}],
|
|
39
|
+
isMock: false,
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
expect(harness.holder.api?.folders[0]?.sessions?.[0].updated_at).toBe('2026-10-24T01:00:00.000Z');
|
|
43
|
+
|
|
44
|
+
await harness.push(undefined, undefined, { a: { updatedAt: '2026-10-24T01:05:00.000Z' } });
|
|
45
|
+
expect(harness.holder.api?.folders[0]?.sessions?.[0].updated_at).toBe('2026-10-24T01:05:00.000Z');
|
|
46
|
+
harness.server.stop();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('never moves a row backwards, so a late frame cannot reorder the list', async () => {
|
|
50
|
+
const harness = await mountProvider();
|
|
51
|
+
// The structure already carries a stamp the activity payload has not caught
|
|
52
|
+
// up with — the normal case right after a turn boundary.
|
|
53
|
+
await harness.push({
|
|
54
|
+
folders: [{
|
|
55
|
+
id: 1, name: 'ws', isExpanded: true, hasMore: false, totalSessions: 1,
|
|
56
|
+
sessions: [{ id: 'a', folder_id: 1, title: 'A', updated_at: '2026-10-24T02:00:00.000Z' }],
|
|
57
|
+
}],
|
|
58
|
+
isMock: false,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
await harness.push(undefined, undefined, { a: { updatedAt: '2026-10-24T01:59:00.000Z' } });
|
|
62
|
+
expect(harness.holder.api?.folders[0]?.sessions?.[0].updated_at).toBe('2026-10-24T02:00:00.000Z');
|
|
63
|
+
harness.server.stop();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('lights the subagent chevron for a subagent seen mid-run', async () => {
|
|
67
|
+
const harness = await mountProvider();
|
|
68
|
+
await harness.push(structureBody());
|
|
69
|
+
expect(harness.holder.api?.folders[0]?.sessions?.[0].hasSubagents).toBeFalsy();
|
|
70
|
+
|
|
71
|
+
await harness.push(undefined, undefined, { a: { updatedAt: '2026-10-24T01:05:00.000Z', hasSubagents: true } });
|
|
72
|
+
expect(harness.holder.api?.folders[0]?.sessions?.[0].hasSubagents).toBe(true);
|
|
73
|
+
harness.server.stop();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('a live read that saw no subagent does not clear a chevron the scan lit', async () => {
|
|
77
|
+
// The two sources run the SAME presence check, but the structure's answer
|
|
78
|
+
// came from a full scan and this one from a live read of one file. A
|
|
79
|
+
// `false` here is "not seen in this read", so it must not unlight a row —
|
|
80
|
+
// the merge ORs against the structure for exactly that reason.
|
|
81
|
+
const harness = await mountProvider();
|
|
82
|
+
await harness.push({
|
|
83
|
+
folders: [{
|
|
84
|
+
id: 1, name: 'ws', isExpanded: true, hasMore: false, totalSessions: 1,
|
|
85
|
+
sessions: [{ id: 'a', folder_id: 1, title: 'A', hasSubagents: true }],
|
|
86
|
+
}],
|
|
87
|
+
isMock: false,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await harness.push(undefined, undefined, { a: { updatedAt: '2026-10-24T01:05:00.000Z', hasSubagents: false } });
|
|
91
|
+
expect(harness.holder.api?.folders[0]?.sessions?.[0].hasSubagents).toBe(true);
|
|
92
|
+
harness.server.stop();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('leaves an id the payload does not name on the structure stamp', async () => {
|
|
96
|
+
const harness = await mountProvider();
|
|
97
|
+
await harness.push({
|
|
98
|
+
folders: [{
|
|
99
|
+
id: 1, name: 'ws', isExpanded: true, hasMore: false, totalSessions: 2,
|
|
100
|
+
sessions: [
|
|
101
|
+
{ id: 'a', folder_id: 1, title: 'A', updated_at: '2026-10-24T01:00:00.000Z' },
|
|
102
|
+
{ id: 'b', folder_id: 1, title: 'B', updated_at: '2026-10-24T00:30:00.000Z' },
|
|
103
|
+
],
|
|
104
|
+
}],
|
|
105
|
+
isMock: false,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await harness.push(undefined, undefined, { a: { updatedAt: '2026-10-24T01:05:00.000Z' } });
|
|
109
|
+
const sessions = harness.holder.api?.folders[0]?.sessions ?? [];
|
|
110
|
+
expect(sessions[0]?.updated_at).toBe('2026-10-24T01:05:00.000Z');
|
|
111
|
+
expect(sessions[1]?.updated_at).toBe('2026-10-24T00:30:00.000Z');
|
|
112
|
+
harness.server.stop();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Shared harness for the `SidebarDataProvider` suites.
|
|
8
|
+
*
|
|
9
|
+
* Driven through a REAL server and a real socket rather than a stubbed fetch,
|
|
10
|
+
* because what the provider does is merge three topics and apply the optimistic
|
|
11
|
+
* overlays — the parts that used to be "does it re-read". A stubbed socket
|
|
12
|
+
* would only prove the fake's timing.
|
|
13
|
+
*
|
|
14
|
+
* Split out when the suites together crossed the repo's per-file ceiling: the
|
|
15
|
+
* harness is setup, and the rules under test belong in their own files.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { Window } from 'happy-dom';
|
|
19
|
+
import { h, render } from 'preact';
|
|
20
|
+
import { act } from 'preact/test-utils';
|
|
21
|
+
|
|
22
|
+
import { SidebarDataProvider, useSidebarData } from '@/client/hooks/chat/omp/session-list';
|
|
23
|
+
import type { SidebarDataHandle } from '@/client/hooks/chat/omp/session-list';
|
|
24
|
+
import { resetRealtimeClient } from '@/shared/lib/realtime/client';
|
|
25
|
+
import { TOPIC_SIDEBAR, TOPIC_SIDEBAR_ACTIVITY, TOPIC_SIDEBAR_STATUS } from '@/shared/lib/realtime/protocol';
|
|
26
|
+
import { startRealtimeTestServer, type RealtimeTestServer } from '@/test-support/realtime-server';
|
|
27
|
+
import type { TopicResolver } from '@/server/lib/realtime/hub.server';
|
|
28
|
+
import { installDomGlobals, pristineWebSocket } from '@/test-support/pristine-globals';
|
|
29
|
+
import type { SidebarActivityPayload, SidebarPayload, SidebarStatusPayload } from '@/client/hooks/chat/omp/session-list';
|
|
30
|
+
import type { WorkspaceFolderData } from '@/shared/types';
|
|
31
|
+
|
|
32
|
+
let container: HTMLElement | undefined;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Point the DOM globals at a happy-dom window whose origin is the test
|
|
36
|
+
* listener, because the client builds its socket URL from `window.location` —
|
|
37
|
+
* a window on a different port would dial a server that is not there, and the
|
|
38
|
+
* test would be measuring a connection failure rather than the channel.
|
|
39
|
+
*/
|
|
40
|
+
function installDom(origin: string): void {
|
|
41
|
+
const win = new Window({ url: origin });
|
|
42
|
+
installDomGlobals(win);
|
|
43
|
+
// happy-dom's WebSocket does not reach a real listener, and the channel is
|
|
44
|
+
// what is under test, so the real implementation wins this global.
|
|
45
|
+
(globalThis as unknown as Record<string, unknown>).WebSocket = pristineWebSocket;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Unmount and drop the socket between cases. Each suite calls this in its own `afterEach`. */
|
|
49
|
+
export function teardownProvider(): void {
|
|
50
|
+
if (container) render(null, container);
|
|
51
|
+
container?.remove();
|
|
52
|
+
container = undefined;
|
|
53
|
+
resetRealtimeClient();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type Status = 'stream' | 'finish' | 'abort';
|
|
57
|
+
|
|
58
|
+
/** A structure payload carrying one session, `a`. */
|
|
59
|
+
export function structureBody(): SidebarPayload {
|
|
60
|
+
return {
|
|
61
|
+
folders: [{ id: 1, name: 'ws', isExpanded: true, hasMore: false, totalSessions: 1, sessions: [{ id: 'a', folder_id: 1, title: 'A' }] }],
|
|
62
|
+
isMock: false,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The volatile map for `a`. */
|
|
67
|
+
export function statusBody(status?: Status): SidebarStatusPayload {
|
|
68
|
+
return status ? { a: { streamStatus: status } } : {};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The live-activity map: empty until a case publishes one. */
|
|
72
|
+
export function activityBody(): SidebarActivityPayload {
|
|
73
|
+
return {};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** One folder holding the given sessions, shaped only as far as the strip reads it. */
|
|
77
|
+
export function folders(sessions: Array<{ id: string; streamStatus?: Status }>): WorkspaceFolderData[] {
|
|
78
|
+
return [{ id: 1, name: 'ws', sessions }] as unknown as WorkspaceFolderData[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The status the provider currently renders for session `a`. */
|
|
82
|
+
export function renderedStatus(api: SidebarDataHandle | null): Status | undefined {
|
|
83
|
+
return api?.folders[0]?.sessions?.[0].streamStatus;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Let Preact's queued effects run.
|
|
88
|
+
*
|
|
89
|
+
* A microtask drain, and deliberately nothing more: the socket's own progress
|
|
90
|
+
* is awaited through the harness, so this only has to flush the render a
|
|
91
|
+
* received frame queued.
|
|
92
|
+
*/
|
|
93
|
+
async function flush(): Promise<void> {
|
|
94
|
+
for (let i = 0; i < 8; i += 1) await Promise.resolve();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface Harness {
|
|
98
|
+
server: RealtimeTestServer;
|
|
99
|
+
holder: { api: SidebarDataHandle | null };
|
|
100
|
+
/** Republish any of the three sidebar topics from a fresh body. */
|
|
101
|
+
push: (structure?: SidebarPayload, status?: SidebarStatusPayload, activity?: SidebarActivityPayload) => Promise<void>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Mount the real provider against a real realtime server. */
|
|
105
|
+
export async function mountProvider(): Promise<Harness> {
|
|
106
|
+
const server = await startRealtimeTestServer(new Map<string, TopicResolver>([
|
|
107
|
+
[TOPIC_SIDEBAR, async () => structureBody()],
|
|
108
|
+
[TOPIC_SIDEBAR_STATUS, async () => statusBody()],
|
|
109
|
+
[TOPIC_SIDEBAR_ACTIVITY, async () => activityBody()],
|
|
110
|
+
]));
|
|
111
|
+
installDom(`http://127.0.0.1:${server.port}`);
|
|
112
|
+
|
|
113
|
+
const holder: { api: SidebarDataHandle | null } = { api: null };
|
|
114
|
+
function Probe() {
|
|
115
|
+
holder.api = useSidebarData();
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
const el = document.createElement('div');
|
|
119
|
+
document.body.appendChild(el);
|
|
120
|
+
container = el;
|
|
121
|
+
await act(async () => {
|
|
122
|
+
render(h(SidebarDataProvider, { children: h(Probe, null) }), el);
|
|
123
|
+
});
|
|
124
|
+
// The structure snapshot must have landed before the provider can report a
|
|
125
|
+
// value at all; the subscription is opened by the render above, so this
|
|
126
|
+
// awaits the client's own state rather than a duration.
|
|
127
|
+
await server.waitForTopic<SidebarPayload>(TOPIC_SIDEBAR, (value) => value !== null);
|
|
128
|
+
await act(async () => {
|
|
129
|
+
await flush();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
server,
|
|
134
|
+
holder,
|
|
135
|
+
push: async (structure, status, activity) => {
|
|
136
|
+
// Await the CLIENT's value, not just the publish: the socket round trip
|
|
137
|
+
// and the provider's merge are the things under test, so resolving on the
|
|
138
|
+
// send would assert against a state that has not happened yet.
|
|
139
|
+
if (structure) {
|
|
140
|
+
server.publish(TOPIC_SIDEBAR, structure);
|
|
141
|
+
await server.waitForTopic(TOPIC_SIDEBAR, (value) => JSON.stringify(value) === JSON.stringify(structure));
|
|
142
|
+
}
|
|
143
|
+
if (status) {
|
|
144
|
+
server.publish(TOPIC_SIDEBAR_STATUS, status);
|
|
145
|
+
await server.waitForTopic(TOPIC_SIDEBAR_STATUS, (value) => JSON.stringify(value) === JSON.stringify(status));
|
|
146
|
+
}
|
|
147
|
+
if (activity) {
|
|
148
|
+
server.publish(TOPIC_SIDEBAR_ACTIVITY, activity);
|
|
149
|
+
await server.waitForTopic(TOPIC_SIDEBAR_ACTIVITY, (value) => JSON.stringify(value) === JSON.stringify(activity));
|
|
150
|
+
}
|
|
151
|
+
await act(async () => {
|
|
152
|
+
await flush();
|
|
153
|
+
});
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Run `body` with Preact's effects flushed around it. */
|
|
159
|
+
export async function withAct(body: () => void | Promise<void>): Promise<void> {
|
|
160
|
+
await act(async () => {
|
|
161
|
+
await body();
|
|
162
|
+
await flush();
|
|
163
|
+
});
|
|
164
|
+
}
|
|
@@ -4,159 +4,34 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* The mounted `SidebarDataProvider` over the unified realtime channel
|
|
7
|
+
* The mounted `SidebarDataProvider` over the unified realtime channel: the
|
|
8
|
+
* STATUS half, the seen-strip overlay, and the title hint.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
* because
|
|
11
|
-
*
|
|
12
|
-
* socket would only prove the fake's timing.
|
|
13
|
-
*
|
|
14
|
-
* Kept: the two-topic merge, the authoritative status map (including a session
|
|
15
|
-
* the structure has not scanned), the seen-strip on an acked terminal badge, and
|
|
16
|
-
* the title hint.
|
|
10
|
+
* The activity topic's merge rules live in `session-list.activity.test.ts` —
|
|
11
|
+
* separate because the two halves answer different questions and the file was
|
|
12
|
+
* over the repo's ceiling with both.
|
|
17
13
|
*/
|
|
18
14
|
|
|
19
15
|
import { afterAll, afterEach, describe, expect, test } from 'bun:test';
|
|
20
|
-
import { Window } from 'happy-dom';
|
|
21
|
-
import { h, render } from 'preact';
|
|
22
|
-
import { act } from 'preact/test-utils';
|
|
23
16
|
|
|
24
17
|
import { applySeenStrip, SESSION_TITLE_HINT_SIGNAL } from '@/client/hooks/chat/omp/sidebar-overlay';
|
|
25
18
|
import { publishClientSignal } from '@/client/lib/signals';
|
|
26
|
-
import {
|
|
27
|
-
import
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
let container: HTMLElement | undefined;
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Point the DOM globals at a happy-dom window whose origin is the test
|
|
41
|
-
* listener, because the client builds its socket URL from `window.location` —
|
|
42
|
-
* a window on a different port would dial a server that is not there, and the
|
|
43
|
-
* test would be measuring a connection failure rather than the channel.
|
|
44
|
-
*/
|
|
45
|
-
function installDom(origin: string): void {
|
|
46
|
-
const win = new Window({ url: origin });
|
|
47
|
-
installDomGlobals(win);
|
|
48
|
-
// happy-dom's WebSocket does not reach a real listener, and the channel is
|
|
49
|
-
// what is under test, so the real implementation wins this global.
|
|
50
|
-
(globalThis as unknown as Record<string, unknown>).WebSocket = pristineWebSocket;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
afterEach(() => {
|
|
56
|
-
if (container) render(null, container);
|
|
57
|
-
container?.remove();
|
|
58
|
-
container = undefined;
|
|
59
|
-
resetRealtimeClient();
|
|
60
|
-
});
|
|
19
|
+
import { restoreDomGlobals } from '@/test-support/pristine-globals';
|
|
20
|
+
import {
|
|
21
|
+
folders,
|
|
22
|
+
mountProvider,
|
|
23
|
+
renderedStatus,
|
|
24
|
+
statusBody,
|
|
25
|
+
teardownProvider,
|
|
26
|
+
withAct,
|
|
27
|
+
} from '@/client/hooks/chat/omp/session-list.test-util';
|
|
28
|
+
|
|
29
|
+
afterEach(teardownProvider);
|
|
61
30
|
|
|
62
31
|
afterAll(() => {
|
|
63
32
|
restoreDomGlobals();
|
|
64
33
|
});
|
|
65
34
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
type Status = 'stream' | 'finish' | 'abort';
|
|
69
|
-
|
|
70
|
-
/** A structure payload carrying one session, `a`. */
|
|
71
|
-
function structureBody(): SidebarPayload {
|
|
72
|
-
return {
|
|
73
|
-
folders: [{ id: 1, name: 'ws', isExpanded: true, hasMore: false, totalSessions: 1, sessions: [{ id: 'a', folder_id: 1, title: 'A' }] }],
|
|
74
|
-
isMock: false,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** The volatile map for `a`. */
|
|
79
|
-
function statusBody(status?: Status): SidebarStatusPayload {
|
|
80
|
-
return status ? { a: { streamStatus: status } } : {};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** One folder holding the given sessions, shaped only as far as the strip reads it. */
|
|
84
|
-
function folders(sessions: Array<{ id: string; streamStatus?: Status }>): WorkspaceFolderData[] {
|
|
85
|
-
return [{ id: 1, name: 'ws', sessions }] as unknown as WorkspaceFolderData[];
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** The status the provider currently renders for session `a`. */
|
|
89
|
-
function renderedStatus(api: SidebarDataHandle | null): Status | undefined {
|
|
90
|
-
return api?.folders[0]?.sessions?.[0].streamStatus;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Let Preact's queued effects run.
|
|
95
|
-
*
|
|
96
|
-
* A microtask drain, and deliberately nothing more: the socket's own progress
|
|
97
|
-
* is awaited through the harness (`waitForFrames`), so this only has to flush
|
|
98
|
-
* the render a received frame queued.
|
|
99
|
-
*/
|
|
100
|
-
async function flush(): Promise<void> {
|
|
101
|
-
for (let i = 0; i < 8; i += 1) await Promise.resolve();
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
interface Harness {
|
|
105
|
-
server: RealtimeTestServer;
|
|
106
|
-
holder: { api: SidebarDataHandle | null };
|
|
107
|
-
/** Republish both topics from the current bodies. */
|
|
108
|
-
push: (structure?: SidebarPayload, status?: SidebarStatusPayload) => Promise<void>;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** Mount the real provider against a real realtime server. */
|
|
112
|
-
async function mountProvider(): Promise<Harness> {
|
|
113
|
-
const server = await startRealtimeTestServer(new Map<string, TopicResolver>([
|
|
114
|
-
[TOPIC_SIDEBAR, async () => structureBody()],
|
|
115
|
-
[TOPIC_SIDEBAR_STATUS, async () => statusBody()],
|
|
116
|
-
]));
|
|
117
|
-
installDom(`http://127.0.0.1:${server.port}`);
|
|
118
|
-
|
|
119
|
-
const holder: { api: SidebarDataHandle | null } = { api: null };
|
|
120
|
-
function Probe() {
|
|
121
|
-
holder.api = useSidebarData();
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
|
-
const el = document.createElement('div');
|
|
125
|
-
document.body.appendChild(el);
|
|
126
|
-
container = el;
|
|
127
|
-
await act(async () => {
|
|
128
|
-
render(h(SidebarDataProvider, { children: h(Probe, null) }), el);
|
|
129
|
-
});
|
|
130
|
-
// The structure snapshot must have landed before the provider can report a
|
|
131
|
-
// value at all; the subscription is opened by the render above, so this
|
|
132
|
-
// awaits the client's own state rather than a duration.
|
|
133
|
-
await server.waitForTopic<SidebarPayload>(TOPIC_SIDEBAR, (value) => value !== null);
|
|
134
|
-
await act(async () => {
|
|
135
|
-
await flush();
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
return {
|
|
139
|
-
server,
|
|
140
|
-
holder,
|
|
141
|
-
push: async (structure, status) => {
|
|
142
|
-
// Await the CLIENT's value, not just the publish: the socket round trip
|
|
143
|
-
// and the provider's merge are the things under test, so resolving on the
|
|
144
|
-
// send would assert against a state that has not happened yet.
|
|
145
|
-
if (structure) {
|
|
146
|
-
server.publish(TOPIC_SIDEBAR, structure);
|
|
147
|
-
await server.waitForTopic(TOPIC_SIDEBAR, (value) => JSON.stringify(value) === JSON.stringify(structure));
|
|
148
|
-
}
|
|
149
|
-
if (status) {
|
|
150
|
-
server.publish(TOPIC_SIDEBAR_STATUS, status);
|
|
151
|
-
await server.waitForTopic(TOPIC_SIDEBAR_STATUS, (value) => JSON.stringify(value) === JSON.stringify(status));
|
|
152
|
-
}
|
|
153
|
-
await act(async () => {
|
|
154
|
-
await flush();
|
|
155
|
-
});
|
|
156
|
-
},
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
35
|
describe('SidebarDataProvider reads both sidebar topics', () => {
|
|
161
36
|
test('reports a live run for a session the structure has not scanned yet', async () => {
|
|
162
37
|
// A fresh spawn: omp writes the session file only when the first assistant
|
|
@@ -227,9 +102,7 @@ describe('SidebarDataProvider carries the sent text for a placeholder row', () =
|
|
|
227
102
|
const harness = await mountProvider();
|
|
228
103
|
expect(harness.holder.api?.titleHint('omp-1')).toBeUndefined();
|
|
229
104
|
|
|
230
|
-
await
|
|
231
|
-
hint('omp-1', 'Perbaiki tombol stop');
|
|
232
|
-
});
|
|
105
|
+
await withAct(() => hint('omp-1', 'Perbaiki tombol stop'));
|
|
233
106
|
|
|
234
107
|
expect(harness.holder.api?.titleHint('omp-1')).toBe('Perbaiki tombol stop');
|
|
235
108
|
expect(harness.holder.api?.titleHint('omp-2')).toBeUndefined();
|
|
@@ -239,14 +112,10 @@ describe('SidebarDataProvider carries the sent text for a placeholder row', () =
|
|
|
239
112
|
|
|
240
113
|
test('ignores a blank hint and drops one once the session is listed', async () => {
|
|
241
114
|
const harness = await mountProvider();
|
|
242
|
-
await
|
|
243
|
-
hint('a', ' ');
|
|
244
|
-
});
|
|
115
|
+
await withAct(() => hint('a', ' '));
|
|
245
116
|
expect(harness.holder.api?.titleHint('a')).toBeUndefined();
|
|
246
117
|
|
|
247
|
-
await
|
|
248
|
-
hint('a', 'sent text');
|
|
249
|
-
});
|
|
118
|
+
await withAct(() => hint('a', 'sent text'));
|
|
250
119
|
expect(harness.holder.api?.titleHint('a')).toBe('sent text');
|
|
251
120
|
|
|
252
121
|
// The session is in the structure, so the placeholder is not drawn and the
|
|
@@ -257,3 +126,14 @@ describe('SidebarDataProvider carries the sent text for a placeholder row', () =
|
|
|
257
126
|
harness.server.stop();
|
|
258
127
|
});
|
|
259
128
|
});
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* `sidebar:activity` is the topic a tool call republishes, and the merge rules
|
|
132
|
+
* are what make a payload that carries ONLY the running sessions sufficient.
|
|
133
|
+
*
|
|
134
|
+
* The structure is the authority for every row and is re-scanned at every turn
|
|
135
|
+
* boundary; this topic only ever moves the two fields a run advances mid-turn.
|
|
136
|
+
* Two ways that can go wrong, both pinned below: a late frame moving a row
|
|
137
|
+
* BACKWARDS (which reorders the sidebar under the reader), and the merge
|
|
138
|
+
* re-sorting 731 rows for a stamp that did not move.
|
|
139
|
+
*/
|
|
@@ -31,7 +31,7 @@ import { useRealtimeTopic } from '@/client/hooks/ui/realtime';
|
|
|
31
31
|
import { useInputRequiredAlert } from '@/client/hooks/ui/input-required-alert';
|
|
32
32
|
|
|
33
33
|
import { realtimeClient } from '@/shared/lib/realtime/client';
|
|
34
|
-
import { TOPIC_SIDEBAR, TOPIC_SIDEBAR_STATUS } from '@/shared/lib/realtime/protocol';
|
|
34
|
+
import { TOPIC_SIDEBAR, TOPIC_SIDEBAR_ACTIVITY, TOPIC_SIDEBAR_STATUS } from '@/shared/lib/realtime/protocol';
|
|
35
35
|
import {
|
|
36
36
|
applySeenStrip,
|
|
37
37
|
SESSION_TITLE_HINT_SIGNAL,
|
|
@@ -39,11 +39,11 @@ import {
|
|
|
39
39
|
import { subscribeClientSignal } from '@/client/lib/signals';
|
|
40
40
|
import { buildSidebarSessionStatus } from '@/client/hooks/chat/omp/session-statuses';
|
|
41
41
|
import type { WorkspaceFolderData } from '@/shared/types';
|
|
42
|
-
import type { SidebarPayload, SidebarStatusPayload } from '@/shared/types/realtime';
|
|
42
|
+
import type { SidebarActivityPayload, SidebarPayload, SidebarStatusPayload } from '@/shared/types/realtime';
|
|
43
43
|
|
|
44
44
|
/** Re-exported so a consumer (and a test) names the shape without reaching
|
|
45
45
|
* into a server module for it. */
|
|
46
|
-
export type { SidebarPayload, SidebarStatusPayload };
|
|
46
|
+
export type { SidebarActivityPayload, SidebarPayload, SidebarStatusPayload };
|
|
47
47
|
|
|
48
48
|
export interface SidebarDataHandle {
|
|
49
49
|
folders: WorkspaceFolderData[];
|
|
@@ -115,9 +115,65 @@ function mergeStatus(
|
|
|
115
115
|
}));
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Advance each row's live fields from the activity payload.
|
|
120
|
+
*
|
|
121
|
+
* `sidebar:activity` carries only the RUNNING sessions (see its doc), so an id
|
|
122
|
+
* it does not name keeps whatever the structure gave it. Three rules, each of
|
|
123
|
+
* which is a way this can go wrong:
|
|
124
|
+
*
|
|
125
|
+
* - the `updated_at` comparison is what makes two sources safe to
|
|
126
|
+
* interleave: the structure is re-scanned at every turn boundary and may
|
|
127
|
+
* already carry a stamp this payload has not caught up with, and a late
|
|
128
|
+
* activity frame must never move a row BACKWARDS (that reorders the
|
|
129
|
+
* sidebar under the reader);
|
|
130
|
+
* - `hasSubagents` only ever turns ON, because the payload is a live read of
|
|
131
|
+
* what is on disk and a subagent transcript does not disappear when its run
|
|
132
|
+
* ends — a `false` here is "not seen yet", not "none exist";
|
|
133
|
+
* - nothing else is touched. Every other field of a row changes on a
|
|
134
|
+
* STRUCTURAL event, and those publish the structure for their own reason.
|
|
135
|
+
*/
|
|
136
|
+
function mergeActivity(
|
|
137
|
+
folders: WorkspaceFolderData[],
|
|
138
|
+
activity: SidebarActivityPayload | null,
|
|
139
|
+
): WorkspaceFolderData[] {
|
|
140
|
+
if (!activity) return folders;
|
|
141
|
+
let changed = false;
|
|
142
|
+
const merged = folders.map((folder) => ({
|
|
143
|
+
...folder,
|
|
144
|
+
sessions: (folder.sessions ?? []).map((session) => {
|
|
145
|
+
const live = activity[String(session.id)];
|
|
146
|
+
if (!live) return session;
|
|
147
|
+
const updated_at = live.updatedAt && isNewerStamp(live.updatedAt, session.updated_at)
|
|
148
|
+
? live.updatedAt
|
|
149
|
+
: session.updated_at;
|
|
150
|
+
const hasSubagents = session.hasSubagents || live.hasSubagents === true;
|
|
151
|
+
if (updated_at === session.updated_at && hasSubagents === Boolean(session.hasSubagents)) return session;
|
|
152
|
+
changed = true;
|
|
153
|
+
return { ...session, updated_at, hasSubagents };
|
|
154
|
+
}),
|
|
155
|
+
}));
|
|
156
|
+
// A stable identity matters: the sidebar's own `useMemo` chain (filter, sort,
|
|
157
|
+
// pending overlay) keys on this array, and returning a fresh one per activity
|
|
158
|
+
// frame would re-sort 731 rows for a stamp that did not move.
|
|
159
|
+
return changed ? merged : folders;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Whether `next` is a later ISO stamp than `current`. An absent current is older. */
|
|
163
|
+
function isNewerStamp(next: string, current?: string): boolean {
|
|
164
|
+
if (!current) return true;
|
|
165
|
+
const nextMs = Date.parse(next);
|
|
166
|
+
const currentMs = Date.parse(current);
|
|
167
|
+
// An unparseable value on either side is not evidence of a newer stamp; the
|
|
168
|
+
// structure's own field stays authoritative.
|
|
169
|
+
if (!Number.isFinite(nextMs) || !Number.isFinite(currentMs)) return false;
|
|
170
|
+
return nextMs > currentMs;
|
|
171
|
+
}
|
|
172
|
+
|
|
118
173
|
export function SidebarDataProvider({ children, initialFolders = [] }: { children: ReactNode; initialFolders?: WorkspaceFolderData[] }) {
|
|
119
174
|
const structure = useRealtimeTopic<SidebarPayload>(TOPIC_SIDEBAR);
|
|
120
175
|
const status = useRealtimeTopic<SidebarStatusPayload>(TOPIC_SIDEBAR_STATUS);
|
|
176
|
+
const activity = useRealtimeTopic<SidebarActivityPayload>(TOPIC_SIDEBAR_ACTIVITY);
|
|
121
177
|
|
|
122
178
|
// Session ids whose badge the user has already dismissed on this mount.
|
|
123
179
|
// Optimistic strip: keeps the click→disappearance instant and stops the
|
|
@@ -132,8 +188,8 @@ export function SidebarDataProvider({ children, initialFolders = [] }: { childre
|
|
|
132
188
|
const [overlayVersion, setOverlayVersion] = useState(0);
|
|
133
189
|
|
|
134
190
|
const folders = useMemo(
|
|
135
|
-
() => mergeStatus(structure.data?.folders ?? initialFolders, status.data),
|
|
136
|
-
[structure.data, status.data, initialFolders],
|
|
191
|
+
() => mergeActivity(mergeStatus(structure.data?.folders ?? initialFolders, status.data), activity.data),
|
|
192
|
+
[structure.data, status.data, activity.data, initialFolders],
|
|
137
193
|
);
|
|
138
194
|
const isMock = structure.data?.isMock ?? false;
|
|
139
195
|
const initializing = structure.data === null;
|
|
@@ -173,6 +229,10 @@ export function SidebarDataProvider({ children, initialFolders = [] }: { childre
|
|
|
173
229
|
const refresh = useCallback(() => {
|
|
174
230
|
realtimeClient.refresh(TOPIC_SIDEBAR);
|
|
175
231
|
realtimeClient.refresh(TOPIC_SIDEBAR_STATUS);
|
|
232
|
+
// The activity stamps are what a tool call advances, so a manual refresh
|
|
233
|
+
// that skipped them would re-read the list and leave its labels where they
|
|
234
|
+
// were.
|
|
235
|
+
realtimeClient.refresh(TOPIC_SIDEBAR_ACTIVITY);
|
|
176
236
|
}, []);
|
|
177
237
|
|
|
178
238
|
// The spinner follows a user's click only: it is raised here and lowered when
|
|
@@ -188,10 +248,10 @@ export function SidebarDataProvider({ children, initialFolders = [] }: { childre
|
|
|
188
248
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
189
249
|
}, [structure.data]);
|
|
190
250
|
|
|
191
|
-
// A background push — every tool call
|
|
192
|
-
// topic's own indicator, so the toolbar button spins for it too.
|
|
193
|
-
// the list moved with nothing on screen saying so.
|
|
194
|
-
const isRefreshing = refreshing || structure.refreshing;
|
|
251
|
+
// A background push — every tool call advances a live session's activity —
|
|
252
|
+
// pulses the topic's own indicator, so the toolbar button spins for it too.
|
|
253
|
+
// Without this the list moved with nothing on screen saying so.
|
|
254
|
+
const isRefreshing = refreshing || structure.refreshing || activity.refreshing;
|
|
195
255
|
|
|
196
256
|
// Fires for ANY session, including one this tab never opened — the cue that an
|
|
197
257
|
// agent is blocked on an answer must not depend on the open timeline.
|
|
@@ -35,6 +35,14 @@ export interface GeneratingState {
|
|
|
35
35
|
ownRun: boolean;
|
|
36
36
|
/** Non-reactive mirror of `ownRun` for the session-load guard. */
|
|
37
37
|
ownRunRef: { current: boolean };
|
|
38
|
+
/**
|
|
39
|
+
* The STREAM has spoken for the run in flight — an `agent_start` or a resume
|
|
40
|
+
* of a live run, both of which call `setGenerating(true)` without ownership.
|
|
41
|
+
* False for a run this page merely DISPATCHED and whose first frame has not
|
|
42
|
+
* arrived, which is what keeps a reconnect in that window from settling a
|
|
43
|
+
* turn that has not started.
|
|
44
|
+
*/
|
|
45
|
+
streamRunRef: { current: boolean };
|
|
38
46
|
/**
|
|
39
47
|
* Set the generating state. `owned` marks a run THIS PAGE dispatched; every
|
|
40
48
|
* caller but the send path omits it, so an adopted run never claims ownership
|
|
@@ -70,6 +78,7 @@ export function useGeneratingState(): GeneratingState {
|
|
|
70
78
|
const [isGenerating, setIsGenerating] = useState(false);
|
|
71
79
|
const isGeneratingRef = useRef(false);
|
|
72
80
|
const ownRunRef = useRef(false);
|
|
81
|
+
const streamRunRef = useRef(false);
|
|
73
82
|
const [ownRun, setOwnRun] = useState(false);
|
|
74
83
|
const [generatingVerb, setGeneratingVerbState] = useState('');
|
|
75
84
|
const activityRef = useRef('');
|
|
@@ -80,8 +89,15 @@ export function useGeneratingState(): GeneratingState {
|
|
|
80
89
|
// then re-rendered again — an unbounded fetch loop.
|
|
81
90
|
const setGenerating = useCallback((v: boolean, owned = false) => {
|
|
82
91
|
isGeneratingRef.current = v;
|
|
83
|
-
if (!v)
|
|
84
|
-
|
|
92
|
+
if (!v) {
|
|
93
|
+
ownRunRef.current = false;
|
|
94
|
+
streamRunRef.current = false;
|
|
95
|
+
} else {
|
|
96
|
+
if (owned) ownRunRef.current = true;
|
|
97
|
+
// Not owned: the stream itself announced this run (`agent_start`, or the
|
|
98
|
+
// mount probe resuming a live one).
|
|
99
|
+
else streamRunRef.current = true;
|
|
100
|
+
}
|
|
85
101
|
// Mirror onto state: `timelineRunning` is computed during render, so a
|
|
86
102
|
// ref-only ownership flag could not reach the footer's withheld guard.
|
|
87
103
|
setOwnRun(ownRunRef.current);
|
|
@@ -98,6 +114,7 @@ export function useGeneratingState(): GeneratingState {
|
|
|
98
114
|
isGeneratingRef,
|
|
99
115
|
ownRun,
|
|
100
116
|
ownRunRef,
|
|
117
|
+
streamRunRef,
|
|
101
118
|
setGenerating,
|
|
102
119
|
generatingVerb,
|
|
103
120
|
activityRef,
|