@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/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
|
|
@@ -2,9 +2,9 @@ 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
8
|
|
|
9
9
|
/**
|
|
10
10
|
* The line box both sides of a code card share — gutter row and code line.
|
|
@@ -43,23 +43,20 @@ export function Eval({ tool }: { tool: ToolCallData }) {
|
|
|
43
43
|
const isError = tool.status === 'error' || output.includes('ToolError:') || output.includes('Command exited with code');
|
|
44
44
|
const isAborted = output.includes('Command aborted') || tool.status === 'aborted';
|
|
45
45
|
|
|
46
|
-
const syntaxReady = useSyntaxReady();
|
|
47
|
-
|
|
48
46
|
const jsonResult = useMemo(
|
|
49
47
|
() => (!isError && !isAborted ? tryParseJson(output) : { isValid: false }),
|
|
50
48
|
[output, isError, isAborted]
|
|
51
49
|
);
|
|
52
50
|
|
|
53
51
|
const truncatedCode = useMemo(() => truncateTailLines(code, MAX_OUTPUT_LINES), [code]);
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
);
|
|
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');
|
|
58
55
|
// The result is shown whole: an eval's output IS the answer, so hiding its
|
|
59
56
|
// head behind a "… earlier lines hidden" line misreports what the cell returned.
|
|
60
|
-
const highlightedJson =
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
const highlightedJson = useDeferredHighlight(
|
|
58
|
+
jsonResult.isValid && jsonResult.pretty ? jsonResult.pretty : '',
|
|
59
|
+
'json',
|
|
63
60
|
);
|
|
64
61
|
|
|
65
62
|
return (
|
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
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* One file's matches inside a `grep`/`glob` card.
|
|
8
|
+
*
|
|
9
|
+
* Two costs live here, and both were paid on every expand of the card:
|
|
10
|
+
*
|
|
11
|
+
* 1. **The tokenizer.** Every file's lines were tokenized in the parent's render,
|
|
12
|
+
* in one synchronous pass — the profile of a `grep` expand put 40% of its
|
|
13
|
+
* long task inside Shiki's regex engine. The highlight is deferred per FILE
|
|
14
|
+
* (`useDeferredHighlightLines`), so the card paints the matches first and the
|
|
15
|
+
* colour follows on an idle slice.
|
|
16
|
+
* 2. **The row count.** `MAX_OUTPUT_LINES` caps the whole output, not one file,
|
|
17
|
+
* so a single 900-line file group mounted 900 rows inside a `max-h-80`
|
|
18
|
+
* scroller that shows ~14. The window is per file now, opened on the first
|
|
19
|
+
* lines (a match group is read from its top) with the rest one click away.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { useMemo, useState } from 'preact/hooks';
|
|
23
|
+
import { ChevronRight, FileText } from 'lucide-preact';
|
|
24
|
+
import { getLanguageFromPath } from '@/shared/lib/code/language';
|
|
25
|
+
import { useDeferredHighlightLines } from '@/client/hooks/code/deferred-highlight';
|
|
26
|
+
|
|
27
|
+
/** Rows mounted for one file before the reader asks for the rest. */
|
|
28
|
+
export const MAX_MATCH_LINES_PER_FILE = 60;
|
|
29
|
+
|
|
30
|
+
interface MatchRow {
|
|
31
|
+
lineNum: number;
|
|
32
|
+
text: string;
|
|
33
|
+
isMatch: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface FileMatchesProps {
|
|
37
|
+
path: string;
|
|
38
|
+
lines: MatchRow[];
|
|
39
|
+
matchCount: number;
|
|
40
|
+
isGlobList: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function FileMatches({ path, lines, matchCount, isGlobList }: FileMatchesProps) {
|
|
44
|
+
const [showAll, setShowAll] = useState(false);
|
|
45
|
+
const visible = useMemo(
|
|
46
|
+
() => (showAll ? lines : lines.slice(0, MAX_MATCH_LINES_PER_FILE)),
|
|
47
|
+
[lines, showAll],
|
|
48
|
+
);
|
|
49
|
+
const hidden = lines.length - visible.length;
|
|
50
|
+
|
|
51
|
+
const lang = getLanguageFromPath(path);
|
|
52
|
+
const highlighted = useDeferredHighlightLines(
|
|
53
|
+
useMemo(() => visible.map((line) => line.text).join('\n'), [visible]),
|
|
54
|
+
lang,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="overflow-hidden rounded-lg border border-ink/8 bg-paper">
|
|
59
|
+
{/* File Header */}
|
|
60
|
+
<div className="flex items-center justify-between border-b border-ink/6 bg-canvas/40 px-2.5 py-1.5">
|
|
61
|
+
<div className="flex min-w-0 items-center gap-1.5">
|
|
62
|
+
<FileText size={12} className="shrink-0 text-ink/40" />
|
|
63
|
+
<span className="truncate font-mono text-[11px] font-medium text-ink/85">{path}</span>
|
|
64
|
+
</div>
|
|
65
|
+
{matchCount > 0 && !isGlobList && (
|
|
66
|
+
<span className="ml-2 shrink-0 rounded bg-ink/5 px-1.5 py-0.2 font-mono text-[9.5px] text-ink/50">
|
|
67
|
+
{matchCount} {matchCount === 1 ? 'hit' : 'hits'}
|
|
68
|
+
</span>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
{/* Code Lines inside file — satu scroll horizontal per file, semua row selebar konten terlebar */}
|
|
73
|
+
{visible.length > 0 && (
|
|
74
|
+
<div className="overflow-x-auto overscroll-x-contain">
|
|
75
|
+
<div className="w-max min-w-full divide-y divide-ink/[0.04] font-mono text-[11px] leading-relaxed select-text">
|
|
76
|
+
{visible.map((line, lineIdx) => (
|
|
77
|
+
<div
|
|
78
|
+
key={lineIdx}
|
|
79
|
+
className={`flex items-start gap-2.5 px-2.5 py-1 transition-colors ${
|
|
80
|
+
line.isMatch ? 'bg-warning/[0.08] text-ink' : 'bg-paper text-ink/60 hover:bg-ink/[0.02]'
|
|
81
|
+
}`}
|
|
82
|
+
>
|
|
83
|
+
<span
|
|
84
|
+
className={`sticky left-0 z-10 flex w-9 shrink-0 select-none items-center justify-end bg-inherit font-mono text-[10px] ${
|
|
85
|
+
line.isMatch ? 'font-semibold text-warning-dark dark:text-warning' : 'text-ink/30'
|
|
86
|
+
}`}
|
|
87
|
+
>
|
|
88
|
+
{line.isMatch && <ChevronRight size={10} className="mr-0.5 text-warning" />}
|
|
89
|
+
{line.lineNum}
|
|
90
|
+
</span>
|
|
91
|
+
<pre
|
|
92
|
+
className="shiki whitespace-pre font-mono"
|
|
93
|
+
dangerouslySetInnerHTML={{ __html: highlighted[lineIdx] || ' ' }}
|
|
94
|
+
/>
|
|
95
|
+
</div>
|
|
96
|
+
))}
|
|
97
|
+
</div>
|
|
98
|
+
{hidden > 0 && (
|
|
99
|
+
<button
|
|
100
|
+
type="button"
|
|
101
|
+
onClick={() => setShowAll(true)}
|
|
102
|
+
className="w-full border-t border-ink/6 bg-canvas/40 px-2.5 py-1 text-left font-mono text-[10px] text-ink/55 transition-colors hover:bg-ink/5 hover:text-ink"
|
|
103
|
+
>
|
|
104
|
+
… {hidden} more {hidden === 1 ? 'line' : 'lines'} — show all {lines.length}
|
|
105
|
+
</button>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { useEffect, useMemo, useState } from 'preact/hooks';
|
|
2
2
|
import { FileCode, FileImage, FileText, Folder, FolderOpen, Info, Loader2 } from 'lucide-preact';
|
|
3
3
|
import { getLanguageFromPath } from '@/shared/lib/code/language';
|
|
4
|
-
import {
|
|
5
|
-
import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
|
|
4
|
+
import { useDeferredHighlight } from '@/client/hooks/code/deferred-highlight';
|
|
6
5
|
import { parseDirListing, parseNumberedCode } from '@/shared/lib/code/parser';
|
|
7
6
|
import { getImageMimeType } from '@/shared/lib/fs/file-kind';
|
|
8
7
|
import { buildFsRawUrl, isVirtualPath, stripReadSelector } from '@/shared/lib/fs/paths';
|
|
@@ -192,11 +191,12 @@ export function Read({ tool, targetFilePath, output }: ReadPanelProps) {
|
|
|
192
191
|
const truncatedCode = useMemo(() => truncateTailLines(parsedCode.cleanCode, MAX_OUTPUT_LINES), [parsedCode.cleanCode]);
|
|
193
192
|
const visibleLines = truncatedCode.skipped > 0 ? parsedCode.lines.slice(truncatedCode.skipped) : parsedCode.lines;
|
|
194
193
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
194
|
+
// The tokenizer runs off the render that a tap scheduled (see
|
|
195
|
+
// `deferred-highlight`): a 1000-line read measured a 220–300 ms long task on
|
|
196
|
+
// a throttled phone, all of it before the card could paint. The plain-text
|
|
197
|
+
// frame it draws first carries the same characters, and a block already in
|
|
198
|
+
// the memo renders highlighted on that first paint.
|
|
199
|
+
const highlightedCode = useDeferredHighlight(truncatedCode.text, lang);
|
|
200
200
|
|
|
201
201
|
return (
|
|
202
202
|
<div className="space-y-2">
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { useMemo, useState } from 'preact/hooks';
|
|
2
|
-
import {
|
|
2
|
+
import { FileSearch, FolderSearch } from 'lucide-preact';
|
|
3
3
|
import type { ToolCallData } from '@/shared/types';
|
|
4
4
|
import { CopyButton } from '@/client/components/common/CopyButton';
|
|
5
|
-
import {
|
|
6
|
-
import { highlightLines } from '@/shared/lib/code/syntax-highlight';
|
|
7
|
-
import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
|
|
5
|
+
import { FileMatches } from '@/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches';
|
|
8
6
|
import { MAX_OUTPUT_LINES, truncateTailLines } from '@/client/components/workspace/chat-timeline/tool-renderers/shared/truncate';
|
|
9
7
|
|
|
10
8
|
interface MatchLine {
|
|
@@ -189,18 +187,6 @@ export function SearchTool({ tool }: { tool: ToolCallData }) {
|
|
|
189
187
|
.filter((f): f is ParsedFileMatches => f !== null);
|
|
190
188
|
}, [files, filterText]);
|
|
191
189
|
|
|
192
|
-
const syntaxReady = useSyntaxReady();
|
|
193
|
-
|
|
194
|
-
const highlightedByFile = useMemo(() => {
|
|
195
|
-
return filteredFiles.map((file) => {
|
|
196
|
-
const lang = getLanguageFromPath(file.path);
|
|
197
|
-
return highlightLines(
|
|
198
|
-
file.lines.map((line) => line.text).join('\n'),
|
|
199
|
-
lang
|
|
200
|
-
);
|
|
201
|
-
});
|
|
202
|
-
}, [filteredFiles, syntaxReady]);
|
|
203
|
-
|
|
204
190
|
if (!output && files.length === 0) {
|
|
205
191
|
return (
|
|
206
192
|
<div className="flex items-center gap-2 rounded-lg border border-dashed border-ink/15 px-3 py-2.5 text-[11.5px] text-ink/45">
|
|
@@ -262,49 +248,13 @@ export function SearchTool({ tool }: { tool: ToolCallData }) {
|
|
|
262
248
|
)}
|
|
263
249
|
<div className="max-h-80 space-y-2 overflow-y-auto overscroll-contain pr-0.5">
|
|
264
250
|
{filteredFiles.map((file, fileIdx) => (
|
|
265
|
-
<
|
|
266
|
-
{
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
{file.matchCount > 0 && !isGlobList && (
|
|
273
|
-
<span className="ml-2 shrink-0 rounded bg-ink/5 px-1.5 py-0.2 font-mono text-[9.5px] text-ink/50">
|
|
274
|
-
{file.matchCount} {file.matchCount === 1 ? 'hit' : 'hits'}
|
|
275
|
-
</span>
|
|
276
|
-
)}
|
|
277
|
-
</div>
|
|
278
|
-
|
|
279
|
-
{/* Code Lines inside file — satu scroll horizontal per file, semua row selebar konten terlebar */}
|
|
280
|
-
{file.lines.length > 0 && (
|
|
281
|
-
<div className="overflow-x-auto overscroll-x-contain">
|
|
282
|
-
<div className="w-max min-w-full divide-y divide-ink/[0.04] font-mono text-[11px] leading-relaxed select-text">
|
|
283
|
-
{file.lines.map((line, lineIdx) => (
|
|
284
|
-
<div
|
|
285
|
-
key={lineIdx}
|
|
286
|
-
className={`flex items-start gap-2.5 px-2.5 py-1 transition-colors ${
|
|
287
|
-
line.isMatch ? 'bg-warning/[0.08] text-ink' : 'bg-paper text-ink/60 hover:bg-ink/[0.02]'
|
|
288
|
-
}`}
|
|
289
|
-
>
|
|
290
|
-
<span
|
|
291
|
-
className={`sticky left-0 z-10 flex w-9 shrink-0 select-none items-center justify-end bg-inherit font-mono text-[10px] ${
|
|
292
|
-
line.isMatch ? 'font-semibold text-warning-dark dark:text-warning' : 'text-ink/30'
|
|
293
|
-
}`}
|
|
294
|
-
>
|
|
295
|
-
{line.isMatch && <ChevronRight size={10} className="mr-0.5 text-warning" />}
|
|
296
|
-
{line.lineNum}
|
|
297
|
-
</span>
|
|
298
|
-
<pre
|
|
299
|
-
className="shiki whitespace-pre font-mono"
|
|
300
|
-
dangerouslySetInnerHTML={{ __html: highlightedByFile[fileIdx]?.[lineIdx] || ' ' }}
|
|
301
|
-
/>
|
|
302
|
-
</div>
|
|
303
|
-
))}
|
|
304
|
-
</div>
|
|
305
|
-
</div>
|
|
306
|
-
)}
|
|
307
|
-
</div>
|
|
251
|
+
<FileMatches
|
|
252
|
+
key={fileIdx}
|
|
253
|
+
path={file.path}
|
|
254
|
+
lines={file.lines}
|
|
255
|
+
matchCount={file.matchCount}
|
|
256
|
+
isGlobList={isGlobList}
|
|
257
|
+
/>
|
|
308
258
|
))}
|
|
309
259
|
|
|
310
260
|
{filteredFiles.length === 0 && (
|
package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ArtifactBody.tsx
CHANGED
|
@@ -37,8 +37,7 @@ import { MAX_OUTPUT_LINES, truncateTailLines } from '@/client/components/workspa
|
|
|
37
37
|
import { artifactViewKind } from '@/shared/lib/chat/tool/artifact-view';
|
|
38
38
|
import { getLanguageFromPath } from '@/shared/lib/code/language';
|
|
39
39
|
import { parseNumberedCode } from '@/shared/lib/code/parser';
|
|
40
|
-
import {
|
|
41
|
-
import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
|
|
40
|
+
import { useDeferredHighlightLines } from '@/client/hooks/code/deferred-highlight';
|
|
42
41
|
|
|
43
42
|
interface ArtifactBodyProps {
|
|
44
43
|
/** The whole artifact, verbatim. */
|
|
@@ -103,13 +102,9 @@ export function ArtifactBody({ text, path = '' }: ArtifactBodyProps) {
|
|
|
103
102
|
* and `121|code`) and a blank line keeps its place in the count.
|
|
104
103
|
*/
|
|
105
104
|
function ArtifactCode({ lines, path }: { lines: string[]; path: string }) {
|
|
106
|
-
const syntaxReady = useSyntaxReady();
|
|
107
105
|
const parsed = useMemo(() => parseNumberedCode(lines.join('\n')), [lines]);
|
|
108
106
|
const lang = getLanguageFromPath(path);
|
|
109
|
-
const html =
|
|
110
|
-
() => highlightLines(parsed.cleanCode, lang),
|
|
111
|
-
[parsed.cleanCode, lang, syntaxReady],
|
|
112
|
-
);
|
|
107
|
+
const html = useDeferredHighlightLines(parsed.cleanCode, lang);
|
|
113
108
|
|
|
114
109
|
return (
|
|
115
110
|
<div className="flex items-start font-mono text-[11px] leading-[20px] select-text">
|