Skip to content

Commit e6448b3

Browse files
committed
fix(workspace): improve workspace tool behavior
1 parent c59b069 commit e6448b3

2 files changed

Lines changed: 181 additions & 13 deletions

File tree

desktop/src/renderer/app/workspace/workspace-right-sidebar.tsx

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -504,14 +504,11 @@ function WorkspaceFilesPanel({ api, root }: { api: ApiClient; root?: string }) {
504504
}, [model, treeQuery.data]);
505505
const handleFileTreeClick = useCallback(
506506
(event: ReactMouseEvent<HTMLDivElement>) => {
507-
if (!root || !(event.target instanceof Element)) {
507+
if (!root) {
508508
return;
509509
}
510510

511-
const row = event.target.closest<HTMLElement>(
512-
"[data-item-path][data-item-type='file']",
513-
);
514-
const path = row?.dataset.itemPath;
511+
const path = getClickedFileTreePath(event);
515512
if (!path) {
516513
return;
517514
}
@@ -567,6 +564,33 @@ function WorkspaceFilesPanel({ api, root }: { api: ApiClient; root?: string }) {
567564
);
568565
}
569566

567+
function getClickedFileTreePath(event: ReactMouseEvent<HTMLElement>) {
568+
const directTarget =
569+
event.target instanceof Element
570+
? event.target.closest<HTMLElement>(
571+
"[data-item-path][data-item-type='file']",
572+
)
573+
: null;
574+
if (directTarget?.dataset.itemPath) {
575+
return directTarget.dataset.itemPath;
576+
}
577+
578+
for (const target of event.nativeEvent.composedPath()) {
579+
if (!(target instanceof HTMLElement)) {
580+
continue;
581+
}
582+
if (
583+
target.dataset.itemType === "file" &&
584+
typeof target.dataset.itemPath === "string" &&
585+
target.dataset.itemPath.length > 0
586+
) {
587+
return target.dataset.itemPath;
588+
}
589+
}
590+
591+
return null;
592+
}
593+
570594
function WorkspaceGitPanel({ api, root }: { api: ApiClient; root?: string }) {
571595
const openWorkspaceTool = useWorkspaceToolStore(
572596
(state) => state.openWorkspaceTool,

desktop/src/renderer/app/workspace/workspace-tool-host.tsx

Lines changed: 152 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ import type {
1010
import type { FileDiffMetadata } from "@pierre/diffs";
1111
import type { CSSProperties } from "react";
1212

13-
import { parsePatchFiles } from "@pierre/diffs";
13+
import {
14+
getFiletypeFromFileName,
15+
getHighlighterOptions,
16+
parsePatchFiles,
17+
preloadHighlighter,
18+
} from "@pierre/diffs";
1419
import { FileDiff } from "@pierre/diffs/react";
1520
import {
1621
RiAddLine as Add,
@@ -37,7 +42,12 @@ import {
3742
CollapsibleContent,
3843
CollapsibleTrigger,
3944
} from "@/components/ui/collapsible";
40-
import { Dialog, DialogContent } from "@/components/ui/dialog";
45+
import {
46+
Dialog,
47+
DialogContent,
48+
DialogDescription,
49+
DialogTitle,
50+
} from "@/components/ui/dialog";
4151
import { Skeleton } from "@/components/ui/skeleton";
4252
import { getApiClient } from "@/platform/api-client";
4353
import { queryKeys } from "@/platform/query-keys";
@@ -164,6 +174,12 @@ export function WorkspaceToolDialogHost({ api }: { api: ApiClient }) {
164174
className="!flex h-[min(90vh,960px)] !w-[calc(100vw-24px)] !max-w-[calc(100vw-24px)] flex-row gap-0 overflow-hidden p-0 sm:!w-[calc(100vw-40px)] sm:!max-w-[calc(100vw-40px)]"
165175
showCloseButton={false}
166176
>
177+
<DialogTitle className="sr-only">
178+
{workspaceToolDisplayTitle(instance)}
179+
</DialogTitle>
180+
<DialogDescription className="sr-only">
181+
Workspace tool dialog
182+
</DialogDescription>
167183
<WorkspaceToolWorkbench
168184
activeToolId={instance.id}
169185
api={api}
@@ -457,6 +473,9 @@ export function WorkspaceToolWindowPage({ toolId }: { toolId: string }) {
457473

458474
return (
459475
<div className="flex h-screen min-h-0 bg-background text-foreground">
476+
<WorkspaceToolWindowTitleBridge
477+
title={workspaceToolWindowDocumentTitle(activeTool)}
478+
/>
460479
<WorkspaceToolWorkbench
461480
activeToolId={activeTool.id}
462481
api={api}
@@ -473,6 +492,21 @@ export function WorkspaceToolWindowPage({ toolId }: { toolId: string }) {
473492
);
474493
}
475494

495+
function WorkspaceToolWindowTitleBridge({ title }: { title: string }) {
496+
const syncTitle = useCallback(
497+
(node: HTMLSpanElement | null) => {
498+
if (!node) {
499+
return;
500+
}
501+
502+
document.title = title;
503+
},
504+
[title],
505+
);
506+
507+
return <span hidden ref={syncTitle} />;
508+
}
509+
476510
function WorkspaceToolHeader({
477511
title,
478512
onClose,
@@ -614,17 +648,49 @@ function nextWorkspaceToolTitle(
614648
kind: "browser" | "terminal",
615649
label: string,
616650
) {
617-
const nextOrdinal = tools.filter((tool) => tool.kind === kind).length + 1;
651+
const ordinalPattern = new RegExp(`^${escapeRegExp(label)}\\s+(\\d+)$`, "u");
652+
const nextOrdinal =
653+
Math.max(
654+
0,
655+
...tools
656+
.filter((tool) => tool.kind === kind)
657+
.map((tool) => {
658+
const match = ordinalPattern.exec(tool.title.trim());
659+
return match ? Number(match[1]) : 0;
660+
}),
661+
) + 1;
662+
618663
return `${label} ${nextOrdinal}`;
619664
}
620665

666+
function escapeRegExp(value: string) {
667+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
668+
}
669+
621670
function workspaceToolDisplayTitle(instance: WorkspaceToolInstance) {
622671
const root = instance.kind === "browser" ? undefined : instance.root;
623672
const rootName = root ? workspaceToolRootName(root) : undefined;
624673

625674
return rootName ? `${instance.title} · ${rootName}` : instance.title;
626675
}
627676

677+
function workspaceToolWindowDocumentTitle(instance: WorkspaceToolInstance) {
678+
return `Angel Engine · ${workspaceToolKindLabel(instance)}: ${workspaceToolDisplayTitle(instance)}`;
679+
}
680+
681+
function workspaceToolKindLabel(instance: WorkspaceToolInstance) {
682+
switch (instance.kind) {
683+
case "browser":
684+
return "Browser";
685+
case "file-preview":
686+
return "File";
687+
case "git-diff":
688+
return "Git";
689+
case "terminal":
690+
return "Terminal";
691+
}
692+
}
693+
628694
function workspaceToolIcon(instance: WorkspaceToolInstance) {
629695
switch (instance.kind) {
630696
case "browser":
@@ -852,12 +918,13 @@ function WorkspaceToolPatchFileItem({
852918
{formatWorkspaceToolPatchSource(diff.source)}
853919
</div>
854920
) : null}
855-
<FileDiff
856-
className="block overflow-hidden bg-background"
857-
disableWorkerPool
921+
<WorkspaceToolFileDiff
858922
fileDiff={diff.fileDiff}
859-
options={diffOptions}
860-
style={diffHostStyle}
923+
preloadKey={workspaceToolFileDiffKey(
924+
diff.source,
925+
diff.fileDiff,
926+
index,
927+
)}
861928
/>
862929
</div>
863930
))}
@@ -867,6 +934,73 @@ function WorkspaceToolPatchFileItem({
867934
);
868935
}
869936

937+
function WorkspaceToolFileDiff({
938+
fileDiff,
939+
preloadKey,
940+
}: {
941+
fileDiff: FileDiffMetadata;
942+
preloadKey: string;
943+
}) {
944+
const preloadQuery = useQuery({
945+
queryFn: () => preloadWorkspaceToolFileDiffHighlighter(fileDiff),
946+
queryKey: [
947+
"workspace-tool-file-diff-highlighter",
948+
preloadKey,
949+
workspaceToolFileDiffVersion(fileDiff),
950+
],
951+
retry: false,
952+
staleTime: Infinity,
953+
});
954+
955+
if (!preloadQuery.data && !preloadQuery.isError) {
956+
return (
957+
<div className="space-y-2 p-2">
958+
<Skeleton className="h-6 w-48 rounded-md" />
959+
<Skeleton className="h-40 w-full rounded-md" />
960+
</div>
961+
);
962+
}
963+
964+
if (preloadQuery.isError) {
965+
return (
966+
<WorkspaceToolEmpty
967+
detail={getErrorMessage(preloadQuery.error)}
968+
title="Diff unavailable"
969+
/>
970+
);
971+
}
972+
973+
return (
974+
<FileDiff
975+
className="block overflow-hidden bg-background"
976+
disableWorkerPool
977+
fileDiff={fileDiff}
978+
key={preloadKey}
979+
options={diffOptions}
980+
style={diffHostStyle}
981+
/>
982+
);
983+
}
984+
985+
async function preloadWorkspaceToolFileDiffHighlighter(
986+
fileDiff: FileDiffMetadata,
987+
) {
988+
const names = [fileDiff.name, fileDiff.prevName].filter(
989+
(name): name is string => name != null,
990+
);
991+
const languages = new Set(
992+
names.map((name) => fileDiff.lang ?? getFiletypeFromFileName(name)),
993+
);
994+
995+
await Promise.all(
996+
[...languages].map((language) =>
997+
preloadHighlighter(getHighlighterOptions(language, diffOptions)),
998+
),
999+
);
1000+
1001+
return true;
1002+
}
1003+
8701004
function buildWorkspaceToolPatchList(
8711005
stagedPatch: string,
8721006
unstagedPatch: string,
@@ -993,6 +1127,16 @@ function workspaceToolFileDiffKey(
9931127
return `${source}:${index}:${fileDiff.cacheKey ?? fileDiff.prevName ?? ""}:${fileDiff.name}`;
9941128
}
9951129

1130+
function workspaceToolFileDiffVersion(fileDiff: FileDiffMetadata) {
1131+
return [
1132+
fileDiff.unifiedLineCount,
1133+
fileDiff.splitLineCount,
1134+
...fileDiff.hunks.map((hunk) => hunk.hunkSpecs ?? ""),
1135+
...fileDiff.deletionLines,
1136+
...fileDiff.additionLines,
1137+
].join("\n");
1138+
}
1139+
9961140
function WorkspaceToolEmpty({
9971141
detail,
9981142
title,

0 commit comments

Comments
 (0)