@kabmo-labs/kabmo 0.8.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 +314 -314
- package/dist/client/static/js/{index-qwpvd9fv.js → index-bpptfkz2.js} +922 -892
- package/dist/client/static/js/index-k315p1e8.css +1 -0
- package/package.json +9 -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/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/layout/desktop-layout/WorkspacePanels.tsx +41 -78
- package/src/client/components/layout/desktop-layout/index.tsx +47 -67
- package/src/client/components/layout/desktop-layout/resizer/index.tsx +6 -3
- package/src/client/components/layout/session-sidebar/Footer.tsx +8 -15
- package/src/client/components/mobile/LayoutWrapper.tsx +1 -0
- 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/settings/Modal.tsx +8 -0
- package/src/client/components/settings/categories/AppearanceSettings.tsx +3 -0
- 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 +8 -2
- package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +16 -8
- 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/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.tsx +8 -11
- 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/OutlineSection.test.tsx +18 -0
- package/src/client/components/workspace/file-explorer/OutlineSection.tsx +10 -3
- 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/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 +19 -24
- 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/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/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/settings-modal.ts +56 -0
- package/src/client/hooks/ui/updates/shell-download.test.ts +2 -0
- package/src/client/hooks/ui/updates/shell-download.ts +55 -14
- package/src/client/hooks/ui/updates/shell-offer.test.ts +26 -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-tabs.test.ts +62 -5
- package/src/client/hooks/workspace/file-tabs.ts +24 -11
- package/src/client/hooks/workspace/navbar-session-actions.ts +72 -0
- package/src/client/hooks/workspace/worktree/index.ts +20 -5
- 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 +37 -8
- package/src/server/lib/auth/guard.test.ts +28 -0
- package/src/server/lib/auth/guard.ts +10 -0
- package/src/server/lib/fs/worktree/create.ts +2 -3
- package/src/server/lib/fs/worktree/list.ts +13 -0
- package/src/server/lib/fs/worktree/remove.ts +3 -3
- 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/telemetry/compute.ts +13 -1
- package/src/server/lib/omp/session/telemetry.test.ts +33 -0
- 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.ts +94 -3
- package/src/server/lib/updates/shell-cache.ts +105 -0
- package/src/server/lib/updates/shell-pages.test.ts +33 -2
- package/src/server/lib/updates/shell-pages.ts +17 -5
- package/src/server/lib/updates/shell-release-desktop.test.ts +166 -0
- package/src/server/lib/updates/shell-release.test.ts +10 -27
- package/src/server/lib/updates/shell-release.ts +107 -111
- 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/updates/android.ts +6 -1
- package/src/server/routes/updates/desktop.ts +29 -7
- package/src/server/routes/updates/ios.ts +5 -1
- package/src/server/routes/worktrees/list.ts +11 -1
- package/src/server/routes/worktrees/routes.test.ts +17 -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-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/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 +4 -4
- 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-panel-widths.ts +124 -0
- package/src/shared/types/realtime.ts +29 -0
- package/src/shared/types/settings/state.ts +10 -0
- package/src/test-support/loopback.ts +183 -0
- package/dist/client/static/js/index-gtbpewr7.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/chat-timeline/target-picker/index.test.tsx +0 -227
- package/src/client/components/workspace/chat-timeline/target-picker/index.tsx +0 -260
- 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/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}`}>
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The diff body: CodeMirror's merge view, mounted once and reconfigured.
|
|
8
|
+
*
|
|
9
|
+
* The panel above this owns the toolbar, the git actions and the persisted view
|
|
10
|
+
* choices; this owns the two editors. Keeping them apart is what lets a stage /
|
|
11
|
+
* unstage / discard re-read the documents without re-mounting the editor (which
|
|
12
|
+
* would drop the reader's scroll position, and does so on every action).
|
|
13
|
+
*
|
|
14
|
+
* `Diff Only` ↔ `Full Code` is a compartment reconfigure, not a re-fetch: the
|
|
15
|
+
* documents are already both sides of the file, and the server's `-U<n>` probe
|
|
16
|
+
* existed only because the old table renderer could not collapse anything.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
20
|
+
|
|
21
|
+
import { useScrollRestore } from '@/client/hooks/editor/use-scroll-restore';
|
|
22
|
+
import { EDITOR_LINE_HEIGHT } from '@/shared/lib/code/editor/typography';
|
|
23
|
+
import {
|
|
24
|
+
countChanges,
|
|
25
|
+
foldWhitespaceOnly,
|
|
26
|
+
mountSplitDiff,
|
|
27
|
+
mountUnifiedDiff,
|
|
28
|
+
type DiffViewHandle,
|
|
29
|
+
} from '@/client/components/workspace/diff-panel/merge';
|
|
30
|
+
import type { DiffContentMode, DiffViewMode } from '@/client/components/workspace/diff-panel/Toolbar';
|
|
31
|
+
|
|
32
|
+
interface MergeDiffViewProps {
|
|
33
|
+
/** The document before the change. */
|
|
34
|
+
original: string;
|
|
35
|
+
/** The document after it. */
|
|
36
|
+
next: string;
|
|
37
|
+
language: string;
|
|
38
|
+
viewMode: DiffViewMode;
|
|
39
|
+
contentMode: DiffContentMode;
|
|
40
|
+
wordWrap: boolean;
|
|
41
|
+
ignoreWhitespace: boolean;
|
|
42
|
+
/** The tab this diff belongs to, so its offset is remembered per shape. */
|
|
43
|
+
tabKey: string | null;
|
|
44
|
+
/**
|
|
45
|
+
* Additions and deletions of the pair actually on screen. Omitted by a host
|
|
46
|
+
* that draws its own badge from the patch (the chat's tool card).
|
|
47
|
+
*/
|
|
48
|
+
onCounts?: (counts: { additions: number; deletions: number }) => void;
|
|
49
|
+
/** Nothing to compare (both sides empty). */
|
|
50
|
+
empty: boolean;
|
|
51
|
+
fontFamily: string;
|
|
52
|
+
fontSize: number;
|
|
53
|
+
/** The editor theme's CSS variables (`editorThemeVars`); absent means `auto`. */
|
|
54
|
+
themeVars?: Record<string, string>;
|
|
55
|
+
/** The two Shiki theme names to tokenize with. */
|
|
56
|
+
themes?: { light: string; dark: string };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function MergeDiffView({
|
|
60
|
+
original,
|
|
61
|
+
next,
|
|
62
|
+
language,
|
|
63
|
+
viewMode,
|
|
64
|
+
contentMode,
|
|
65
|
+
wordWrap,
|
|
66
|
+
ignoreWhitespace,
|
|
67
|
+
tabKey,
|
|
68
|
+
onCounts,
|
|
69
|
+
empty,
|
|
70
|
+
fontFamily,
|
|
71
|
+
fontSize,
|
|
72
|
+
themeVars,
|
|
73
|
+
themes,
|
|
74
|
+
}: MergeDiffViewProps) {
|
|
75
|
+
const hostRef = useRef<HTMLDivElement | null>(null);
|
|
76
|
+
const handleRef = useRef<DiffViewHandle | null>(null);
|
|
77
|
+
const [scroller, setScroller] = useState<HTMLElement | null>(null);
|
|
78
|
+
/** The pair the live view holds, so a change is a re-state rather than a re-mount. */
|
|
79
|
+
const pairRef = useRef<{ original: string; next: string }>({ original, next });
|
|
80
|
+
|
|
81
|
+
const left = ignoreWhitespace ? foldWhitespaceOnly(original, next) : original;
|
|
82
|
+
|
|
83
|
+
// The scroll offset is per SHAPE: unified and split rows are different
|
|
84
|
+
// heights, so one view's position says nothing about the other's. The hook
|
|
85
|
+
// wires the element's own `scroll` listener when it is handed one, so there is
|
|
86
|
+
// no binding to spread here.
|
|
87
|
+
useScrollRestore({
|
|
88
|
+
tabKey,
|
|
89
|
+
surface: viewMode === 'split' ? 'split' : 'unified',
|
|
90
|
+
element: scroller,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Mount, and re-mount only when the SHAPE changes — the two shapes are
|
|
94
|
+
// different objects (an `EditorView` and a `MergeView`), so there is nothing
|
|
95
|
+
// to reconfigure between them.
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
const host = hostRef.current;
|
|
98
|
+
if (!host || empty) return;
|
|
99
|
+
const options = {
|
|
100
|
+
original: left,
|
|
101
|
+
next,
|
|
102
|
+
language,
|
|
103
|
+
wordWrap,
|
|
104
|
+
collapse: contentMode,
|
|
105
|
+
themes,
|
|
106
|
+
scrollTestId: 'diff-scroller',
|
|
107
|
+
onScroller: setScroller,
|
|
108
|
+
} as const;
|
|
109
|
+
const handle =
|
|
110
|
+
viewMode === 'split' ? mountSplitDiff(host, { ...options }) : mountUnifiedDiff(host, { ...options });
|
|
111
|
+
handleRef.current = handle;
|
|
112
|
+
pairRef.current = { original: left, next };
|
|
113
|
+
|
|
114
|
+
return () => {
|
|
115
|
+
handle.destroy();
|
|
116
|
+
handleRef.current = null;
|
|
117
|
+
};
|
|
118
|
+
// Re-mounted on the shape, the language, the emptiness and the THEME only:
|
|
119
|
+
// every other input is a reconfigure below. A theme is baked into the
|
|
120
|
+
// tokenizer plugin (Shiki is handed theme NAMES at tokenize time), so a
|
|
121
|
+
// colour change rebuilds it — the documents are re-stated by the effect
|
|
122
|
+
// below, and a diff has no history to lose.
|
|
123
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
124
|
+
}, [viewMode, language, empty, themes?.light, themes?.dark]);
|
|
125
|
+
|
|
126
|
+
// Documents changed (a refresh, a stage, a discard, the whitespace toggle).
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
const handle = handleRef.current;
|
|
129
|
+
if (!handle) return;
|
|
130
|
+
if (pairRef.current.original === left && pairRef.current.next === next) return;
|
|
131
|
+
pairRef.current = { original: left, next };
|
|
132
|
+
handle.setDocuments(left, next);
|
|
133
|
+
}, [left, next]);
|
|
134
|
+
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
handleRef.current?.setCollapse(contentMode);
|
|
137
|
+
}, [contentMode]);
|
|
138
|
+
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
handleRef.current?.setWordWrap(wordWrap);
|
|
141
|
+
}, [wordWrap]);
|
|
142
|
+
|
|
143
|
+
// Counts describe the pair ON SCREEN, so the badge cannot disagree with the
|
|
144
|
+
// rows under it (git's own `+n/-n` still counts a hidden whitespace change).
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
if (!onCounts) return;
|
|
147
|
+
onCounts(empty ? { additions: 0, deletions: 0 } : countChanges(left, next));
|
|
148
|
+
}, [left, next, empty, onCounts]);
|
|
149
|
+
|
|
150
|
+
if (empty) {
|
|
151
|
+
return (
|
|
152
|
+
<div className="flex-1 flex items-center justify-center text-ink/40 font-mono text-xs p-8">
|
|
153
|
+
No differences between working file and target.
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<div
|
|
160
|
+
ref={hostRef}
|
|
161
|
+
className="flex-1 min-h-0 overflow-hidden bg-paper text-ink"
|
|
162
|
+
style={{
|
|
163
|
+
...themeVars,
|
|
164
|
+
fontFamily,
|
|
165
|
+
fontSize: `${fontSize}px`,
|
|
166
|
+
lineHeight: EDITOR_LINE_HEIGHT,
|
|
167
|
+
// A short diff would otherwise leave the merge view's own height at the
|
|
168
|
+
// panel's, which is what its `.cm-mergeView` rule expects.
|
|
169
|
+
['--surface-pad' as string]: '8px',
|
|
170
|
+
}}
|
|
171
|
+
// A read-only CodeMirror swallows `drop` too (its handler answers `true`
|
|
172
|
+
// for a read-only state, which cancels the event), so a file dropped on a
|
|
173
|
+
// diff would reach neither the panel's handler nor anything else. Marking
|
|
174
|
+
// the host is what lets the enclosing drop target claim the gesture in the
|
|
175
|
+
// CAPTURE phase and open the file — the same rule the code surface states.
|
|
176
|
+
data-code-surface=""
|
|
177
|
+
/>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `MergeDiffView` with the editor theme applied.
|
|
8
|
+
*
|
|
9
|
+
* A wrapper rather than a prop at the call site because the diff panel and the
|
|
10
|
+
* chat's diff card are the same surface: both must wear the editor theme the
|
|
11
|
+
* user picked, and the panel's own file is at the size ceiling. Keeping the
|
|
12
|
+
* subscription here means the theme reaches every diff with no second
|
|
13
|
+
* `useEditorTheme` call to keep in step.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { MergeDiffView } from '@/client/components/workspace/diff-panel/MergeDiffView';
|
|
17
|
+
import { useEditorTheme } from '@/client/hooks/editor/theme';
|
|
18
|
+
|
|
19
|
+
type MergeDiffViewProps = Parameters<typeof MergeDiffView>[0];
|
|
20
|
+
|
|
21
|
+
export function ThemedMergeDiffView(props: MergeDiffViewProps) {
|
|
22
|
+
const editorTheme = useEditorTheme();
|
|
23
|
+
return <MergeDiffView {...props} themeVars={editorTheme.vars} themes={editorTheme.themes} />;
|
|
24
|
+
}
|