@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,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The shell chrome the shared cluster must carry, on BOTH layouts.
|
|
8
|
+
*
|
|
9
|
+
* Two bugs reached a user, and both are invisible in a browser:
|
|
10
|
+
*
|
|
11
|
+
* 1. **`app-region` inherits.** WebView2's own spec says the property inherits,
|
|
12
|
+
* and Chromium 152 began enforcing it. So a header marked `app-region: drag`
|
|
13
|
+
* turns every descendant into a drag handle — and a drag handle swallows the
|
|
14
|
+
* click, silently. The desktop navbar wrapped this cluster in a
|
|
15
|
+
* `titlebar-no-drag` div and the PHONE's header did not, so on Windows every
|
|
16
|
+
* control in a phone-width window (reload, context, sign out, the right
|
|
17
|
+
* panel, `⋯`) looked right and did nothing.
|
|
18
|
+
* 2. **The page-drawn caption buttons lived in one layout.** A desktop shell
|
|
19
|
+
* window can be narrower than `App`'s 768px breakpoint — all three shells
|
|
20
|
+
* allow a 720px window — so a Windows/Linux window resized down switches to
|
|
21
|
+
* the phone layout, which drew no close/minimise/maximise at all and had no
|
|
22
|
+
* native title bar behind them.
|
|
23
|
+
*
|
|
24
|
+
* Both are asserted on the RENDERED tree, because both were a call site's
|
|
25
|
+
* omission: a test reading the component's source would have passed while the
|
|
26
|
+
* phone stayed broken. macOS is covered too — it keeps its own traffic lights,
|
|
27
|
+
* so the page must draw no second set.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { afterAll, afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
31
|
+
import { Window } from 'happy-dom';
|
|
32
|
+
import { h, render } from 'preact';
|
|
33
|
+
import { act } from 'preact/test-utils';
|
|
34
|
+
import { definePluginApp } from '@kabmo-labs/kabmo-sdk/app';
|
|
35
|
+
import { HeaderControls } from '@/client/components/common/header-controls';
|
|
36
|
+
import { SessionStateProvider } from '@/client/components/common/session-state-provider/index';
|
|
37
|
+
import { registerPluginBundle, resetPluginSlots } from '@/client/lib/plugins/slots';
|
|
38
|
+
import { primeAuthState } from '@/client/hooks/ui/auth';
|
|
39
|
+
import { resetRealtimeClient } from '@/shared/lib/realtime/client';
|
|
40
|
+
import { invalidateModelsCache } from '@/shared/lib/models/client';
|
|
41
|
+
import { sessionTelemetryTopic, TOPIC_PANELS } from '@/shared/lib/realtime/protocol';
|
|
42
|
+
import type { PanelRegistryPayload, SessionContextTelemetry } from '@/shared/types';
|
|
43
|
+
import { startRealtimeTestServer, type RealtimeTestServer } from '@/test-support/realtime-server';
|
|
44
|
+
import { installDomGlobals, pristineWebSocket, restoreDomGlobals } from '@/test-support/pristine-globals';
|
|
45
|
+
import type { TopicResolver } from '@/server/lib/realtime/hub.server';
|
|
46
|
+
|
|
47
|
+
const SESSION = 'session-shell-chrome';
|
|
48
|
+
const nativeFetch = Bun.fetch;
|
|
49
|
+
const Noop = () => null;
|
|
50
|
+
|
|
51
|
+
let container: HTMLElement;
|
|
52
|
+
let server: RealtimeTestServer;
|
|
53
|
+
|
|
54
|
+
const PANELS: PanelRegistryPayload = {
|
|
55
|
+
panels: [{ pluginId: 'demo', name: 'Demo', version: '1.0.0', appUrl: '/api/panels/bundle/demo.js' }],
|
|
56
|
+
marketplaces: [],
|
|
57
|
+
errors: [],
|
|
58
|
+
plugins: [{ pluginId: 'demo', name: 'Demo', version: '1.0.0', isPackage: true, built: true, enabled: true, bundled: false, gitOrigin: null }],
|
|
59
|
+
catalog: [],
|
|
60
|
+
disabledPanels: [],
|
|
61
|
+
gitStatus: {},
|
|
62
|
+
autoUpdate: false,
|
|
63
|
+
syncBlocked: {},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function telemetry(): SessionContextTelemetry {
|
|
67
|
+
return {
|
|
68
|
+
sessionId: SESSION,
|
|
69
|
+
sessionTitle: 'Shell',
|
|
70
|
+
modelId: 'kenari/deepseek-v4-1-flash',
|
|
71
|
+
modelName: 'deepseek-v4-1-flash',
|
|
72
|
+
timestamp: 'today',
|
|
73
|
+
contextUsed: 12_000,
|
|
74
|
+
contextLimit: 100_000,
|
|
75
|
+
contextPercent: 12,
|
|
76
|
+
messagesCount: 3,
|
|
77
|
+
userCount: 1,
|
|
78
|
+
assistantCount: 2,
|
|
79
|
+
totalCost: 0.01,
|
|
80
|
+
costFormatted: '$0.01',
|
|
81
|
+
cacheHitAverage: 90,
|
|
82
|
+
lastMessage: { input: 10, output: 4, reasoning: 0, cacheRead: 2, cacheWrite: 0, cacheHitPercent: 16.6 },
|
|
83
|
+
distribution: { userTokens: 1, userPercent: 25, assistantTokens: 1, assistantPercent: 25, toolTokens: 1, toolPercent: 25, otherTokens: 1, otherPercent: 25 },
|
|
84
|
+
rawMessages: [],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The shell's two globals, which the DOM's own `window` type does not declare. */
|
|
89
|
+
type Shell = {
|
|
90
|
+
__kabmoCall?: (method: string, params?: Record<string, unknown>) => Promise<unknown>;
|
|
91
|
+
__kabmoDesktop?: Record<string, unknown>;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
const WINDOWS = { platform: 'windows', shellVersion: '0.1.0', overlayTitlebar: true, windowControls: true };
|
|
96
|
+
const MACOS = { platform: 'macos', shellVersion: '0.1.0', overlayTitlebar: true, trafficLightInset: '5rem' };
|
|
97
|
+
|
|
98
|
+
/** Install a shell identity for one case; `afterEach` removes it. */
|
|
99
|
+
function withShell(identity: Record<string, unknown> | null) {
|
|
100
|
+
// The shell's globals are not in the DOM's `window` type; one cast, at the seam.
|
|
101
|
+
const shell = window as unknown as Shell;
|
|
102
|
+
shell.__kabmoCall = async () => null;
|
|
103
|
+
if (identity) shell.__kabmoDesktop = identity;
|
|
104
|
+
else delete shell.__kabmoDesktop;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
beforeEach(async () => {
|
|
108
|
+
invalidateModelsCache();
|
|
109
|
+
const panels: TopicResolver = async () => PANELS;
|
|
110
|
+
const telemetryTopic: TopicResolver = async () => telemetry();
|
|
111
|
+
server = await startRealtimeTestServer(new Map<string, TopicResolver>([
|
|
112
|
+
[TOPIC_PANELS, panels],
|
|
113
|
+
[sessionTelemetryTopic(SESSION), telemetryTopic],
|
|
114
|
+
]));
|
|
115
|
+
installDomGlobals(new Window({ url: `http://127.0.0.1:${server.port}` }));
|
|
116
|
+
const target = globalThis as unknown as Record<string, unknown>;
|
|
117
|
+
target.WebSocket = pristineWebSocket;
|
|
118
|
+
target.fetch = async (input: unknown) => {
|
|
119
|
+
const json = (body: unknown) => new Response(JSON.stringify(body), {
|
|
120
|
+
status: 200,
|
|
121
|
+
headers: { 'content-type': 'application/json' },
|
|
122
|
+
});
|
|
123
|
+
const url = String(input);
|
|
124
|
+
if (url.includes('/api/sessions/')) return json({ state: {} });
|
|
125
|
+
if (url.includes('/api/telemetry/context')) return json({ telemetry: telemetry() });
|
|
126
|
+
if (url.includes('/api/models')) return json({ modelList: [], connectedProviders: [] });
|
|
127
|
+
return json({});
|
|
128
|
+
};
|
|
129
|
+
resetPluginSlots();
|
|
130
|
+
registerPluginBundle('demo', {
|
|
131
|
+
default: definePluginApp((app) => app.headerPanel({ id: 'h', title: 'Demo Stats', component: Noop })),
|
|
132
|
+
});
|
|
133
|
+
primeAuthState(true, true);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
afterEach(() => {
|
|
137
|
+
if (container) {
|
|
138
|
+
render(null, container);
|
|
139
|
+
container.remove();
|
|
140
|
+
}
|
|
141
|
+
resetPluginSlots();
|
|
142
|
+
resetRealtimeClient();
|
|
143
|
+
server?.stop();
|
|
144
|
+
(globalThis as unknown as Record<string, unknown>).fetch = nativeFetch;
|
|
145
|
+
const shell = window as unknown as Shell;
|
|
146
|
+
delete shell.__kabmoDesktop;
|
|
147
|
+
delete shell.__kabmoCall;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
afterAll(() => {
|
|
151
|
+
restoreDomGlobals();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
async function settle(): Promise<void> {
|
|
155
|
+
await server.waitForTopic(TOPIC_PANELS, (value) => value !== null);
|
|
156
|
+
await server.waitForTopic(sessionTelemetryTopic(SESSION), (value) => value !== null);
|
|
157
|
+
for (let i = 0; i < 10; i += 1) await act(async () => {});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Every control in the cluster, by its accessible name. */
|
|
161
|
+
function controlNames(): string[] {
|
|
162
|
+
const names: string[] = [];
|
|
163
|
+
for (const element of container.querySelectorAll('button, [title]')) {
|
|
164
|
+
const label = element.getAttribute('aria-label') ?? element.getAttribute('title') ?? '';
|
|
165
|
+
names.push(label.replace(/ \([^)]*\)$/, ''));
|
|
166
|
+
}
|
|
167
|
+
return names;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function mount(layout: 'desktop' | 'mobile', width: number): Promise<void> {
|
|
171
|
+
Object.defineProperty(window, 'innerWidth', { value: width, configurable: true, writable: true });
|
|
172
|
+
container = document.createElement('div');
|
|
173
|
+
document.body.appendChild(container);
|
|
174
|
+
await act(async () => {
|
|
175
|
+
render(
|
|
176
|
+
h(SessionStateProvider, {
|
|
177
|
+
sessionId: SESSION,
|
|
178
|
+
children: h(HeaderControls, {
|
|
179
|
+
layout,
|
|
180
|
+
sessionId: SESSION,
|
|
181
|
+
showRightPanel: layout === 'desktop',
|
|
182
|
+
onToggleRightPanel: () => {},
|
|
183
|
+
}),
|
|
184
|
+
}),
|
|
185
|
+
container,
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
await settle();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function unmount(): void {
|
|
192
|
+
render(null, container);
|
|
193
|
+
container.remove();
|
|
194
|
+
container = undefined as unknown as HTMLElement;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** A desktop shell window is allowed to be 720px — below the 768px breakpoint. */
|
|
198
|
+
const SHELL_MIN_WIDTH = 720;
|
|
199
|
+
const PHONE_WIDTH = 390;
|
|
200
|
+
|
|
201
|
+
describe('shell chrome', () => {
|
|
202
|
+
test('the cluster marks itself no-drag on both layouts', async () => {
|
|
203
|
+
for (const [layout, width] of [['desktop', 1024], ['mobile', PHONE_WIDTH]] as const) {
|
|
204
|
+
await mount(layout, width);
|
|
205
|
+
const root = container.firstElementChild as HTMLElement;
|
|
206
|
+
expect(root.className).toContain('titlebar-no-drag');
|
|
207
|
+
unmount();
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('a Windows shell draws the caption buttons on both layouts', async () => {
|
|
212
|
+
withShell(WINDOWS);
|
|
213
|
+
for (const [layout, width] of [['desktop', 1024], ['mobile', PHONE_WIDTH]] as const) {
|
|
214
|
+
await mount(layout, width);
|
|
215
|
+
const names = controlNames();
|
|
216
|
+
expect(names).toContain('Minimize');
|
|
217
|
+
expect(names).toContain('Maximize');
|
|
218
|
+
expect(names).toContain('Close');
|
|
219
|
+
unmount();
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* The case the bug was actually reported for: a Windows window at the shell's
|
|
225
|
+
* own minimum width, which the app renders with the PHONE layout.
|
|
226
|
+
*/
|
|
227
|
+
test('a Windows window at its minimum width still has its caption buttons', async () => {
|
|
228
|
+
withShell(WINDOWS);
|
|
229
|
+
await mount('mobile', SHELL_MIN_WIDTH);
|
|
230
|
+
const names = controlNames();
|
|
231
|
+
expect(names).toContain('Close');
|
|
232
|
+
expect(names).toContain('Minimize');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test('a macOS shell draws no caption buttons of its own', async () => {
|
|
236
|
+
withShell(MACOS);
|
|
237
|
+
await mount('desktop', 1024);
|
|
238
|
+
const names = controlNames();
|
|
239
|
+
expect(names).not.toContain('Minimize');
|
|
240
|
+
expect(names).not.toContain('Close');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
/** A browser tab has no shell, so a caption button would be a dead control. */
|
|
244
|
+
test('a browser draws no caption buttons', async () => {
|
|
245
|
+
withShell(null);
|
|
246
|
+
await mount('desktop', 1024);
|
|
247
|
+
expect(controlNames()).not.toContain('Close');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* The caption buttons issue the shell's own verbs, from the phone layout too.
|
|
252
|
+
*
|
|
253
|
+
* A button that reaches the DOM but never calls the bridge is the same dead
|
|
254
|
+
* control in a different disguise.
|
|
255
|
+
*/
|
|
256
|
+
test('a caption click reaches the bridge from the phone layout', async () => {
|
|
257
|
+
const calls: string[] = [];
|
|
258
|
+
withShell(WINDOWS);
|
|
259
|
+
;(window as unknown as Shell).__kabmoCall = async (method) => {
|
|
260
|
+
calls.push(method);
|
|
261
|
+
return null;
|
|
262
|
+
};
|
|
263
|
+
await mount('mobile', PHONE_WIDTH);
|
|
264
|
+
const close = Array.from(container.querySelectorAll('button'))
|
|
265
|
+
.find((button) => button.getAttribute('aria-label') === 'Close') as HTMLElement;
|
|
266
|
+
expect(close).toBeTruthy();
|
|
267
|
+
await act(async () => {
|
|
268
|
+
close.click();
|
|
269
|
+
});
|
|
270
|
+
expect(calls).toContain('window_close');
|
|
271
|
+
});
|
|
272
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The shell update's dialog: confirm, install, report.
|
|
8
|
+
*
|
|
9
|
+
* ## Why the message lives HERE and not in the footer
|
|
10
|
+
*
|
|
11
|
+
* The footer is a row of controls, one of them 24px wide. A sentence like "Kabmo
|
|
12
|
+
* 0.3.2 installed — restarting" pushed into that row pushed the controls around
|
|
13
|
+
* and read as a status line for the SIDEBAR rather than for the update the user
|
|
14
|
+
* just asked for. The control therefore stays a control, and everything a run has
|
|
15
|
+
* to say — the confirmation before it, the progress during it, the shell's own
|
|
16
|
+
* refusal after it — is a dialog.
|
|
17
|
+
*
|
|
18
|
+
* ## The confirmation is not ceremony
|
|
19
|
+
*
|
|
20
|
+
* The click replaces the app the user is looking at and restarts it. That is not
|
|
21
|
+
* something a 24px glyph should do on a single press with no words, which is why
|
|
22
|
+
* the first thing the dialog says is what will happen and what will be lost
|
|
23
|
+
* (nothing but the window: the servers and the session are the console's own
|
|
24
|
+
* state, and they survive).
|
|
25
|
+
*
|
|
26
|
+
* ## Every phase reports the shell's own words
|
|
27
|
+
*
|
|
28
|
+
* `done` and `error` print the sentence the SHELL sent. Nothing here invents
|
|
29
|
+
* "Update failed": a shell that verified the download and declined knows why, and
|
|
30
|
+
* that reason is the only thing a user can act on.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { AlertCircle, Check, Download, Loader2 } from 'lucide-preact';
|
|
34
|
+
import { Modal } from '@/client/components/common/Modal';
|
|
35
|
+
import {
|
|
36
|
+
shellDownloadLabel,
|
|
37
|
+
type ShellDownload,
|
|
38
|
+
} from '@/client/hooks/ui/updates/shell-download';
|
|
39
|
+
import type { ShellUpdatePhase } from '@/client/hooks/ui/updates/shell-update';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* What the dialog is showing.
|
|
43
|
+
*
|
|
44
|
+
* `confirm` is the dialog's own phase, not the hook's: the hook's `idle` means "no
|
|
45
|
+
* run has happened", and the dialog is only ever open on a run the user is about to
|
|
46
|
+
* start or one that already ran.
|
|
47
|
+
*/
|
|
48
|
+
export type ShellUpdateModalPhase = ShellUpdatePhase | 'confirm';
|
|
49
|
+
|
|
50
|
+
interface ShellUpdateModalProps {
|
|
51
|
+
download: ShellDownload;
|
|
52
|
+
phase: ShellUpdateModalPhase;
|
|
53
|
+
message: string | null;
|
|
54
|
+
onConfirm: () => void;
|
|
55
|
+
onClose: () => void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** What this platform actually does with the artifact, in the platform's terms. */
|
|
59
|
+
function installNote(download: ShellDownload): string {
|
|
60
|
+
switch (download.platform) {
|
|
61
|
+
case 'macos':
|
|
62
|
+
return 'Kabmo downloads the new build, checks its signature and replaces itself. The window reopens on the new version.';
|
|
63
|
+
case 'windows':
|
|
64
|
+
return 'Kabmo downloads the new installer and runs it silently. The window closes and reopens on the new version.';
|
|
65
|
+
case 'linux':
|
|
66
|
+
return 'Kabmo downloads the new AppImage over the one it is running from and restarts. A package install (.deb / .rpm) cannot replace itself — that build is offered the release page instead.';
|
|
67
|
+
default:
|
|
68
|
+
return 'Kabmo installs the new build and restarts.';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const SECONDARY =
|
|
73
|
+
'rounded-md border border-ink/20 px-3 py-1.5 text-[11px] font-medium text-ink transition-colors hover:border-ink/40 cursor-pointer';
|
|
74
|
+
const PRIMARY =
|
|
75
|
+
'inline-flex items-center gap-1.5 rounded-md bg-ink px-3 py-1.5 text-[11px] font-medium text-paper transition-opacity hover:opacity-90 disabled:opacity-60 disabled:cursor-not-allowed cursor-pointer';
|
|
76
|
+
|
|
77
|
+
export function ShellUpdateModal({ download, phase, message, onConfirm, onClose }: ShellUpdateModalProps) {
|
|
78
|
+
const label = shellDownloadLabel(download);
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<Modal
|
|
82
|
+
onClose={onClose}
|
|
83
|
+
maxWidthClass="max-w-sm"
|
|
84
|
+
header={<h2 className="text-sm font-semibold text-ink">{label}</h2>}
|
|
85
|
+
footer={
|
|
86
|
+
<div className="flex items-center justify-end gap-2 px-5 pb-4">
|
|
87
|
+
{phase === 'confirm' && (
|
|
88
|
+
<>
|
|
89
|
+
<button type="button" className={SECONDARY} onClick={onClose}>
|
|
90
|
+
Cancel
|
|
91
|
+
</button>
|
|
92
|
+
<button type="button" className={PRIMARY} onClick={onConfirm}>
|
|
93
|
+
<Download size={13} />
|
|
94
|
+
<span>Install update</span>
|
|
95
|
+
</button>
|
|
96
|
+
</>
|
|
97
|
+
)}
|
|
98
|
+
|
|
99
|
+
{phase === 'working' && (
|
|
100
|
+
<button type="button" className={PRIMARY} disabled>
|
|
101
|
+
<Loader2 size={13} className="animate-spin motion-reduce:animate-none" />
|
|
102
|
+
<span>Installing…</span>
|
|
103
|
+
</button>
|
|
104
|
+
)}
|
|
105
|
+
|
|
106
|
+
{phase === 'done' && (
|
|
107
|
+
<button type="button" className={PRIMARY} onClick={onClose}>
|
|
108
|
+
<Check size={13} />
|
|
109
|
+
<span>Close</span>
|
|
110
|
+
</button>
|
|
111
|
+
)}
|
|
112
|
+
|
|
113
|
+
{phase === 'error' && (
|
|
114
|
+
<>
|
|
115
|
+
<button type="button" className={SECONDARY} onClick={onClose}>
|
|
116
|
+
Close
|
|
117
|
+
</button>
|
|
118
|
+
{/* Retry re-runs the SAME offer: the release is still the one the
|
|
119
|
+
console read, and a failure here is the shell's download or its
|
|
120
|
+
verification, not a stale version. */}
|
|
121
|
+
<button type="button" className={PRIMARY} onClick={onConfirm}>
|
|
122
|
+
<Download size={13} />
|
|
123
|
+
<span>Try again</span>
|
|
124
|
+
</button>
|
|
125
|
+
</>
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
}
|
|
129
|
+
>
|
|
130
|
+
<div className="px-5 py-4 space-y-3 text-[11px] leading-5 text-ink/70">
|
|
131
|
+
{phase === 'confirm' && (
|
|
132
|
+
<>
|
|
133
|
+
<p>{installNote(download)}</p>
|
|
134
|
+
<p className="text-ink/50">
|
|
135
|
+
Version {download.latest} · the console keeps running; only this window restarts.
|
|
136
|
+
</p>
|
|
137
|
+
</>
|
|
138
|
+
)}
|
|
139
|
+
|
|
140
|
+
{phase === 'working' && (
|
|
141
|
+
<p className="flex items-center gap-2 text-ink">
|
|
142
|
+
<Loader2 size={14} className="animate-spin motion-reduce:animate-none" />
|
|
143
|
+
<span>Downloading and verifying the new build…</span>
|
|
144
|
+
</p>
|
|
145
|
+
)}
|
|
146
|
+
|
|
147
|
+
{(phase === 'done' || phase === 'error') && (
|
|
148
|
+
<p
|
|
149
|
+
role="status"
|
|
150
|
+
className={`flex items-start gap-2 ${phase === 'error' ? 'text-error' : 'text-ink'}`}
|
|
151
|
+
>
|
|
152
|
+
{phase === 'error' ? (
|
|
153
|
+
<AlertCircle size={14} className="mt-0.5 shrink-0" />
|
|
154
|
+
) : (
|
|
155
|
+
<Check size={14} className="mt-0.5 shrink-0" />
|
|
156
|
+
)}
|
|
157
|
+
<span>{message ?? (phase === 'error' ? 'The update failed.' : 'Update installed.')}</span>
|
|
158
|
+
</p>
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
161
|
+
</Modal>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The footer control is a CONTROL; every word a run says is in the dialog.
|
|
8
|
+
*
|
|
9
|
+
* That split is the whole point of this suite. The footer is a 24px row of glyphs,
|
|
10
|
+
* and a status sentence pushed into it moved the neighbouring controls and read as
|
|
11
|
+
* a status line for the SIDEBAR rather than for the update the user just asked for.
|
|
12
|
+
* So the cases below assert the RENDERED tree in both states, not the module's
|
|
13
|
+
* constants: a table in a module would pass while the footer rendered something
|
|
14
|
+
* else.
|
|
15
|
+
*
|
|
16
|
+
* Three behaviours are load-bearing and each was a decision:
|
|
17
|
+
*
|
|
18
|
+
* 1. **A confirmation comes FIRST.** The click replaces the app and restarts it, so
|
|
19
|
+
* a 24px glyph must not do that on a single press with no words.
|
|
20
|
+
* 2. **`done` and `error` print the SHELL's sentence**, inside the dialog. Nothing
|
|
21
|
+
* here invents "Update failed": a shell that verified the download and declined
|
|
22
|
+
* knows why, and that reason is the only thing a user can act on.
|
|
23
|
+
* 3. **A shell that cannot install it gets a LINK**, and the dialog never opens.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'bun:test';
|
|
27
|
+
import { Window } from 'happy-dom';
|
|
28
|
+
import { h, render } from 'preact';
|
|
29
|
+
import { act } from 'preact/test-utils';
|
|
30
|
+
import { ShellUpdateButton } from '@/client/components/common/shell-update-button';
|
|
31
|
+
import type { ShellDownload } from '@/client/hooks/ui/updates/shell-download';
|
|
32
|
+
import { installDomGlobals, restoreDomGlobals } from '@/test-support/pristine-globals';
|
|
33
|
+
|
|
34
|
+
const DESKTOP: ShellDownload = {
|
|
35
|
+
url: 'https://github.com/kabmo-labs/kabmo/releases/download/v0.3.2/Kabmo-0.3.2-mac-arm64.zip',
|
|
36
|
+
latest: '0.3.2',
|
|
37
|
+
releaseUrl: 'https://github.com/kabmo-labs/kabmo/releases/tag/v0.8.0',
|
|
38
|
+
direct: true,
|
|
39
|
+
kind: 'update',
|
|
40
|
+
platform: 'macos',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const PAGE: ShellDownload = { ...DESKTOP, url: 'https://github.com/kabmo-labs/kabmo/releases/tag/v0.8.0', direct: false };
|
|
44
|
+
|
|
45
|
+
let container: HTMLElement;
|
|
46
|
+
|
|
47
|
+
/** Install (or remove) a bridge on the page. */
|
|
48
|
+
function bridge(call: ((method: string, params?: Record<string, unknown>) => Promise<unknown>) | null): void {
|
|
49
|
+
const page = globalThis as unknown as { window: { __kabmoCall?: unknown } };
|
|
50
|
+
if (call) page.window.__kabmoCall = call;
|
|
51
|
+
else delete page.window.__kabmoCall;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function mount(download: ShellDownload): Promise<void> {
|
|
55
|
+
await act(async () => {
|
|
56
|
+
render(h(ShellUpdateButton, { download, variant: 'icon' }), container);
|
|
57
|
+
});
|
|
58
|
+
for (let i = 0; i < 5; i += 1) await act(async () => {});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The dialog, by its role — the same locator a browser spec would use.
|
|
63
|
+
*
|
|
64
|
+
* Queried on `document.body`, not on the render container: the dialog is a PORTAL
|
|
65
|
+
* (see the component), which is what keeps it out of the footer row's own text.
|
|
66
|
+
*/
|
|
67
|
+
function dialog(): HTMLElement | null {
|
|
68
|
+
return document.body.querySelector('[role="dialog"]');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Both helpers search the whole body, because the dialog is a PORTAL: its controls
|
|
73
|
+
* live beside the render container, not inside it. The footer control is inside the
|
|
74
|
+
* container, which is itself in the body, so one query covers both.
|
|
75
|
+
*/
|
|
76
|
+
function byName(name: string): HTMLElement | null {
|
|
77
|
+
return document.body.querySelector(`[aria-label="${name}"]`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function buttonByText(text: string): HTMLElement | null {
|
|
81
|
+
const buttons = [...document.body.querySelectorAll('button')];
|
|
82
|
+
return (buttons.find((button) => button.textContent?.trim() === text) as HTMLElement | undefined) ?? null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function click(element: HTMLElement | null): Promise<void> {
|
|
86
|
+
if (!element) throw new Error('nothing to click');
|
|
87
|
+
await act(async () => {
|
|
88
|
+
element.click();
|
|
89
|
+
});
|
|
90
|
+
for (let i = 0; i < 10; i += 1) await act(async () => {});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
beforeAll(() => {
|
|
94
|
+
installDomGlobals(new Window({ url: 'http://localhost' }));
|
|
95
|
+
container = document.createElement('div');
|
|
96
|
+
document.body.appendChild(container);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
afterEach(() => {
|
|
100
|
+
act(() => render(null, container));
|
|
101
|
+
bridge(null);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
afterAll(() => {
|
|
105
|
+
container.remove();
|
|
106
|
+
restoreDomGlobals();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('a shell that can install its own update', () => {
|
|
110
|
+
test('the dialog is OUTSIDE the footer row, so the row says nothing about a run', async () => {
|
|
111
|
+
// A dialog rendered inside the footer would put its sentence into the row's own
|
|
112
|
+
// text content (measured in the browser before the portal: the row read as the
|
|
113
|
+
// dialog's body). It also has to be a portal for a second reason — the row is a
|
|
114
|
+
// 24px flex strip, and any `overflow`/`transform` added to it would clip a
|
|
115
|
+
// `fixed` overlay.
|
|
116
|
+
bridge(async () => ({ success: true, message: 'ok' }));
|
|
117
|
+
await mount(DESKTOP);
|
|
118
|
+
await click(byName('Update Kabmo 0.3.2 for macOS'));
|
|
119
|
+
|
|
120
|
+
const modal = document.body.querySelector('[role="dialog"]');
|
|
121
|
+
expect(modal).not.toBeNull();
|
|
122
|
+
expect(container.contains(modal)).toBe(false);
|
|
123
|
+
// The control itself carries no status text in any phase.
|
|
124
|
+
expect(byName('Update Kabmo 0.3.2 for macOS')?.textContent?.trim()).toBe('');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('draws a button, and opens a CONFIRMATION rather than installing on the press', async () => {
|
|
128
|
+
const calls: string[] = [];
|
|
129
|
+
bridge(async (method) => {
|
|
130
|
+
calls.push(method);
|
|
131
|
+
return { success: true, version: '0.3.2', message: 'Kabmo 0.3.2 installed — restarting.', relaunching: true };
|
|
132
|
+
});
|
|
133
|
+
await mount(DESKTOP);
|
|
134
|
+
|
|
135
|
+
const control = byName('Update Kabmo 0.3.2 for macOS');
|
|
136
|
+
expect(control?.tagName).toBe('BUTTON');
|
|
137
|
+
// Nothing has been asked of the shell yet — that is the confirmation's job.
|
|
138
|
+
expect(calls).toEqual([]);
|
|
139
|
+
|
|
140
|
+
await click(control);
|
|
141
|
+
expect(dialog()).not.toBeNull();
|
|
142
|
+
expect(dialog()?.textContent).toContain('Version 0.3.2');
|
|
143
|
+
expect(dialog()?.textContent).toContain('replaces itself');
|
|
144
|
+
// Still nothing: the press opened a dialog, it did not install.
|
|
145
|
+
expect(calls).toEqual([]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('installing happens on the dialog’s own button, and the report stays IN the dialog', async () => {
|
|
149
|
+
const calls: { method: string; params?: Record<string, unknown> }[] = [];
|
|
150
|
+
bridge(async (method, params) => {
|
|
151
|
+
calls.push({ method, params });
|
|
152
|
+
return { success: true, version: '0.3.2', message: 'Kabmo 0.3.2 installed — restarting.', relaunching: true };
|
|
153
|
+
});
|
|
154
|
+
await mount(DESKTOP);
|
|
155
|
+
await click(byName('Update Kabmo 0.3.2 for macOS'));
|
|
156
|
+
await click(buttonByText('Install update'));
|
|
157
|
+
|
|
158
|
+
expect(calls).toEqual([
|
|
159
|
+
{ method: 'update_apply', params: { url: DESKTOP.url, version: '0.3.2' } },
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
// The sentence is the SHELL's, and it is inside the dialog.
|
|
163
|
+
expect(dialog()?.textContent).toContain('Kabmo 0.3.2 installed — restarting.');
|
|
164
|
+
// …and the footer carries no status line of its own: the control is still a
|
|
165
|
+
// control, with its own label.
|
|
166
|
+
expect(byName('Update Kabmo 0.3.2 for macOS')).not.toBeNull();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('a refusal keeps the shell’s reason in the dialog, and offers another try', async () => {
|
|
170
|
+
bridge(async () => ({ success: false, message: 'The downloaded file is not a signed Kabmo build.' }));
|
|
171
|
+
await mount(DESKTOP);
|
|
172
|
+
await click(byName('Update Kabmo 0.3.2 for macOS'));
|
|
173
|
+
await click(buttonByText('Install update'));
|
|
174
|
+
|
|
175
|
+
expect(dialog()?.textContent).toContain('The downloaded file is not a signed Kabmo build.');
|
|
176
|
+
expect(buttonByText('Try again')).not.toBeNull();
|
|
177
|
+
expect(buttonByText('Close')).not.toBeNull();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('closing after a failure, then reopening, starts at the confirmation again', async () => {
|
|
181
|
+
// A dismissed run is not a run: the next press must not greet the user with the
|
|
182
|
+
// previous attempt's refusal.
|
|
183
|
+
bridge(async () => ({ success: false, message: 'The download could not be downloaded.' }));
|
|
184
|
+
await mount(DESKTOP);
|
|
185
|
+
await click(byName('Update Kabmo 0.3.2 for macOS'));
|
|
186
|
+
await click(buttonByText('Install update'));
|
|
187
|
+
await click(buttonByText('Close'));
|
|
188
|
+
expect(dialog()).toBeNull();
|
|
189
|
+
|
|
190
|
+
await click(byName('Update Kabmo 0.3.2 for macOS'));
|
|
191
|
+
expect(dialog()?.textContent).toContain('Version 0.3.2');
|
|
192
|
+
expect(dialog()?.textContent).not.toContain('could not be downloaded');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('Cancel closes without asking the shell anything', async () => {
|
|
196
|
+
const calls: string[] = [];
|
|
197
|
+
bridge(async (method) => {
|
|
198
|
+
calls.push(method);
|
|
199
|
+
return { success: true, message: 'ok' };
|
|
200
|
+
});
|
|
201
|
+
await mount(DESKTOP);
|
|
202
|
+
await click(byName('Update Kabmo 0.3.2 for macOS'));
|
|
203
|
+
await click(buttonByText('Cancel'));
|
|
204
|
+
|
|
205
|
+
expect(dialog()).toBeNull();
|
|
206
|
+
expect(calls).toEqual([]);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('the phone footer’s variant', () => {
|
|
211
|
+
test('draws the short label on the same button, and opens the same dialog', async () => {
|
|
212
|
+
// The two layouts pass different `variant`s because a 24px glyph and a word are
|
|
213
|
+
// different rows — but they must be the same CONTROL: a link on one layout and a
|
|
214
|
+
// button on the other is the drift this component exists to prevent.
|
|
215
|
+
bridge(async () => ({ success: true, version: '0.3.2', message: 'Kabmo 0.3.2 installed — restarting.' }));
|
|
216
|
+
await act(async () => {
|
|
217
|
+
render(h(ShellUpdateButton, { download: DESKTOP, variant: 'label' }), container);
|
|
218
|
+
});
|
|
219
|
+
for (let i = 0; i < 5; i += 1) await act(async () => {});
|
|
220
|
+
|
|
221
|
+
const control = byName('Update Kabmo 0.3.2 for macOS');
|
|
222
|
+
expect(control?.tagName).toBe('BUTTON');
|
|
223
|
+
expect(control?.textContent?.trim()).toBe('Update macOS');
|
|
224
|
+
|
|
225
|
+
await click(control);
|
|
226
|
+
expect(dialog()?.textContent).toContain('Version 0.3.2');
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe('a shell that cannot install it', () => {
|
|
231
|
+
test('draws a link to the release page, and no dialog', async () => {
|
|
232
|
+
bridge(async () => null);
|
|
233
|
+
await mount(PAGE);
|
|
234
|
+
|
|
235
|
+
const control = byName('Update Kabmo 0.3.2 for macOS');
|
|
236
|
+
expect(control?.tagName).toBe('A');
|
|
237
|
+
expect(control?.getAttribute('href')).toBe(PAGE.url);
|
|
238
|
+
expect(control?.getAttribute('target')).toBe('_blank');
|
|
239
|
+
|
|
240
|
+
await click(control);
|
|
241
|
+
expect(dialog()).toBeNull();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test('a browser draws the link too, because there is no shell to ask', async () => {
|
|
245
|
+
bridge(null);
|
|
246
|
+
await mount(DESKTOP);
|
|
247
|
+
|
|
248
|
+
expect(byName('Update Kabmo 0.3.2 for macOS')?.tagName).toBe('A');
|
|
249
|
+
expect(dialog()).toBeNull();
|
|
250
|
+
});
|
|
251
|
+
});
|