@kabmo-labs/kabmo 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/client/index.html +1 -1
- package/dist/client/index.js +403 -384
- package/dist/client/static/js/{index-0wvbca0b.js → index-bpptfkz2.js} +905 -875
- package/dist/client/static/js/index-k315p1e8.css +1 -0
- package/package.json +10 -1
- package/src/client/components/common/code-surface/documents.test.ts +120 -0
- package/src/client/components/common/code-surface/documents.ts +201 -0
- package/src/client/components/common/code-surface/extensions.test.ts +110 -0
- package/src/client/components/common/code-surface/extensions.ts +162 -0
- package/src/client/components/common/code-surface/handle.ts +102 -0
- package/src/client/components/common/code-surface/index.tsx +253 -314
- package/src/client/components/common/code-surface/keymap.test.ts +133 -0
- package/src/client/components/common/code-surface/keymap.ts +267 -0
- package/src/client/components/common/code-surface/marks.ts +74 -0
- package/src/client/components/common/code-surface/shiki.ts +234 -0
- package/src/client/components/common/code-surface/sync.test.ts +185 -0
- package/src/client/components/common/code-surface/sync.ts +58 -0
- package/src/client/components/common/code-surface/theme.test.ts +202 -0
- package/src/client/components/common/code-surface/theme.ts +111 -0
- package/src/client/components/common/editor-view-buttons/index.test.tsx +148 -0
- package/src/client/components/common/editor-view-buttons/index.tsx +109 -0
- package/src/client/components/common/header-controls/index.tsx +24 -1
- package/src/client/components/common/header-controls/shell-chrome.test.tsx +272 -0
- package/src/client/components/common/shell-update-button/Modal.tsx +163 -0
- package/src/client/components/common/shell-update-button/index.test.tsx +251 -0
- package/src/client/components/common/shell-update-button/index.tsx +125 -0
- package/src/client/components/common/toolbar-row/index.test.tsx +166 -0
- package/src/client/components/common/toolbar-row/index.tsx +189 -0
- package/src/client/components/common/workspace-options-menu/index.tsx +19 -1
- package/src/client/components/layout/desktop-layout/TopNavbar.tsx +5 -6
- package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +41 -78
- package/src/client/components/layout/desktop-layout/index.tsx +55 -69
- package/src/client/components/layout/desktop-layout/resizer/index.tsx +6 -3
- package/src/client/components/layout/session-sidebar/CategoryHeader.tsx +24 -0
- package/src/client/components/layout/session-sidebar/CategoryHeader.worktree.test.tsx +126 -0
- package/src/client/components/layout/session-sidebar/CategoryItem.tsx +4 -0
- package/src/client/components/layout/session-sidebar/Footer.tsx +8 -15
- package/src/client/components/layout/session-sidebar/SessionItem.tsx +12 -0
- package/src/client/components/layout/session-sidebar/SessionList.tsx +4 -0
- package/src/client/components/layout/session-sidebar/SessionsView.tsx +24 -0
- package/src/client/components/layout/session-sidebar/WorktreeBadge.test.ts +48 -0
- package/src/client/components/layout/session-sidebar/WorktreeBadge.tsx +55 -0
- package/src/client/components/mobile/LayoutWrapper.tsx +23 -16
- package/src/client/components/mobile/RightSidebar.tsx +21 -10
- package/src/client/components/mobile/SessionSidebar.tsx +8 -0
- package/src/client/components/mobile/header-titlebar.test.tsx +172 -0
- package/src/client/components/mobile/mobile-main-view/Header.tsx +10 -1
- package/src/client/components/mobile/mobile-right-sidebar/FullDiff.tsx +3 -0
- package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +63 -38
- package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.test.tsx +137 -0
- package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.tsx +14 -3
- package/src/client/components/mobile/mobile-session-sidebar/Footer.tsx +9 -21
- package/src/client/components/mobile/mobile-session-sidebar/Header.tsx +6 -2
- package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +16 -0
- package/src/client/components/mobile/mobile-session-sidebar/List.tsx +4 -0
- package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +5 -0
- package/src/client/components/mobile/mobile-session-sidebar/WorktreeDialog.tsx +49 -0
- package/src/client/components/mobile/worktree-session.ts +51 -0
- package/src/client/components/settings/Modal.tsx +8 -0
- package/src/client/components/settings/categories/AppearanceSettings.tsx +3 -0
- package/src/client/components/settings/categories/ProjectSettings.tsx +22 -8
- package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.test.tsx +167 -0
- package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.tsx +184 -0
- package/src/client/components/settings/categories/project-settings/WorktreeSettings.tsx +146 -0
- package/src/client/components/settings/categories/project-settings/worktree-row.test.ts +83 -0
- package/src/client/components/settings/categories/project-settings/worktree-row.ts +47 -0
- package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +36 -55
- package/src/client/components/workspace/chat-timeline/PendingChatView.tsx +8 -0
- package/src/client/components/workspace/chat-timeline/SystemNotice.tsx +9 -4
- package/src/client/components/workspace/chat-timeline/TaskResultContent.tsx +9 -7
- package/src/client/components/workspace/chat-timeline/index.tsx +5 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.test.tsx +132 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.tsx +120 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.test.tsx +319 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.tsx +285 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/index.test.ts +2 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/hashline-patch/index.tsx +34 -29
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/AstEdit.tsx +11 -9
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Bash.tsx +10 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Edit.tsx +25 -8
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.test.tsx +146 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.tsx +32 -17
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.test.ts +114 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.tsx +111 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Read.tsx +7 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/SearchTool.tsx +9 -59
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ArtifactBody.tsx +2 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.test.ts +27 -28
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.tsx +39 -26
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ExcerptCode.tsx +8 -8
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/HighlightedCode.tsx +35 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/JsonCodeBlock.tsx +4 -4
- package/src/client/components/workspace/diff-panel/MergeDiffView.tsx +179 -0
- package/src/client/components/workspace/diff-panel/ThemedMergeDiffView.tsx +24 -0
- package/src/client/components/workspace/diff-panel/Toolbar.tsx +224 -186
- package/src/client/components/workspace/diff-panel/ToolbarSegments.tsx +98 -0
- package/src/client/components/workspace/diff-panel/index.tsx +76 -145
- package/src/client/components/workspace/diff-panel/merge.ts +315 -0
- package/src/client/components/workspace/editor/Body.tsx +4 -7
- package/src/client/components/workspace/editor/CommandPalette.tsx +2 -2
- package/src/client/components/workspace/editor/FileDocument.tsx +58 -63
- package/src/client/components/workspace/editor/FilePane.tsx +4 -7
- package/src/client/components/workspace/editor/FindWidget.tsx +7 -2
- package/src/client/components/workspace/editor/Toolbar.tsx +220 -123
- package/src/client/components/workspace/editor/index.tsx +7 -9
- package/src/client/components/workspace/file-explorer/ContextMenu.tsx +2 -1
- package/src/client/components/workspace/file-explorer/Modals.tsx +40 -12
- package/src/client/components/workspace/file-explorer/OutlineSection.test.tsx +18 -0
- package/src/client/components/workspace/file-explorer/OutlineSection.tsx +10 -3
- package/src/client/components/workspace/file-explorer/RootMenu.tsx +14 -5
- package/src/client/components/workspace/file-explorer/Toolbar.tsx +74 -0
- package/src/client/components/workspace/file-explorer/TreeItem.tsx +38 -3
- package/src/client/components/workspace/file-explorer/filter.ts +47 -0
- package/src/client/components/workspace/file-explorer/index.tsx +77 -64
- package/src/client/components/workspace/file-explorer/local-edit.test.ts +135 -0
- package/src/client/components/workspace/search-panel/Body.tsx +1 -1
- package/src/client/components/workspace/worktree-modal/index.test.tsx +118 -0
- package/src/client/components/workspace/worktree-modal/index.tsx +261 -0
- package/src/client/hooks/chat/omp/reconnect.test.ts +338 -0
- package/src/client/hooks/chat/omp/reconnect.ts +187 -0
- package/src/client/hooks/chat/omp/run-liveness.test.ts +71 -0
- package/src/client/hooks/chat/omp/run-liveness.ts +65 -0
- package/src/client/hooks/chat/omp/session-list.activity.test.ts +114 -0
- package/src/client/hooks/chat/omp/session-list.test-util.ts +164 -0
- package/src/client/hooks/chat/omp/session-list.test.ts +30 -150
- package/src/client/hooks/chat/omp/session-list.tsx +69 -9
- package/src/client/hooks/chat/timeline/generating.ts +19 -2
- package/src/client/hooks/chat/timeline/index.ts +27 -28
- package/src/client/hooks/chat/timeline/lost-run.test.ts +329 -0
- package/src/client/hooks/chat/timeline/lost-run.ts +177 -0
- package/src/client/hooks/chat/timeline/send-execute.test.ts +2 -0
- package/src/client/hooks/chat/timeline/send-steer.test.ts +2 -0
- package/src/client/hooks/chat/timeline/send.test.ts +2 -0
- package/src/client/hooks/chat/timeline/send.ts +11 -1
- package/src/client/hooks/chat/timeline/session-load.test-util.ts +205 -0
- package/src/client/hooks/chat/timeline/session-load.test.ts +61 -162
- package/src/client/hooks/chat/timeline/session-load.ts +69 -37
- package/src/client/hooks/chat/timeline/session-switch.test.ts +65 -0
- package/src/client/hooks/chat/timeline/session-switch.ts +63 -0
- package/src/client/hooks/chat/timeline/spawn-target.ts +52 -0
- package/src/client/hooks/chat/timeline/tool-open.test.ts +74 -0
- package/src/client/hooks/chat/timeline/tool-open.ts +52 -8
- package/src/client/hooks/code/deferred-highlight.test.ts +182 -0
- package/src/client/hooks/code/deferred-highlight.ts +129 -0
- package/src/client/hooks/editor/find-state.ts +1 -1
- package/src/client/hooks/editor/theme.test.ts +206 -0
- package/src/client/hooks/editor/theme.ts +82 -0
- package/src/client/hooks/editor/use-editor-drop.test.ts +101 -0
- package/src/client/hooks/editor/use-editor-drop.ts +67 -14
- package/src/client/hooks/editor/use-editor-find.ts +7 -7
- package/src/client/hooks/editor/use-outline-view.test.ts +182 -0
- package/src/client/hooks/editor/use-outline-view.ts +94 -7
- package/src/client/hooks/editor/use-scroll-restore.test.ts +55 -2
- package/src/client/hooks/editor/use-scroll-restore.ts +124 -4
- package/src/client/hooks/editor/use-search-reveal.ts +1 -1
- package/src/client/hooks/ui/long-press.test.ts +174 -0
- package/src/client/hooks/ui/long-press.ts +112 -0
- package/src/client/hooks/ui/settings-modal.ts +56 -0
- package/src/client/hooks/ui/updates/shell-download.test.ts +75 -0
- package/src/client/hooks/ui/updates/shell-download.ts +153 -28
- package/src/client/hooks/ui/updates/shell-offer.test.ts +213 -0
- package/src/client/hooks/ui/updates/shell-update.test.ts +140 -0
- package/src/client/hooks/ui/updates/shell-update.ts +119 -0
- package/src/client/hooks/workspace/diff-file.ts +29 -10
- package/src/client/hooks/workspace/diff-git-actions.ts +147 -0
- package/src/client/hooks/workspace/editor-view.ts +61 -0
- package/src/client/hooks/workspace/file-create.ts +41 -11
- package/src/client/hooks/workspace/file-listing.ts +56 -1
- package/src/client/hooks/workspace/file-tabs.test.ts +62 -5
- package/src/client/hooks/workspace/file-tabs.ts +24 -11
- package/src/client/hooks/workspace/file-tree-actions.ts +37 -5
- package/src/client/hooks/workspace/navbar-session-actions.ts +72 -0
- package/src/client/hooks/workspace/worktree/index.ts +165 -0
- package/src/client/lib/desktop/bridge.ts +45 -0
- package/src/client/lib/search-reveal.ts +1 -1
- package/src/client/main.tsx +5 -1
- package/src/client/tailwind.css +56 -8
- package/src/server/index.ts +7 -2
- package/src/server/lib/auth/guard.test.ts +28 -0
- package/src/server/lib/auth/guard.ts +10 -0
- package/src/server/lib/bundler/titlebar-css.test.ts +42 -1
- package/src/server/lib/db/schema.ts +4 -0
- package/src/server/lib/db/worktree-registry.server.ts +91 -0
- package/src/server/lib/fs/canonical.ts +52 -0
- package/src/server/lib/fs/relative-path.ts +48 -0
- package/src/server/lib/fs/root-worktree.test.ts +116 -0
- package/src/server/lib/fs/root.ts +39 -10
- package/src/server/lib/fs/worktree/carry.ts +134 -0
- package/src/server/lib/fs/worktree/create.test.ts +249 -0
- package/src/server/lib/fs/worktree/create.ts +262 -0
- package/src/server/lib/fs/worktree/errors.ts +43 -0
- package/src/server/lib/fs/worktree/list.test.ts +207 -0
- package/src/server/lib/fs/worktree/list.ts +142 -0
- package/src/server/lib/fs/worktree/location.test.ts +206 -0
- package/src/server/lib/fs/worktree/location.ts +114 -0
- package/src/server/lib/fs/worktree/project-id.ts +53 -0
- package/src/server/lib/fs/worktree/remove.test.ts +242 -0
- package/src/server/lib/fs/worktree/remove.ts +152 -0
- package/src/server/lib/fs/worktree/settings.server.ts +56 -0
- package/src/server/lib/fs/worktree/topology.ts +142 -0
- package/src/server/lib/lifecycle/claim-port.test.ts +156 -0
- package/src/server/lib/lifecycle/port-guard.test.ts +20 -164
- package/src/server/lib/lifecycle/port-guard.ts +39 -7
- package/src/server/lib/lifecycle/probe-health.test.ts +166 -0
- package/src/server/lib/lifecycle/probe.test.ts +77 -164
- package/src/server/lib/lifecycle/probe.ts +121 -13
- package/src/server/lib/omp/rpc/frame-fold-signals.test.ts +112 -0
- package/src/server/lib/omp/rpc/frame-fold.ts +10 -5
- package/src/server/lib/omp/session/sidebar-activity.server.test.ts +162 -0
- package/src/server/lib/omp/session/sidebar-activity.server.ts +70 -0
- package/src/server/lib/omp/session/sidebar-data.canonical.test.ts +155 -0
- package/src/server/lib/omp/session/sidebar-data.server.ts +28 -1
- package/src/server/lib/omp/session/telemetry/compute.ts +13 -1
- package/src/server/lib/omp/session/telemetry.test.ts +33 -0
- package/src/server/lib/panels/local-edits.server.ts +33 -0
- package/src/server/lib/panels/sync.fixture.test-util.ts +92 -0
- package/src/server/lib/panels/sync.local-edits.test.ts +150 -0
- package/src/server/lib/panels/sync.server.test.ts +125 -147
- package/src/server/lib/panels/sync.server.ts +153 -48
- package/src/server/lib/realtime/signals.server.ts +10 -0
- package/src/server/lib/realtime/topics/catalog.server.ts +4 -1
- package/src/server/lib/realtime/topics.server.ts +16 -0
- package/src/server/lib/updates/shell-assets.test.ts +114 -0
- package/src/server/lib/updates/shell-assets.ts +200 -0
- package/src/server/lib/updates/shell-cache.ts +105 -0
- package/src/server/lib/updates/shell-pages.test.ts +187 -0
- package/src/server/lib/updates/shell-pages.ts +157 -0
- package/src/server/lib/updates/shell-release-desktop.test.ts +166 -0
- package/src/server/lib/updates/shell-release.test.ts +59 -71
- package/src/server/lib/updates/shell-release.ts +178 -198
- package/src/server/routes/fs/action-create.test.ts +18 -13
- package/src/server/routes/fs/action-nested.test.ts +184 -0
- package/src/server/routes/fs/action.ts +47 -16
- package/src/server/routes/fs/git-merge.test.ts +11 -1
- package/src/server/routes/fs/read.test.ts +24 -7
- package/src/server/routes/fs/scope.test.ts +15 -10
- package/src/server/routes/fs/scope.ts +3 -14
- package/src/server/routes/index.ts +2 -0
- package/src/server/routes/updates/android.ts +6 -1
- package/src/server/routes/updates/desktop.ts +59 -0
- package/src/server/routes/updates/index.ts +2 -0
- package/src/server/routes/updates/ios.ts +5 -1
- package/src/server/routes/worktrees/create.ts +138 -0
- package/src/server/routes/worktrees/index.ts +25 -0
- package/src/server/routes/worktrees/list.ts +134 -0
- package/src/server/routes/worktrees/remove.ts +102 -0
- package/src/server/routes/worktrees/routes.test.ts +326 -0
- package/src/server/routes/worktrees/shared.ts +90 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.test.ts +36 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.ts +18 -0
- package/src/shared/lib/chat/timeline/run-signals.test.ts +2 -0
- package/src/shared/lib/code/editor/comment-syntax.test.ts +37 -0
- package/src/shared/lib/code/editor/comment-syntax.ts +55 -0
- package/src/shared/lib/code/editor/find-replace.test.ts +134 -0
- package/src/shared/lib/code/editor/{find.ts → find-replace.ts} +23 -107
- package/src/shared/lib/code/editor/{find.test.ts → find-scan.test.ts} +19 -73
- package/src/shared/lib/code/editor/find-scan.ts +177 -0
- package/src/shared/lib/code/editor/keymap.ts +3 -3
- package/src/shared/lib/code/editor/lines.test.ts +7 -55
- package/src/shared/lib/code/editor/lines.ts +8 -48
- package/src/shared/lib/code/editor/marks.test.ts +4 -5
- package/src/shared/lib/code/editor/marks.ts +13 -25
- package/src/shared/lib/code/editor/theme-loader.ts +97 -0
- package/src/shared/lib/code/editor/themes.generated.ts +156 -0
- package/src/shared/lib/code/editor/themes.test.ts +165 -0
- package/src/shared/lib/code/editor/themes.ts +165 -0
- package/src/shared/lib/code/highlight-memo.test.ts +103 -0
- package/src/shared/lib/code/highlight-memo.ts +149 -0
- package/src/shared/lib/code/highlighter-warmup.ts +112 -0
- package/src/shared/lib/code/highlighter.ts +3 -1
- package/src/shared/lib/code/idle.ts +37 -0
- package/src/shared/lib/code/outline/rows.test.ts +96 -0
- package/src/shared/lib/code/outline/rows.ts +94 -0
- package/src/shared/lib/code/syntax-highlight.test.ts +1 -79
- package/src/shared/lib/code/syntax-highlight.ts +2 -3
- package/src/shared/lib/fs/file-kind-classify.test.ts +25 -11
- package/src/shared/lib/fs/file-kind.ts +47 -13
- package/src/shared/lib/fs/file-tree.test.ts +183 -0
- package/src/shared/lib/fs/file-tree.ts +159 -0
- package/src/shared/lib/fs/file-url.test.ts +52 -0
- package/src/shared/lib/fs/file-url.ts +37 -0
- package/src/shared/lib/fs/patch-sides.test.ts +114 -0
- package/src/shared/lib/fs/patch-sides.ts +139 -0
- package/src/shared/lib/fs/paths.test.ts +24 -1
- package/src/shared/lib/fs/paths.ts +17 -0
- package/src/shared/lib/fs/worktree/settings-keys.ts +18 -0
- package/src/shared/lib/omp/session/prewarm.ts +7 -0
- package/src/shared/lib/realtime/protocol.ts +16 -0
- package/src/shared/lib/ui/toolbar-overflow.test.ts +83 -0
- package/src/shared/lib/ui/toolbar-overflow.ts +86 -0
- package/src/shared/lib/updates/shell-arch.ts +54 -0
- package/src/shared/lib/updates/shell-ua.ts +24 -4
- package/src/shared/lib/workspace/active-project.ts +23 -0
- package/src/shared/lib/workspace/editor-view-mode.test.ts +70 -0
- package/src/shared/lib/workspace/editor-view-mode.ts +48 -0
- package/src/shared/lib/workspace/panel-widths.test.ts +30 -0
- package/src/shared/lib/workspace/panel-widths.ts +25 -5
- package/src/shared/lib/workspace/session-state/cache.ts +159 -0
- package/src/shared/lib/workspace/session-state/harness.test-util.ts +55 -0
- package/src/shared/lib/workspace/session-state/merge.ts +100 -0
- package/src/shared/lib/workspace/session-state/store-load.test.ts +206 -0
- package/src/shared/lib/workspace/session-state/store.test.ts +17 -139
- package/src/shared/lib/workspace/session-state/store.ts +92 -130
- package/src/shared/lib/workspace/workspace-modules.test.ts +49 -1
- package/src/shared/lib/workspace/workspace-panel-widths.ts +124 -0
- package/src/shared/lib/workspace/worktree/branch-name.test.ts +95 -0
- package/src/shared/lib/workspace/worktree/branch-name.ts +115 -0
- package/src/shared/lib/workspace/worktree/types.ts +104 -0
- package/src/shared/types/fs.ts +10 -0
- package/src/shared/types/realtime.ts +29 -0
- package/src/shared/types/settings/state.ts +10 -0
- package/src/shared/types/workspace.ts +14 -0
- package/src/test-support/file-explorer-disk.test-util.ts +76 -0
- package/src/test-support/file-explorer.test-util.ts +71 -40
- package/src/test-support/loopback.ts +183 -0
- package/dist/client/static/js/index-nzyjf92m.css +0 -1
- package/src/client/components/common/code-editor/handler.test.ts +0 -274
- package/src/client/components/common/code-editor/handler.ts +0 -175
- package/src/client/components/common/code-editor/index.tsx +0 -314
- package/src/client/components/common/code-surface/measure.ts +0 -148
- package/src/client/components/workspace/diff-panel/SplitView.tsx +0 -209
- package/src/client/components/workspace/diff-panel/UnifiedView.tsx +0 -150
- package/src/client/hooks/editor/scroll-container.test.ts +0 -102
- package/src/client/hooks/editor/scroll-container.ts +0 -26
- package/src/client/hooks/editor/use-code-window.ts +0 -100
- package/src/client/hooks/editor/use-editor-history.test.ts +0 -216
- package/src/client/hooks/editor/use-editor-history.ts +0 -134
- package/src/client/hooks/editor/use-occurrences.test.ts +0 -236
- package/src/client/hooks/editor/use-occurrences.ts +0 -98
- package/src/shared/lib/code/editor/commands.test.ts +0 -155
- package/src/shared/lib/code/editor/commands.ts +0 -260
- package/src/shared/lib/code/editor/edits.test.ts +0 -100
- package/src/shared/lib/code/editor/edits.ts +0 -122
- package/src/shared/lib/code/editor/history.test.ts +0 -118
- package/src/shared/lib/code/editor/history.ts +0 -101
- package/src/shared/lib/code/editor/layers.test.ts +0 -77
- package/src/shared/lib/code/editor/layers.ts +0 -67
- package/src/shared/lib/code/editor/line-edits.test.ts +0 -131
- package/src/shared/lib/code/editor/line-edits.ts +0 -159
- package/src/shared/lib/code/editor/occurrences.test.ts +0 -117
- package/src/shared/lib/code/editor/occurrences.ts +0 -111
- package/src/shared/lib/code/editor/reset-css.test.ts +0 -35
- package/src/shared/lib/code/editor/reset-css.ts +0 -32
- package/src/shared/lib/code/editor/text-commands.test.ts +0 -178
- package/src/shared/lib/code/editor/text-commands.ts +0 -154
- package/src/shared/lib/code/lazy-window.test.ts +0 -161
- package/src/shared/lib/code/lazy-window.ts +0 -156
- package/src/shared/lib/code/windowed-highlight.ts +0 -114
- package/src/shared/lib/fs/diff-parser.test.ts +0 -116
- package/src/shared/lib/fs/diff-parser.ts +0 -213
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The `sidebar:activity` resolver, against a real sessions directory.
|
|
8
|
+
*
|
|
9
|
+
* The rules under test are the ones that keep this payload small enough to be
|
|
10
|
+
* republished per tool call:
|
|
11
|
+
*
|
|
12
|
+
* - it carries ONLY sessions with a run in flight — every other row keeps the
|
|
13
|
+
* stamp the structure gave it, which is why the structure does not have to
|
|
14
|
+
* be re-scanned;
|
|
15
|
+
* - a live run with no transcript yet contributes nothing (a fresh spawn:
|
|
16
|
+
* omp writes the file only when the first assistant message settles);
|
|
17
|
+
* - the stamp it reports is the newest ENTRY's timestamp, the same quantity
|
|
18
|
+
* the structure ships as `updated_at`, so the client can compare them;
|
|
19
|
+
* - the subagent flag is reported alongside, because a subagent's transcript
|
|
20
|
+
* appears mid-run and lights the parent row's roster chevron.
|
|
21
|
+
*
|
|
22
|
+
* Driven through `localRuns()` rather than a stubbed registry: that set is the
|
|
23
|
+
* real "a run is in flight" source (the mock SSE stream uses it too), so the
|
|
24
|
+
* test exercises the same read `loadSidebarStatus` does.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { afterAll, afterEach, describe, expect, test } from 'bun:test';
|
|
28
|
+
import fs from 'fs';
|
|
29
|
+
import * as path from 'path';
|
|
30
|
+
|
|
31
|
+
import { loadSidebarActivity } from '@/server/lib/omp/session/sidebar-activity.server';
|
|
32
|
+
import { localRuns } from '@/server/lib/omp/session/local-runs.server';
|
|
33
|
+
import { clearSessionFileCaches } from '@/server/lib/omp/session/files';
|
|
34
|
+
import { SESSION_TITLE_SLOT_BYTES } from '@/server/lib/omp/session/files';
|
|
35
|
+
|
|
36
|
+
const tempDirs: string[] = [];
|
|
37
|
+
const previousAgentDir = process.env.PI_CODING_AGENT_DIR;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Point the agent dir at a scratch tree, because `findSessionFileById` resolves
|
|
41
|
+
* through the real sessions root. `PI_CODING_AGENT_DIR` is read per call, so no
|
|
42
|
+
* module reload is needed — but the file caches are process-wide and keyed by
|
|
43
|
+
* path, so they are cleared before and after.
|
|
44
|
+
*/
|
|
45
|
+
function useAgentDir(): string {
|
|
46
|
+
const dir = fs.mkdtempSync(path.join(fs.realpathSync('/tmp'), 'kabmo-activity-'));
|
|
47
|
+
tempDirs.push(dir);
|
|
48
|
+
process.env.PI_CODING_AGENT_DIR = dir;
|
|
49
|
+
clearSessionFileCaches();
|
|
50
|
+
return dir;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A title slot line, exactly as omp writes it, so the scan reads a real file. */
|
|
54
|
+
function titleSlot(title: string): string {
|
|
55
|
+
const raw = JSON.stringify({ type: 'title', title });
|
|
56
|
+
return raw + ' '.repeat(Math.max(0, SESSION_TITLE_SLOT_BYTES - 1 - raw.length));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface SessionFixture {
|
|
60
|
+
id: string;
|
|
61
|
+
entries?: Array<{ type: string; timestamp?: string }>;
|
|
62
|
+
subagents?: string[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Write one session file under `<agentDir>/sessions/<project>/`.
|
|
67
|
+
*
|
|
68
|
+
* The entries are minimal but carry the field the scan dates the file by (the
|
|
69
|
+
* newest entry's own `timestamp`), which is the whole quantity under test.
|
|
70
|
+
*/
|
|
71
|
+
function writeSession(agentDir: string, fixture: SessionFixture): string {
|
|
72
|
+
const projectDir = path.join(agentDir, 'sessions', '-tmp-project');
|
|
73
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
74
|
+
const file = path.join(projectDir, `2026-10-24T00-00-00-000Z_${fixture.id}.jsonl`);
|
|
75
|
+
const lines = [
|
|
76
|
+
titleSlot(`Session ${fixture.id}`),
|
|
77
|
+
JSON.stringify({ type: 'session', id: fixture.id, cwd: '/tmp/project', timestamp: '2026-10-24T00:00:00.000Z' }),
|
|
78
|
+
...(fixture.entries ?? []).map((entry) => JSON.stringify(entry)),
|
|
79
|
+
];
|
|
80
|
+
fs.writeFileSync(file, `${lines.join('\n')}\n`);
|
|
81
|
+
|
|
82
|
+
if (fixture.subagents?.length) {
|
|
83
|
+
// A sibling directory named after the session file, holding one .jsonl per
|
|
84
|
+
// subagent — the shape `sessionHasSubagents` reads.
|
|
85
|
+
const sibling = file.replace(/\.jsonl$/, '');
|
|
86
|
+
fs.mkdirSync(sibling, { recursive: true });
|
|
87
|
+
for (const id of fixture.subagents) {
|
|
88
|
+
fs.writeFileSync(path.join(sibling, `${id}.jsonl`), `${JSON.stringify({ type: 'message' })}\n`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
clearSessionFileCaches();
|
|
92
|
+
return file;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
afterEach(() => {
|
|
96
|
+
localRuns().clear();
|
|
97
|
+
clearSessionFileCaches();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
afterAll(() => {
|
|
101
|
+
if (previousAgentDir === undefined) delete process.env.PI_CODING_AGENT_DIR;
|
|
102
|
+
else process.env.PI_CODING_AGENT_DIR = previousAgentDir;
|
|
103
|
+
for (const dir of tempDirs) fs.rmSync(dir, { recursive: true, force: true });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('loadSidebarActivity', () => {
|
|
107
|
+
test('carries nothing when no run is in flight', async () => {
|
|
108
|
+
const agentDir = useAgentDir();
|
|
109
|
+
writeSession(agentDir, { id: 'idle', entries: [{ type: 'message', timestamp: '2026-10-24T01:00:00.000Z' }] });
|
|
110
|
+
expect(await loadSidebarActivity()).toEqual({});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('reports the newest entry timestamp of a session with a live run', async () => {
|
|
114
|
+
const agentDir = useAgentDir();
|
|
115
|
+
writeSession(agentDir, {
|
|
116
|
+
id: 'live',
|
|
117
|
+
entries: [
|
|
118
|
+
{ type: 'message', timestamp: '2026-10-24T01:00:00.000Z' },
|
|
119
|
+
{ type: 'message', timestamp: '2026-10-24T01:05:00.000Z' },
|
|
120
|
+
],
|
|
121
|
+
});
|
|
122
|
+
localRuns().add('live');
|
|
123
|
+
|
|
124
|
+
const activity = await loadSidebarActivity();
|
|
125
|
+
expect(Object.keys(activity)).toEqual(['live']);
|
|
126
|
+
expect(activity.live?.updatedAt).toBe('2026-10-24T01:05:00.000Z');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('never reports a session without a run, however recent its transcript', async () => {
|
|
130
|
+
const agentDir = useAgentDir();
|
|
131
|
+
writeSession(agentDir, { id: 'other', entries: [{ type: 'message', timestamp: '2026-10-24T01:00:00.000Z' }] });
|
|
132
|
+
writeSession(agentDir, { id: 'live', entries: [{ type: 'message', timestamp: '2026-10-24T01:00:00.000Z' }] });
|
|
133
|
+
localRuns().add('live');
|
|
134
|
+
|
|
135
|
+
expect(Object.keys(await loadSidebarActivity())).toEqual(['live']);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('omits a live run whose transcript does not exist yet', async () => {
|
|
139
|
+
// A fresh spawn: omp writes the session file only when the first assistant
|
|
140
|
+
// message settles, so there is no stamp to report. Its row is the client's
|
|
141
|
+
// pending row until the scan finds the file.
|
|
142
|
+
useAgentDir();
|
|
143
|
+
localRuns().add('not-written-yet');
|
|
144
|
+
expect(await loadSidebarActivity()).toEqual({});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('reports the subagent flag, which moves mid-run', async () => {
|
|
148
|
+
const agentDir = useAgentDir();
|
|
149
|
+
writeSession(agentDir, {
|
|
150
|
+
id: 'with-sub',
|
|
151
|
+
entries: [{ type: 'message', timestamp: '2026-10-24T01:00:00.000Z' }],
|
|
152
|
+
subagents: ['Sleeper'],
|
|
153
|
+
});
|
|
154
|
+
writeSession(agentDir, { id: 'plain', entries: [{ type: 'message', timestamp: '2026-10-24T01:00:00.000Z' }] });
|
|
155
|
+
localRuns().add('with-sub');
|
|
156
|
+
localRuns().add('plain');
|
|
157
|
+
|
|
158
|
+
const activity = await loadSidebarActivity();
|
|
159
|
+
expect(activity['with-sub']?.hasSubagents).toBe(true);
|
|
160
|
+
expect(activity.plain?.hasSubagents).toBe(false);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The `sidebar:activity` resolver: the two row fields a RUNNING session can
|
|
8
|
+
* still move between turn boundaries.
|
|
9
|
+
*
|
|
10
|
+
* Split out of `sidebar-data.server.ts` (which owns the structure and the
|
|
11
|
+
* status) because it answers a different question at a different cadence: the
|
|
12
|
+
* structure is re-scanned at turn boundaries, while this is resolved on every
|
|
13
|
+
* tool call — and its whole reason to exist is that the structure must NOT be.
|
|
14
|
+
*
|
|
15
|
+
* A run appends to its transcript on every tool call, so its row's age and its
|
|
16
|
+
* `LATEST_SESSION` position move mid-run. Republishing the structure for that
|
|
17
|
+
* cost 216 KB over 731 sessions per tool call (measured on the author's
|
|
18
|
+
* install, against 87 bytes for one live entry here), which is what made a
|
|
19
|
+
* phone's stream feel slow. Only a RUNNING session can move between boundaries,
|
|
20
|
+
* so only those are carried.
|
|
21
|
+
*
|
|
22
|
+
* A live run with no session FILE yet (a fresh spawn: omp writes it only when
|
|
23
|
+
* the first assistant message settles) contributes nothing — there is no stamp
|
|
24
|
+
* to report, and its sidebar row is the client's pending row until the scan
|
|
25
|
+
* finds the file.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { isMockMode } from '@/server/mock.server';
|
|
29
|
+
import { getLiveRunSessionIds } from '@/server/lib/omp/rpc/session-registry';
|
|
30
|
+
import { findSessionFileById } from '@/server/lib/omp/session/locator';
|
|
31
|
+
import { scanSessionInfoCached } from '@/server/lib/omp/session/files';
|
|
32
|
+
import { sessionHasSubagents } from '@/server/lib/omp/session/subagent-presence';
|
|
33
|
+
import type { SidebarActivityPayload } from '@/shared/types/realtime';
|
|
34
|
+
|
|
35
|
+
export async function loadSidebarActivity(): Promise<SidebarActivityPayload> {
|
|
36
|
+
const activity: SidebarActivityPayload = {};
|
|
37
|
+
// MOCK answers nothing, and that is not a shortcut: the simulated stream is
|
|
38
|
+
// an SSE route, not an omp child, so no `tool_execution_end` frame ever fires
|
|
39
|
+
// there and there is no between-boundary movement to report. Its rows also
|
|
40
|
+
// read `created_at` (the mock `sessions` table has no `updated_at`), so a
|
|
41
|
+
// `chat_sessions` stamp would be a different quantity under the same name —
|
|
42
|
+
// the one thing this payload must never be.
|
|
43
|
+
if (isMockMode()) return activity;
|
|
44
|
+
|
|
45
|
+
const live = getLiveRunSessionIds();
|
|
46
|
+
if (live.size === 0) return activity;
|
|
47
|
+
const entries = await Promise.all(
|
|
48
|
+
[...live].map(async (sessionId) => {
|
|
49
|
+
const filePath = await findSessionFileById(sessionId);
|
|
50
|
+
if (!filePath) return null;
|
|
51
|
+
const info = await scanSessionInfoCached(filePath);
|
|
52
|
+
if (!info) return null;
|
|
53
|
+
// `modified` is the newest entry's own timestamp, which is what the
|
|
54
|
+
// structure ships as `updated_at` — the same quantity, so the client can
|
|
55
|
+
// compare the two as ISO strings.
|
|
56
|
+
const updatedAt = info.modified.toISOString();
|
|
57
|
+
// The chevron's flag moves mid-run too: a subagent writes its own
|
|
58
|
+
// transcript beside the parent, and the row it lights is the parent's.
|
|
59
|
+
// Cost is a stat plus (on a change) one readdir, and the presence module
|
|
60
|
+
// memoizes on `(last entry, sibling mtime)` — measured 4-8 ms on a cache
|
|
61
|
+
// miss, 0 on a hit.
|
|
62
|
+
const hasSubagents = await sessionHasSubagents(filePath, updatedAt);
|
|
63
|
+
return [sessionId, { updatedAt, hasSubagents }] as const;
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
for (const entry of entries) {
|
|
67
|
+
if (entry) activity[entry[0]] = entry[1];
|
|
68
|
+
}
|
|
69
|
+
return activity;
|
|
70
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A workspace folder finds its sessions through a CANONICAL path, not through
|
|
8
|
+
* the spelling the folder was registered under.
|
|
9
|
+
*
|
|
10
|
+
* `resolveRootsByCwd` runs every session's cwd through `resolveProjectRoot`,
|
|
11
|
+
* which realpaths it, so the bucket key is canonical. A folder row holds the
|
|
12
|
+
* spelling the user's path was registered with. On macOS those differ for
|
|
13
|
+
* anything under `/var` (a symlink to `/private/var`), which is where every
|
|
14
|
+
* temp directory lives — measured: a real-mode workspace in a temp directory
|
|
15
|
+
* listed ZERO sessions, and the worktree badge could never appear because the
|
|
16
|
+
* session was not even in the folder it belongs to.
|
|
17
|
+
*
|
|
18
|
+
* The symlink here is created by the test rather than borrowed from
|
|
19
|
+
* `os.tmpdir()`, because on Linux the two spellings are identical and a
|
|
20
|
+
* platform-shaped fixture would pass with the fix removed.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from 'bun:test';
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import { tmpdir } from 'node:os';
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
|
|
28
|
+
import { getDb } from '@/server/db.server';
|
|
29
|
+
import { loadSidebarStructure } from '@/server/lib/omp/session/sidebar-data.server';
|
|
30
|
+
import { invalidateOmpSidebarData } from '@/server/lib/omp/session/reader';
|
|
31
|
+
import { clearSessionFileCaches } from '@/server/lib/omp/session/files';
|
|
32
|
+
import { isolateDb, releaseDb } from '@/test-support/isolated-db';
|
|
33
|
+
import { removeDir } from '@/test-support/remove-dir';
|
|
34
|
+
|
|
35
|
+
const saved = {
|
|
36
|
+
agentDir: Bun.env.PI_CODING_AGENT_DIR,
|
|
37
|
+
xdg: Bun.env.XDG_DATA_HOME,
|
|
38
|
+
mock: Bun.env.MOCK,
|
|
39
|
+
sync: Bun.env.SYNC_WORKSPACE,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
let root = '';
|
|
43
|
+
let sessionsRoot = '';
|
|
44
|
+
/** The directory a session actually ran in — what the scan resolves to. */
|
|
45
|
+
let realProject = '';
|
|
46
|
+
/** A symlink to it, which is how the folder is registered. */
|
|
47
|
+
let linkedProject = '';
|
|
48
|
+
|
|
49
|
+
beforeAll(async () => {
|
|
50
|
+
root = await fs.promises.mkdtemp(join(tmpdir(), 'omc-sidebar-canonical-'));
|
|
51
|
+
const agent = join(root, 'agent');
|
|
52
|
+
sessionsRoot = join(agent, 'sessions');
|
|
53
|
+
await fs.promises.mkdir(sessionsRoot, { recursive: true });
|
|
54
|
+
|
|
55
|
+
realProject = join(root, 'real-project');
|
|
56
|
+
await fs.promises.mkdir(realProject, { recursive: true });
|
|
57
|
+
linkedProject = join(root, 'linked-project');
|
|
58
|
+
await fs.promises.symlink(realProject, linkedProject, 'dir');
|
|
59
|
+
|
|
60
|
+
Bun.env.PI_CODING_AGENT_DIR = agent;
|
|
61
|
+
// XDG would redirect the sessions dir to $XDG_DATA_HOME/omp when it exists.
|
|
62
|
+
delete Bun.env.XDG_DATA_HOME;
|
|
63
|
+
Bun.env.MOCK = 'false';
|
|
64
|
+
Bun.env.SYNC_WORKSPACE = 'false';
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
afterAll(() => {
|
|
68
|
+
if (saved.agentDir === undefined) delete Bun.env.PI_CODING_AGENT_DIR;
|
|
69
|
+
else Bun.env.PI_CODING_AGENT_DIR = saved.agentDir;
|
|
70
|
+
if (saved.xdg === undefined) delete Bun.env.XDG_DATA_HOME;
|
|
71
|
+
else Bun.env.XDG_DATA_HOME = saved.xdg;
|
|
72
|
+
if (saved.mock === undefined) delete Bun.env.MOCK;
|
|
73
|
+
else Bun.env.MOCK = saved.mock;
|
|
74
|
+
if (saved.sync === undefined) delete Bun.env.SYNC_WORKSPACE;
|
|
75
|
+
else Bun.env.SYNC_WORKSPACE = saved.sync;
|
|
76
|
+
removeDir(root);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
beforeEach(async () => {
|
|
80
|
+
await isolateDb();
|
|
81
|
+
// The sessions root is shared by the file, so each case starts from an empty
|
|
82
|
+
// one: a file left by a previous case would be listed by the next, which is
|
|
83
|
+
// what made the first run of this suite fail.
|
|
84
|
+
await fs.promises.rm(join(sessionsRoot, 'project'), { recursive: true, force: true });
|
|
85
|
+
invalidateOmpSidebarData();
|
|
86
|
+
clearSessionFileCaches();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
afterEach(async () => {
|
|
90
|
+
await releaseDb();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
/** One session file whose cwd is the CANONICAL spelling, as omp would record it. */
|
|
94
|
+
async function writeSession(id: string, cwd: string): Promise<void> {
|
|
95
|
+
const dir = join(sessionsRoot, 'project');
|
|
96
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
97
|
+
const lines = [
|
|
98
|
+
JSON.stringify({ type: 'session', id, cwd, timestamp: '2026-10-23T00:00:00.000Z' }),
|
|
99
|
+
JSON.stringify({
|
|
100
|
+
type: 'message',
|
|
101
|
+
id: `${id}-m1`,
|
|
102
|
+
parentId: null,
|
|
103
|
+
timestamp: '2026-10-23T00:00:01.000Z',
|
|
104
|
+
message: { role: 'user', content: 'hello from the canonical bucket' },
|
|
105
|
+
}),
|
|
106
|
+
];
|
|
107
|
+
await Bun.write(join(dir, `${id}.jsonl`), `${lines.join('\n')}\n`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** A workspace folder row bound to `projectPath`. */
|
|
111
|
+
async function registerFolder(projectPath: string): Promise<number> {
|
|
112
|
+
const db = await getDb();
|
|
113
|
+
await db.run('INSERT INTO workspace_folders (name, project_path) VALUES (?, ?)', ['ws', projectPath]);
|
|
114
|
+
const row = await db.get<{ id: number }>('SELECT id FROM workspace_folders ORDER BY id DESC LIMIT 1');
|
|
115
|
+
return Number(row?.id);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
describe('sidebar folders resolve sessions through a canonical path', () => {
|
|
119
|
+
test('a folder registered under a symlinked spelling lists the sessions that ran in its target', async () => {
|
|
120
|
+
// The session's cwd is the REAL directory; the folder names the symlink.
|
|
121
|
+
// Without the canonical bucket key this is an empty folder.
|
|
122
|
+
await writeSession('canonical-hit', realProject);
|
|
123
|
+
await registerFolder(linkedProject);
|
|
124
|
+
|
|
125
|
+
const payload = await loadSidebarStructure();
|
|
126
|
+
const folder = payload.folders.find((entry) => entry.project_path === linkedProject);
|
|
127
|
+
|
|
128
|
+
expect(folder).toBeDefined();
|
|
129
|
+
expect(folder?.sessions.map((session) => session.id)).toEqual(['canonical-hit']);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('a folder registered under the real spelling keeps listing its sessions', async () => {
|
|
133
|
+
// The control: the fallback to the raw spelling must not be the only path
|
|
134
|
+
// that works, or the fix would trade one empty folder for another.
|
|
135
|
+
await writeSession('canonical-plain', realProject);
|
|
136
|
+
await registerFolder(realProject);
|
|
137
|
+
|
|
138
|
+
const payload = await loadSidebarStructure();
|
|
139
|
+
const folder = payload.folders.find((entry) => entry.project_path === realProject);
|
|
140
|
+
|
|
141
|
+
expect(folder?.sessions.map((session) => session.id)).toEqual(['canonical-plain']);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('a session in an unrelated directory stays out of the folder', async () => {
|
|
145
|
+
const other = join(root, 'other-project');
|
|
146
|
+
await fs.promises.mkdir(other, { recursive: true });
|
|
147
|
+
await writeSession('canonical-elsewhere', other);
|
|
148
|
+
await registerFolder(linkedProject);
|
|
149
|
+
|
|
150
|
+
const payload = await loadSidebarStructure();
|
|
151
|
+
const folder = payload.folders.find((entry) => entry.project_path === linkedProject);
|
|
152
|
+
|
|
153
|
+
expect(folder?.sessions).toEqual([]);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -26,6 +26,9 @@ import { loadStreamStates, type SessionRunModel } from '@/shared/lib/omp/session
|
|
|
26
26
|
import { healStaleStreamStatuses } from '@/shared/lib/omp/session/stream-heal.server';
|
|
27
27
|
import { getAwaitingInputSessionIds, getLiveRunSessionIds, listRpcSessions } from '@/server/lib/omp/rpc/session-registry';
|
|
28
28
|
import type { SessionItemData, SessionSortOption, WorkspaceFolderData } from '@/shared/types';
|
|
29
|
+
import { sessionWorktreeMap } from '@/server/lib/fs/worktree/topology';
|
|
30
|
+
import { canonicalPath } from '@/server/lib/fs/canonical';
|
|
31
|
+
import type { SessionWorktreeInfo } from '@/shared/lib/workspace/worktree/types';
|
|
29
32
|
import type { OmpSession } from '@/shared/types/omp/session';
|
|
30
33
|
|
|
31
34
|
/** What `GET /api/sessions/list` returns and sidebars consume. */
|
|
@@ -236,11 +239,30 @@ async function buildRealFolders(folderRows: FolderRow[], archivedIds: Set<string
|
|
|
236
239
|
|
|
237
240
|
return Promise.all(folderRows.map(async (folder) => {
|
|
238
241
|
const root = folder.project_path ?? '';
|
|
239
|
-
|
|
242
|
+
// The bucket key is CANONICAL: `resolveRootsByCwd` resolves each session's
|
|
243
|
+
// cwd through `resolveProjectRoot`, which realpaths it, while a folder row
|
|
244
|
+
// holds the spelling the user's path was registered under. On macOS those
|
|
245
|
+
// differ for anything under `/var` (a symlink to `/private/var`), so a
|
|
246
|
+
// project in a temp directory or under a symlinked home listed ZERO sessions
|
|
247
|
+
// — measured: every real-mode session was missing from the sidebar, and the
|
|
248
|
+
// worktree badge could never appear because the session was not even in the
|
|
249
|
+
// folder it belongs to.
|
|
250
|
+
const rootKey = root ? await canonicalPath(root) : '';
|
|
251
|
+
const rootSessions: OmpSession[] = root
|
|
252
|
+
? (sessionsByRoot.get(rootKey) ?? sessionsByRoot.get(root) ?? [])
|
|
253
|
+
: [];
|
|
254
|
+
// One topology read per repository, however many sessions it has. A chat in
|
|
255
|
+
// the checkout itself is absent from this map, which is exactly what the row
|
|
256
|
+
// draws (an ordinary row with no badge).
|
|
257
|
+
const worktrees = await sessionWorktreeMap(
|
|
258
|
+
root,
|
|
259
|
+
rootSessions.map((session) => ({ id: session.id, cwd: session.cwd })),
|
|
260
|
+
);
|
|
240
261
|
const folderSessions = await Promise.all(rootSessions.map(async (session: OmpSession) => {
|
|
241
262
|
const hasSub = session.path
|
|
242
263
|
? await sessionHasSubagents(session.path, session.modified)
|
|
243
264
|
: false;
|
|
265
|
+
const worktree: SessionWorktreeInfo | undefined = worktrees.get(session.id);
|
|
244
266
|
return {
|
|
245
267
|
id: session.id,
|
|
246
268
|
folder_id: folder.id,
|
|
@@ -250,6 +272,11 @@ async function buildRealFolders(folderRows: FolderRow[], archivedIds: Set<string
|
|
|
250
272
|
is_active: 0,
|
|
251
273
|
is_archived: archivedIds.has(String(session.id)) ? 1 : 0,
|
|
252
274
|
hasSubagents: hasSub,
|
|
275
|
+
// The session's own working directory, and the worktree it resolves to
|
|
276
|
+
// when that cwd is a linked checkout. Both travel so no client and no
|
|
277
|
+
// plugin has to ask git.
|
|
278
|
+
cwd: session.cwd,
|
|
279
|
+
...(worktree ? { worktree } : {}),
|
|
253
280
|
};
|
|
254
281
|
}));
|
|
255
282
|
return {
|
|
@@ -10,6 +10,15 @@ import { scanSessionEntries } from '@/server/lib/omp/session/telemetry/scan';
|
|
|
10
10
|
* Compute real `SessionContextTelemetry` for a session file (all-zero on
|
|
11
11
|
* unreadable/empty). `rawLimit` caps materialized raw items (0 = none): the
|
|
12
12
|
* panel reads full pages from `computeRawMessagesPage` in ./telemetry-raw.ts.
|
|
13
|
+
*
|
|
14
|
+
* The default is 0 — no raw items — and that is load-bearing rather than
|
|
15
|
+
* tidy: this value is published as the `session:<id>:telemetry` topic, which
|
|
16
|
+
* the navbar ring subscribes to for EVERY open session and the server
|
|
17
|
+
* republishes on every tool call. Measured on a 111-message transcript, the
|
|
18
|
+
* full `rawMessages` list (each item carrying its whole entry as
|
|
19
|
+
* `rawPayload`) was **690 KB of a 691 KB snapshot** — 99.9% of every push,
|
|
20
|
+
* per tool call, per tab. Nothing reads it: the panel pages raw items from
|
|
21
|
+
* `/api/telemetry/raw-messages`, and `/api/telemetry/context` strips them.
|
|
13
22
|
*/
|
|
14
23
|
export async function computeRealSessionTelemetry(
|
|
15
24
|
filePath: string,
|
|
@@ -106,7 +115,10 @@ export async function computeRealSessionTelemetry(
|
|
|
106
115
|
|
|
107
116
|
if (role === 'user' || role === 'assistant') {
|
|
108
117
|
messagesCount++;
|
|
109
|
-
|
|
118
|
+
// `rawLimit` is a CAP, so 0 means "none" — the guard must not read 0 as
|
|
119
|
+
// "unlimited", which is what it did and what put the whole transcript on
|
|
120
|
+
// the telemetry topic (see the doc above).
|
|
121
|
+
if (rawLimit > 0 && rawMessages.length < rawLimit) {
|
|
110
122
|
const isAssistant = role === 'assistant';
|
|
111
123
|
const snippet = text.slice(0, 70);
|
|
112
124
|
rawMessages.push({
|
|
@@ -152,3 +152,36 @@ describe('computeRealSessionTelemetry context anchor', () => {
|
|
|
152
152
|
expect(telemetry.contextUsed).toBe(2200);
|
|
153
153
|
});
|
|
154
154
|
});
|
|
155
|
+
|
|
156
|
+
describe('computeRealSessionTelemetry raw items', () => {
|
|
157
|
+
// The default is the TELEMETRY TOPIC's payload, which the navbar ring keeps
|
|
158
|
+
// subscribed for every open session and the server republishes on every tool
|
|
159
|
+
// call. A raw item carries its whole entry as `rawPayload`, so reading 0 as
|
|
160
|
+
// "unlimited" put the entire transcript on that topic — measured 690 KB of a
|
|
161
|
+
// 691 KB snapshot on a 111-message session.
|
|
162
|
+
test('materializes no raw items by default', async () => {
|
|
163
|
+
const file = await writeSession([
|
|
164
|
+
header,
|
|
165
|
+
userMessage('u1', 'first turn'),
|
|
166
|
+
assistantMessage('a1', { usage: { input: 10, output: 5 } }),
|
|
167
|
+
userMessage('u2', 'second turn'),
|
|
168
|
+
assistantMessage('a2', { usage: { input: 20, output: 5 } }),
|
|
169
|
+
]);
|
|
170
|
+
const telemetry = await computeRealSessionTelemetry(file, 'ses_telemetry');
|
|
171
|
+
expect(telemetry.messagesCount).toBe(4);
|
|
172
|
+
expect(telemetry.rawMessages).toEqual([]);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('caps raw items at a positive limit', async () => {
|
|
176
|
+
const file = await writeSession([
|
|
177
|
+
header,
|
|
178
|
+
userMessage('u1', 'first turn'),
|
|
179
|
+
assistantMessage('a1', { usage: { input: 10, output: 5 } }),
|
|
180
|
+
userMessage('u2', 'second turn'),
|
|
181
|
+
assistantMessage('a2', { usage: { input: 20, output: 5 } }),
|
|
182
|
+
]);
|
|
183
|
+
const telemetry = await computeRealSessionTelemetry(file, 'ses_telemetry', undefined, 2);
|
|
184
|
+
expect(telemetry.rawMessages).toHaveLength(2);
|
|
185
|
+
expect(telemetry.rawMessages[0]?.id).toBe('u1');
|
|
186
|
+
});
|
|
187
|
+
});
|
|
@@ -124,3 +124,36 @@ export function changedPaths(baseline: TrackedFileHashes, current: TrackedFileHa
|
|
|
124
124
|
}
|
|
125
125
|
return [...changed].sort();
|
|
126
126
|
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Whether a recorded baseline still describes this install.
|
|
130
|
+
*
|
|
131
|
+
* A baseline can only be evidence about the user while it agrees with what is on
|
|
132
|
+
* disk, and the test is exact and one-directional:
|
|
133
|
+
*
|
|
134
|
+
* > wherever the install and the store AGREE, the baseline must agree too.
|
|
135
|
+
*
|
|
136
|
+
* Kabmo is the only thing that writes store content into an install, and it
|
|
137
|
+
* re-takes the baseline when it does — so a path where the install already
|
|
138
|
+
* equals the store while the baseline says otherwise proves the baseline
|
|
139
|
+
* describes a copy this install never had. That is not hypothetical: the product
|
|
140
|
+
* rename rewrote every bundled file while an older baseline was still recorded,
|
|
141
|
+
* and reading that baseline as "the user edited 24 files" made the update refuse
|
|
142
|
+
* forever, with remove-and-reinstall as the only way out (measured).
|
|
143
|
+
*
|
|
144
|
+
* The converse is deliberately NOT required, so a genuine edit stays detectable:
|
|
145
|
+
* a user edit makes the install differ from the store, so those paths are
|
|
146
|
+
* skipped here and the remaining ones still vouch for the baseline. The edit is
|
|
147
|
+
* then reported by `changedPaths` rather than overwritten.
|
|
148
|
+
*/
|
|
149
|
+
export function baselineDescribesInstall(
|
|
150
|
+
baseline: TrackedFileHashes,
|
|
151
|
+
install: TrackedFileHashes,
|
|
152
|
+
store: TrackedFileHashes,
|
|
153
|
+
): boolean {
|
|
154
|
+
for (const [path, hash] of Object.entries(install)) {
|
|
155
|
+
if (hash !== store[path]) continue;
|
|
156
|
+
if (baseline[path] !== hash) return false;
|
|
157
|
+
}
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The two-directory fixture the bundled-sync suites run against: a WORKING
|
|
8
|
+
* marketplace (what is installed) and a STORE (what the package ships).
|
|
9
|
+
*
|
|
10
|
+
* Split out because the two suites that pin the sync would otherwise each carry
|
|
11
|
+
* a copy of the same setup, and the paths are the whole contract — a copy that
|
|
12
|
+
* forgot `KABMO_BUNDLED_MARKETPLACE_DIR` would silently compare the real
|
|
13
|
+
* package's store against a temp install and pass for the wrong reason.
|
|
14
|
+
*
|
|
15
|
+
* `isolateDb` is part of the setup, not a separate step: the baselines and the
|
|
16
|
+
* refusal records live in `app_settings`, and a suite that skipped it would read
|
|
17
|
+
* (and write) the developer's own database.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import { mkdtempSync } from 'node:fs';
|
|
22
|
+
import { tmpdir } from 'node:os';
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
|
|
25
|
+
import { invalidatePanelScan } from '@/server/lib/panels/registry.server';
|
|
26
|
+
import { hashTrackedFiles } from '@/server/lib/panels/local-edits.server';
|
|
27
|
+
import { writeBaseline } from '@/server/lib/panels/baseline.server';
|
|
28
|
+
import { isolateDb, releaseDb } from '@/test-support/isolated-db';
|
|
29
|
+
|
|
30
|
+
export interface SyncFixture {
|
|
31
|
+
/** The working marketplace root (installs live here). */
|
|
32
|
+
root: string;
|
|
33
|
+
/** The bundled store root (the package's copy). */
|
|
34
|
+
store: string;
|
|
35
|
+
/** A scratch directory for git repositories. */
|
|
36
|
+
work: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const FIXTURE_MANIFEST = { id: 'demo', name: 'Demo', version: '1.0.0', app: 'dist/app.js' };
|
|
40
|
+
|
|
41
|
+
/** Write a plugin directory: manifest, bundle, and any extra files. */
|
|
42
|
+
export function writePlugin(dir: string, manifest: unknown, files: Record<string, string> = {}): void {
|
|
43
|
+
fs.mkdirSync(join(dir, 'dist'), { recursive: true });
|
|
44
|
+
fs.writeFileSync(join(dir, 'kabmo.json'), JSON.stringify(manifest));
|
|
45
|
+
fs.writeFileSync(join(dir, 'dist', 'app.js'), 'export default {};');
|
|
46
|
+
for (const [name, content] of Object.entries(files)) {
|
|
47
|
+
const target = join(dir, name);
|
|
48
|
+
fs.mkdirSync(join(target, '..'), { recursive: true });
|
|
49
|
+
fs.writeFileSync(target, content);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Create the fixture and point the process at it. Call from `beforeEach`. */
|
|
54
|
+
export async function setupSyncFixture(): Promise<SyncFixture> {
|
|
55
|
+
const fixture: SyncFixture = {
|
|
56
|
+
root: mkdtempSync(join(tmpdir(), 'omp-sync-work-')),
|
|
57
|
+
store: mkdtempSync(join(tmpdir(), 'omp-sync-store-')),
|
|
58
|
+
work: mkdtempSync(join(tmpdir(), 'omp-sync-repos-')),
|
|
59
|
+
};
|
|
60
|
+
process.env.KABMO_MARKETPLACE_DIR = fixture.root;
|
|
61
|
+
process.env.KABMO_BUNDLED_MARKETPLACE_DIR = fixture.store;
|
|
62
|
+
await isolateDb();
|
|
63
|
+
invalidatePanelScan();
|
|
64
|
+
return fixture;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Tear the fixture down. Call from `afterEach`. */
|
|
68
|
+
export async function teardownSyncFixture(fixture: SyncFixture): Promise<void> {
|
|
69
|
+
delete process.env.KABMO_MARKETPLACE_DIR;
|
|
70
|
+
delete process.env.KABMO_BUNDLED_MARKETPLACE_DIR;
|
|
71
|
+
await releaseDb();
|
|
72
|
+
invalidatePanelScan();
|
|
73
|
+
for (const dir of [fixture.root, fixture.store, fixture.work]) {
|
|
74
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Install a bundled plugin the way the install path does: the store's copy is
|
|
80
|
+
* written into the working marketplace, and the baseline is taken from what
|
|
81
|
+
* landed. `files` therefore go into the STORE, or the install would carry a file
|
|
82
|
+
* the store never had — a state an install cannot produce.
|
|
83
|
+
*/
|
|
84
|
+
export async function installBundled(
|
|
85
|
+
fixture: SyncFixture,
|
|
86
|
+
version: string,
|
|
87
|
+
files: Record<string, string> = {},
|
|
88
|
+
): Promise<void> {
|
|
89
|
+
writePlugin(join(fixture.store, 'plugins', 'demo'), { ...FIXTURE_MANIFEST, version }, files);
|
|
90
|
+
writePlugin(join(fixture.root, 'plugins', 'demo'), { ...FIXTURE_MANIFEST, version }, files);
|
|
91
|
+
await writeBaseline('demo', await hashTrackedFiles(join(fixture.root, 'plugins', 'demo')));
|
|
92
|
+
}
|