@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
|
@@ -151,6 +151,7 @@ function makeSendHarness(options: {
|
|
|
151
151
|
const params: string[] = [];
|
|
152
152
|
const seeds: SessionSeed[] = [];
|
|
153
153
|
const spawnSelectionRef = { current: { plan: true, goal: false } };
|
|
154
|
+
const spawnWorktreeRef = { current: null as string | null };
|
|
154
155
|
const adoptedSessionIdRef = { current: null };
|
|
155
156
|
const deps: ChatTimelineSendDeps = {
|
|
156
157
|
folders: options.folders ?? [],
|
|
@@ -181,6 +182,7 @@ function makeSendHarness(options: {
|
|
|
181
182
|
deferredComposerPickRef: options.deferredPick ?? { current: null },
|
|
182
183
|
accessModeRef: { current: 'always-ask' },
|
|
183
184
|
spawnSelectionRef,
|
|
185
|
+
spawnWorktreeRef,
|
|
184
186
|
abortControllerRef: { current: null },
|
|
185
187
|
setInputValue: (v) => {
|
|
186
188
|
drafts.push(v);
|
|
@@ -58,6 +58,13 @@ export interface ChatTimelineSendDeps {
|
|
|
58
58
|
* outside the render that produced the pick, and it is the FIRST moment a
|
|
59
59
|
* child exists to carry the selection. */
|
|
60
60
|
spawnSelectionRef: { current: { plan: boolean; goal: boolean } | null };
|
|
61
|
+
/**
|
|
62
|
+
* The worktree the pending chat picked (`chat.spawnTarget`), read through a
|
|
63
|
+
* REF because the composer is remounted by a stretch, a tab switch and a
|
|
64
|
+
* reload while this handler is created once per deps record — the same reason
|
|
65
|
+
* the waiting pick keeps one.
|
|
66
|
+
*/
|
|
67
|
+
spawnWorktreeRef: { current: string | null };
|
|
61
68
|
/** Picks the composer made while a turn was streaming. Pushed onto the
|
|
62
69
|
* session here, immediately before the prompt they were meant for. */
|
|
63
70
|
deferredComposerPickRef: DeferredModelStore;
|
|
@@ -104,6 +111,7 @@ export function useChatTimelineSend(deps: ChatTimelineSendDeps): ChatTimelineSen
|
|
|
104
111
|
deferredComposerPickRef,
|
|
105
112
|
accessModeRef,
|
|
106
113
|
spawnSelectionRef,
|
|
114
|
+
spawnWorktreeRef,
|
|
107
115
|
abortControllerRef,
|
|
108
116
|
setInputValue,
|
|
109
117
|
setSearchParams,
|
|
@@ -234,7 +242,9 @@ export function useChatTimelineSend(deps: ChatTimelineSendDeps): ChatTimelineSen
|
|
|
234
242
|
// when the spawn fails (e.g. MOCK=true or no workspace context).
|
|
235
243
|
if (!isOmpSession) {
|
|
236
244
|
const currentFolder = folders.find(f => String(f.id) === String(selectedFolderId));
|
|
237
|
-
|
|
245
|
+
// A worktree pick wins: the chat is BORN there, so no relocation is ever
|
|
246
|
+
// needed and the child's cwd is right for its whole life.
|
|
247
|
+
const cwd = spawnWorktreeRef.current || currentFolder?.project_path || currentFolder?.name;
|
|
238
248
|
if (cwd) {
|
|
239
249
|
const images = collectImages(attachments);
|
|
240
250
|
const promptText = await buildPromptText(text, textFiles);
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The harness both session-load suites share.
|
|
8
|
+
*
|
|
9
|
+
* Split out when the suite passed the per-file ceiling. It mounts the REAL
|
|
10
|
+
* `useSessionLoad` through a `Probe` component and gates every `/api/chat/:id`
|
|
11
|
+
* response, so a suite decides when — and with what — a fetch lands.
|
|
12
|
+
*
|
|
13
|
+
* The committed-history contract itself:
|
|
14
|
+
*
|
|
15
|
+
* While a run is live the timeline's rows are the STREAM's, and a committed
|
|
16
|
+
* fetch is a lagging snapshot: for a fresh spawn there may be no transcript on
|
|
17
|
+
* disk at all (omp buffers its writes until the first assistant message
|
|
18
|
+
* settles), and it never carries a segment still streaming. Applying one
|
|
19
|
+
* mid-run cost the operator their own turn in both directions, measured on real
|
|
20
|
+
* sessions:
|
|
21
|
+
*
|
|
22
|
+
* - an empty payload EMPTIED the list, so the turn they had just sent
|
|
23
|
+
* vanished until a reload;
|
|
24
|
+
* - a payload carrying the file's own copy of that turn was merged UNDER the
|
|
25
|
+
* live rows, whose ids differ (`msg-…-user` vs the echoed id), so the turn
|
|
26
|
+
* rendered twice.
|
|
27
|
+
*
|
|
28
|
+
* Both shapes needed the same hole: the guard was keyed on the AI placeholder,
|
|
29
|
+
* which the stream RELEASES at the first assistant `message_end` while the run
|
|
30
|
+
* continues. These cases pin the run as the ownership signal, and pin the two
|
|
31
|
+
* directions the guard must still let through when no run is live.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { afterAll, afterEach, beforeAll } from 'bun:test';
|
|
35
|
+
import { Window } from 'happy-dom';
|
|
36
|
+
import { h, render } from 'preact';
|
|
37
|
+
import { act } from 'preact/test-utils';
|
|
38
|
+
import { useCallback, useRef, useState } from 'preact/hooks';
|
|
39
|
+
import { installDomGlobals, restoreDomGlobals } from '@/test-support/pristine-globals';
|
|
40
|
+
import { useSessionLoad } from '@/client/hooks/chat/timeline/session-load';
|
|
41
|
+
import type { ChatMessageData } from '@/shared/types';
|
|
42
|
+
|
|
43
|
+
/** The runner's own fetch, reached through `Bun` so a leak is not mistaken for it. */
|
|
44
|
+
const originalFetch = Bun.fetch;
|
|
45
|
+
|
|
46
|
+
/** The optimistic bubble `executeSend` mounts for a locally sent turn. */
|
|
47
|
+
export const OPTIMISTIC_USER: ChatMessageData = { id: 'msg-1-user', role: 'user', content: 'halo', date: 'Today, 12:22 PM' };
|
|
48
|
+
/** The empty assistant row the stream fills. */
|
|
49
|
+
export const AI_PLACEHOLDER: ChatMessageData = { id: 'msg-2-ai', role: 'ai', content: '' };
|
|
50
|
+
/** The same turn as the transcript records it, under its own id. */
|
|
51
|
+
export const ECHOED_TURN: ChatMessageData = { id: 'omp-echo', role: 'user', content: 'halo' };
|
|
52
|
+
|
|
53
|
+
interface LoadGate {
|
|
54
|
+
url: string;
|
|
55
|
+
resolve: (body: unknown) => void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const gates: LoadGate[] = [];
|
|
59
|
+
|
|
60
|
+
/** One mount's observable state: what the timeline holds and what drives it. */
|
|
61
|
+
export interface ProbeApi {
|
|
62
|
+
messages: ChatMessageData[];
|
|
63
|
+
adoptedSessionIdRef: { current: string | null };
|
|
64
|
+
setMessages: (next: ChatMessageData[]) => void;
|
|
65
|
+
isGeneratingRef: { current: boolean };
|
|
66
|
+
ownRunRef: { current: boolean };
|
|
67
|
+
aiPlaceholderIdRef: { current: string | null };
|
|
68
|
+
optimisticUserIdRef: { current: string | null };
|
|
69
|
+
generatingCalls: boolean[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function makeApi(): ProbeApi {
|
|
73
|
+
return {
|
|
74
|
+
messages: [],
|
|
75
|
+
adoptedSessionIdRef: { current: null },
|
|
76
|
+
setMessages: () => {},
|
|
77
|
+
isGeneratingRef: { current: false },
|
|
78
|
+
ownRunRef: { current: false },
|
|
79
|
+
aiPlaceholderIdRef: { current: null },
|
|
80
|
+
optimisticUserIdRef: { current: null },
|
|
81
|
+
generatingCalls: [],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function Probe({ sessionId, api }: { sessionId: string | null; api: ProbeApi }) {
|
|
86
|
+
const [messages, setMessages] = useState<ChatMessageData[]>([]);
|
|
87
|
+
const isGeneratingRef = useRef(false);
|
|
88
|
+
const ownRunRef = useRef(false);
|
|
89
|
+
const streamRunRef = useRef(false);
|
|
90
|
+
const serverRunRef = useRef(false);
|
|
91
|
+
const aiPlaceholderIdRef = useRef<string | null>(null);
|
|
92
|
+
const optimisticUserIdRef = useRef<string | null>(null);
|
|
93
|
+
const pendingUserDisplaysRef = useRef<{ sent: string; display: string }[]>([]);
|
|
94
|
+
const metaRefreshedRef = useRef<string | null>(null);
|
|
95
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
96
|
+
const jumpActiveRef = useRef(false);
|
|
97
|
+
const jumpCountRef = useRef(0);
|
|
98
|
+
// Stable across renders, like the timeline's own `setGenerating` throat: a
|
|
99
|
+
// fresh identity would re-run the load effect on every render.
|
|
100
|
+
const setGenerating = useCallback((v: boolean) => {
|
|
101
|
+
isGeneratingRef.current = v;
|
|
102
|
+
api.generatingCalls.push(v);
|
|
103
|
+
}, [api]);
|
|
104
|
+
|
|
105
|
+
api.messages = messages;
|
|
106
|
+
api.setMessages = setMessages;
|
|
107
|
+
api.isGeneratingRef = isGeneratingRef;
|
|
108
|
+
api.ownRunRef = ownRunRef;
|
|
109
|
+
api.aiPlaceholderIdRef = aiPlaceholderIdRef;
|
|
110
|
+
api.optimisticUserIdRef = optimisticUserIdRef;
|
|
111
|
+
|
|
112
|
+
const { adoptedSessionIdRef } = useSessionLoad({
|
|
113
|
+
sessionId,
|
|
114
|
+
setLocalMessages: setMessages,
|
|
115
|
+
setGenerating,
|
|
116
|
+
isGeneratingRef,
|
|
117
|
+
ownRunRef,
|
|
118
|
+
streamRunRef,
|
|
119
|
+
serverRunRef,
|
|
120
|
+
aiPlaceholderIdRef,
|
|
121
|
+
optimisticUserIdRef,
|
|
122
|
+
pendingUserDisplaysRef,
|
|
123
|
+
cancelStreamingCoalescer: () => {},
|
|
124
|
+
metaRefreshedRef,
|
|
125
|
+
scrollRef,
|
|
126
|
+
jumpActiveRef,
|
|
127
|
+
jumpCountRef,
|
|
128
|
+
});
|
|
129
|
+
api.adoptedSessionIdRef = adoptedSessionIdRef;
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let container: HTMLElement | undefined;
|
|
134
|
+
|
|
135
|
+
beforeAll(() => {
|
|
136
|
+
installDomGlobals(new Window({ url: 'http://localhost' }));
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
afterAll(() => {
|
|
140
|
+
restoreDomGlobals();
|
|
141
|
+
globalThis.fetch = originalFetch;
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
afterEach(() => {
|
|
145
|
+
if (container) render(null, container);
|
|
146
|
+
container?.remove();
|
|
147
|
+
container = undefined;
|
|
148
|
+
gates.length = 0;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
/** Stub the loader so the test decides when (and with what) the fetch lands. */
|
|
152
|
+
export function stubChatFetch(): void {
|
|
153
|
+
globalThis.fetch = (async (input: RequestInfo | URL) => {
|
|
154
|
+
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
|
|
155
|
+
const { promise, resolve } = Promise.withResolvers<Response>();
|
|
156
|
+
gates.push({ url, resolve: (body) => resolve(new Response(JSON.stringify(body))) });
|
|
157
|
+
return promise;
|
|
158
|
+
}) as typeof fetch;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Mount the real hook and let its session-load effect register its fetch. */
|
|
162
|
+
export async function mount(sessionId: string, api: ProbeApi): Promise<void> {
|
|
163
|
+
container ??= document.body.appendChild(document.createElement('div'));
|
|
164
|
+
await act(async () => {
|
|
165
|
+
render(h(Probe, { sessionId, api }), container as HTMLElement);
|
|
166
|
+
});
|
|
167
|
+
await act(async () => {});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Land the registered fetch, as the server's response would. */
|
|
171
|
+
export async function resolveLoad(messages: unknown[], index = 0): Promise<void> {
|
|
172
|
+
const gate = gates[index];
|
|
173
|
+
if (!gate) throw new Error('no session fetch was registered');
|
|
174
|
+
await act(async () => {
|
|
175
|
+
gate.resolve({ session: { id: 'sess-1', messages }, hasMore: false, oldestIndex: 0 });
|
|
176
|
+
});
|
|
177
|
+
for (let i = 0; i < 3; i += 1) await act(async () => {});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Re-render the mounted Probe with a new session id, which is what the spawn
|
|
182
|
+
* adoption (`new-…` → UUID) does when the URL flips while the child streams:
|
|
183
|
+
* the session-load effect re-runs and ISSUES its history fetch at that moment,
|
|
184
|
+
* mid-run. The fetch registered before this call is `gates[0]`; the one this
|
|
185
|
+
* issues is `gates[1]`.
|
|
186
|
+
*/
|
|
187
|
+
export async function switchSession(sessionId: string, api: ProbeApi): Promise<void> {
|
|
188
|
+
await act(async () => {
|
|
189
|
+
render(h(Probe, { sessionId, api }), container as HTMLElement);
|
|
190
|
+
});
|
|
191
|
+
await act(async () => {});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** A run this page started, with its optimistic rows already on screen. */
|
|
195
|
+
export async function startLiveRun(api: ProbeApi): Promise<void> {
|
|
196
|
+
await act(async () => {
|
|
197
|
+
api.isGeneratingRef.current = true;
|
|
198
|
+
api.ownRunRef.current = true;
|
|
199
|
+
api.optimisticUserIdRef.current = OPTIMISTIC_USER.id;
|
|
200
|
+
api.setMessages([OPTIMISTIC_USER, AI_PLACEHOLDER]);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export const ids = (api: ProbeApi) => api.messages.map((m) => m.id);
|
|
205
|
+
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* The committed-history fetch vs a run this page started.
|
|
7
|
+
* The committed-history fetch vs a run this page started, and a spawn's arrival.
|
|
8
8
|
*
|
|
9
9
|
* While a run is live the timeline's rows are the STREAM's, and a committed
|
|
10
10
|
* fetch is a lagging snapshot: for a fresh spawn there may be no transcript on
|
|
@@ -23,170 +23,25 @@
|
|
|
23
23
|
* which the stream RELEASES at the first assistant `message_end` while the run
|
|
24
24
|
* continues. These cases pin the run as the ownership signal, and pin the two
|
|
25
25
|
* directions the guard must still let through when no run is live.
|
|
26
|
+
*
|
|
27
|
+
* The harness is `session-load.test-util.ts`.
|
|
26
28
|
*/
|
|
27
29
|
|
|
28
|
-
import {
|
|
29
|
-
import { Window } from 'happy-dom';
|
|
30
|
-
import { h, render } from 'preact';
|
|
30
|
+
import { describe, expect, test } from 'bun:test';
|
|
31
31
|
import { act } from 'preact/test-utils';
|
|
32
|
-
import {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const ECHOED_TURN: ChatMessageData = { id: 'omp-echo', role: 'user', content: 'halo' };
|
|
46
|
-
|
|
47
|
-
interface LoadGate {
|
|
48
|
-
url: string;
|
|
49
|
-
resolve: (body: unknown) => void;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const gates: LoadGate[] = [];
|
|
53
|
-
|
|
54
|
-
/** One mount's observable state: what the timeline holds and what drives it. */
|
|
55
|
-
interface ProbeApi {
|
|
56
|
-
messages: ChatMessageData[];
|
|
57
|
-
setMessages: (next: ChatMessageData[]) => void;
|
|
58
|
-
isGeneratingRef: { current: boolean };
|
|
59
|
-
ownRunRef: { current: boolean };
|
|
60
|
-
aiPlaceholderIdRef: { current: string | null };
|
|
61
|
-
optimisticUserIdRef: { current: string | null };
|
|
62
|
-
generatingCalls: boolean[];
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function makeApi(): ProbeApi {
|
|
66
|
-
return {
|
|
67
|
-
messages: [],
|
|
68
|
-
setMessages: () => {},
|
|
69
|
-
isGeneratingRef: { current: false },
|
|
70
|
-
ownRunRef: { current: false },
|
|
71
|
-
aiPlaceholderIdRef: { current: null },
|
|
72
|
-
optimisticUserIdRef: { current: null },
|
|
73
|
-
generatingCalls: [],
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function Probe({ sessionId, api }: { sessionId: string | null; api: ProbeApi }) {
|
|
78
|
-
const [messages, setMessages] = useState<ChatMessageData[]>([]);
|
|
79
|
-
const isGeneratingRef = useRef(false);
|
|
80
|
-
const ownRunRef = useRef(false);
|
|
81
|
-
const aiPlaceholderIdRef = useRef<string | null>(null);
|
|
82
|
-
const optimisticUserIdRef = useRef<string | null>(null);
|
|
83
|
-
const metaRefreshedRef = useRef<string | null>(null);
|
|
84
|
-
const scrollRef = useRef<HTMLDivElement>(null);
|
|
85
|
-
const jumpActiveRef = useRef(false);
|
|
86
|
-
const jumpCountRef = useRef(0);
|
|
87
|
-
// Stable across renders, like the timeline's own `setGenerating` throat: a
|
|
88
|
-
// fresh identity would re-run the load effect on every render.
|
|
89
|
-
const setGenerating = useCallback((v: boolean) => {
|
|
90
|
-
isGeneratingRef.current = v;
|
|
91
|
-
api.generatingCalls.push(v);
|
|
92
|
-
}, [api]);
|
|
93
|
-
|
|
94
|
-
api.messages = messages;
|
|
95
|
-
api.setMessages = setMessages;
|
|
96
|
-
api.isGeneratingRef = isGeneratingRef;
|
|
97
|
-
api.ownRunRef = ownRunRef;
|
|
98
|
-
api.aiPlaceholderIdRef = aiPlaceholderIdRef;
|
|
99
|
-
api.optimisticUserIdRef = optimisticUserIdRef;
|
|
100
|
-
|
|
101
|
-
useSessionLoad({
|
|
102
|
-
sessionId,
|
|
103
|
-
setLocalMessages: setMessages,
|
|
104
|
-
setGenerating,
|
|
105
|
-
isGeneratingRef,
|
|
106
|
-
ownRunRef,
|
|
107
|
-
aiPlaceholderIdRef,
|
|
108
|
-
optimisticUserIdRef,
|
|
109
|
-
cancelStreamingCoalescer: () => {},
|
|
110
|
-
metaRefreshedRef,
|
|
111
|
-
scrollRef,
|
|
112
|
-
jumpActiveRef,
|
|
113
|
-
jumpCountRef,
|
|
114
|
-
});
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
let container: HTMLElement | undefined;
|
|
119
|
-
|
|
120
|
-
beforeAll(() => {
|
|
121
|
-
installDomGlobals(new Window({ url: 'http://localhost' }));
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
afterAll(() => {
|
|
125
|
-
restoreDomGlobals();
|
|
126
|
-
globalThis.fetch = originalFetch;
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
afterEach(() => {
|
|
130
|
-
if (container) render(null, container);
|
|
131
|
-
container?.remove();
|
|
132
|
-
container = undefined;
|
|
133
|
-
gates.length = 0;
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
/** Stub the loader so the test decides when (and with what) the fetch lands. */
|
|
137
|
-
function stubChatFetch(): void {
|
|
138
|
-
globalThis.fetch = (async (input: RequestInfo | URL) => {
|
|
139
|
-
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
|
|
140
|
-
const { promise, resolve } = Promise.withResolvers<Response>();
|
|
141
|
-
gates.push({ url, resolve: (body) => resolve(new Response(JSON.stringify(body))) });
|
|
142
|
-
return promise;
|
|
143
|
-
}) as typeof fetch;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/** Mount the real hook and let its session-load effect register its fetch. */
|
|
147
|
-
async function mount(sessionId: string, api: ProbeApi): Promise<void> {
|
|
148
|
-
container ??= document.body.appendChild(document.createElement('div'));
|
|
149
|
-
await act(async () => {
|
|
150
|
-
render(h(Probe, { sessionId, api }), container as HTMLElement);
|
|
151
|
-
});
|
|
152
|
-
await act(async () => {});
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/** Land the registered fetch, as the server's response would. */
|
|
156
|
-
async function resolveLoad(messages: unknown[], index = 0): Promise<void> {
|
|
157
|
-
const gate = gates[index];
|
|
158
|
-
if (!gate) throw new Error('no session fetch was registered');
|
|
159
|
-
await act(async () => {
|
|
160
|
-
gate.resolve({ session: { id: 'sess-1', messages }, hasMore: false, oldestIndex: 0 });
|
|
161
|
-
});
|
|
162
|
-
for (let i = 0; i < 3; i += 1) await act(async () => {});
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Re-render the mounted Probe with a new session id, which is what the spawn
|
|
167
|
-
* adoption (`new-…` → UUID) does when the URL flips while the child streams:
|
|
168
|
-
* the session-load effect re-runs and ISSUES its history fetch at that moment,
|
|
169
|
-
* mid-run. The fetch registered before this call is `gates[0]`; the one this
|
|
170
|
-
* issues is `gates[1]`.
|
|
171
|
-
*/
|
|
172
|
-
async function switchSession(sessionId: string, api: ProbeApi): Promise<void> {
|
|
173
|
-
await act(async () => {
|
|
174
|
-
render(h(Probe, { sessionId, api }), container as HTMLElement);
|
|
175
|
-
});
|
|
176
|
-
await act(async () => {});
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/** A run this page started, with its optimistic rows already on screen. */
|
|
180
|
-
async function startLiveRun(api: ProbeApi): Promise<void> {
|
|
181
|
-
await act(async () => {
|
|
182
|
-
api.isGeneratingRef.current = true;
|
|
183
|
-
api.ownRunRef.current = true;
|
|
184
|
-
api.optimisticUserIdRef.current = OPTIMISTIC_USER.id;
|
|
185
|
-
api.setMessages([OPTIMISTIC_USER, AI_PLACEHOLDER]);
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const ids = (api: ProbeApi) => api.messages.map((m) => m.id);
|
|
32
|
+
import {
|
|
33
|
+
AI_PLACEHOLDER,
|
|
34
|
+
ECHOED_TURN,
|
|
35
|
+
OPTIMISTIC_USER,
|
|
36
|
+
ids,
|
|
37
|
+
makeApi,
|
|
38
|
+
mount,
|
|
39
|
+
resolveLoad,
|
|
40
|
+
gates,
|
|
41
|
+
startLiveRun,
|
|
42
|
+
stubChatFetch,
|
|
43
|
+
switchSession,
|
|
44
|
+
} from '@/client/hooks/chat/timeline/session-load.test-util';
|
|
190
45
|
|
|
191
46
|
describe('a committed fetch while a run this page started is live', () => {
|
|
192
47
|
test('keeps the live timeline when the payload carries the turn under its own id', async () => {
|
|
@@ -271,6 +126,50 @@ describe('a committed fetch while a run this page started is live', () => {
|
|
|
271
126
|
});
|
|
272
127
|
});
|
|
273
128
|
|
|
129
|
+
describe('a spawn that adopted its id after the run ended', () => {
|
|
130
|
+
/**
|
|
131
|
+
* The chat opened from a WORKSPACE (`?folderId=` and no `?sessionId=`), so the
|
|
132
|
+
* first send starts from a null session id and the URL flips to the child's
|
|
133
|
+
* UUID when the spawn answers. `isSpawnAdopt` only recognises a `new-…` →
|
|
134
|
+
* UUID move, and `isAdoptingInFlight` also required the run to still be
|
|
135
|
+
* generating — so a FAST child (the scripted one answers in milliseconds)
|
|
136
|
+
* finished before the URL flip, the reset branch ran, and the turn that had
|
|
137
|
+
* just rendered was cleared. Measured in CI on three specs at once
|
|
138
|
+
* (`rename.e2e.ts`, `editor.e2e.ts`'s drop case, `chat.e2e.ts`'s settle
|
|
139
|
+
* case): the answer was on screen and then the timeline was empty.
|
|
140
|
+
*/
|
|
141
|
+
test('keeps the optimistic rows when the adopted id arrives after the run settled', async () => {
|
|
142
|
+
stubChatFetch();
|
|
143
|
+
const api = makeApi();
|
|
144
|
+
await mount('', api);
|
|
145
|
+
// The send: optimistic rows mounted, the run owned by this page…
|
|
146
|
+
await act(async () => {
|
|
147
|
+
api.isGeneratingRef.current = true;
|
|
148
|
+
api.ownRunRef.current = true;
|
|
149
|
+
api.optimisticUserIdRef.current = OPTIMISTIC_USER.id;
|
|
150
|
+
api.setMessages([OPTIMISTIC_USER, AI_PLACEHOLDER]);
|
|
151
|
+
});
|
|
152
|
+
// …and it ENDED before the spawn's id reached the URL.
|
|
153
|
+
await act(async () => {
|
|
154
|
+
api.isGeneratingRef.current = false;
|
|
155
|
+
api.ownRunRef.current = false;
|
|
156
|
+
});
|
|
157
|
+
// The spawn answered: the send path records the id it adopted, and the URL
|
|
158
|
+
// moves to it.
|
|
159
|
+
api.adoptedSessionIdRef.current = 'sess-1';
|
|
160
|
+
|
|
161
|
+
await switchSession('sess-1', api);
|
|
162
|
+
// The fetch the URL flip issued answers with the transcript omp has not
|
|
163
|
+
// written yet — the second half of the same bug, and the one that emptied
|
|
164
|
+
// the timeline even once the reset was skipped.
|
|
165
|
+
// `gates[0]`: the mount was a null session, so this flip issued the FIRST
|
|
166
|
+
// read.
|
|
167
|
+
await resolveLoad([], 0);
|
|
168
|
+
|
|
169
|
+
expect(ids(api)).toEqual([OPTIMISTIC_USER.id, AI_PLACEHOLDER.id]);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
274
173
|
describe('a committed fetch with no local run', () => {
|
|
275
174
|
test('lands the transcript, which is the reload-mid-run and open-a-session path', async () => {
|
|
276
175
|
stubChatFetch();
|