@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
package/src/client/components/workspace/chat-timeline/tool-renderers/hashline-patch/index.tsx
CHANGED
|
@@ -13,8 +13,7 @@
|
|
|
13
13
|
import { useMemo } from 'preact/hooks';
|
|
14
14
|
import type { HashlineSection, HashlineVerb } from '@/shared/lib/omp/session/hashline-patch';
|
|
15
15
|
import { getLanguageFromPath } from '@/shared/lib/code/language';
|
|
16
|
-
import {
|
|
17
|
-
import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
|
|
16
|
+
import { useDeferredHighlightLines } from '@/client/hooks/code/deferred-highlight';
|
|
18
17
|
|
|
19
18
|
/** A hashline `CUT` removes lines the model already saw, so it reads as the
|
|
20
19
|
* destructive half of the patch; `PUT` carries the replacement body. */
|
|
@@ -22,16 +21,38 @@ function opTone(verb: HashlineVerb): string {
|
|
|
22
21
|
return verb === 'CUT' ? 'bg-error/10 text-error' : 'bg-success/10 text-success';
|
|
23
22
|
}
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
24
|
+
/**
|
|
25
|
+
* One op's added lines. A component rather than a call in the parent's map: the
|
|
26
|
+
* deferred highlight is a hook, and a hook cannot run inside a callback — the
|
|
27
|
+
* parent used to tokenize EVERY op of EVERY file in one synchronous pass, which
|
|
28
|
+
* is the whole cost this card was paying on expand.
|
|
29
|
+
*/
|
|
30
|
+
function OpBody({ body, lang }: { body: string[]; lang: string }) {
|
|
31
|
+
const html = useDeferredHighlightLines(useMemo(() => body.join('\n'), [body]), lang);
|
|
32
|
+
return (
|
|
33
|
+
<>
|
|
34
|
+
{body.map((row, rowIndex) => {
|
|
35
|
+
const markup = html[rowIndex] || '';
|
|
36
|
+
return markup ? (
|
|
37
|
+
<div
|
|
38
|
+
key={rowIndex}
|
|
39
|
+
className="shiki whitespace-pre bg-success/[0.05] px-2.5 font-mono text-[11px] leading-relaxed"
|
|
40
|
+
dangerouslySetInnerHTML={{ __html: markup }}
|
|
41
|
+
/>
|
|
42
|
+
) : (
|
|
43
|
+
<div
|
|
44
|
+
key={rowIndex}
|
|
45
|
+
className="whitespace-pre bg-success/[0.05] px-2.5 font-mono text-[11px] leading-relaxed text-success/90"
|
|
46
|
+
>
|
|
47
|
+
{row}
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
})}
|
|
51
|
+
</>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
34
54
|
|
|
55
|
+
export function HashlinePatch({ sections }: { sections: HashlineSection[] }) {
|
|
35
56
|
return (
|
|
36
57
|
<div className="space-y-1.5">
|
|
37
58
|
<div className="flex items-center justify-between text-[9.5px] font-semibold uppercase tracking-[0.14em] text-ink/40">
|
|
@@ -43,7 +64,7 @@ export function HashlinePatch({ sections }: { sections: HashlineSection[] }) {
|
|
|
43
64
|
)}
|
|
44
65
|
</div>
|
|
45
66
|
<div className="overflow-hidden rounded-lg border border-ink/8">
|
|
46
|
-
{sections.map((section
|
|
67
|
+
{sections.map((section) => (
|
|
47
68
|
<div key={section.path} className="border-b border-ink/6 last:border-b-0">
|
|
48
69
|
<div className="flex min-w-0 items-center gap-2 bg-canvas/40 px-2.5 py-1.5">
|
|
49
70
|
<span className="truncate font-mono text-[10.5px] font-medium text-ink">{section.path}</span>
|
|
@@ -65,23 +86,7 @@ export function HashlinePatch({ sections }: { sections: HashlineSection[] }) {
|
|
|
65
86
|
</span>
|
|
66
87
|
{op.spec && <span className="font-mono text-[10.5px] text-ink/70">{op.spec}</span>}
|
|
67
88
|
</div>
|
|
68
|
-
{op.body.
|
|
69
|
-
const html = htmlByOp[secIdx]?.[index]?.[rowIndex] || '';
|
|
70
|
-
return html ? (
|
|
71
|
-
<div
|
|
72
|
-
key={rowIndex}
|
|
73
|
-
className="shiki whitespace-pre bg-success/[0.05] px-2.5 font-mono text-[11px] leading-relaxed"
|
|
74
|
-
dangerouslySetInnerHTML={{ __html: html }}
|
|
75
|
-
/>
|
|
76
|
-
) : (
|
|
77
|
-
<div
|
|
78
|
-
key={rowIndex}
|
|
79
|
-
className="whitespace-pre bg-success/[0.05] px-2.5 font-mono text-[11px] leading-relaxed text-success/90"
|
|
80
|
-
>
|
|
81
|
-
{row}
|
|
82
|
-
</div>
|
|
83
|
-
);
|
|
84
|
-
})}
|
|
89
|
+
<OpBody body={op.body} lang={getLanguageFromPath(section.path)} />
|
|
85
90
|
</div>
|
|
86
91
|
))}
|
|
87
92
|
</div>
|
|
@@ -2,8 +2,7 @@ import { useMemo } from 'preact/hooks';
|
|
|
2
2
|
import { ArrowRight, CheckCircle2, Clock, Wand2 } from 'lucide-preact';
|
|
3
3
|
import type { ToolCallData } from '@/shared/types';
|
|
4
4
|
import { getLanguageFromPath } from '@/shared/lib/code/language';
|
|
5
|
-
import {
|
|
6
|
-
import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
|
|
5
|
+
import { useDeferredHighlightLines } from '@/client/hooks/code/deferred-highlight';
|
|
7
6
|
|
|
8
7
|
interface AstEditItem {
|
|
9
8
|
file?: unknown;
|
|
@@ -51,14 +50,17 @@ export function AstEdit({ tool }: { tool: ToolCallData }) {
|
|
|
51
50
|
? details.changes
|
|
52
51
|
: parseAstChanges(output);
|
|
53
52
|
|
|
54
|
-
const syntaxReady = useSyntaxReady();
|
|
55
53
|
const diffLang = getLanguageFromPath(paths[0]);
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
54
|
+
const diffCode = useMemo(
|
|
55
|
+
() =>
|
|
56
|
+
diffLines
|
|
57
|
+
.map((line: string) =>
|
|
58
|
+
line.startsWith('+') || line.startsWith('-') || line.startsWith(' ') ? line.slice(1) : line
|
|
59
|
+
)
|
|
60
|
+
.join('\n'),
|
|
61
|
+
[diffLines],
|
|
62
|
+
);
|
|
63
|
+
const diffHtml = useDeferredHighlightLines(diffCode, diffLang);
|
|
62
64
|
|
|
63
65
|
// If Staged Proposal Mode (oh-my-pi xd://ast_edit)
|
|
64
66
|
if (isStaged || ops.length > 0 || diffLines.length > 0) {
|
|
@@ -5,8 +5,9 @@ import { bashServiceOf, daemonTone, procUptime } from '@/shared/lib/omp/session/
|
|
|
5
5
|
import { CopyButton } from '@/client/components/common/CopyButton';
|
|
6
6
|
import { stripAnsiCodes } from '@/shared/lib/code/ansi';
|
|
7
7
|
import { isCodeLike } from '@/shared/lib/code/language';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { tryParseJson } from '@/shared/lib/code/syntax-highlight';
|
|
9
|
+
import { useDeferredHighlight } from '@/client/hooks/code/deferred-highlight';
|
|
10
|
+
import { HighlightedCode } from '@/client/components/workspace/chat-timeline/tool-renderers/shared/HighlightedCode';
|
|
10
11
|
import { JsonCodeBlock } from '@/client/components/workspace/chat-timeline/tool-renderers/shared/JsonCodeBlock';
|
|
11
12
|
import { OutputWindow } from '@/client/components/workspace/chat-timeline/tool-renderers/shared/OutputWindow';
|
|
12
13
|
import { MAX_OUTPUT_LINES, truncateTailLines } from '@/client/components/workspace/chat-timeline/tool-renderers/shared/truncate';
|
|
@@ -75,8 +76,9 @@ export function Bash({ tool }: { tool: ToolCallData }) {
|
|
|
75
76
|
() => (jsonResult.isValid && jsonResult.pretty ? truncateTailLines(jsonResult.pretty, MAX_OUTPUT_LINES) : null),
|
|
76
77
|
[jsonResult]
|
|
77
78
|
);
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
// Deferred: a command line is short, but the grammar's first compile is not,
|
|
80
|
+
// and this card opens while a run streams.
|
|
81
|
+
const highlightedCommand = useDeferredHighlight(command, 'bash');
|
|
80
82
|
|
|
81
83
|
const service = bashServiceOf(tool);
|
|
82
84
|
const serviceTone = daemonTone(service?.state);
|
|
@@ -181,7 +183,7 @@ export function Bash({ tool }: { tool: ToolCallData }) {
|
|
|
181
183
|
<div className="flex items-start gap-2.5 px-3 py-2 font-mono text-[11.5px] leading-relaxed">
|
|
182
184
|
<span className="select-none font-bold text-ink/40">$</span>
|
|
183
185
|
<pre
|
|
184
|
-
className="min-w-0 flex-1 overflow-x-auto whitespace-pre-wrap break-all text-ink/90 select-text"
|
|
186
|
+
className="shiki min-w-0 flex-1 overflow-x-auto whitespace-pre-wrap break-all text-ink/90 select-text"
|
|
185
187
|
dangerouslySetInnerHTML={{ __html: highlightedCommand }}
|
|
186
188
|
/>
|
|
187
189
|
</div>
|
|
@@ -242,9 +244,10 @@ export function Bash({ tool }: { tool: ToolCallData }) {
|
|
|
242
244
|
className="max-h-72 overflow-y-auto overscroll-contain p-3 font-mono text-[11px] leading-relaxed select-text"
|
|
243
245
|
render={(visible) =>
|
|
244
246
|
outputIsCode ? (
|
|
245
|
-
<
|
|
247
|
+
<HighlightedCode
|
|
248
|
+
code={visible.join('\n')}
|
|
249
|
+
language={outputLang}
|
|
246
250
|
className="overflow-x-auto whitespace-pre text-ink/85 leading-relaxed"
|
|
247
|
-
dangerouslySetInnerHTML={{ __html: highlightLines(visible.join('\n'), outputLang).join('\n') }}
|
|
248
251
|
/>
|
|
249
252
|
) : (
|
|
250
253
|
visible.map((line, idx) => {
|
|
@@ -9,8 +9,7 @@ import { ExcerptCode } from '@/client/components/workspace/chat-timeline/tool-re
|
|
|
9
9
|
import { ToolPanelHeader } from '@/client/components/workspace/chat-timeline/tool-renderers/shared/ToolPanelHeader';
|
|
10
10
|
import { parseEditOutput } from '@/shared/lib/chat/excerpt';
|
|
11
11
|
import { getLanguageFromPath } from '@/shared/lib/code/language';
|
|
12
|
-
import {
|
|
13
|
-
import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
|
|
12
|
+
import { useDeferredHighlight } from '@/client/hooks/code/deferred-highlight';
|
|
14
13
|
import { MAX_OUTPUT_LINES, truncateTailLines } from '@/client/components/workspace/chat-timeline/tool-renderers/shared/truncate';
|
|
15
14
|
import { outputMarkdown, readToolOutput } from '@/shared/lib/chat/tool-output';
|
|
16
15
|
import { hashlinePatchFromArgs } from '@/shared/lib/omp/session/hashline-patch';
|
|
@@ -146,13 +145,31 @@ export function Edit({ tool }: { tool: ToolCallData }) {
|
|
|
146
145
|
|
|
147
146
|
const truncatedOld = useMemo(() => (oldString ? truncateTailLines(oldString, MAX_OUTPUT_LINES) : null), [oldString]);
|
|
148
147
|
const truncatedNew = useMemo(() => (newString ? truncateTailLines(newString, MAX_OUTPUT_LINES) : null), [newString]);
|
|
149
|
-
const syntaxReady = useSyntaxReady();
|
|
150
|
-
const highlightedOld = useMemo(() => (truncatedOld ? highlightCode(truncatedOld.text, lang) : ''), [truncatedOld, lang, syntaxReady]);
|
|
151
|
-
const highlightedNew = useMemo(() => (truncatedNew ? highlightCode(truncatedNew.text, lang) : ''), [truncatedNew, lang, syntaxReady]);
|
|
152
148
|
const previewLineCount = useMemo(() => (newContent ? newContent.split('\n').length : 0), [newContent]);
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Which of the three blocks this card actually DRAWS.
|
|
152
|
+
*
|
|
153
|
+
* They are mutually exclusive in the markup below — a card with a diff shows
|
|
154
|
+
* neither the replacement pair nor the write preview — and the highlights
|
|
155
|
+
* used to run for all three on every render regardless: three tokenizer passes
|
|
156
|
+
* to paint one block, measured as the largest single contributor to an `edit`
|
|
157
|
+
* card's expand. Gating the input on the branch that renders it makes the two
|
|
158
|
+
* unused passes `''`, which the hook treats as nothing to compute.
|
|
159
|
+
*/
|
|
160
|
+
const showsReplacement = !diffText && Boolean(oldString || newString);
|
|
161
|
+
const showsPreview = !diffText && !oldString && !newString && Boolean(newContent);
|
|
162
|
+
const highlightedOld = useDeferredHighlight(
|
|
163
|
+
showsReplacement && truncatedOld ? truncatedOld.text : '',
|
|
164
|
+
lang,
|
|
165
|
+
);
|
|
166
|
+
const highlightedNew = useDeferredHighlight(
|
|
167
|
+
showsReplacement && truncatedNew ? truncatedNew.text : '',
|
|
168
|
+
lang,
|
|
169
|
+
);
|
|
170
|
+
const highlightedPreview = useDeferredHighlight(
|
|
171
|
+
showsPreview && newContent ? newContent.split('\n').slice(0, 80).join('\n') : '',
|
|
172
|
+
lang,
|
|
156
173
|
);
|
|
157
174
|
|
|
158
175
|
// omp answers an edit with the excerpt it touched, not with prose: numbered
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The `eval` card body.
|
|
8
|
+
*
|
|
9
|
+
* The alignment defect is pinned here, and it was reported from a live
|
|
10
|
+
* transcript rather than found by reading the code: **the gutter numbers
|
|
11
|
+
* drifted off the code.** The gutter row was `text-[10px]` while the code was
|
|
12
|
+
* `text-[11px]`, and both inherited `leading-relaxed` — a ratio, so the row box
|
|
13
|
+
* was 16.25px against the code's 17.875px and every number sat higher than its
|
|
14
|
+
* line by a growing amount (measured after a reload: line 12's number was a
|
|
15
|
+
* full line above its code). The two sides now share one explicit line box,
|
|
16
|
+
* which is what the alignment case asserts — by comparing the two elements' own
|
|
17
|
+
* leading class, so a future edit that changes one side is caught instead of
|
|
18
|
+
* copied.
|
|
19
|
+
*
|
|
20
|
+
* The result cases cover the OTHER half of the same report ("output does not
|
|
21
|
+
* render while streaming"): the panel does read `tool.output` as it arrives, so
|
|
22
|
+
* what made a live eval look empty was the card being folded — pinned in
|
|
23
|
+
* `hooks/chat/timeline/tool-open.test.ts`, where a running eval opens itself.
|
|
24
|
+
*
|
|
25
|
+
* Rendered with `h()` against happy-dom, following `panels/Read.test.ts`.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from 'bun:test';
|
|
29
|
+
import { Window } from 'happy-dom';
|
|
30
|
+
import { h, render } from 'preact';
|
|
31
|
+
import { act } from 'preact/test-utils';
|
|
32
|
+
import { Eval } from '@/client/components/workspace/chat-timeline/tool-renderers/panels/Eval';
|
|
33
|
+
import type { ToolCallData } from '@/shared/types';
|
|
34
|
+
|
|
35
|
+
const DOM_GLOBALS = ['window', 'document', 'navigator', 'Node', 'Element', 'HTMLElement', 'Event', 'MouseEvent', 'KeyboardEvent'] as const;
|
|
36
|
+
const nativeGlobals: Partial<Record<(typeof DOM_GLOBALS)[number], unknown>> = {};
|
|
37
|
+
|
|
38
|
+
let container: HTMLElement;
|
|
39
|
+
|
|
40
|
+
beforeAll(() => {
|
|
41
|
+
const win = new Window({ url: 'http://localhost' });
|
|
42
|
+
const target = globalThis as unknown as Record<string, unknown>;
|
|
43
|
+
for (const key of DOM_GLOBALS) {
|
|
44
|
+
nativeGlobals[key] = target[key];
|
|
45
|
+
target[key] = (win as unknown as Record<string, unknown>)[key];
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterAll(() => {
|
|
50
|
+
const target = globalThis as unknown as Record<string, unknown>;
|
|
51
|
+
for (const key of DOM_GLOBALS) {
|
|
52
|
+
if (nativeGlobals[key] === undefined) delete target[key];
|
|
53
|
+
else target[key] = nativeGlobals[key];
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
container = document.createElement('div');
|
|
59
|
+
document.body.appendChild(container);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
function mount(tool: ToolCallData): HTMLElement {
|
|
63
|
+
act(() => {
|
|
64
|
+
render(h(Eval, { tool }), container);
|
|
65
|
+
});
|
|
66
|
+
return container;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const CODE = ['const a = 1;', 'const b = 2;', 'console.log(a + b);'].join('\n');
|
|
70
|
+
|
|
71
|
+
function evalTool(overrides: Partial<ToolCallData> = {}): ToolCallData {
|
|
72
|
+
return {
|
|
73
|
+
id: 'call_1',
|
|
74
|
+
type: 'eval',
|
|
75
|
+
name: 'eval',
|
|
76
|
+
title: 'Sum two numbers',
|
|
77
|
+
input: { language: 'js', code: CODE },
|
|
78
|
+
status: 'running',
|
|
79
|
+
...overrides,
|
|
80
|
+
} as ToolCallData;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The `leading-[…]` token a node carries, or null. */
|
|
84
|
+
function leadingOf(el: Element): string | null {
|
|
85
|
+
return /(?:^|\s)(leading-\[\d+px\])/.exec(el.className)?.[1] ?? null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The code card: the gutter/pre pair whose pre holds the SCRIPT. */
|
|
89
|
+
function codeCard(root: HTMLElement): { gutter: Element; pre: Element } {
|
|
90
|
+
const gutter = root.querySelectorAll('.sticky')[0];
|
|
91
|
+
const pre = gutter?.parentElement?.querySelector('pre');
|
|
92
|
+
if (!gutter || !pre) throw new Error('code card not rendered');
|
|
93
|
+
return { gutter, pre };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
describe('Eval — result rendering', () => {
|
|
97
|
+
test('a running call renders the output it has so far', () => {
|
|
98
|
+
// omp's `tool_execution_update` carries the cell's output SO FAR, and the
|
|
99
|
+
// panel reads it from `tool.output` — which is what the fold writes from
|
|
100
|
+
// the frame's `content`. Verified against a real omp run: `content` and
|
|
101
|
+
// `details.cells[].output` are byte-identical on every update frame.
|
|
102
|
+
const root = mount(evalTool({ output: 'live 1\nlive 2' }));
|
|
103
|
+
const text = root.textContent ?? '';
|
|
104
|
+
expect(text).toContain('live 1');
|
|
105
|
+
expect(text).toContain('live 2');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('renders only the script while a running call has produced nothing yet', () => {
|
|
109
|
+
const root = mount(evalTool({ output: '' }));
|
|
110
|
+
expect(root.querySelectorAll('pre')).toHaveLength(1);
|
|
111
|
+
expect(root.textContent).toContain('Script Evaluation');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('Eval — gutter aligns with the code', () => {
|
|
116
|
+
test('the gutter and the code share one line box', () => {
|
|
117
|
+
const { gutter, pre } = codeCard(mount(evalTool()));
|
|
118
|
+
const gutterLeading = leadingOf(gutter);
|
|
119
|
+
expect(gutterLeading).not.toBeNull();
|
|
120
|
+
// The bug: an inherited RATIO (`leading-relaxed`) resolved against two
|
|
121
|
+
// different font sizes, so the boxes differed by 1.625px per line.
|
|
122
|
+
expect(leadingOf(pre)).toBe(gutterLeading);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('one gutter row per code line', () => {
|
|
126
|
+
const { gutter } = codeCard(mount(evalTool()));
|
|
127
|
+
expect(gutter.children).toHaveLength(CODE.split('\n').length);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('the JSON result block aligns the same way', () => {
|
|
131
|
+
// The pretty-printed result draws its own gutter, and it had the same
|
|
132
|
+
// mismatch (plus a `py-2` against the code's `p-2.5`).
|
|
133
|
+
const root = mount(evalTool({ output: '{"a": 1, "b": [2, 3]}', status: 'success' }));
|
|
134
|
+
const gutters = root.querySelectorAll('.sticky');
|
|
135
|
+
const jsonGutter = gutters[1];
|
|
136
|
+
expect(jsonGutter).toBeDefined();
|
|
137
|
+
const pre = jsonGutter.parentElement?.querySelector('pre');
|
|
138
|
+
expect(pre).not.toBeNull();
|
|
139
|
+
// Explicit on BOTH, not merely equal: `null === null` would pass while both
|
|
140
|
+
// sides inherit a ratio against different font sizes, which is the bug.
|
|
141
|
+
expect(leadingOf(jsonGutter)).not.toBeNull();
|
|
142
|
+
expect(leadingOf(pre as Element)).toBe(leadingOf(jsonGutter));
|
|
143
|
+
// `{"a": 1, "b": [2, 3]}` pretty-prints to 7 lines.
|
|
144
|
+
expect(jsonGutter.children).toHaveLength(7);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -2,9 +2,27 @@ import { useMemo } from 'preact/hooks';
|
|
|
2
2
|
import { AlertCircle, Ban, Clock, Code2, Play } from 'lucide-preact';
|
|
3
3
|
import type { ToolCallData } from '@/shared/types';
|
|
4
4
|
import { CopyButton } from '@/client/components/common/CopyButton';
|
|
5
|
-
import {
|
|
5
|
+
import { tryParseJson } from '@/shared/lib/code/syntax-highlight';
|
|
6
6
|
import { MAX_OUTPUT_LINES, truncateTailLines } from '@/client/components/workspace/chat-timeline/tool-renderers/shared/truncate';
|
|
7
|
-
import {
|
|
7
|
+
import { useDeferredHighlight } from '@/client/hooks/code/deferred-highlight';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The line box both sides of a code card share — gutter row and code line.
|
|
11
|
+
*
|
|
12
|
+
* A gutter row and the code line beside it are separate elements, so they agree
|
|
13
|
+
* only when their line boxes do. They did not: the gutter was `text-[10px]`
|
|
14
|
+
* while the code was `text-[11px]`, and both inherited `leading-relaxed` — a
|
|
15
|
+
* RATIO, so 10 × 1.625 = 16.25px against 11 × 1.625 = 17.875px. Each number
|
|
16
|
+
* then sat 1.6px higher than its line, and by the tenth line the number was a
|
|
17
|
+
* whole line above the code it labels (measured in the browser after a reload:
|
|
18
|
+
* `gutterRowH 16.25`, `preLineHeight 17.875`, `firstRowOffset 10`).
|
|
19
|
+
*
|
|
20
|
+
* The box is stated ONCE and applied to both containers, because a `<div>`
|
|
21
|
+
* holding one line takes its height from `line-height` — no per-row `h-[…]`
|
|
22
|
+
* that could drift from the constant. This is the same shape `Read.tsx` and
|
|
23
|
+
* `ExcerptCode` use.
|
|
24
|
+
*/
|
|
25
|
+
const LINE_BOX = 'leading-[18px]';
|
|
8
26
|
|
|
9
27
|
/** Panel khusus untuk tool `eval` — eksekusi kode script, browser eval, dsb. */
|
|
10
28
|
export function Eval({ tool }: { tool: ToolCallData }) {
|
|
@@ -25,23 +43,20 @@ export function Eval({ tool }: { tool: ToolCallData }) {
|
|
|
25
43
|
const isError = tool.status === 'error' || output.includes('ToolError:') || output.includes('Command exited with code');
|
|
26
44
|
const isAborted = output.includes('Command aborted') || tool.status === 'aborted';
|
|
27
45
|
|
|
28
|
-
const syntaxReady = useSyntaxReady();
|
|
29
|
-
|
|
30
46
|
const jsonResult = useMemo(
|
|
31
47
|
() => (!isError && !isAborted ? tryParseJson(output) : { isValid: false }),
|
|
32
48
|
[output, isError, isAborted]
|
|
33
49
|
);
|
|
34
50
|
|
|
35
51
|
const truncatedCode = useMemo(() => truncateTailLines(code, MAX_OUTPUT_LINES), [code]);
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
);
|
|
52
|
+
// A cell's script is short but the grammar compile is not, and an eval card
|
|
53
|
+
// opens while its own output streams.
|
|
54
|
+
const highlightedCode = useDeferredHighlight(truncatedCode.text, 'javascript');
|
|
40
55
|
// The result is shown whole: an eval's output IS the answer, so hiding its
|
|
41
56
|
// head behind a "… earlier lines hidden" line misreports what the cell returned.
|
|
42
|
-
const highlightedJson =
|
|
43
|
-
|
|
44
|
-
|
|
57
|
+
const highlightedJson = useDeferredHighlight(
|
|
58
|
+
jsonResult.isValid && jsonResult.pretty ? jsonResult.pretty : '',
|
|
59
|
+
'json',
|
|
45
60
|
);
|
|
46
61
|
|
|
47
62
|
return (
|
|
@@ -78,14 +93,14 @@ export function Eval({ tool }: { tool: ToolCallData }) {
|
|
|
78
93
|
… {truncatedCode.skipped} earlier lines hidden
|
|
79
94
|
</div>
|
|
80
95
|
)}
|
|
81
|
-
<div className=
|
|
82
|
-
<div className=
|
|
96
|
+
<div className={`flex max-h-56 items-start overflow-x-auto font-mono text-[11px] ${LINE_BOX} select-text`}>
|
|
97
|
+
<div className={`sticky left-0 flex-shrink-0 select-none border-r border-ink/6 bg-canvas/50 py-2.5 pl-2.5 pr-2 text-right text-[10px] ${LINE_BOX} tabular-nums text-ink/25`}>
|
|
83
98
|
{truncatedCode.text.split('\n').map((_, idx) => (
|
|
84
99
|
<div key={idx}>{idx + 1}</div>
|
|
85
100
|
))}
|
|
86
101
|
</div>
|
|
87
102
|
<pre
|
|
88
|
-
className=
|
|
103
|
+
className={`flex-1 overflow-x-auto p-2.5 whitespace-pre ${LINE_BOX} text-ink/85 font-mono`}
|
|
89
104
|
dangerouslySetInnerHTML={{ __html: highlightedCode }}
|
|
90
105
|
/>
|
|
91
106
|
</div>
|
|
@@ -157,14 +172,14 @@ export function Eval({ tool }: { tool: ToolCallData }) {
|
|
|
157
172
|
</div>
|
|
158
173
|
|
|
159
174
|
{jsonResult.isValid && jsonResult.pretty ? (
|
|
160
|
-
<div className=
|
|
161
|
-
<div className=
|
|
175
|
+
<div className={`flex max-h-56 items-start overflow-x-auto font-mono text-[11px] ${LINE_BOX} select-text`}>
|
|
176
|
+
<div className={`sticky left-0 flex-shrink-0 select-none border-r border-ink/6 bg-canvas/50 py-2.5 pl-2.5 pr-2 text-right text-[10px] ${LINE_BOX} tabular-nums text-ink/25`}>
|
|
162
177
|
{jsonResult.pretty.split('\n').map((_, idx) => (
|
|
163
178
|
<div key={idx}>{idx + 1}</div>
|
|
164
179
|
))}
|
|
165
180
|
</div>
|
|
166
181
|
<pre
|
|
167
|
-
className=
|
|
182
|
+
className={`flex-1 overflow-x-auto p-2.5 whitespace-pre ${LINE_BOX} text-ink/85 font-mono`}
|
|
168
183
|
dangerouslySetInnerHTML={{ __html: highlightedJson }}
|
|
169
184
|
/>
|
|
170
185
|
</div>
|
package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.test.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* One file's matches inside a `grep` card.
|
|
8
|
+
*
|
|
9
|
+
* The card used to mount every line of every file group inside a `max-h-80`
|
|
10
|
+
* scroller, and tokenize them all in one pass. Two properties are pinned here,
|
|
11
|
+
* because both were the measured cost of expanding the card:
|
|
12
|
+
*
|
|
13
|
+
* 1. **A file group mounts a bounded number of rows**, with the rest one click
|
|
14
|
+
* away — `MAX_OUTPUT_LINES` caps the whole output, so a single 900-line group
|
|
15
|
+
* used to draw 900 rows to show ~14.
|
|
16
|
+
* 2. **The rows that ARE mounted carry their line numbers and text from the
|
|
17
|
+
* first paint**, before the deferred highlight lands.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from 'bun:test';
|
|
21
|
+
import { Window } from 'happy-dom';
|
|
22
|
+
import { h, render } from 'preact';
|
|
23
|
+
import { act } from 'preact/test-utils';
|
|
24
|
+
|
|
25
|
+
import { FileMatches, MAX_MATCH_LINES_PER_FILE } from '@/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches';
|
|
26
|
+
import { clearHighlightMemo } from '@/shared/lib/code/highlight-memo';
|
|
27
|
+
|
|
28
|
+
const DOM_GLOBALS = ['window', 'document', 'navigator', 'Node', 'Element', 'HTMLElement', 'Event', 'MouseEvent'] as const;
|
|
29
|
+
const nativeGlobals: Partial<Record<(typeof DOM_GLOBALS)[number], unknown>> = {};
|
|
30
|
+
let container: HTMLElement;
|
|
31
|
+
|
|
32
|
+
beforeAll(() => {
|
|
33
|
+
const win = new Window({ url: 'http://localhost' });
|
|
34
|
+
const target = globalThis as unknown as Record<string, unknown>;
|
|
35
|
+
for (const key of DOM_GLOBALS) {
|
|
36
|
+
nativeGlobals[key] = target[key];
|
|
37
|
+
target[key] = (win as unknown as Record<string, unknown>)[key];
|
|
38
|
+
}
|
|
39
|
+
container = document.createElement('div');
|
|
40
|
+
document.body.appendChild(container);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterAll(() => {
|
|
44
|
+
const target = globalThis as unknown as Record<string, unknown>;
|
|
45
|
+
for (const key of DOM_GLOBALS) {
|
|
46
|
+
if (nativeGlobals[key] === undefined) delete target[key];
|
|
47
|
+
else target[key] = nativeGlobals[key];
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
clearHighlightMemo();
|
|
53
|
+
// A fresh root per test: `render` keeps its vnode tree on the element it was
|
|
54
|
+
// given, so clearing the markup in place would leave preact diffing against a
|
|
55
|
+
// tree that is no longer there and skip the render entirely.
|
|
56
|
+
container = document.createElement('div');
|
|
57
|
+
document.body.appendChild(container);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/** Rows the card drew, counted by the gutter numbers it printed. */
|
|
61
|
+
function rowCount(el: HTMLElement): number {
|
|
62
|
+
return el.querySelectorAll('pre.shiki').length;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function matches(total: number) {
|
|
66
|
+
return Array.from({ length: total }, (_, index) => ({
|
|
67
|
+
lineNum: index + 1,
|
|
68
|
+
text: `const value${index} = ${index};`,
|
|
69
|
+
isMatch: index % 5 === 0,
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
describe('FileMatches', () => {
|
|
74
|
+
test('mounts a bounded number of rows for a long file group', async () => {
|
|
75
|
+
await act(async () => {
|
|
76
|
+
render(h(FileMatches, { path: 'src/a.ts', lines: matches(300), matchCount: 60, isGlobList: false }), container);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(rowCount(container)).toBe(MAX_MATCH_LINES_PER_FILE);
|
|
80
|
+
expect(container.textContent).toContain('const value0 = 0;');
|
|
81
|
+
expect(container.textContent).not.toContain('const value299 = 299;');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('offers the hidden rows, and reveals every one of them', async () => {
|
|
85
|
+
await act(async () => {
|
|
86
|
+
render(h(FileMatches, { path: 'src/a.ts', lines: matches(300), matchCount: 60, isGlobList: false }), container);
|
|
87
|
+
});
|
|
88
|
+
const reveal = Array.from(container.querySelectorAll('button')).find((b) => (b.textContent ?? '').includes('show all'));
|
|
89
|
+
expect(reveal).toBeDefined();
|
|
90
|
+
expect(reveal?.textContent).toContain('240');
|
|
91
|
+
|
|
92
|
+
await act(async () => {
|
|
93
|
+
reveal?.click();
|
|
94
|
+
});
|
|
95
|
+
expect(rowCount(container)).toBe(300);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('a short group draws no reveal control at all', async () => {
|
|
99
|
+
await act(async () => {
|
|
100
|
+
render(h(FileMatches, { path: 'src/a.ts', lines: matches(4), matchCount: 1, isGlobList: false }), container);
|
|
101
|
+
});
|
|
102
|
+
expect(rowCount(container)).toBe(4);
|
|
103
|
+
expect(container.textContent).not.toContain('show all');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('the first paint carries the text, before any highlight is computed', async () => {
|
|
107
|
+
await act(async () => {
|
|
108
|
+
render(h(FileMatches, { path: 'src/a.ts', lines: matches(3), matchCount: 1, isGlobList: false }), container);
|
|
109
|
+
});
|
|
110
|
+
// Nothing tokenized yet, and the rows still read correctly.
|
|
111
|
+
expect(container.textContent).toContain('const value2 = 2;');
|
|
112
|
+
expect(container.querySelectorAll('pre.shiki')).toHaveLength(3);
|
|
113
|
+
});
|
|
114
|
+
});
|