@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,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* One file's matches inside a `grep`/`glob` card.
|
|
8
|
+
*
|
|
9
|
+
* Two costs live here, and both were paid on every expand of the card:
|
|
10
|
+
*
|
|
11
|
+
* 1. **The tokenizer.** Every file's lines were tokenized in the parent's render,
|
|
12
|
+
* in one synchronous pass — the profile of a `grep` expand put 40% of its
|
|
13
|
+
* long task inside Shiki's regex engine. The highlight is deferred per FILE
|
|
14
|
+
* (`useDeferredHighlightLines`), so the card paints the matches first and the
|
|
15
|
+
* colour follows on an idle slice.
|
|
16
|
+
* 2. **The row count.** `MAX_OUTPUT_LINES` caps the whole output, not one file,
|
|
17
|
+
* so a single 900-line file group mounted 900 rows inside a `max-h-80`
|
|
18
|
+
* scroller that shows ~14. The window is per file now, opened on the first
|
|
19
|
+
* lines (a match group is read from its top) with the rest one click away.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { useMemo, useState } from 'preact/hooks';
|
|
23
|
+
import { ChevronRight, FileText } from 'lucide-preact';
|
|
24
|
+
import { getLanguageFromPath } from '@/shared/lib/code/language';
|
|
25
|
+
import { useDeferredHighlightLines } from '@/client/hooks/code/deferred-highlight';
|
|
26
|
+
|
|
27
|
+
/** Rows mounted for one file before the reader asks for the rest. */
|
|
28
|
+
export const MAX_MATCH_LINES_PER_FILE = 60;
|
|
29
|
+
|
|
30
|
+
interface MatchRow {
|
|
31
|
+
lineNum: number;
|
|
32
|
+
text: string;
|
|
33
|
+
isMatch: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface FileMatchesProps {
|
|
37
|
+
path: string;
|
|
38
|
+
lines: MatchRow[];
|
|
39
|
+
matchCount: number;
|
|
40
|
+
isGlobList: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function FileMatches({ path, lines, matchCount, isGlobList }: FileMatchesProps) {
|
|
44
|
+
const [showAll, setShowAll] = useState(false);
|
|
45
|
+
const visible = useMemo(
|
|
46
|
+
() => (showAll ? lines : lines.slice(0, MAX_MATCH_LINES_PER_FILE)),
|
|
47
|
+
[lines, showAll],
|
|
48
|
+
);
|
|
49
|
+
const hidden = lines.length - visible.length;
|
|
50
|
+
|
|
51
|
+
const lang = getLanguageFromPath(path);
|
|
52
|
+
const highlighted = useDeferredHighlightLines(
|
|
53
|
+
useMemo(() => visible.map((line) => line.text).join('\n'), [visible]),
|
|
54
|
+
lang,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="overflow-hidden rounded-lg border border-ink/8 bg-paper">
|
|
59
|
+
{/* File Header */}
|
|
60
|
+
<div className="flex items-center justify-between border-b border-ink/6 bg-canvas/40 px-2.5 py-1.5">
|
|
61
|
+
<div className="flex min-w-0 items-center gap-1.5">
|
|
62
|
+
<FileText size={12} className="shrink-0 text-ink/40" />
|
|
63
|
+
<span className="truncate font-mono text-[11px] font-medium text-ink/85">{path}</span>
|
|
64
|
+
</div>
|
|
65
|
+
{matchCount > 0 && !isGlobList && (
|
|
66
|
+
<span className="ml-2 shrink-0 rounded bg-ink/5 px-1.5 py-0.2 font-mono text-[9.5px] text-ink/50">
|
|
67
|
+
{matchCount} {matchCount === 1 ? 'hit' : 'hits'}
|
|
68
|
+
</span>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
{/* Code Lines inside file — satu scroll horizontal per file, semua row selebar konten terlebar */}
|
|
73
|
+
{visible.length > 0 && (
|
|
74
|
+
<div className="overflow-x-auto overscroll-x-contain">
|
|
75
|
+
<div className="w-max min-w-full divide-y divide-ink/[0.04] font-mono text-[11px] leading-relaxed select-text">
|
|
76
|
+
{visible.map((line, lineIdx) => (
|
|
77
|
+
<div
|
|
78
|
+
key={lineIdx}
|
|
79
|
+
className={`flex items-start gap-2.5 px-2.5 py-1 transition-colors ${
|
|
80
|
+
line.isMatch ? 'bg-warning/[0.08] text-ink' : 'bg-paper text-ink/60 hover:bg-ink/[0.02]'
|
|
81
|
+
}`}
|
|
82
|
+
>
|
|
83
|
+
<span
|
|
84
|
+
className={`sticky left-0 z-10 flex w-9 shrink-0 select-none items-center justify-end bg-inherit font-mono text-[10px] ${
|
|
85
|
+
line.isMatch ? 'font-semibold text-warning-dark dark:text-warning' : 'text-ink/30'
|
|
86
|
+
}`}
|
|
87
|
+
>
|
|
88
|
+
{line.isMatch && <ChevronRight size={10} className="mr-0.5 text-warning" />}
|
|
89
|
+
{line.lineNum}
|
|
90
|
+
</span>
|
|
91
|
+
<pre
|
|
92
|
+
className="shiki whitespace-pre font-mono"
|
|
93
|
+
dangerouslySetInnerHTML={{ __html: highlighted[lineIdx] || ' ' }}
|
|
94
|
+
/>
|
|
95
|
+
</div>
|
|
96
|
+
))}
|
|
97
|
+
</div>
|
|
98
|
+
{hidden > 0 && (
|
|
99
|
+
<button
|
|
100
|
+
type="button"
|
|
101
|
+
onClick={() => setShowAll(true)}
|
|
102
|
+
className="w-full border-t border-ink/6 bg-canvas/40 px-2.5 py-1 text-left font-mono text-[10px] text-ink/55 transition-colors hover:bg-ink/5 hover:text-ink"
|
|
103
|
+
>
|
|
104
|
+
… {hidden} more {hidden === 1 ? 'line' : 'lines'} — show all {lines.length}
|
|
105
|
+
</button>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { useEffect, useMemo, useState } from 'preact/hooks';
|
|
2
2
|
import { FileCode, FileImage, FileText, Folder, FolderOpen, Info, Loader2 } from 'lucide-preact';
|
|
3
3
|
import { getLanguageFromPath } from '@/shared/lib/code/language';
|
|
4
|
-
import {
|
|
5
|
-
import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
|
|
4
|
+
import { useDeferredHighlight } from '@/client/hooks/code/deferred-highlight';
|
|
6
5
|
import { parseDirListing, parseNumberedCode } from '@/shared/lib/code/parser';
|
|
7
6
|
import { getImageMimeType } from '@/shared/lib/fs/file-kind';
|
|
8
7
|
import { buildFsRawUrl, isVirtualPath, stripReadSelector } from '@/shared/lib/fs/paths';
|
|
@@ -192,11 +191,12 @@ export function Read({ tool, targetFilePath, output }: ReadPanelProps) {
|
|
|
192
191
|
const truncatedCode = useMemo(() => truncateTailLines(parsedCode.cleanCode, MAX_OUTPUT_LINES), [parsedCode.cleanCode]);
|
|
193
192
|
const visibleLines = truncatedCode.skipped > 0 ? parsedCode.lines.slice(truncatedCode.skipped) : parsedCode.lines;
|
|
194
193
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
194
|
+
// The tokenizer runs off the render that a tap scheduled (see
|
|
195
|
+
// `deferred-highlight`): a 1000-line read measured a 220–300 ms long task on
|
|
196
|
+
// a throttled phone, all of it before the card could paint. The plain-text
|
|
197
|
+
// frame it draws first carries the same characters, and a block already in
|
|
198
|
+
// the memo renders highlighted on that first paint.
|
|
199
|
+
const highlightedCode = useDeferredHighlight(truncatedCode.text, lang);
|
|
200
200
|
|
|
201
201
|
return (
|
|
202
202
|
<div className="space-y-2">
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { useMemo, useState } from 'preact/hooks';
|
|
2
|
-
import {
|
|
2
|
+
import { FileSearch, FolderSearch } from 'lucide-preact';
|
|
3
3
|
import type { ToolCallData } from '@/shared/types';
|
|
4
4
|
import { CopyButton } from '@/client/components/common/CopyButton';
|
|
5
|
-
import {
|
|
6
|
-
import { highlightLines } from '@/shared/lib/code/syntax-highlight';
|
|
7
|
-
import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
|
|
5
|
+
import { FileMatches } from '@/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches';
|
|
8
6
|
import { MAX_OUTPUT_LINES, truncateTailLines } from '@/client/components/workspace/chat-timeline/tool-renderers/shared/truncate';
|
|
9
7
|
|
|
10
8
|
interface MatchLine {
|
|
@@ -189,18 +187,6 @@ export function SearchTool({ tool }: { tool: ToolCallData }) {
|
|
|
189
187
|
.filter((f): f is ParsedFileMatches => f !== null);
|
|
190
188
|
}, [files, filterText]);
|
|
191
189
|
|
|
192
|
-
const syntaxReady = useSyntaxReady();
|
|
193
|
-
|
|
194
|
-
const highlightedByFile = useMemo(() => {
|
|
195
|
-
return filteredFiles.map((file) => {
|
|
196
|
-
const lang = getLanguageFromPath(file.path);
|
|
197
|
-
return highlightLines(
|
|
198
|
-
file.lines.map((line) => line.text).join('\n'),
|
|
199
|
-
lang
|
|
200
|
-
);
|
|
201
|
-
});
|
|
202
|
-
}, [filteredFiles, syntaxReady]);
|
|
203
|
-
|
|
204
190
|
if (!output && files.length === 0) {
|
|
205
191
|
return (
|
|
206
192
|
<div className="flex items-center gap-2 rounded-lg border border-dashed border-ink/15 px-3 py-2.5 text-[11.5px] text-ink/45">
|
|
@@ -262,49 +248,13 @@ export function SearchTool({ tool }: { tool: ToolCallData }) {
|
|
|
262
248
|
)}
|
|
263
249
|
<div className="max-h-80 space-y-2 overflow-y-auto overscroll-contain pr-0.5">
|
|
264
250
|
{filteredFiles.map((file, fileIdx) => (
|
|
265
|
-
<
|
|
266
|
-
{
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
{file.matchCount > 0 && !isGlobList && (
|
|
273
|
-
<span className="ml-2 shrink-0 rounded bg-ink/5 px-1.5 py-0.2 font-mono text-[9.5px] text-ink/50">
|
|
274
|
-
{file.matchCount} {file.matchCount === 1 ? 'hit' : 'hits'}
|
|
275
|
-
</span>
|
|
276
|
-
)}
|
|
277
|
-
</div>
|
|
278
|
-
|
|
279
|
-
{/* Code Lines inside file — satu scroll horizontal per file, semua row selebar konten terlebar */}
|
|
280
|
-
{file.lines.length > 0 && (
|
|
281
|
-
<div className="overflow-x-auto overscroll-x-contain">
|
|
282
|
-
<div className="w-max min-w-full divide-y divide-ink/[0.04] font-mono text-[11px] leading-relaxed select-text">
|
|
283
|
-
{file.lines.map((line, lineIdx) => (
|
|
284
|
-
<div
|
|
285
|
-
key={lineIdx}
|
|
286
|
-
className={`flex items-start gap-2.5 px-2.5 py-1 transition-colors ${
|
|
287
|
-
line.isMatch ? 'bg-warning/[0.08] text-ink' : 'bg-paper text-ink/60 hover:bg-ink/[0.02]'
|
|
288
|
-
}`}
|
|
289
|
-
>
|
|
290
|
-
<span
|
|
291
|
-
className={`sticky left-0 z-10 flex w-9 shrink-0 select-none items-center justify-end bg-inherit font-mono text-[10px] ${
|
|
292
|
-
line.isMatch ? 'font-semibold text-warning-dark dark:text-warning' : 'text-ink/30'
|
|
293
|
-
}`}
|
|
294
|
-
>
|
|
295
|
-
{line.isMatch && <ChevronRight size={10} className="mr-0.5 text-warning" />}
|
|
296
|
-
{line.lineNum}
|
|
297
|
-
</span>
|
|
298
|
-
<pre
|
|
299
|
-
className="shiki whitespace-pre font-mono"
|
|
300
|
-
dangerouslySetInnerHTML={{ __html: highlightedByFile[fileIdx]?.[lineIdx] || ' ' }}
|
|
301
|
-
/>
|
|
302
|
-
</div>
|
|
303
|
-
))}
|
|
304
|
-
</div>
|
|
305
|
-
</div>
|
|
306
|
-
)}
|
|
307
|
-
</div>
|
|
251
|
+
<FileMatches
|
|
252
|
+
key={fileIdx}
|
|
253
|
+
path={file.path}
|
|
254
|
+
lines={file.lines}
|
|
255
|
+
matchCount={file.matchCount}
|
|
256
|
+
isGlobList={isGlobList}
|
|
257
|
+
/>
|
|
308
258
|
))}
|
|
309
259
|
|
|
310
260
|
{filteredFiles.length === 0 && (
|
package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ArtifactBody.tsx
CHANGED
|
@@ -37,8 +37,7 @@ import { MAX_OUTPUT_LINES, truncateTailLines } from '@/client/components/workspa
|
|
|
37
37
|
import { artifactViewKind } from '@/shared/lib/chat/tool/artifact-view';
|
|
38
38
|
import { getLanguageFromPath } from '@/shared/lib/code/language';
|
|
39
39
|
import { parseNumberedCode } from '@/shared/lib/code/parser';
|
|
40
|
-
import {
|
|
41
|
-
import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
|
|
40
|
+
import { useDeferredHighlightLines } from '@/client/hooks/code/deferred-highlight';
|
|
42
41
|
|
|
43
42
|
interface ArtifactBodyProps {
|
|
44
43
|
/** The whole artifact, verbatim. */
|
|
@@ -103,13 +102,9 @@ export function ArtifactBody({ text, path = '' }: ArtifactBodyProps) {
|
|
|
103
102
|
* and `121|code`) and a blank line keeps its place in the count.
|
|
104
103
|
*/
|
|
105
104
|
function ArtifactCode({ lines, path }: { lines: string[]; path: string }) {
|
|
106
|
-
const syntaxReady = useSyntaxReady();
|
|
107
105
|
const parsed = useMemo(() => parseNumberedCode(lines.join('\n')), [lines]);
|
|
108
106
|
const lang = getLanguageFromPath(path);
|
|
109
|
-
const html =
|
|
110
|
-
() => highlightLines(parsed.cleanCode, lang),
|
|
111
|
-
[parsed.cleanCode, lang, syntaxReady],
|
|
112
|
-
);
|
|
107
|
+
const html = useDeferredHighlightLines(parsed.cleanCode, lang);
|
|
113
108
|
|
|
114
109
|
return (
|
|
115
110
|
<div className="flex items-start font-mono text-[11px] leading-[20px] select-text">
|
package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.test.ts
CHANGED
|
@@ -4,19 +4,20 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* A
|
|
7
|
+
* A diff card's own chrome: the height ceiling and the counts.
|
|
8
8
|
*
|
|
9
|
-
* The bug: the wrapper carried only `max-h-72 overflow-hidden
|
|
10
|
-
*
|
|
11
|
-
* whose height is `auto` resolves to `auto`, so the view grew to
|
|
12
|
-
* content — measured 1521px inside a 288px parent — and `overflow-hidden`
|
|
13
|
-
* clipped the rest. `scrollHeight === clientHeight`, so nothing could scroll
|
|
14
|
-
*
|
|
9
|
+
* The bug the ceiling pins: the wrapper carried only `max-h-72 overflow-hidden`
|
|
10
|
+
* and the view inside resolved `h-full` against its parent. `height: 100%`
|
|
11
|
+
* against a box whose height is `auto` resolves to `auto`, so the view grew to
|
|
12
|
+
* its full content — measured 1521px inside a 288px parent — and `overflow-hidden`
|
|
13
|
+
* clipped the rest. `scrollHeight === clientHeight`, so nothing could scroll.
|
|
14
|
+
* `min-h-0` on the flex column is what gives the child a definite height.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
16
|
+
* The ROWS are not asserted here any more. The card renders through
|
|
17
|
+
* CodeMirror's merge view, which draws only its viewport — and happy-dom
|
|
18
|
+
* computes no layout, so a viewport is empty. Row rendering is covered where it
|
|
19
|
+
* can be observed: `git-panel.e2e.ts` against the real panel, and
|
|
20
|
+
* `patch-sides.test.ts` for the pair the card hands over.
|
|
20
21
|
*/
|
|
21
22
|
|
|
22
23
|
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
|
|
@@ -49,11 +50,11 @@ afterAll(() => {
|
|
|
49
50
|
/** omp's excerpt diff, long enough to overflow any ceiling. */
|
|
50
51
|
const EXCERPT = Array.from({ length: 120 }, (_, i) => `${i + 1}|line ${i + 1}`).join('\n');
|
|
51
52
|
|
|
52
|
-
async function mount() {
|
|
53
|
+
async function mount(text = EXCERPT) {
|
|
53
54
|
container = document.createElement('div');
|
|
54
55
|
document.body.appendChild(container);
|
|
55
56
|
await act(async () => {
|
|
56
|
-
render(h(DiffView, { text
|
|
57
|
+
render(h(DiffView, { text, path: 'src/x.ts' }), container);
|
|
57
58
|
});
|
|
58
59
|
return container;
|
|
59
60
|
}
|
|
@@ -75,25 +76,23 @@ describe('DiffView height ceiling', () => {
|
|
|
75
76
|
expect(wrap.className).toContain('flex');
|
|
76
77
|
expect(wrap.className).toContain('flex-col');
|
|
77
78
|
// Without `min-h-0` a flex item refuses to shrink below its content, the
|
|
78
|
-
// view's
|
|
79
|
+
// view's height resolves to `auto`, and nothing scrolls.
|
|
79
80
|
expect(wrap.className).toContain('min-h-0');
|
|
80
81
|
});
|
|
81
82
|
|
|
82
|
-
test('the
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
expect(
|
|
87
|
-
expect(
|
|
83
|
+
test('the header counts come from the patch, not from git', async () => {
|
|
84
|
+
// ` 1|a / -2|b / +2|c` is an excerpt diff: one deletion, one addition.
|
|
85
|
+
const el = await mount(' 1|a\n-2|b\n+2|c');
|
|
86
|
+
const text = el.textContent ?? '';
|
|
87
|
+
expect(text).toContain('+1');
|
|
88
|
+
expect(text).toContain('\u22121');
|
|
88
89
|
});
|
|
89
90
|
|
|
90
|
-
test('a
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
expect(container.textContent).toContain('c');
|
|
97
|
-
expect(container.textContent).not.toContain('2|b');
|
|
91
|
+
test('a header-only patch falls back to the raw text', async () => {
|
|
92
|
+
const el = await mount('--- a/x\n+++ b/x\n');
|
|
93
|
+
// Nothing to compare means nothing to draw: the reader still sees the bytes
|
|
94
|
+
// the tool returned rather than an empty box.
|
|
95
|
+
expect(el.textContent).toContain('--- a/x');
|
|
96
|
+
expect(el.querySelector('.cm-editor')).toBeNull();
|
|
98
97
|
});
|
|
99
98
|
});
|
|
@@ -17,20 +17,23 @@
|
|
|
17
17
|
* first, so ONE parser serves both — and because the conversion recovers the
|
|
18
18
|
* gutter numbers, the rows get real line numbers instead of `...`.
|
|
19
19
|
*
|
|
20
|
-
* The rendering
|
|
21
|
-
* diff
|
|
22
|
-
*
|
|
20
|
+
* The rendering is the diff panel's own merge view, so a diff in the chat and a
|
|
21
|
+
* diff of the working tree are the same surface rather than two implementations
|
|
22
|
+
* that drift. A card has only the PATCH, not the two documents, so the two sides
|
|
23
|
+
* are reconstructed from the parsed rows — a context row appears on both, a `-`
|
|
24
|
+
* only on the original, a `+` only on the new one.
|
|
23
25
|
*/
|
|
24
26
|
|
|
25
27
|
import { useMemo, useState } from 'preact/hooks';
|
|
26
28
|
import type { ReactNode } from 'preact/compat';
|
|
27
29
|
import { Columns2, Rows3, WrapText } from 'lucide-preact';
|
|
28
|
-
import {
|
|
30
|
+
import { patchSides } from '@/shared/lib/fs/patch-sides';
|
|
29
31
|
import { excerptDiffToUnified, isExcerptDiff } from '@/shared/lib/fs/excerpt-diff';
|
|
30
32
|
import { getLanguageFromPath } from '@/shared/lib/code/language';
|
|
31
33
|
import { CopyButton } from '@/client/components/common/CopyButton';
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
+
import { MergeDiffView } from '@/client/components/workspace/diff-panel/MergeDiffView';
|
|
35
|
+
import { useEditorFontStack } from '@/client/hooks/editor/font';
|
|
36
|
+
import { EDITOR_DEFAULT_FONT_SIZE } from '@/shared/lib/code/editor/typography';
|
|
34
37
|
|
|
35
38
|
/** A small icon-only toolbar button, `aria-pressed` for its own state. */
|
|
36
39
|
function ToggleButton({
|
|
@@ -80,21 +83,19 @@ export function DiffView({ text, path = '', maxHeightClass = 'max-h-72' }: DiffV
|
|
|
80
83
|
// omp's excerpt is converted first; a git unified diff passes through.
|
|
81
84
|
const unified = useMemo(() => (isExcerptDiff(text) ? excerptDiffToUnified(text) : text), [text]);
|
|
82
85
|
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
);
|
|
86
|
+
// A card has a PATCH and no documents, so the two sides are rebuilt from it:
|
|
87
|
+
// context on both, `-` on the original, `+` on the new one. That is the same
|
|
88
|
+
// alignment the merge view would compute from two files, restricted to the
|
|
89
|
+
// hunks the patch carries.
|
|
90
|
+
const sides = useMemo(() => patchSides(unified), [unified]);
|
|
91
|
+
const editorFontFamily = useEditorFontStack();
|
|
89
92
|
|
|
90
93
|
// The excerpt's header carries no filename, so the caller's path is the only
|
|
91
|
-
// name the block can show
|
|
92
|
-
const fileLabel =
|
|
93
|
-
|
|
94
|
-
: path.split('/').pop() ?? '';
|
|
95
|
-
const language = getLanguageFromPath(path || parsed.splitRows[0]?.right?.text || '');
|
|
94
|
+
// name the block can show — unless the patch names itself.
|
|
95
|
+
const fileLabel = sides.hasHunk ? '' : (path.split('/').pop() ?? '');
|
|
96
|
+
const language = getLanguageFromPath(path || sides.next.split('\n')[0] || '');
|
|
96
97
|
|
|
97
|
-
if (
|
|
98
|
+
if (sides.rows === 0) {
|
|
98
99
|
return (
|
|
99
100
|
<pre className="max-h-48 overflow-auto rounded-lg border border-ink/8 bg-paper p-3 font-mono text-[11px] leading-relaxed whitespace-pre-wrap break-all text-ink/80 select-text">
|
|
100
101
|
{text}
|
|
@@ -109,8 +110,8 @@ export function DiffView({ text, path = '', maxHeightClass = 'max-h-72' }: DiffV
|
|
|
109
110
|
<span className="truncate font-mono text-[10.5px] text-ink/60">{fileLabel}</span>
|
|
110
111
|
)}
|
|
111
112
|
<span className="flex items-center gap-1.5 font-mono text-[10px]">
|
|
112
|
-
<span className="text-success">+{
|
|
113
|
-
<span className="text-error">{'\u2212'}{
|
|
113
|
+
<span className="text-success">+{sides.additions}</span>
|
|
114
|
+
<span className="text-error">{'\u2212'}{sides.deletions}</span>
|
|
114
115
|
</span>
|
|
115
116
|
<span className="ml-auto flex items-center gap-1">
|
|
116
117
|
<ToggleButton
|
|
@@ -150,12 +151,24 @@ export function DiffView({ text, path = '', maxHeightClass = 'max-h-72' }: DiffV
|
|
|
150
151
|
own content and this wrapper only bounds it. */}
|
|
151
152
|
<div className={`flex ${maxHeightClass} min-h-0 flex-col overflow-hidden`}>
|
|
152
153
|
{/* No tab key: this diff lives inside a chat card, and the timeline —
|
|
153
|
-
not the card — is what the reader scrolls.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
154
|
+
not the card — is what the reader scrolls. No counts either: the
|
|
155
|
+
header above already prints them from the patch. */}
|
|
156
|
+
<MergeDiffView
|
|
157
|
+
original={sides.original}
|
|
158
|
+
next={sides.next}
|
|
159
|
+
language={language}
|
|
160
|
+
viewMode={split ? 'split' : 'unified'}
|
|
161
|
+
// Collapsed gaps: the reconstructed sides carry blank padding up to
|
|
162
|
+
// each hunk's real line, so showing every row would draw thousands of
|
|
163
|
+
// empty lines between two changes.
|
|
164
|
+
contentMode="diff"
|
|
165
|
+
wordWrap={wordWrap}
|
|
166
|
+
ignoreWhitespace={false}
|
|
167
|
+
tabKey={null}
|
|
168
|
+
empty={false}
|
|
169
|
+
fontFamily={editorFontFamily}
|
|
170
|
+
fontSize={EDITOR_DEFAULT_FONT_SIZE}
|
|
171
|
+
/>
|
|
159
172
|
</div>
|
|
160
173
|
</div>
|
|
161
174
|
);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { useMemo } from 'preact/hooks';
|
|
2
2
|
import { getLanguageFromPath } from '@/shared/lib/code/language';
|
|
3
|
-
import {
|
|
4
|
-
import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
|
|
3
|
+
import { useDeferredHighlightLines } from '@/client/hooks/code/deferred-highlight';
|
|
5
4
|
import { CopyButton } from '@/client/components/common/CopyButton';
|
|
6
5
|
import { MAX_OUTPUT_LINES, truncateTailLines } from '@/client/components/workspace/chat-timeline/tool-renderers/shared/truncate';
|
|
7
6
|
|
|
@@ -45,16 +44,17 @@ interface ExcerptCodeProps {
|
|
|
45
44
|
export function ExcerptCode({ lines, path = '', tag }: ExcerptCodeProps) {
|
|
46
45
|
const rows = useMemo(() => splitExcerptRows(lines), [lines]);
|
|
47
46
|
const lang = getLanguageFromPath(path);
|
|
48
|
-
const syntaxReady = useSyntaxReady();
|
|
49
47
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
// Only the tail is rendered, so only the tail is highlighted — tokenizing the
|
|
49
|
+
// dropped head would be work whose output is thrown away. The tokenizer itself
|
|
50
|
+
// runs off the expand that asked for it (see `deferred-highlight`).
|
|
51
|
+
const { code: visibleCode, hidden } = useMemo(() => {
|
|
53
52
|
const code = rows.map((row) => row.code);
|
|
54
53
|
const truncated = truncateTailLines(code.join('\n'), MAX_OUTPUT_LINES);
|
|
55
54
|
const visible = truncated.skipped > 0 ? code.slice(truncated.skipped) : code;
|
|
56
|
-
return {
|
|
57
|
-
}, [rows
|
|
55
|
+
return { code: visible.join('\n'), hidden: truncated.skipped };
|
|
56
|
+
}, [rows]);
|
|
57
|
+
const html = useDeferredHighlightLines(visibleCode, lang);
|
|
58
58
|
|
|
59
59
|
const visibleRows = hidden > 0 ? rows.slice(hidden) : rows;
|
|
60
60
|
|
package/src/client/components/workspace/chat-timeline/tool-renderers/shared/HighlightedCode.tsx
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A `<pre>` whose contents are tokenized off the render that asked for them.
|
|
8
|
+
*
|
|
9
|
+
* A hook cannot be called inside a `render` callback (Bash's `OutputWindow` and
|
|
10
|
+
* SearchTool's per-file rows both draw code from one), so the deferred highlight
|
|
11
|
+
* lives in this component instead: a caller hands over the code and the
|
|
12
|
+
* language, and the markup arrives one idle slice later.
|
|
13
|
+
*
|
|
14
|
+
* The `shiki` class is on the `<pre>` from the first paint, so the plain-text
|
|
15
|
+
* frame and the tokenized one are the same element — the swap changes colour
|
|
16
|
+
* and nothing else.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { useDeferredHighlightLines } from '@/client/hooks/code/deferred-highlight';
|
|
20
|
+
|
|
21
|
+
interface HighlightedCodeProps {
|
|
22
|
+
code: string;
|
|
23
|
+
language: string;
|
|
24
|
+
className?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function HighlightedCode({ code, language, className = '' }: HighlightedCodeProps) {
|
|
28
|
+
const lines = useDeferredHighlightLines(code, language);
|
|
29
|
+
return (
|
|
30
|
+
<pre
|
|
31
|
+
className={`shiki ${className}`}
|
|
32
|
+
dangerouslySetInnerHTML={{ __html: lines.join('\n') }}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
}
|
package/src/client/components/workspace/chat-timeline/tool-renderers/shared/JsonCodeBlock.tsx
CHANGED
|
@@ -2,8 +2,7 @@ import type { ReactNode } from 'preact/compat';
|
|
|
2
2
|
import { useMemo } from 'preact/hooks';
|
|
3
3
|
import { Braces } from 'lucide-preact';
|
|
4
4
|
import { CopyButton } from '@/client/components/common/CopyButton';
|
|
5
|
-
import {
|
|
6
|
-
import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
|
|
5
|
+
import { useDeferredHighlight } from '@/client/hooks/code/deferred-highlight';
|
|
7
6
|
|
|
8
7
|
interface JsonCodeBlockProps {
|
|
9
8
|
/** Formatted JSON string to render */
|
|
@@ -33,8 +32,9 @@ export function JsonCodeBlock({
|
|
|
33
32
|
}: JsonCodeBlockProps) {
|
|
34
33
|
const effectiveMaxHeight = maxHeightClass || (compact ? 'max-h-40' : 'max-h-56');
|
|
35
34
|
const lines = useMemo(() => jsonString.split('\n'), [jsonString]);
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
// `json` is an eager grammar, so the memo usually answers on the first paint;
|
|
36
|
+
// a cold card still draws the text immediately and colours it a beat later.
|
|
37
|
+
const highlighted = useDeferredHighlight(jsonString, 'json');
|
|
38
38
|
|
|
39
39
|
return (
|
|
40
40
|
<div className={`overflow-hidden ${compact ? 'rounded-md' : 'rounded-lg'} border border-ink/8 bg-paper ${className}`}>
|