@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
|
@@ -23,6 +23,8 @@ import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
|
|
23
23
|
import type { ChatMessageData } from '@/shared/types';
|
|
24
24
|
import { SESSION_META_RETRY_SCHEDULE_MS } from '@/shared/lib/workspace/refresh-cadence';
|
|
25
25
|
import { useSessionPagination } from '@/client/hooks/chat/timeline/session-pagination';
|
|
26
|
+
import { useLostRunRecovery } from '@/client/hooks/chat/timeline/lost-run';
|
|
27
|
+
import { isOwnSpawn, isSpawnArrival } from '@/client/hooks/chat/timeline/session-switch';
|
|
26
28
|
|
|
27
29
|
export interface SessionDataShape {
|
|
28
30
|
id?: string;
|
|
@@ -52,6 +54,12 @@ export interface UseSessionLoadDeps {
|
|
|
52
54
|
* UI sets `isGeneratingRef` too, and treating that as ownership discarded
|
|
53
55
|
* the whole transcript, leaving an empty timeline over a running session. */
|
|
54
56
|
ownRunRef: { current: boolean };
|
|
57
|
+
/** The stream has announced the run in flight (see `streamRunRef`). */
|
|
58
|
+
streamRunRef: { current: boolean };
|
|
59
|
+
/** The SERVER reports a run for this session (the `stream` row). Together
|
|
60
|
+
* with `streamRunRef` this is the evidence the reconnect recovery needs: a
|
|
61
|
+
* run whose FIRST frame was lost has no `agent_start` on this client. */
|
|
62
|
+
serverRunRef: { current: boolean };
|
|
55
63
|
/** Live AI placeholder ref: the row the streaming answer is filling. Read on
|
|
56
64
|
* a session switch (cleared with the optimistic user id) and by the stream
|
|
57
65
|
* callbacks; it is NOT the timeline-ownership signal — the placeholder is
|
|
@@ -60,6 +68,10 @@ export interface UseSessionLoadDeps {
|
|
|
60
68
|
/** Live optimistic user bubble ref: cleared together with the placeholder on
|
|
61
69
|
* a session switch so stale ids cannot survive into the next session. */
|
|
62
70
|
optimisticUserIdRef: { current: string | null };
|
|
71
|
+
/** Raw composer text of steer/follow-up turns awaiting omp's echo. Cleared
|
|
72
|
+
* when a lost run is settled, or a later echo could be re-labelled with an
|
|
73
|
+
* earlier message's text. */
|
|
74
|
+
pendingUserDisplaysRef: { current: { sent: string; display: string }[] };
|
|
63
75
|
/** Drop queued stream updates belonging to the previous session (coalescer). */
|
|
64
76
|
cancelStreamingCoalescer: () => void;
|
|
65
77
|
metaRefreshedRef: { current: string | null };
|
|
@@ -73,7 +85,7 @@ export interface UseSessionLoadDeps {
|
|
|
73
85
|
}
|
|
74
86
|
|
|
75
87
|
export function useSessionLoad(deps: UseSessionLoadDeps) {
|
|
76
|
-
const { sessionId, setLocalMessages, setGenerating, isGeneratingRef, ownRunRef, aiPlaceholderIdRef, optimisticUserIdRef, cancelStreamingCoalescer, metaRefreshedRef, scrollRef, jumpActiveRef, jumpCountRef } = deps;
|
|
88
|
+
const { sessionId, setLocalMessages, setGenerating, isGeneratingRef, ownRunRef, streamRunRef, serverRunRef, aiPlaceholderIdRef, optimisticUserIdRef, pendingUserDisplaysRef, cancelStreamingCoalescer, metaRefreshedRef, scrollRef, jumpActiveRef, jumpCountRef } = deps;
|
|
77
89
|
|
|
78
90
|
const [sessionData, setSessionData] = useState<SessionDataShape | null>(null);
|
|
79
91
|
const [sessionLoading, setSessionLoading] = useState(false);
|
|
@@ -163,19 +175,16 @@ export function useSessionLoad(deps: UseSessionLoadDeps) {
|
|
|
163
175
|
* A committed fetch is a LAGGING snapshot while a run is live. For a fresh
|
|
164
176
|
* spawn there may be no transcript on disk at all (omp buffers its writes
|
|
165
177
|
* until the first assistant message settles), and it never carries a segment
|
|
166
|
-
* still streaming. Applying one mid-run costs the operator their own turn
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
* (`msg-…-user` vs the echoed id), so the turn rendered twice.
|
|
178
|
+
* still streaming. Applying one mid-run costs the operator their own turn —
|
|
179
|
+
* measured: an empty payload emptied the list, and a payload carrying the
|
|
180
|
+
* file's own copy of that turn was merged UNDER the live rows, whose ids
|
|
181
|
+
* differ, so the turn rendered twice.
|
|
171
182
|
*
|
|
172
183
|
* Keyed on the RUN THIS PAGE STARTED, never on `isGeneratingRef` alone: the
|
|
173
|
-
* mount probe that resumes a live run
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
* into a list whose head was empty. A run adopted that way must still load
|
|
178
|
-
* its history; the stream resume continues from there. */
|
|
184
|
+
* mount probe that resumes a live run sets that flag too, so keying on it
|
|
185
|
+
* made OPENING a streaming session discard its whole committed transcript
|
|
186
|
+
* (measured: 0 rows, only a "Load earlier messages" button). An adopted run
|
|
187
|
+
* must still load its history; the resume continues from there. */
|
|
179
188
|
const timelineOwnedByLiveRun = useCallback(
|
|
180
189
|
() => ownRunRef.current && isGeneratingRef.current,
|
|
181
190
|
[isGeneratingRef, ownRunRef],
|
|
@@ -193,40 +202,57 @@ export function useSessionLoad(deps: UseSessionLoadDeps) {
|
|
|
193
202
|
[isGeneratingRef],
|
|
194
203
|
);
|
|
195
204
|
|
|
205
|
+
// A channel that comes back after the run's `message_end`/`agent_end` were
|
|
206
|
+
// lost with it: the timeline holds the stream's last partial segment and
|
|
207
|
+
// nothing else will ever finalize it (see `lost-run.ts`). A run that is
|
|
208
|
+
// STILL LIVE at reconnect is left alone here, but its lost segments are
|
|
209
|
+
// remembered and re-read when it settles.
|
|
210
|
+
const { reloadMessages, needsReloadRef } = useLostRunRecovery({
|
|
211
|
+
sessionId,
|
|
212
|
+
sessionIdRef,
|
|
213
|
+
adoptedSessionIdRef,
|
|
214
|
+
isGeneratingRef,
|
|
215
|
+
streamRunRef,
|
|
216
|
+
serverRunRef,
|
|
217
|
+
setGenerating,
|
|
218
|
+
setLocalMessages,
|
|
219
|
+
applyWindow,
|
|
220
|
+
aiPlaceholderIdRef,
|
|
221
|
+
optimisticUserIdRef,
|
|
222
|
+
pendingUserDisplaysRef,
|
|
223
|
+
cancelStreamingCoalescer,
|
|
224
|
+
});
|
|
225
|
+
|
|
196
226
|
// Fetch session messages and details from API
|
|
197
227
|
useEffect(() => {
|
|
198
228
|
let active = true;
|
|
199
229
|
setSessionLoading(true);
|
|
200
|
-
// Track the previous
|
|
201
|
-
//
|
|
202
|
-
// transition (new-… → real UUID) keeps its bubbles.
|
|
230
|
+
// Track the previous id so a real switch clears the timeline while a
|
|
231
|
+
// spawn's arrival keeps its bubbles (see `session-switch.ts`).
|
|
203
232
|
if (sessionId !== prevSessionIdRef.current) {
|
|
204
|
-
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
const
|
|
209
|
-
|
|
233
|
+
// A spawn's arrival keeps the optimistic rows; anything else is a switch
|
|
234
|
+
// to another session. The two shapes — `new-…` → UUID and a chat opened
|
|
235
|
+
// from a workspace whose URL moves straight to the child's UUID — and why
|
|
236
|
+
// the second one used to clear a finished turn are in `session-switch.ts`.
|
|
237
|
+
const spawnArrival = isSpawnArrival({
|
|
238
|
+
sessionId,
|
|
239
|
+
previousSessionId: prevSessionIdRef.current,
|
|
240
|
+
adoptedSessionId: adoptedSessionIdRef.current,
|
|
241
|
+
});
|
|
242
|
+
if (!spawnArrival) {
|
|
210
243
|
setLocalMessages([]);
|
|
211
244
|
setGenerating(false);
|
|
212
245
|
adoptedSessionIdRef.current = null;
|
|
213
246
|
seededModelRef.current = null;
|
|
214
|
-
// Metadata
|
|
215
|
-
// in place let a
|
|
216
|
-
// composer — display a level that session never ran with, until the
|
|
217
|
-
// fetch replaced it.
|
|
247
|
+
// Metadata and the thinking seed belong to ONE session: leaving either
|
|
248
|
+
// in place let a composer display a level its session never ran with.
|
|
218
249
|
setSessionData(null);
|
|
219
|
-
// The thinking seed belongs to the previous session's spawn; keeping it
|
|
220
|
-
// would report that level for the newly opened session.
|
|
221
250
|
seededThinkingLevelRef.current = null;
|
|
222
251
|
// The placeholder/optimistic ids belong to the PREVIOUS session's
|
|
223
|
-
// in-flight send
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
// the run ownership this page held — the guard reads `ownRunRef`, so a
|
|
228
|
-
// stale value there is what used to discard a mid-run session's
|
|
229
|
-
// committed history on return.)
|
|
252
|
+
// in-flight send, and `message_end` never arrives after a switch away.
|
|
253
|
+
// (`setGenerating(false)` above releases the run ownership this page
|
|
254
|
+
// held — the guard reads `ownRunRef`, so a stale value there is what
|
|
255
|
+
// discarded a mid-run session's committed history on return.)
|
|
230
256
|
aiPlaceholderIdRef.current = null;
|
|
231
257
|
optimisticUserIdRef.current = null;
|
|
232
258
|
// Drop coalesced message_update frames queued by the previous
|
|
@@ -259,6 +285,10 @@ export function useSessionLoad(deps: UseSessionLoadDeps) {
|
|
|
259
285
|
// means this payload is a lagging snapshot of a timeline the stream owns.
|
|
260
286
|
const ownedAtIssue = timelineOwnedByLiveRun();
|
|
261
287
|
const ownedByLiveRun = () => ownedAtIssue || timelineOwnedByLiveRun();
|
|
288
|
+
/** Its transcript may legitimately be missing — omp buffers its writes
|
|
289
|
+
* until the first assistant message settles — so an empty payload is
|
|
290
|
+
* "not written yet", never "nothing happened". */
|
|
291
|
+
const ownSpawn = () => isOwnSpawn(sessionId, adoptedSessionIdRef.current);
|
|
262
292
|
fetch(`/api/chat/${sessionId}`)
|
|
263
293
|
.then(res => res.json())
|
|
264
294
|
.then(data => {
|
|
@@ -273,13 +303,13 @@ export function useSessionLoad(deps: UseSessionLoadDeps) {
|
|
|
273
303
|
// operator's turn (see `timelineOwnedByLiveRun`).
|
|
274
304
|
if (!ownedByLiveRun()) {
|
|
275
305
|
if (fetched.length > 0) setLocalMessages(fetched);
|
|
276
|
-
//
|
|
277
|
-
else if (!
|
|
306
|
+
// An own spawn has nothing committed YET (see `isOwnSpawn`).
|
|
307
|
+
else if (!ownSpawn()) setLocalMessages([]);
|
|
278
308
|
}
|
|
279
309
|
applyWindow(data.hasMore, data.oldestIndex);
|
|
280
310
|
} else {
|
|
281
311
|
setSessionData(seededSessionData());
|
|
282
|
-
if (!ownedByLiveRun()) setLocalMessages([]);
|
|
312
|
+
if (!ownedByLiveRun() && !ownSpawn()) setLocalMessages([]);
|
|
283
313
|
}
|
|
284
314
|
})
|
|
285
315
|
.catch(err => {
|
|
@@ -313,5 +343,7 @@ export function useSessionLoad(deps: UseSessionLoadDeps) {
|
|
|
313
343
|
adoptedSessionIdRef,
|
|
314
344
|
refreshSessionMeta,
|
|
315
345
|
seedSession,
|
|
346
|
+
reloadMessages,
|
|
347
|
+
needsReloadRef,
|
|
316
348
|
};
|
|
317
349
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `isSpawnArrival` / `isOwnSpawn` — the two questions a session-id change asks.
|
|
8
|
+
*
|
|
9
|
+
* Both are pure so every ordering can be stated directly. The orderings are the
|
|
10
|
+
* point: the browser is where they were found (three CI specs at once showed an
|
|
11
|
+
* answer on screen and then an empty timeline), but reproducing a child that
|
|
12
|
+
* answers before the URL flips is not something a browser spec can be asked to
|
|
13
|
+
* do on demand.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, expect, test } from 'bun:test';
|
|
17
|
+
import { isOwnSpawn, isSpawnArrival } from '@/client/hooks/chat/timeline/session-switch';
|
|
18
|
+
|
|
19
|
+
describe('isSpawnArrival', () => {
|
|
20
|
+
test('the id the send path recorded is an arrival, whatever the previous id was', () => {
|
|
21
|
+
// A chat opened from a WORKSPACE: no `?sessionId=` at all, so the previous
|
|
22
|
+
// id is null and the URL moves straight to the child's UUID. This is the
|
|
23
|
+
// shape that used to clear a finished turn.
|
|
24
|
+
expect(
|
|
25
|
+
isSpawnArrival({ sessionId: 'uuid-1', previousSessionId: null, adoptedSessionId: 'uuid-1' }),
|
|
26
|
+
).toBe(true);
|
|
27
|
+
// A pending chat, mid-run or after it: same fact.
|
|
28
|
+
expect(
|
|
29
|
+
isSpawnArrival({ sessionId: 'uuid-1', previousSessionId: 'new-9', adoptedSessionId: 'uuid-1' }),
|
|
30
|
+
).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('`new-…` → UUID is still an arrival when the record was already consumed', () => {
|
|
34
|
+
// The pending store hands the adoption to the session-state provider once,
|
|
35
|
+
// so the URL's own shape is the only evidence left.
|
|
36
|
+
expect(
|
|
37
|
+
isSpawnArrival({ sessionId: 'uuid-1', previousSessionId: 'new-9', adoptedSessionId: null }),
|
|
38
|
+
).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('opening another session is not an arrival', () => {
|
|
42
|
+
expect(
|
|
43
|
+
isSpawnArrival({ sessionId: 'uuid-2', previousSessionId: 'uuid-1', adoptedSessionId: 'uuid-1' }),
|
|
44
|
+
).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('a null or unchanged id is not an arrival', () => {
|
|
48
|
+
expect(isSpawnArrival({ sessionId: null, previousSessionId: 'uuid-1', adoptedSessionId: null })).toBe(false);
|
|
49
|
+
expect(
|
|
50
|
+
isSpawnArrival({ sessionId: 'uuid-1', previousSessionId: 'uuid-1', adoptedSessionId: 'uuid-1' }),
|
|
51
|
+
).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('isOwnSpawn', () => {
|
|
56
|
+
test('a pending id and the adopted UUID are both own spawns', () => {
|
|
57
|
+
expect(isOwnSpawn('new-9', null)).toBe(true);
|
|
58
|
+
expect(isOwnSpawn('uuid-1', 'uuid-1')).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('a session this page did not spawn is not one', () => {
|
|
62
|
+
expect(isOwnSpawn('uuid-2', 'uuid-1')).toBe(false);
|
|
63
|
+
expect(isOwnSpawn(null, 'uuid-1')).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Which rows a session-switch keeps, and which a committed payload may replace.
|
|
8
|
+
*
|
|
9
|
+
* Split out of `session-load.ts` for the file-size ceiling, and it is one
|
|
10
|
+
* subject rather than an arbitrary cut: both functions answer "does this arrival
|
|
11
|
+
* belong to the session the timeline is already showing?".
|
|
12
|
+
*
|
|
13
|
+
* The distinction they encode was paid for twice, in opposite directions.
|
|
14
|
+
*
|
|
15
|
+
* - **A spawn's arrival is not a session switch.** The optimistic bubbles are
|
|
16
|
+
* the operator's own turn and the row the stream is filling; clearing them
|
|
17
|
+
* deleted a turn that had already rendered. Two shapes reach here and only one
|
|
18
|
+
* was ever recognised: `new-…` → UUID, and a chat opened from a WORKSPACE
|
|
19
|
+
* (`?folderId=` and no `?sessionId=`), whose previous id is `null` and whose
|
|
20
|
+
* URL moves straight to the child's UUID. The second was keyed on the
|
|
21
|
+
* generating flag, so a FAST child — the scripted one answers in milliseconds
|
|
22
|
+
* — finished before the URL flipped and the reset ran anyway. Measured in CI
|
|
23
|
+
* on three specs at once: the answer was on screen, then the timeline was
|
|
24
|
+
* empty. The id the send path recorded is the fact that survives either
|
|
25
|
+
* ordering.
|
|
26
|
+
* - **A committed payload is a lagging snapshot** while a run this page started
|
|
27
|
+
* owns the rows, and an own spawn's transcript may legitimately be empty (omp
|
|
28
|
+
* buffers its writes until the first assistant message settles). Either way it
|
|
29
|
+
* must not empty the timeline.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/** The ids a session-switch decision needs, all of them refs. */
|
|
33
|
+
export interface SessionSwitchFacts {
|
|
34
|
+
sessionId: string | null;
|
|
35
|
+
previousSessionId: string | null;
|
|
36
|
+
/** The id a spawn answered with, recorded by the send path. */
|
|
37
|
+
adoptedSessionId: string | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Whether this change is a spawn's arrival rather than the user opening another
|
|
42
|
+
* session — in which case the optimistic rows stay.
|
|
43
|
+
*/
|
|
44
|
+
export function isSpawnArrival({ sessionId, previousSessionId, adoptedSessionId }: SessionSwitchFacts): boolean {
|
|
45
|
+
if (!sessionId) return false;
|
|
46
|
+
// The recorded id is checked FIRST and covers both orderings: it is written
|
|
47
|
+
// before the URL moves, so a child that answered mid-run and one that answered
|
|
48
|
+
// after the run ended are the same fact here.
|
|
49
|
+
if (adoptedSessionId === sessionId) return true;
|
|
50
|
+
// `new-…` → UUID. Kept as a fallback for a spawn whose record was already
|
|
51
|
+
// consumed (the pending store hands the adoption to the session-state
|
|
52
|
+
// provider once), where the URL's own shape is the only evidence left.
|
|
53
|
+
return previousSessionId?.startsWith('new-') === true && !sessionId.startsWith('new-');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Whether `sessionId` is one THIS PAGE spawned, so an empty committed payload
|
|
58
|
+
* means "not written yet" rather than "nothing happened".
|
|
59
|
+
*/
|
|
60
|
+
export function isOwnSpawn(sessionId: string | null, adoptedSessionId: string | null): boolean {
|
|
61
|
+
if (!sessionId) return false;
|
|
62
|
+
return sessionId.startsWith('new-') || adoptedSessionId === sessionId;
|
|
63
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The worktree a pending chat picked, as a ref the SEND path can read.
|
|
8
|
+
*
|
|
9
|
+
* The composer is remounted by a stretch, a tab switch and a reload, while the
|
|
10
|
+
* send handler is created once per deps record — so a pick read from render
|
|
11
|
+
* state there would be missed, which is the one outcome the pick exists to
|
|
12
|
+
* prevent. The persisted copy is `chat.spawnTarget` (it survives a reload, and
|
|
13
|
+
* a second tab sees it); the ref is its render-visible mirror.
|
|
14
|
+
*
|
|
15
|
+
* Same shape as the waiting pick's ref (`waiting/choice.ts`), for the same
|
|
16
|
+
* reason: a value written before the send and read at send time cannot travel
|
|
17
|
+
* through props that rebuild.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { useEffect, useRef } from 'preact/hooks';
|
|
21
|
+
import { useSessionState } from '@/client/hooks/workspace/session-state/index';
|
|
22
|
+
|
|
23
|
+
/** Where the chat that has not started will run. */
|
|
24
|
+
export interface SpawnTarget {
|
|
25
|
+
folderId: number | null;
|
|
26
|
+
/** Absolute worktree path, or null for the folder's own checkout. */
|
|
27
|
+
worktreePath: string | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** `chat.spawnTarget` — the persisted pick, shared by both layouts' composers. */
|
|
31
|
+
export const SPAWN_TARGET_KEY = 'chat.spawnTarget';
|
|
32
|
+
|
|
33
|
+
export interface SpawnTargetBinding {
|
|
34
|
+
/** The pick, for a control that draws it (the composer's target picker). */
|
|
35
|
+
target: SpawnTarget | null;
|
|
36
|
+
setTarget: (next: SpawnTarget | null) => void;
|
|
37
|
+
/** The send path's view: set before the request goes out. */
|
|
38
|
+
worktreePathRef: { current: string | null };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function useSpawnTarget(sessionId: string | null): SpawnTargetBinding {
|
|
42
|
+
const [target, setTarget] = useSessionState<SpawnTarget | null>(SPAWN_TARGET_KEY, null);
|
|
43
|
+
const worktreePathRef = useRef<string | null>(null);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
// A pick for ANOTHER session must not leak into this one: the store is keyed
|
|
47
|
+
// by session, so a change of session id re-reads and this effect re-mirrors.
|
|
48
|
+
worktreePathRef.current = target?.worktreePath ?? null;
|
|
49
|
+
}, [target, sessionId]);
|
|
50
|
+
|
|
51
|
+
return { target, setTarget, worktreePathRef };
|
|
52
|
+
}
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { afterAll, beforeAll, beforeEach, describe, expect, test } from 'bun:test';
|
|
21
21
|
import { Window } from 'happy-dom';
|
|
22
22
|
import { h, render } from 'preact';
|
|
23
|
+
import { useState } from 'preact/hooks';
|
|
23
24
|
import { act } from 'preact/test-utils';
|
|
24
25
|
import { useToolOpenState, TOOL_OPEN_STATE_KEY, type ToolOpenState } from '@/client/hooks/chat/timeline/tool-open';
|
|
25
26
|
import { SessionStateProvider } from '@/client/components/common/session-state-provider';
|
|
@@ -139,3 +140,76 @@ describe('useToolOpenState', () => {
|
|
|
139
140
|
expect(remounted.current?.openMap.a).toBe(true);
|
|
140
141
|
});
|
|
141
142
|
});
|
|
143
|
+
|
|
144
|
+
describe('useToolOpenState — a streaming eval opens itself', () => {
|
|
145
|
+
/**
|
|
146
|
+
* Mount a probe whose tools can be REPLACED, so a status transition is
|
|
147
|
+
* observable: the reported bug is about a card that was open for the run and
|
|
148
|
+
* must not snap shut when the call returns.
|
|
149
|
+
*/
|
|
150
|
+
async function mountMutable(initial: ToolCallData[]) {
|
|
151
|
+
const api: { current: ToolOpenState | null; setTools: (next: ToolCallData[]) => void } = {
|
|
152
|
+
current: null,
|
|
153
|
+
setTools: () => {},
|
|
154
|
+
};
|
|
155
|
+
function Probe() {
|
|
156
|
+
const [tools, setTools] = useState(initial);
|
|
157
|
+
api.setTools = setTools;
|
|
158
|
+
api.current = useToolOpenState(tools, false);
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
container = document.createElement('div');
|
|
162
|
+
document.body.appendChild(container);
|
|
163
|
+
await act(async () => {
|
|
164
|
+
render(h(SessionStateProvider, { sessionId: SESSION, children: h(Probe, {}) }), container);
|
|
165
|
+
});
|
|
166
|
+
await act(async () => {
|
|
167
|
+
for (let i = 0; i < 8; i++) await Promise.resolve();
|
|
168
|
+
});
|
|
169
|
+
return api;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const evalTool = (partial: Partial<ToolCallData> = {}) =>
|
|
173
|
+
tool('e1', { type: 'eval', name: 'eval', title: 'Counting lines', ...partial });
|
|
174
|
+
|
|
175
|
+
test('a running eval is open, so its streamed output is visible', async () => {
|
|
176
|
+
const api = await mountMutable([evalTool({ status: 'running' })]);
|
|
177
|
+
expect(api.current?.openMap.e1).toBe(true);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('it STAYS open once the call settles', async () => {
|
|
181
|
+
const api = await mountMutable([evalTool({ status: 'running' })]);
|
|
182
|
+
await act(async () => {
|
|
183
|
+
api.setTools([evalTool({ status: 'success', output: 'line 1' })]);
|
|
184
|
+
});
|
|
185
|
+
// Re-deriving from `status` alone closed the card at the exact moment the
|
|
186
|
+
// reader wants the result — the `wait` lesson, restated for eval.
|
|
187
|
+
expect(api.current?.openMap.e1).toBe(true);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('a settled eval from a reloaded transcript stays closed', async () => {
|
|
191
|
+
// History is not a live run: opening every past eval would bury the
|
|
192
|
+
// conversation under scripts nobody asked to see again.
|
|
193
|
+
const api = await mountMutable([evalTool({ status: 'success', output: 'line 1' })]);
|
|
194
|
+
expect(api.current?.openMap.e1).toBe(false);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('a running bash is still folded', async () => {
|
|
198
|
+
// The auto-open is for the card whose body IS the answer, not for every
|
|
199
|
+
// streaming call — a bash log belongs folded.
|
|
200
|
+
const api = await mountMutable([tool('b1', { status: 'running' })]);
|
|
201
|
+
expect(api.current?.openMap.b1).toBe(false);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('the reader can still close a running eval, and it stays closed', async () => {
|
|
205
|
+
const api = await mountMutable([evalTool({ status: 'running' })]);
|
|
206
|
+
await act(async () => {
|
|
207
|
+
api.current?.toggle('e1');
|
|
208
|
+
});
|
|
209
|
+
expect(api.current?.openMap.e1).toBe(false);
|
|
210
|
+
await act(async () => {
|
|
211
|
+
api.setTools([evalTool({ status: 'success', output: 'line 1' })]);
|
|
212
|
+
});
|
|
213
|
+
expect(api.current?.openMap.e1).toBe(false);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* session blob.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import { useCallback, useMemo } from 'preact/hooks';
|
|
21
|
+
import { useCallback, useEffect, useMemo, useRef } from 'preact/hooks';
|
|
22
22
|
import { useSessionState } from '@/client/hooks/workspace/session-state';
|
|
23
23
|
import { isSkippedTool } from '@/shared/lib/chat/tool-status';
|
|
24
24
|
import type { ToolCallData } from '@/shared/types/chat';
|
|
@@ -42,6 +42,22 @@ export const TOOL_OPEN_STATE_KEY = 'chat.toolOpen';
|
|
|
42
42
|
* belongs folded, while a wait's is three short rows.
|
|
43
43
|
*
|
|
44
44
|
* A stored choice still wins, so closing it stays closed.
|
|
45
|
+
*
|
|
46
|
+
* An `eval` is the second such card, and it is the one a reader reported: a
|
|
47
|
+
* script cell streams its stdout as it runs, and a collapsed card showed
|
|
48
|
+
* `Running` with the output invisible for the whole call — the panel had drawn
|
|
49
|
+
* it all along, folded away, and only expanding by hand (or waiting for the
|
|
50
|
+
* call to return) revealed it. It differs from `bash` the same way `wait` does:
|
|
51
|
+
* an eval's output IS the answer, not a log to consult afterwards, and the
|
|
52
|
+
* reader is watching it arrive.
|
|
53
|
+
*
|
|
54
|
+
* A card that was OPENED because it was running stays open once it settles —
|
|
55
|
+
* `watchedIds` records that, in memory only. Collapsing at the settle would
|
|
56
|
+
* take the output away at the exact moment the reader wants to read it (the
|
|
57
|
+
* `wait` lesson above), while re-deriving from `status` alone would do exactly
|
|
58
|
+
* that. The record is deliberately not persisted: a reloaded transcript is
|
|
59
|
+
* history, and every historical eval reopening itself would bury the
|
|
60
|
+
* conversation under scripts nobody asked to see again.
|
|
45
61
|
*/
|
|
46
62
|
function defaultOpen(tool: ToolCallData, autoOpenFirst: boolean, index: number): boolean {
|
|
47
63
|
if (isSkippedTool(tool)) return false;
|
|
@@ -50,6 +66,12 @@ function defaultOpen(tool: ToolCallData, autoOpenFirst: boolean, index: number):
|
|
|
50
66
|
return autoOpenFirst && index === 0;
|
|
51
67
|
}
|
|
52
68
|
|
|
69
|
+
/** True while this call is a script cell that is still producing output. */
|
|
70
|
+
function isStreamingEval(tool: ToolCallData): boolean {
|
|
71
|
+
if (tool.status !== 'running') return false;
|
|
72
|
+
return (tool.name || tool.type) === 'eval';
|
|
73
|
+
}
|
|
74
|
+
|
|
53
75
|
export interface ToolOpenState {
|
|
54
76
|
/** Resolved openness per tool id — stored choice first, default second. */
|
|
55
77
|
openMap: Record<string, boolean>;
|
|
@@ -59,28 +81,50 @@ export interface ToolOpenState {
|
|
|
59
81
|
|
|
60
82
|
export function useToolOpenState(tools: ToolCallData[], autoOpenFirst: boolean): ToolOpenState {
|
|
61
83
|
const [stored, setStored] = useSessionState<Record<string, boolean>>(TOOL_OPEN_STATE_KEY, {});
|
|
84
|
+
/** Ids opened BECAUSE they were streaming; see `defaultOpen`'s eval note. */
|
|
85
|
+
const watchedRef = useRef<Set<string>>(new Set());
|
|
86
|
+
|
|
87
|
+
const streamingEvalIds = useMemo(
|
|
88
|
+
() => tools.filter(isStreamingEval).map((tool) => tool.id),
|
|
89
|
+
[tools],
|
|
90
|
+
);
|
|
91
|
+
// Recorded after the render that opened them, so the open survives the call
|
|
92
|
+
// settling. In memory only: a reloaded transcript is history.
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
for (const id of streamingEvalIds) watchedRef.current.add(id);
|
|
95
|
+
}, [streamingEvalIds]);
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The default a card is RENDERING, before any stored choice. One function for
|
|
99
|
+
* the map and for `toggle`, because a toggle that flips a different default
|
|
100
|
+
* than the one on screen takes two clicks to close an auto-opened card — the
|
|
101
|
+
* streaming eval made that reachable, since its default now depends on more
|
|
102
|
+
* than `status`.
|
|
103
|
+
*/
|
|
104
|
+
const renderedDefault = useCallback(
|
|
105
|
+
(tool: ToolCallData, index: number): boolean =>
|
|
106
|
+
isStreamingEval(tool) || watchedRef.current.has(tool.id) || defaultOpen(tool, autoOpenFirst, index),
|
|
107
|
+
[autoOpenFirst],
|
|
108
|
+
);
|
|
62
109
|
|
|
63
110
|
const openMap = useMemo(() => {
|
|
64
111
|
const map: Record<string, boolean> = {};
|
|
65
112
|
tools.forEach((tool, index) => {
|
|
66
113
|
const explicit = stored[tool.id];
|
|
67
|
-
map[tool.id] = explicit === undefined ?
|
|
114
|
+
map[tool.id] = explicit === undefined ? renderedDefault(tool, index) : explicit;
|
|
68
115
|
});
|
|
69
116
|
return map;
|
|
70
|
-
}, [tools, stored,
|
|
117
|
+
}, [tools, stored, renderedDefault]);
|
|
71
118
|
|
|
72
119
|
const toggle = useCallback(
|
|
73
120
|
(toolId: string) => {
|
|
74
121
|
setStored((prev) => {
|
|
75
122
|
const tool = tools.find((entry) => entry.id === toolId);
|
|
76
|
-
|
|
77
|
-
// not an absent key — otherwise an auto-opened error would need two
|
|
78
|
-
// clicks to close.
|
|
79
|
-
const current = prev[toolId] ?? (tool ? defaultOpen(tool, autoOpenFirst, tools.indexOf(tool)) : false);
|
|
123
|
+
const current = prev[toolId] ?? (tool ? renderedDefault(tool, tools.indexOf(tool)) : false);
|
|
80
124
|
return { ...prev, [toolId]: !current };
|
|
81
125
|
});
|
|
82
126
|
},
|
|
83
|
-
[setStored, tools,
|
|
127
|
+
[setStored, tools, renderedDefault],
|
|
84
128
|
);
|
|
85
129
|
|
|
86
130
|
return { openMap, toggle };
|