@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
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Where a chat that has not started will run: the workspace, or one of the
|
|
8
|
-
* workspace's worktrees.
|
|
9
|
-
*
|
|
10
|
-
* One control rather than two, because both answers are the same question. The
|
|
11
|
-
* menu is grouped exactly as OpenChamber's draft target is — the project root
|
|
12
|
-
* first, then the two ways to make a worktree, then the worktrees that already
|
|
13
|
-
* exist — and creating one here means the user does not have to leave the draft
|
|
14
|
-
* to get it.
|
|
15
|
-
*
|
|
16
|
-
* The pick is `{folderId, worktreePath}`: choosing a folder CLEARS the worktree
|
|
17
|
-
* (the groups are mutually exclusive), while choosing a worktree keeps its
|
|
18
|
-
* folder, so the row the composer draws can always name both.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
|
22
|
-
import { Check, ChevronDown, Folder, GitBranch } from 'lucide-preact';
|
|
23
|
-
import { useOnClickOutside } from '@/client/hooks/ui/on-click-outside';
|
|
24
|
-
import { useWorktrees } from '@/client/hooks/workspace/worktree/index';
|
|
25
|
-
import type { SpawnTarget } from '@/client/hooks/chat/timeline/spawn-target';
|
|
26
|
-
import { NewWorktreeModal } from '@/client/components/workspace/worktree-modal/index';
|
|
27
|
-
import { createWorktree } from '@/client/hooks/workspace/worktree/index';
|
|
28
|
-
import { lastPathSegment } from '@/shared/lib/fs/paths';
|
|
29
|
-
import type { WorkspaceFolderData } from '@/shared/types';
|
|
30
|
-
|
|
31
|
-
export interface TargetPickerProps {
|
|
32
|
-
folders: WorkspaceFolderData[];
|
|
33
|
-
selectedFolderId: number | null;
|
|
34
|
-
setSelectedFolderId: (id: number | null) => void;
|
|
35
|
-
target: SpawnTarget | null;
|
|
36
|
-
setTarget: (next: SpawnTarget | null) => void;
|
|
37
|
-
/**
|
|
38
|
-
* Reports a completed create/removal to the caller (a toast). Optional: the
|
|
39
|
-
* picker owns the flows, so a caller that only wants the pick needs nothing.
|
|
40
|
-
*/
|
|
41
|
-
onReport?: (message: string) => void;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** The trigger's words: the folder, plus the worktree when one is picked. */
|
|
45
|
-
export function targetLabel(
|
|
46
|
-
folders: WorkspaceFolderData[],
|
|
47
|
-
selectedFolderId: number | null,
|
|
48
|
-
target: SpawnTarget | null,
|
|
49
|
-
): string {
|
|
50
|
-
const folder = folders.find((candidate) => String(candidate.id) === String(selectedFolderId));
|
|
51
|
-
const name = folder?.name ?? 'Select Workspace Context';
|
|
52
|
-
const worktree = lastPathSegment(target?.worktreePath);
|
|
53
|
-
return worktree ? `${name} · ${worktree}` : name;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function TargetPicker({
|
|
57
|
-
folders,
|
|
58
|
-
selectedFolderId,
|
|
59
|
-
setSelectedFolderId,
|
|
60
|
-
target,
|
|
61
|
-
setTarget,
|
|
62
|
-
onReport,
|
|
63
|
-
}: TargetPickerProps) {
|
|
64
|
-
const [open, setOpen] = useState(false);
|
|
65
|
-
const [creating, setCreating] = useState(false);
|
|
66
|
-
const [dialogFolder, setDialogFolder] = useState<WorkspaceFolderData | null>(null);
|
|
67
|
-
const containerRef = useRef<HTMLDivElement>(null);
|
|
68
|
-
useOnClickOutside(containerRef, () => setOpen(false));
|
|
69
|
-
|
|
70
|
-
const selectedFolder = folders.find((candidate) => String(candidate.id) === String(selectedFolderId)) ?? null;
|
|
71
|
-
const scopeRoot = selectedFolder?.project_path ?? null;
|
|
72
|
-
|
|
73
|
-
// The list is read only while the menu is open: it costs a `git status` per
|
|
74
|
-
// worktree, and a closed picker must never pay for it.
|
|
75
|
-
const { worktrees, error, refresh } = useWorktrees(
|
|
76
|
-
{ root: scopeRoot, folderId: selectedFolder?.id ?? null },
|
|
77
|
-
open && Boolean(scopeRoot),
|
|
78
|
-
);
|
|
79
|
-
useEffect(() => {
|
|
80
|
-
if (open) void refresh();
|
|
81
|
-
}, [open, refresh]);
|
|
82
|
-
|
|
83
|
-
const list = useMemo(() => worktrees.filter((worktree) => !worktree.isPrimary), [worktrees]);
|
|
84
|
-
|
|
85
|
-
const pickFolder = (folder: WorkspaceFolderData) => {
|
|
86
|
-
setSelectedFolderId(folder.id);
|
|
87
|
-
// A folder and a worktree are alternative answers, not a pair.
|
|
88
|
-
setTarget(null);
|
|
89
|
-
setOpen(false);
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const pickWorktree = (folder: WorkspaceFolderData, worktreePath: string) => {
|
|
93
|
-
setSelectedFolderId(folder.id);
|
|
94
|
-
setTarget({ folderId: Number(folder.id), worktreePath });
|
|
95
|
-
setOpen(false);
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Quick worktree: a generated branch and folder, created and selected without
|
|
100
|
-
* leaving the draft. Reported rather than silent — the create takes a moment
|
|
101
|
-
* and a pick that changed with no word reads as a mis-click.
|
|
102
|
-
*/
|
|
103
|
-
const quickWorktree = async (folder: WorkspaceFolderData) => {
|
|
104
|
-
if (!folder.project_path) return;
|
|
105
|
-
setCreating(true);
|
|
106
|
-
try {
|
|
107
|
-
const created = await createWorktree(
|
|
108
|
-
{ root: folder.project_path, folderId: folder.id },
|
|
109
|
-
{ mode: 'new' },
|
|
110
|
-
);
|
|
111
|
-
pickWorktree(folder, created.path);
|
|
112
|
-
onReport?.(`Worktree ${created.branch || lastPathSegment(created.path)} created`);
|
|
113
|
-
} catch (error) {
|
|
114
|
-
onReport?.(error instanceof Error ? error.message : String(error));
|
|
115
|
-
} finally {
|
|
116
|
-
setCreating(false);
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
return (
|
|
121
|
-
<div className="relative shrink-0" ref={containerRef}>
|
|
122
|
-
<button
|
|
123
|
-
type="button"
|
|
124
|
-
onClick={() => setOpen((value) => !value)}
|
|
125
|
-
aria-haspopup="menu"
|
|
126
|
-
aria-expanded={open}
|
|
127
|
-
className="group flex items-center space-x-1.5 px-1.5 py-1 rounded-md text-xs font-medium text-ink/75 hover:text-ink hover:bg-ink/5 focus-visible:bg-ink/5 transition-all outline-none cursor-pointer select-none"
|
|
128
|
-
>
|
|
129
|
-
{target?.worktreePath ? (
|
|
130
|
-
<GitBranch size={13} className="text-ink/50 group-hover:text-ink/80 transition-colors flex-shrink-0" />
|
|
131
|
-
) : (
|
|
132
|
-
<Folder size={13} className="text-ink/50 group-hover:text-ink/80 transition-colors flex-shrink-0" />
|
|
133
|
-
)}
|
|
134
|
-
<span className="flex items-center space-x-1 max-w-[260px] truncate tracking-tight">
|
|
135
|
-
<span>{targetLabel(folders, selectedFolderId, target)}</span>
|
|
136
|
-
</span>
|
|
137
|
-
<ChevronDown
|
|
138
|
-
size={13}
|
|
139
|
-
className={`text-ink/40 group-hover:text-ink/70 flex-shrink-0 transition-transform duration-150 ${open ? 'rotate-180 text-ink/70' : ''}`}
|
|
140
|
-
/>
|
|
141
|
-
</button>
|
|
142
|
-
|
|
143
|
-
{open && (
|
|
144
|
-
<div
|
|
145
|
-
role="menu"
|
|
146
|
-
className="absolute bottom-full left-0 mb-1.5 w-72 bg-paper border border-ink/10 rounded-lg shadow-lg z-50 flex flex-col overflow-hidden text-xs py-1"
|
|
147
|
-
>
|
|
148
|
-
<div className="px-3 py-1.5 text-[10px] uppercase tracking-wider text-ink/40 font-semibold border-b border-ink/5">
|
|
149
|
-
Project root
|
|
150
|
-
</div>
|
|
151
|
-
<div className="max-h-56 scrollbar-overlay-container py-1">
|
|
152
|
-
{folders.length === 0 ? (
|
|
153
|
-
<div className="px-3 py-2 text-ink/40 italic">No workspaces available</div>
|
|
154
|
-
) : (
|
|
155
|
-
folders.map((folder) => (
|
|
156
|
-
<button
|
|
157
|
-
key={folder.id}
|
|
158
|
-
type="button"
|
|
159
|
-
role="menuitemradio"
|
|
160
|
-
aria-checked={selectedFolderId === folder.id && !target?.worktreePath}
|
|
161
|
-
onClick={() => pickFolder(folder)}
|
|
162
|
-
className={`w-full text-left px-3 py-1.5 flex items-center justify-between transition-colors cursor-pointer ${
|
|
163
|
-
selectedFolderId === folder.id && !target?.worktreePath
|
|
164
|
-
? 'bg-ink/10 text-ink font-semibold'
|
|
165
|
-
: 'text-ink/80 hover:bg-ink/5 hover:text-ink'
|
|
166
|
-
}`}
|
|
167
|
-
>
|
|
168
|
-
<span className="flex items-center space-x-2 truncate pr-2">
|
|
169
|
-
<Folder size={12} className={selectedFolderId === folder.id ? 'text-ink flex-shrink-0' : 'text-ink/50 flex-shrink-0'} />
|
|
170
|
-
<span className="truncate font-medium">{folder.name}</span>
|
|
171
|
-
</span>
|
|
172
|
-
{selectedFolderId === folder.id && !target?.worktreePath && <Check size={12} className="text-ink flex-shrink-0" />}
|
|
173
|
-
</button>
|
|
174
|
-
))
|
|
175
|
-
)}
|
|
176
|
-
</div>
|
|
177
|
-
|
|
178
|
-
{/* Making one comes before the list, so a long list never hides it. */}
|
|
179
|
-
{selectedFolder && scopeRoot && (
|
|
180
|
-
<>
|
|
181
|
-
<div className="border-t border-ink/5 px-3 py-1.5 text-[10px] uppercase tracking-wider text-ink/40 font-semibold">
|
|
182
|
-
Worktrees
|
|
183
|
-
</div>
|
|
184
|
-
<button
|
|
185
|
-
type="button"
|
|
186
|
-
role="menuitem"
|
|
187
|
-
disabled={creating}
|
|
188
|
-
onClick={() => void quickWorktree(selectedFolder)}
|
|
189
|
-
className="w-full text-left px-3 py-1.5 flex items-center gap-2 text-ink/80 hover:bg-ink/5 hover:text-ink disabled:opacity-50"
|
|
190
|
-
>
|
|
191
|
-
<span aria-hidden="true">⚡</span>
|
|
192
|
-
{creating ? 'Creating…' : 'Quick worktree'}
|
|
193
|
-
</button>
|
|
194
|
-
<button
|
|
195
|
-
type="button"
|
|
196
|
-
role="menuitem"
|
|
197
|
-
onClick={() => {
|
|
198
|
-
setOpen(false);
|
|
199
|
-
setDialogFolder(selectedFolder);
|
|
200
|
-
}}
|
|
201
|
-
className="w-full text-left px-3 py-1.5 flex items-center gap-2 text-ink/80 hover:bg-ink/5 hover:text-ink"
|
|
202
|
-
>
|
|
203
|
-
<GitBranch size={12} className="text-ink/50" />
|
|
204
|
-
New worktree…
|
|
205
|
-
</button>
|
|
206
|
-
<div className="max-h-56 scrollbar-overlay-container py-1">
|
|
207
|
-
{list.length === 0 ? (
|
|
208
|
-
<div className="px-3 py-2 text-ink/40 italic">
|
|
209
|
-
{error ? 'Worktrees could not be read' : 'No worktrees yet'}
|
|
210
|
-
</div>
|
|
211
|
-
) : (
|
|
212
|
-
list.map((worktree) => (
|
|
213
|
-
<button
|
|
214
|
-
key={worktree.path}
|
|
215
|
-
type="button"
|
|
216
|
-
role="menuitemradio"
|
|
217
|
-
aria-checked={target?.worktreePath === worktree.path}
|
|
218
|
-
onClick={() => pickWorktree(selectedFolder, worktree.path)}
|
|
219
|
-
title={worktree.path}
|
|
220
|
-
className={`w-full text-left px-3 py-1.5 flex items-center justify-between transition-colors cursor-pointer ${
|
|
221
|
-
target?.worktreePath === worktree.path
|
|
222
|
-
? 'bg-ink/10 text-ink font-semibold'
|
|
223
|
-
: 'text-ink/80 hover:bg-ink/5 hover:text-ink'
|
|
224
|
-
}`}
|
|
225
|
-
>
|
|
226
|
-
<span className="flex items-center gap-2 truncate pr-2">
|
|
227
|
-
<GitBranch size={12} className="text-ink/50 flex-shrink-0" />
|
|
228
|
-
<span className="truncate font-medium">{worktree.branch || 'detached'}</span>
|
|
229
|
-
{worktree.missing && <span className="text-warning shrink-0">missing</span>}
|
|
230
|
-
{worktree.dirty > 0 && (
|
|
231
|
-
<span className="text-ink/40 shrink-0" title={`${worktree.dirty} uncommitted files`}>
|
|
232
|
-
●{worktree.dirty}
|
|
233
|
-
</span>
|
|
234
|
-
)}
|
|
235
|
-
</span>
|
|
236
|
-
{target?.worktreePath === worktree.path && <Check size={12} className="text-ink flex-shrink-0" />}
|
|
237
|
-
</button>
|
|
238
|
-
))
|
|
239
|
-
)}
|
|
240
|
-
</div>
|
|
241
|
-
</>
|
|
242
|
-
)}
|
|
243
|
-
</div>
|
|
244
|
-
)}
|
|
245
|
-
|
|
246
|
-
<NewWorktreeModal
|
|
247
|
-
isOpen={dialogFolder !== null}
|
|
248
|
-
onClose={() => setDialogFolder(null)}
|
|
249
|
-
root={dialogFolder?.project_path ?? null}
|
|
250
|
-
folderId={dialogFolder ? Number(dialogFolder.id) : null}
|
|
251
|
-
folderName={dialogFolder?.name}
|
|
252
|
-
onCreated={(created) => {
|
|
253
|
-
if (!dialogFolder) return;
|
|
254
|
-
pickWorktree(dialogFolder, created.path);
|
|
255
|
-
onReport?.(`Worktree ${created.branch} created`);
|
|
256
|
-
}}
|
|
257
|
-
/>
|
|
258
|
-
</div>
|
|
259
|
-
);
|
|
260
|
-
}
|
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
import { useMemo } from 'preact/hooks';
|
|
2
|
-
import type { SplitDiffRow } from '@/shared/lib/fs/diff-parser';
|
|
3
|
-
import { highlightLines } from '@/shared/lib/code/syntax-highlight';
|
|
4
|
-
import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
|
|
5
|
-
import { useScrollRestore } from '@/client/hooks/editor/use-scroll-restore';
|
|
6
|
-
|
|
7
|
-
interface SplitViewProps {
|
|
8
|
-
rows: SplitDiffRow[];
|
|
9
|
-
language: string;
|
|
10
|
-
/** Wrap long lines instead of scrolling horizontally. */
|
|
11
|
-
wordWrap: boolean;
|
|
12
|
-
/**
|
|
13
|
-
* The diff tab these rows belong to, so its scroll offset is remembered — or
|
|
14
|
-
* null for a caller with no tab of its own (a diff inside a chat tool card,
|
|
15
|
-
* where the timeline owns the scrolling).
|
|
16
|
-
*/
|
|
17
|
-
tabKey: string | null;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function SplitView({ rows, language, wordWrap, tabKey }: SplitViewProps) {
|
|
21
|
-
const syntaxReady = useSyntaxReady();
|
|
22
|
-
// Its own offset: split rows and unified lines are different heights, and
|
|
23
|
-
// switching shape must not carry one view's position onto the other's rows.
|
|
24
|
-
const scroll = useScrollRestore({ tabKey, surface: 'split' });
|
|
25
|
-
|
|
26
|
-
const htmlByIndex = useMemo(() => {
|
|
27
|
-
const left = new Array<string>(rows.length).fill('');
|
|
28
|
-
const right = new Array<string>(rows.length).fill('');
|
|
29
|
-
let start = -1;
|
|
30
|
-
const flush = (end: number) => {
|
|
31
|
-
if (start < 0) return;
|
|
32
|
-
const group = rows.slice(start, end);
|
|
33
|
-
const leftHtml = highlightLines(group.map((r) => r.left?.text ?? '').join('\n'), language);
|
|
34
|
-
const rightHtml = highlightLines(group.map((r) => r.right?.text ?? '').join('\n'), language);
|
|
35
|
-
for (let i = start; i < end; i++) {
|
|
36
|
-
left[i] = leftHtml[i - start] ?? '';
|
|
37
|
-
right[i] = rightHtml[i - start] ?? '';
|
|
38
|
-
}
|
|
39
|
-
start = -1;
|
|
40
|
-
};
|
|
41
|
-
rows.forEach((row, idx) => {
|
|
42
|
-
if (row.isMeta) flush(idx);
|
|
43
|
-
else if (start < 0) start = idx;
|
|
44
|
-
});
|
|
45
|
-
flush(rows.length);
|
|
46
|
-
return { left, right };
|
|
47
|
-
}, [rows, language, syntaxReady]);
|
|
48
|
-
|
|
49
|
-
if (rows.length === 0) {
|
|
50
|
-
return (
|
|
51
|
-
<div className="flex-1 flex items-center justify-center text-ink/40 font-mono text-xs p-8">
|
|
52
|
-
No differences between working file and target.
|
|
53
|
-
</div>
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return (
|
|
58
|
-
<div
|
|
59
|
-
ref={scroll.ref}
|
|
60
|
-
onScroll={scroll.onScroll}
|
|
61
|
-
data-testid="diff-scroller"
|
|
62
|
-
className="w-full h-full overflow-auto font-mono text-xs select-text bg-paper text-ink code-surface"
|
|
63
|
-
>
|
|
64
|
-
{/* The room under the last row belongs to the content, not the scroller:
|
|
65
|
-
a scroll container's own bottom padding is not part of its scrollable
|
|
66
|
-
overflow, so the padding has to sit inside it to have any effect. */}
|
|
67
|
-
<div className={`pb-4 ${wordWrap ? 'w-full' : 'w-max min-w-full min-w-[700px]'}`}>
|
|
68
|
-
<div className="grid grid-cols-2 sticky top-0 z-10 bg-canvas border-b border-ink/10 text-[11px] text-ink/70 font-sans select-none shadow-xs">
|
|
69
|
-
<div className="px-3 py-1 font-medium border-r border-ink/10 flex items-center justify-between">
|
|
70
|
-
<span>Original (HEAD / Base)</span>
|
|
71
|
-
</div>
|
|
72
|
-
<div className="px-3 py-1 font-medium flex items-center justify-between">
|
|
73
|
-
<span>Modified (Working Tree)</span>
|
|
74
|
-
</div>
|
|
75
|
-
</div>
|
|
76
|
-
|
|
77
|
-
{/* Wrap needs `table-fixed`: auto layout sizes a column by its widest
|
|
78
|
-
cell's UNAMBIGUOUS minimum (`overflow-wrap: break-word` does not
|
|
79
|
-
lower it), so a long line would widen the table and scroll instead of
|
|
80
|
-
wrapping. Fixed layout takes its widths from the FIRST row — which
|
|
81
|
-
here is the `colSpan={4}` meta row, so every column came out equal
|
|
82
|
-
(measured 99px) and the line-number gutters grew to the width of a
|
|
83
|
-
code column. The colgroup states them instead: two 48px gutters and
|
|
84
|
-
the two content columns sharing the rest. */}
|
|
85
|
-
<table className={`border-collapse ${wordWrap ? 'table-fixed w-full' : 'w-full'}`}>
|
|
86
|
-
{wordWrap && (
|
|
87
|
-
<colgroup>
|
|
88
|
-
<col className="w-12" />
|
|
89
|
-
<col />
|
|
90
|
-
<col className="w-12" />
|
|
91
|
-
<col />
|
|
92
|
-
</colgroup>
|
|
93
|
-
)}
|
|
94
|
-
<tbody>
|
|
95
|
-
{rows.map((row, idx) => {
|
|
96
|
-
if (row.isMeta) {
|
|
97
|
-
return (
|
|
98
|
-
<tr key={idx} className="bg-canvas border-y border-ink/10 text-meta select-none">
|
|
99
|
-
<td colSpan={4} className="py-1 px-3 text-[11px] font-semibold text-meta text-center font-mono">
|
|
100
|
-
{row.metaText}
|
|
101
|
-
</td>
|
|
102
|
-
</tr>
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const left = row.left;
|
|
107
|
-
const right = row.right;
|
|
108
|
-
|
|
109
|
-
const isLeftDel = left?.type === 'del';
|
|
110
|
-
const isRightAdd = right?.type === 'add';
|
|
111
|
-
|
|
112
|
-
return (
|
|
113
|
-
<tr key={idx} className="border-b border-ink/[0.04]">
|
|
114
|
-
{/* Left Line Number */}
|
|
115
|
-
<td
|
|
116
|
-
className={`w-12 py-0.5 px-2 text-right text-[10px] select-none border-r border-ink/10 font-mono ${
|
|
117
|
-
wordWrap ? 'align-top' : ''
|
|
118
|
-
} ${
|
|
119
|
-
isLeftDel
|
|
120
|
-
? 'bg-error/15 text-error font-semibold'
|
|
121
|
-
: left
|
|
122
|
-
? 'bg-canvas/50 text-ink/40'
|
|
123
|
-
: 'bg-canvas/80 text-transparent'
|
|
124
|
-
}`}
|
|
125
|
-
>
|
|
126
|
-
{left?.lineNumber || ''}
|
|
127
|
-
</td>
|
|
128
|
-
|
|
129
|
-
{/* Left Content (Original) */}
|
|
130
|
-
<td
|
|
131
|
-
className={`py-0.5 px-2 border-r border-ink/10 text-ink ${
|
|
132
|
-
wordWrap ? 'whitespace-pre-wrap break-words align-top' : 'min-w-[320px] whitespace-pre'
|
|
133
|
-
} ${
|
|
134
|
-
isLeftDel
|
|
135
|
-
? 'bg-error/10'
|
|
136
|
-
: left
|
|
137
|
-
? 'hover:bg-ink/[0.02]'
|
|
138
|
-
: 'bg-canvas/40 select-none'
|
|
139
|
-
}`}
|
|
140
|
-
>
|
|
141
|
-
{left ? (
|
|
142
|
-
<>
|
|
143
|
-
<span className={`inline-block w-4 text-center select-none font-bold ${isLeftDel ? 'text-error' : 'opacity-0'}`}>
|
|
144
|
-
{isLeftDel ? '-' : ' '}
|
|
145
|
-
</span>
|
|
146
|
-
<span
|
|
147
|
-
className="shiki"
|
|
148
|
-
dangerouslySetInnerHTML={{
|
|
149
|
-
__html: left.text ? htmlByIndex.left[idx] || ' ' : ' ',
|
|
150
|
-
}}
|
|
151
|
-
/>
|
|
152
|
-
</>
|
|
153
|
-
) : (
|
|
154
|
-
<span className="opacity-0"> </span>
|
|
155
|
-
)}
|
|
156
|
-
</td>
|
|
157
|
-
|
|
158
|
-
{/* Right Line Number */}
|
|
159
|
-
<td
|
|
160
|
-
className={`w-12 py-0.5 px-2 text-right text-[10px] select-none border-r border-ink/10 font-mono ${
|
|
161
|
-
wordWrap ? 'align-top' : ''
|
|
162
|
-
} ${
|
|
163
|
-
isRightAdd
|
|
164
|
-
? 'bg-success/15 text-success font-semibold'
|
|
165
|
-
: right
|
|
166
|
-
? 'bg-canvas/50 text-ink/40'
|
|
167
|
-
: 'bg-canvas/80 text-transparent'
|
|
168
|
-
}`}
|
|
169
|
-
>
|
|
170
|
-
{right?.lineNumber || ''}
|
|
171
|
-
</td>
|
|
172
|
-
|
|
173
|
-
{/* Right Content (Modified) */}
|
|
174
|
-
<td
|
|
175
|
-
className={`py-0.5 px-2 text-ink ${
|
|
176
|
-
wordWrap ? 'whitespace-pre-wrap break-words align-top' : 'min-w-[320px] whitespace-pre'
|
|
177
|
-
} ${
|
|
178
|
-
isRightAdd
|
|
179
|
-
? 'bg-success/10'
|
|
180
|
-
: right
|
|
181
|
-
? 'hover:bg-ink/[0.02]'
|
|
182
|
-
: 'bg-canvas/40 select-none'
|
|
183
|
-
}`}
|
|
184
|
-
>
|
|
185
|
-
{right ? (
|
|
186
|
-
<>
|
|
187
|
-
<span className={`inline-block w-4 text-center select-none font-bold ${isRightAdd ? 'text-success' : 'opacity-0'}`}>
|
|
188
|
-
{isRightAdd ? '+' : ' '}
|
|
189
|
-
</span>
|
|
190
|
-
<span
|
|
191
|
-
className="shiki"
|
|
192
|
-
dangerouslySetInnerHTML={{
|
|
193
|
-
__html: right.text ? htmlByIndex.right[idx] || ' ' : ' ',
|
|
194
|
-
}}
|
|
195
|
-
/>
|
|
196
|
-
</>
|
|
197
|
-
) : (
|
|
198
|
-
<span className="opacity-0"> </span>
|
|
199
|
-
)}
|
|
200
|
-
</td>
|
|
201
|
-
</tr>
|
|
202
|
-
);
|
|
203
|
-
})}
|
|
204
|
-
</tbody>
|
|
205
|
-
</table>
|
|
206
|
-
</div>
|
|
207
|
-
</div>
|
|
208
|
-
);
|
|
209
|
-
}
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import { useMemo } from 'preact/hooks';
|
|
2
|
-
import type { DiffLine } from '@/shared/lib/fs/diff-parser';
|
|
3
|
-
import { highlightLines } from '@/shared/lib/code/syntax-highlight';
|
|
4
|
-
import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
|
|
5
|
-
import { useScrollRestore } from '@/client/hooks/editor/use-scroll-restore';
|
|
6
|
-
|
|
7
|
-
interface UnifiedViewProps {
|
|
8
|
-
lines: DiffLine[];
|
|
9
|
-
language: string;
|
|
10
|
-
/** Wrap long lines instead of scrolling horizontally. */
|
|
11
|
-
wordWrap: boolean;
|
|
12
|
-
/**
|
|
13
|
-
* The diff tab these rows belong to, so its scroll offset is remembered — or
|
|
14
|
-
* null for a caller with no tab of its own (a diff inside a chat tool card,
|
|
15
|
-
* where the timeline owns the scrolling).
|
|
16
|
-
*/
|
|
17
|
-
tabKey: string | null;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function UnifiedView({ lines, language, wordWrap, tabKey }: UnifiedViewProps) {
|
|
21
|
-
const syntaxReady = useSyntaxReady();
|
|
22
|
-
// The unified and split shapes are different row counts and heights for the
|
|
23
|
-
// same diff, so each keeps its own offset (see `ScrollSurface`).
|
|
24
|
-
const scroll = useScrollRestore({ tabKey, surface: 'unified' });
|
|
25
|
-
|
|
26
|
-
const htmlByIndex = useMemo(() => {
|
|
27
|
-
const out = new Array<string>(lines.length).fill('');
|
|
28
|
-
let start = -1;
|
|
29
|
-
const flush = (end: number) => {
|
|
30
|
-
if (start < 0) return;
|
|
31
|
-
const html = highlightLines(lines.slice(start, end).map((l) => l.text).join('\n'), language);
|
|
32
|
-
for (let i = start; i < end; i++) out[i] = html[i - start] ?? '';
|
|
33
|
-
start = -1;
|
|
34
|
-
};
|
|
35
|
-
lines.forEach((line, idx) => {
|
|
36
|
-
if (line.type === 'meta') flush(idx);
|
|
37
|
-
else if (start < 0) start = idx;
|
|
38
|
-
});
|
|
39
|
-
flush(lines.length);
|
|
40
|
-
return out;
|
|
41
|
-
}, [lines, language, syntaxReady]);
|
|
42
|
-
|
|
43
|
-
if (lines.length === 0) {
|
|
44
|
-
return (
|
|
45
|
-
<div className="flex-1 flex items-center justify-center text-ink/40 font-mono text-xs p-8">
|
|
46
|
-
No differences between working file and target.
|
|
47
|
-
</div>
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return (
|
|
52
|
-
<div
|
|
53
|
-
ref={scroll.ref}
|
|
54
|
-
onScroll={scroll.onScroll}
|
|
55
|
-
data-testid="diff-scroller"
|
|
56
|
-
className="w-full h-full overflow-auto font-mono text-xs select-text bg-paper text-ink code-surface"
|
|
57
|
-
>
|
|
58
|
-
{/* The room under the last row belongs to the content, not the scroller:
|
|
59
|
-
a scroll container's own bottom padding is not part of its scrollable
|
|
60
|
-
overflow, so the padding has to sit inside it to have any effect. */}
|
|
61
|
-
<div className={`pb-4 ${wordWrap ? 'w-full' : 'w-max min-w-full'}`}>
|
|
62
|
-
{/* Wrap needs `table-fixed`: auto layout sizes a column by its widest
|
|
63
|
-
cell's UNAMBIGUOUS minimum (`overflow-wrap: break-word` does not
|
|
64
|
-
lower it), so a long line would still widen the table and scroll
|
|
65
|
-
instead of wrapping. Fixed layout takes the width from the row's
|
|
66
|
-
own box, which is what makes the cell's wrap rule effective. */}
|
|
67
|
-
<table className={`border-collapse ${wordWrap ? 'table-fixed w-full' : 'w-max min-w-full'}`}>
|
|
68
|
-
<tbody>
|
|
69
|
-
{lines.map((line, idx) => {
|
|
70
|
-
if (line.type === 'meta') {
|
|
71
|
-
return (
|
|
72
|
-
<tr key={idx} className="bg-canvas border-y border-ink/10 text-meta select-none">
|
|
73
|
-
<td className="w-12 text-center py-1 px-2 text-[10px] opacity-60 border-r border-ink/10 font-mono">...</td>
|
|
74
|
-
<td className="w-12 text-center py-1 px-2 text-[10px] opacity-60 border-r border-ink/10 font-mono">...</td>
|
|
75
|
-
<td className="py-1 px-3 text-[11px] font-semibold text-meta font-mono tracking-tight">{line.text}</td>
|
|
76
|
-
</tr>
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const isAdd = line.type === 'add';
|
|
81
|
-
const isDel = line.type === 'del';
|
|
82
|
-
|
|
83
|
-
return (
|
|
84
|
-
<tr
|
|
85
|
-
key={idx}
|
|
86
|
-
className={`transition-colors group ${
|
|
87
|
-
isAdd
|
|
88
|
-
? 'bg-success/10 hover:bg-success/15'
|
|
89
|
-
: isDel
|
|
90
|
-
? 'bg-error/10 hover:bg-error/15'
|
|
91
|
-
: 'hover:bg-ink/[0.03]'
|
|
92
|
-
}`}
|
|
93
|
-
>
|
|
94
|
-
{/* Old line number. Top-aligned when the row wraps, or the
|
|
95
|
-
number would float to the middle of a multi-line cell
|
|
96
|
-
instead of naming its first line. */}
|
|
97
|
-
<td
|
|
98
|
-
className={`w-12 py-0.5 px-2 text-right text-[10px] select-none border-r border-ink/10 font-mono ${
|
|
99
|
-
wordWrap ? 'align-top' : ''
|
|
100
|
-
} ${
|
|
101
|
-
isDel
|
|
102
|
-
? 'bg-error/15 text-error font-semibold'
|
|
103
|
-
: isAdd
|
|
104
|
-
? 'bg-success/15 text-success/70'
|
|
105
|
-
: 'bg-canvas/50 text-ink/40'
|
|
106
|
-
}`}
|
|
107
|
-
>
|
|
108
|
-
{line.oldLineNumber || ''}
|
|
109
|
-
</td>
|
|
110
|
-
|
|
111
|
-
{/* New line number */}
|
|
112
|
-
<td
|
|
113
|
-
className={`w-12 py-0.5 px-2 text-right text-[10px] select-none border-r border-ink/10 font-mono ${
|
|
114
|
-
wordWrap ? 'align-top' : ''
|
|
115
|
-
} ${
|
|
116
|
-
isAdd
|
|
117
|
-
? 'bg-success/15 text-success font-semibold'
|
|
118
|
-
: isDel
|
|
119
|
-
? 'bg-error/15 text-error/70'
|
|
120
|
-
: 'bg-canvas/50 text-ink/40'
|
|
121
|
-
}`}
|
|
122
|
-
>
|
|
123
|
-
{line.newLineNumber || ''}
|
|
124
|
-
</td>
|
|
125
|
-
|
|
126
|
-
{/* Change prefix & code text */}
|
|
127
|
-
<td className={`py-0.5 px-3 text-ink pr-6 ${wordWrap ? 'whitespace-pre-wrap break-words align-top' : 'whitespace-pre'}`}>
|
|
128
|
-
<span
|
|
129
|
-
className={`inline-block w-4 text-center select-none font-bold ${
|
|
130
|
-
isAdd ? 'text-success' : isDel ? 'text-error' : 'opacity-0'
|
|
131
|
-
}`}
|
|
132
|
-
>
|
|
133
|
-
{isAdd ? '+' : isDel ? '-' : ' '}
|
|
134
|
-
</span>
|
|
135
|
-
<span
|
|
136
|
-
className="shiki"
|
|
137
|
-
dangerouslySetInnerHTML={{
|
|
138
|
-
__html: line.text ? htmlByIndex[idx] || ' ' : ' ',
|
|
139
|
-
}}
|
|
140
|
-
/>
|
|
141
|
-
</td>
|
|
142
|
-
</tr>
|
|
143
|
-
);
|
|
144
|
-
})}
|
|
145
|
-
</tbody>
|
|
146
|
-
</table>
|
|
147
|
-
</div>
|
|
148
|
-
</div>
|
|
149
|
-
);
|
|
150
|
-
}
|