@@ -10,7 +10,12 @@ import type {
1010import type { FileDiffMetadata } from "@pierre/diffs" ;
1111import 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" ;
1419import { FileDiff } from "@pierre/diffs/react" ;
1520import {
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" ;
4151import { Skeleton } from "@/components/ui/skeleton" ;
4252import { getApiClient } from "@/platform/api-client" ;
4353import { 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+
476510function 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+
621670function 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+
628694function 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+
8701004function 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+
9961140function WorkspaceToolEmpty ( {
9971141 detail,
9981142 title,
0 commit comments