dsh-vscode-workbench 0.2.1 → 0.2.3

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.
@@ -8,7 +8,7 @@ import { FileIcon } from './FileIcon.tsx'
8
8
  import { branchErrorMessage } from './gitError.ts'
9
9
  import { MarkdownPreview, markdownHeadings } from './MarkdownPreview.tsx'
10
10
  import type { MarkdownHeading } from './MarkdownPreview.tsx'
11
- import { MonacoDiffEditor, MonacoEditor, loadMonaco, preloadMonaco, retainEditorModels } from './MonacoEditor.tsx'
11
+ import { MonacoDiffEditor, MonacoEditor, isMacPlatform, loadMonaco, preloadMonaco, retainEditorModels } from './MonacoEditor.tsx'
12
12
  import { DshConversationPanel, DshConversationTheme } from './DshConversationPanel.tsx'
13
13
  import { DshMark } from './DshMark.tsx'
14
14
  import { ActivityBar, normalizeActivityOrder, type ActivityView } from './ActivityBar.tsx'
@@ -21,9 +21,12 @@ import { DocumentPreview, documentPreviewKind, type DocumentPreviewKind } from '
21
21
  import { toggleTerminal } from './shortcuts.ts'
22
22
  import { websocketUrl } from './websocket.ts'
23
23
  import { resetWorkspaceDocuments, WorkspaceRequestScope } from './workspaceState.ts'
24
+ import type { CommitDetail, CommitLanguage, CommitMessagePreferences } from '../commit-message.ts'
24
25
 
25
26
  interface Tab extends FileDocument { instance?: symbol; dirty: boolean; draft: string; loaded?: boolean; line?: number; column?: number; highlight?: string; external?: boolean; preview?: boolean; title?: string; diff?: GitDiff; diffMode?: 'inline' | 'split'; diffSourcePath?:string; imageUrl?:string; documentPreview?:DocumentPreviewKind; documentUrl?:string }
26
- type WorkbenchTheme = 'dsh' | 'dark' | 'light'
27
+ type WorkbenchTheme = 'dsh' | 'dark' | 'light' | 'white' | 'eyecare'
28
+ export type MarkdownDefaultView = 'preview' | 'source'
29
+ export const MARKDOWN_DEFAULT_VIEW_KEY = 'dvw:markdown-default-view'
27
30
  interface TreeProps { sessionId: string; path: string; depth: number; expanded: Set<string>; selected?: string; selectedDirectory: string; statuses: Map<string, string>; onToggle(path: string): void; onSelectDirectory(path: string): void; onOpen(path: string): void; onChange(): void; onUpload(directory: string, files: FileList): void; onAddToDsh(path: string, directory: boolean): void }
28
31
  interface CreatingEntry { kind: 'file' | 'directory'; directory: string; value: string; error?: string }
29
32
 
@@ -93,7 +96,10 @@ function PanelResizer({ className, label, width, min, max, defaultWidth, directi
93
96
  }}/>
94
97
  }
95
98
 
96
- function unloadedTab(path:string):Tab{return {instance:Symbol(path),path,content:'',draft:'',revision:'',size:0,binary:false,tooLarge:false,dirty:false,loaded:false,preview:/\.md$/i.test(path)}}
99
+ /** Read the saved Markdown default without depending on browser storage during unit tests. */
100
+ export function markdownDefaultView():MarkdownDefaultView{try{return localStorage.getItem(MARKDOWN_DEFAULT_VIEW_KEY)==='source'?'source':'preview'}catch{return 'preview'}}
101
+
102
+ function unloadedTab(path:string):Tab{return {instance:Symbol(path),path,content:'',draft:'',revision:'',size:0,binary:false,tooLarge:false,dirty:false,loaded:false,preview:/\.md$/i.test(path)?markdownDefaultView()==='preview':undefined}}
97
103
 
98
104
  function WorkbenchTitlebar({ name }: { name?: string }) {
99
105
  return <div className="dvw-titlebar"><strong>DSH Workbench</strong>{name !== undefined && <span>{name}</span>}</div>
@@ -387,22 +393,33 @@ function GitGroupHeader({ title, count, expanded, onToggle, children }: { title:
387
393
  return <div className="dvw-group-head"><button className="dvw-group-toggle" aria-expanded={expanded} onClick={onToggle}><Icon name={expanded?'chevron-down':'chevron-right'}/><span>{title}</span><span className="dvw-count">{count}</span></button>{children}</div>
388
394
  }
389
395
 
390
- function Git(props: { sessionId: string; onDiff(key:string, diff:GitDiff, title?:string, sourcePath?:string): void }) {
391
- const [status, setStatus] = useState<GitStatus>(); const [commits,setCommits]=useState<GitCommit[]>([]); const [message, setMessage] = useState(''); const [error, setError] = useState<string>(); const [busy,setBusy]=useState(false); const [expanded,setExpanded]=useState({staged:true,changes:true,graph:true});const [expandedCommit,setExpandedCommit]=useState<string>();const [commitFileMap,setCommitFileMap]=useState<Map<string,GitCommitFile[]>>(new Map());const [branchMenu,setBranchMenu]=useState(false);const [branches,setBranches]=useState<GitBranch[]>([]);const [newBranch,setNewBranch]=useState('');const [branchError,setBranchError]=useState<string>()
396
+ function Git(props: { sessionId: string; preferences: CommitMessagePreferences; onDiff(key:string, diff:GitDiff, title?:string, sourcePath?:string): void }) {
397
+ const [status, setStatus] = useState<GitStatus>(); const [commits,setCommits]=useState<GitCommit[]>([]); const [message, setMessage] = useState(''); const [error, setError] = useState<string>(); const [busy,setBusy]=useState(false); const [expanded,setExpanded]=useState({staged:true,changes:true,graph:true});const [expandedCommit,setExpandedCommit]=useState<string>();const [commitFileMap,setCommitFileMap]=useState<Map<string,GitCommitFile[]>>(new Map());const [branchMenu,setBranchMenu]=useState(false);const [branches,setBranches]=useState<GitBranch[]>([]);const [newBranch,setNewBranch]=useState('');const [branchError,setBranchError]=useState<string>();const [generating,setGenerating]=useState(false);const [commitMenu,setCommitMenu]=useState(false)
392
398
  const refresh = useCallback(() => { void Promise.all([api<GitStatus>('git.status',{sessionId:props.sessionId}),api<GitCommit[]>('git.action',{sessionId:props.sessionId,action:'log'})]).then(([nextStatus,nextCommits])=>{setStatus(nextStatus);setCommits(nextCommits);setError(undefined)}).catch(error=>{setStatus(undefined);setCommits([]);setError(error instanceof ApiError&&error.code==='not-repository'?undefined:String(error))}) }, [props.sessionId])
393
399
  const refreshDebounced = useDebouncedCallback(refresh, 500)
394
400
  useEffect(refresh,[refresh]); useEffect(()=>{const fn=()=>refreshDebounced();window.addEventListener('dvw-files-changed',fn);return()=>window.removeEventListener('dvw-files-changed',fn)},[refreshDebounced])
395
401
  const action = async (name: string, path?: string, value?: string) => { setBusy(true);try { const result = await api<GitStatus | GitDiff>('git.action',{sessionId:props.sessionId,action:name,path,value}); if(name==='diff'&&'diff'in result){const key=path??'';props.onDiff(key,result,key.split('/').at(-1)??key,key)} else {setStatus(result as GitStatus);if(name==='commit')refresh()} } catch(error){setError(String(error))}finally{setBusy(false)} }
396
402
  useEffect(()=>{if(!branchMenu)return;const close=()=>setBranchMenu(false);const escape=(event:KeyboardEvent)=>{if(event.key==='Escape')close()};window.addEventListener('pointerdown',close);window.addEventListener('keydown',escape);return()=>{window.removeEventListener('pointerdown',close);window.removeEventListener('keydown',escape)}},[branchMenu])
403
+ useEffect(()=>{if(!commitMenu)return;const close=()=>setCommitMenu(false);const escape=(event:KeyboardEvent)=>{if(event.key==='Escape')close()};window.addEventListener('pointerdown',close);window.addEventListener('keydown',escape);return()=>{window.removeEventListener('pointerdown',close);window.removeEventListener('keydown',escape)}},[commitMenu])
397
404
  const openBranchMenu=async()=>{if(branchMenu){setBranchMenu(false);return}setBusy(true);try{setBranches(await api<GitBranch[]>('git.action',{sessionId:props.sessionId,action:'branch.list'}));setBranchMenu(true)}catch(error){setError(String(error))}finally{setBusy(false)}}
398
405
  const switchBranch=async(name:string)=>{if(name===status?.branch){setBranchMenu(false);return}setBusy(true);try{setStatus(await api<GitStatus>('git.action',{sessionId:props.sessionId,action:'branch.switch',value:name}));setBranchMenu(false);refresh()}catch(error){setError(undefined);setBranchError(branchErrorMessage(error))}finally{setBusy(false)}}
399
406
  const createBranch=async()=>{const name=newBranch.trim();if(name==='')return;setBusy(true);try{setStatus(await api<GitStatus>('git.action',{sessionId:props.sessionId,action:'branch.create',value:name}));setNewBranch('');setBranchMenu(false);refresh()}catch(error){setError(undefined);setBranchError(branchErrorMessage(error))}finally{setBusy(false)}}
400
407
  const toggleCommit=async(commit:GitCommit)=>{if(expandedCommit===commit.hash){setExpandedCommit(undefined);return}setExpandedCommit(commit.hash);if(commitFileMap.has(commit.hash))return;setBusy(true);try{const files=await api<GitCommitFile[]>('git.action',{sessionId:props.sessionId,action:'commit.files',value:commit.hash});setCommitFileMap(current=>new Map(current).set(commit.hash,files))}catch(error){setError(String(error))}finally{setBusy(false)}}
401
408
  const openCommitFile=async(commit:GitCommit,file:GitCommitFile)=>{setBusy(true);try{const result=await api<GitDiff>('git.action',{sessionId:props.sessionId,action:'commit.file.diff',path:file.path,value:commit.hash});props.onDiff(`commit:${commit.hash}:${file.path}`,result,file.path.split('/').at(-1)??file.path,file.path)}catch(error){setError(String(error))}finally{setBusy(false)}}
409
+ const generateMessage=async()=>{if(generating)return;setGenerating(true);setError(undefined);try{const result=await api<{message:string}>('git.commitMessage',{sessionId:props.sessionId,detail:props.preferences.detail,language:props.preferences.language});setMessage(result.message)}catch(error){setError(error instanceof ApiError?error.message:String(error))}finally{setGenerating(false)}}
402
410
  const staged=status?.changes.filter(change=>change.index!=='.'&&change.index!=='?')??[]
403
411
  const changed=status?.changes.filter(change=>change.worktree!=='.'||change.untracked)??[]
404
412
  const renderChange=(change:GitStatus['changes'][number],group:'staged'|'worktree')=><div className="dvw-change" key={`${group}:${change.path}`} onClick={()=>void action('diff',change.path,change.untracked?'untracked':group)}><FileIcon name={change.path.split('/').at(-1) ?? change.path}/><span className="dvw-change-path">{change.path}</span><span className="dvw-badge">{change.untracked?'U':group==='staged'?change.index:change.worktree}</span><button className="dvw-icon" title={group==='staged'?'取消暂存':'暂存更改'} onClick={event=>{event.stopPropagation();void action(group==='staged'?'unstage':'stage',change.path)}}><Icon name={group==='staged'?'remove':'add'}/></button>{group==='worktree'&&<button className="dvw-icon" title="放弃更改" onClick={event=>{event.stopPropagation();if(confirm(`放弃 ${change.path} 的更改?此操作无法撤销。`))void action('discard',change.path,change.untracked?'untracked':undefined)}}><Icon name="discard"/></button>}</div>
405
- return <><div className="dvw-side-head">源代码管理<div className="dvw-toolbar"><button className="dvw-icon" title="刷新" disabled={busy} onClick={refresh}><Icon name="refresh"/></button></div></div>{error&&<div className="dvw-error">{error}</div>}{status&&<><div className="dvw-git-head"><button className="dvw-branch" aria-haspopup="menu" aria-expanded={branchMenu} onClick={()=>void openBranchMenu()}><Icon name="git-branch"/> {status.branch} <Icon name="chevron-down" className="dvw-branch-caret"/></button><span>↑{status.ahead} ↓{status.behind}</span>{branchMenu&&<div className="dvw-branch-menu" role="menu" onPointerDown={event=>event.stopPropagation()}><div className="dvw-branch-current">当前分支:{status.branch}</div><div className="dvw-branch-list">{branches.map(branch=><button key={branch.name} role="menuitem" data-active={branch.name===status.branch} disabled={busy||branch.worktree!==undefined} title={branch.worktree!==undefined?`已在 worktree ${branch.worktree} 中检出`:undefined} onClick={()=>void switchBranch(branch.name)}><Icon name={branch.name===status.branch?'check':'git-branch'}/><span>{branch.name}</span>{branch.worktree!==undefined&&<span className="dvw-branch-worktree">worktree</span>}</button>)}</div><div className="dvw-branch-create"><input className="dvw-input" value={newBranch} placeholder="新分支名称" onChange={event=>setNewBranch(event.target.value)} onKeyDown={event=>{if(event.key==='Enter'){event.preventDefault();void createBranch()}}}/><button className="dvw-icon dvw-branch-create-button" title="创建并切换分支" disabled={busy||!newBranch.trim()} onClick={()=>void createBranch()}><Icon name="add"/></button></div></div>}</div><div className="dvw-commit"><input className="dvw-input" value={message} onChange={event=>setMessage(event.target.value)} onKeyDown={event=>{if(event.ctrlKey&&event.key==='Enter'&&message.trim())void action('commit',undefined,message).then(()=>setMessage(''))}} placeholder={`消息 (Ctrl+Enter 在“${status.branch}”提交)`}/><button className="dvw-commit-button" disabled={busy||!message.trim()} onClick={()=>void action('commit',undefined,message).then(()=>setMessage(''))}><Icon name="check"/> 提交</button></div><div className="dvw-git">{staged.length>0&&<section><GitGroupHeader title="暂存的更改" count={staged.length} expanded={expanded.staged} onToggle={()=>setExpanded(value=>({...value,staged:!value.staged}))}><button className="dvw-icon" title="取消暂存所有更改" onClick={()=>void action('unstage.all')}><Icon name="remove"/></button></GitGroupHeader>{expanded.staged&&staged.map(change=>renderChange(change,'staged'))}</section>}<section><GitGroupHeader title="更改" count={changed.length} expanded={expanded.changes} onToggle={()=>setExpanded(value=>({...value,changes:!value.changes}))}><button className="dvw-icon" title="放弃所有更改" onClick={()=>{if(confirm('放弃所有已跟踪和未跟踪的更改?此操作无法撤销。'))void action('discard.all')}}><Icon name="discard"/></button><button className="dvw-icon" title="暂存所有更改" onClick={()=>void action('stage.all')}><Icon name="add"/></button></GitGroupHeader>{expanded.changes&&changed.map(change=>renderChange(change,'worktree'))}</section><section className="dvw-graph"><GitGroupHeader title="图表" count={commits.length} expanded={expanded.graph} onToggle={()=>setExpanded(value=>({...value,graph:!value.graph}))}><button className="dvw-icon" title="刷新提交图表" onClick={refresh}><Icon name="refresh"/></button></GitGroupHeader>{expanded.graph&&<CommitGraph commits={commits} expanded={expandedCommit} files={commitFileMap} onToggle={commit=>void toggleCommit(commit)} onOpenFile={(commit,file)=>void openCommitFile(commit,file)}/>}</section></div>{branchError!==undefined&&<div className="dvw-confirm-backdrop" role="presentation" onMouseDown={()=>setBranchError(undefined)}><section className="dvw-confirm" role="alertdialog" aria-modal="true" aria-labelledby="dvw-branch-error-title" onMouseDown={event=>event.stopPropagation()}><h2 id="dvw-branch-error-title">无法切换分支</h2><p className="dvw-branch-error-message">{branchError}</p><footer><button autoFocus onClick={()=>setBranchError(undefined)}>确定</button></footer></section></div>}</>}</>
413
+ return <>
414
+ <div className="dvw-side-head">源代码管理<div className="dvw-toolbar"><button className="dvw-icon" title="刷新" disabled={busy} onClick={refresh}><Icon name="refresh"/></button></div></div>
415
+ {error&&<div className="dvw-error">{error}</div>}
416
+ {status&&<>
417
+ <div className="dvw-git-head"><button className="dvw-branch" aria-haspopup="menu" aria-expanded={branchMenu} onClick={()=>void openBranchMenu()}><Icon name="git-branch"/> {status.branch} <Icon name="chevron-down" className="dvw-branch-caret"/></button><span>↑{status.ahead} ↓{status.behind}</span>{branchMenu&&<div className="dvw-branch-menu" role="menu" onPointerDown={event=>event.stopPropagation()}><div className="dvw-branch-current">当前分支:{status.branch}</div><div className="dvw-branch-list">{branches.map(branch=><button key={branch.name} role="menuitem" data-active={branch.name===status.branch} disabled={busy||branch.worktree!==undefined} title={branch.worktree!==undefined?`已在 worktree ${branch.worktree} 中检出`:undefined} onClick={()=>void switchBranch(branch.name)}><Icon name={branch.name===status.branch?'check':'git-branch'}/><span>{branch.name}</span>{branch.worktree!==undefined&&<span className="dvw-branch-worktree">worktree</span>}</button>)}</div><div className="dvw-branch-create"><input className="dvw-input" value={newBranch} placeholder="新分支名称" onChange={event=>setNewBranch(event.target.value)} onKeyDown={event=>{if(event.key==='Enter'){event.preventDefault();void createBranch()}}}/><button className="dvw-icon dvw-branch-create-button" title="创建并切换分支" disabled={busy||!newBranch.trim()} onClick={()=>void createBranch()}><Icon name="add"/></button></div></div>}</div>
418
+ <div className="dvw-commit"><div className="dvw-commit-box"><textarea className="dvw-textarea dvw-commit-input" rows={1} value={message} onChange={event=>setMessage(event.target.value)} onKeyDown={event=>{if((event.ctrlKey||event.metaKey)&&event.key==='Enter'&&message.trim())void action('commit',undefined,message).then(()=>setMessage(''))}} placeholder={`消息 (${isMacPlatform()?'⌘':'Ctrl+'}Enter 在“${status.branch}”提交)`}/><button className="dvw-commit-ai" disabled={busy||generating||status.changes.length===0} title={generating?'正在生成提交信息':'根据当前更改生成提交信息'} aria-label="生成提交信息" onClick={()=>void generateMessage()}><Icon name="sparkle"/></button></div><div className="dvw-commit-actions"><button className="dvw-commit-button" disabled={busy||!message.trim()||status.changes.length===0} title="提交暂存的更改" onClick={()=>void action('commit',undefined,message).then(()=>setMessage(''))}><Icon name="check"/> 提交</button><button className="dvw-commit-more" disabled={busy||!message.trim()||status.changes.length===0} aria-haspopup="menu" aria-expanded={commitMenu} title="更多提交操作" aria-label="更多提交操作" onClick={()=>setCommitMenu(value=>!value)}><Icon name="chevron-down"/></button>{commitMenu&&<div className="dvw-commit-menu" role="menu" onPointerDown={event=>event.stopPropagation()}><button role="menuitem" onClick={()=>{setCommitMenu(false);void action('commit',undefined,message).then(()=>setMessage(''))}}>提交</button><button role="menuitem" onClick={()=>{setCommitMenu(false);void action('commit.amend',undefined,message).then(()=>setMessage(''))}}>提交(修改)</button><div className="dvw-commit-menu-sep" role="separator"/><button role="menuitem" onClick={()=>{setCommitMenu(false);void action('commit',undefined,message).then(()=>action('push')).then(()=>setMessage(''))}}>提交和推送</button><button role="menuitem" onClick={()=>{setCommitMenu(false);void action('commit',undefined,message).then(()=>action('sync')).then(()=>setMessage(''))}}>提交和同步</button></div>}</div></div>
419
+ <div className="dvw-git">{staged.length>0&&<section><GitGroupHeader title="暂存的更改" count={staged.length} expanded={expanded.staged} onToggle={()=>setExpanded(value=>({...value,staged:!value.staged}))}><button className="dvw-icon" title="取消暂存所有更改" onClick={()=>void action('unstage.all')}><Icon name="remove"/></button></GitGroupHeader>{expanded.staged&&staged.map(change=>renderChange(change,'staged'))}</section>}<section><GitGroupHeader title="更改" count={changed.length} expanded={expanded.changes} onToggle={()=>setExpanded(value=>({...value,changes:!value.changes}))}><button className="dvw-icon" title="放弃所有更改" onClick={()=>{if(confirm('放弃所有已跟踪和未跟踪的更改?此操作无法撤销。'))void action('discard.all')}}><Icon name="discard"/></button><button className="dvw-icon" title="暂存所有更改" onClick={()=>void action('stage.all')}><Icon name="add"/></button></GitGroupHeader>{expanded.changes&&changed.map(change=>renderChange(change,'worktree'))}</section><section className="dvw-graph"><GitGroupHeader title="图表" count={commits.length} expanded={expanded.graph} onToggle={()=>setExpanded(value=>({...value,graph:!value.graph}))}><button className="dvw-icon" title="刷新提交图表" onClick={refresh}><Icon name="refresh"/></button></GitGroupHeader>{expanded.graph&&<CommitGraph commits={commits} expanded={expandedCommit} files={commitFileMap} onToggle={commit=>void toggleCommit(commit)} onOpenFile={(commit,file)=>void openCommitFile(commit,file)}/>}</section></div>
420
+ {branchError!==undefined&&<div className="dvw-confirm-backdrop" role="presentation" onMouseDown={()=>setBranchError(undefined)}><section className="dvw-confirm" role="alertdialog" aria-modal="true" aria-labelledby="dvw-branch-error-title" onMouseDown={event=>event.stopPropagation()}><h2 id="dvw-branch-error-title">无法切换分支</h2><p className="dvw-branch-error-message">{branchError}</p><footer><button autoFocus onClick={()=>setBranchError(undefined)}>确定</button></footer></section></div>}
421
+ </>}
422
+ </>
406
423
  }
407
424
 
408
425
  /** Keep range dragging responsive and commit the editor font size after a short pause. */
@@ -426,13 +443,21 @@ export function Workbench({ service, sessionId, cwd, navigation }: { service: Vs
426
443
  useEffect(() => service.subscribe(() => { setSnapshot(service.getSnapshot()); setPanels(service.getPanels()) }), [service])
427
444
  const [workspace,setWorkspace]=useState<{cwd:string;name:string}>(); const [tabs,setTabs]=useState<Tab[]>([]); const [active,setActive]=useState<string>(); const [error,setError]=useState<string>(); const [panelHeight,setPanelHeight]=useState(260); const [sideWidth,setSideWidth]=useState(210); const [primarySideVisible,setPrimarySideVisible]=useState(()=>localStorage.getItem('dvw:primary-side-visible')!=='closed');const [statusVisible,setStatusVisible]=useState(()=>localStorage.getItem('dvw:status-bar-visible')==='open');const [settingsOpen,setSettingsOpen]=useState(false);const [chatWidth,setChatWidth]=useState(()=>clampChatWidth(Number(localStorage.getItem('dvw:dsh-chat-width'))||DSH_CHAT_DEFAULT_WIDTH));const [chatVisible,setChatVisible]=useState(()=>localStorage.getItem('dvw:dsh-chat-visible')!=='closed');const [outlineVisible,setOutlineVisible]=useState(()=>localStorage.getItem('dvw:outline')!=='closed');const [tsIntellisense,setTsIntellisense]=useState(tsIntellisenseEnabled);const [tsIndex,setTsIndex]=useState<{state:'idle'|'loading'|'ready'|'error';files?:number;truncated?:boolean;error?:string}>({state:'idle'});const [lspServers,setLspServers]=useState<LspServerStatus[]>([]);const [lspBusy,setLspBusy]=useState<string>();const [lspError,setLspError]=useState('');const lspManager=useRef<LspManager>();const [fontSize,setFontSize]=useState(()=>{const saved=Math.round(Number(localStorage.getItem('dvw:font-size')));return Number.isFinite(saved)&&saved>=11&&saved<=20?saved:14});const [tabMenu,setTabMenu]=useState<{x:number;y:number;path:string}>()
428
445
  const tabsRef=useRef(tabs);tabsRef.current=tabs
446
+ const [settingsSection,setSettingsSection]=useState<'appearance'|'layout'|'commit'|'language'>('appearance')
429
447
  const tabListRef=useRef<HTMLDivElement>(null)
430
448
  const workspaceRequests=useRef(new WorkspaceRequestScope());const loadedWorkspaceCwd=useRef<string>();const loadingTabs=useRef(new Map<string, symbol>())
431
449
  const dshDark = useDshDarkTheme()
432
- const [theme,setTheme]=useState<WorkbenchTheme>(()=>{const saved=localStorage.getItem('dvw:theme');return saved==='dark'||saved==='light'?saved:'dsh'})
433
- const colorScheme: 'dark' | 'light' = theme === 'dsh' ? dshDark ? 'dark' : 'light' : theme
450
+ const [theme,setTheme]=useState<WorkbenchTheme>(()=>{const saved=localStorage.getItem('dvw:theme');return saved==='dark'||saved==='light'||saved==='white'||saved==='eyecare'?saved:'dsh'})
451
+ const [markdownView,setMarkdownView]=useState<MarkdownDefaultView>(()=>markdownDefaultView())
452
+ const [commitDetail,setCommitDetail]=useState<CommitDetail>(()=>{const saved=localStorage.getItem('dvw:commit-detail');return saved==='concise'||saved==='detailed'?saved:'standard'})
453
+ const [commitLanguage,setCommitLanguage]=useState<CommitLanguage>(()=>{const saved=localStorage.getItem('dvw:commit-language');return saved==='zh'||saved==='en'?saved:'auto'})
454
+ const colorScheme: 'dark' | 'light' | 'white' | 'eyecare' = theme === 'dsh' ? dshDark ? 'dark' : 'light' : theme
455
+ const commitPreferences=useMemo<CommitMessagePreferences>(()=>({detail:commitDetail,language:commitLanguage}),[commitDetail,commitLanguage])
434
456
  useEffect(()=>localStorage.setItem('dvw:theme',theme),[theme])
435
- useEffect(()=>{if(!snapshot.visible||theme==='dsh')return;const body=document.body;const root=document.documentElement;const wasDark=body.hasAttribute('data-ds-dark-theme');const previousColorScheme=root.style.colorScheme;body.toggleAttribute('data-ds-dark-theme',theme==='dark');root.style.colorScheme=theme;return()=>{body.toggleAttribute('data-ds-dark-theme',wasDark);if(previousColorScheme==='')root.style.removeProperty('color-scheme');else root.style.colorScheme=previousColorScheme}},[snapshot.visible,theme])
457
+ useEffect(()=>localStorage.setItem(MARKDOWN_DEFAULT_VIEW_KEY,markdownView),[markdownView])
458
+ useEffect(()=>localStorage.setItem('dvw:commit-detail',commitDetail),[commitDetail])
459
+ useEffect(()=>localStorage.setItem('dvw:commit-language',commitLanguage),[commitLanguage])
460
+ useEffect(()=>{if(!snapshot.visible||theme==='dsh')return;const body=document.body;const root=document.documentElement;const wasDark=body.hasAttribute('data-ds-dark-theme');const previousColorScheme=root.style.colorScheme;body.toggleAttribute('data-ds-dark-theme',theme==='dark');root.style.colorScheme=theme==='dark'?'dark':'light';return()=>{body.toggleAttribute('data-ds-dark-theme',wasDark);if(previousColorScheme==='')root.style.removeProperty('color-scheme');else root.style.colorScheme=previousColorScheme}},[snapshot.visible,theme])
436
461
  useEffect(()=>localStorage.setItem('dvw:outline',outlineVisible?'open':'closed'),[outlineVisible])
437
462
  useEffect(()=>localStorage.setItem('dvw:dsh-chat-visible',chatVisible?'open':'closed'),[chatVisible])
438
463
  useEffect(()=>localStorage.setItem('dvw:dsh-chat-width',String(Math.round(chatWidth))),[chatWidth])
@@ -449,7 +474,7 @@ export function Workbench({ service, sessionId, cwd, navigation }: { service: Vs
449
474
  // The conversation may still be attaching after the chat column opens, so retry for about a second.
450
475
  const addToDsh=useCallback((reference:string)=>{setChatVisible(true);let frames=0;const attempt=()=>{void insertIntoDshComposer(reference).then(done=>{if(done)return;if(++frames<60){requestAnimationFrame(attempt);return}alert('DSH 对话输入框当前不可用,请先选择会话或等待当前回复结束。')})};requestAnimationFrame(attempt)},[])
451
476
  const addEntryToDsh=useCallback((path:string,directory:boolean)=>addToDsh(dshReference(path,{directory})),[addToDsh])
452
- const openForWorkspace=(path:string,line?:number,column?:number,highlight?:string)=>{const existing=tabsRef.current.find(tab=>tab.path===path);if(existing)setTabs(rows=>rows.map(tab=>tab.path===path?{...tab,line,column,highlight,preview:line!==undefined?false:tab.preview}:tab));else setTabs(rows=>[...rows,{...unloadedTab(path),line,column,highlight,preview:line!==undefined?false:undefined}]);setActive(path)}
477
+ const openForWorkspace=(path:string,line?:number,column?:number,highlight?:string)=>{const existing=tabsRef.current.find(tab=>tab.path===path);if(existing)setTabs(rows=>rows.map(tab=>tab.path===path?{...tab,line,column,highlight,preview:line!==undefined?false:tab.preview}:tab));else setTabs(rows=>[...rows,{...unloadedTab(path),line,column,highlight,...(line!==undefined?{preview:false}:{})}]);setActive(path)}
453
478
  useEffect(() => {
454
479
  if (sessionId === undefined) return
455
480
  // Switching sessions inside one workspace must keep the explorer, tabs and layout as they are.
@@ -615,6 +640,8 @@ export function Workbench({ service, sessionId, cwd, navigation }: { service: Vs
615
640
  const refreshLsp=useCallback(async()=>{if(sessionId===undefined)return;try{const list=await api<LspServerStatus[]>('lsp.status',{sessionId});setLspServers(list);await startLsp(list)}catch(error){setLspError(error instanceof Error?error.message:String(error))}},[sessionId,startLsp])
616
641
  const changeLsp=useCallback(async(id:string,action:'install'|'uninstall')=>{if(sessionId===undefined)return;setLspBusy(id);setLspError('');try{const list=await api<LspServerStatus[]>(`lsp.${action}`,{sessionId,id});setLspServers(list);await startLsp(list)}catch(error){setLspError(error instanceof Error?error.message:String(error))}finally{setLspBusy(undefined)}},[sessionId,startLsp])
617
642
  useEffect(()=>{if(!snapshot.visible||sessionId===undefined||workspace===undefined)return;void refreshLsp()},[snapshot.visible,sessionId,workspace,refreshLsp])
643
+ // Opening the language panel re-probes the machine; there is no manual "re-detect" button to click.
644
+ useEffect(()=>{if(settingsOpen&&settingsSection==='language')void refreshLsp()},[settingsOpen,settingsSection,refreshLsp])
618
645
  useEffect(()=>()=>{lspManager.current?.dispose()},[])
619
646
  useEffect(()=>{try{localStorage.setItem(TS_INTELLISENSE_KEY,tsIntellisense?'on':'off')}catch{}},[tsIntellisense])
620
647
  // Definition and reference jumps land in workbench tabs for every language, including LSP servers
@@ -679,18 +706,46 @@ export function Workbench({ service, sessionId, cwd, navigation }: { service: Vs
679
706
  const selectHeading=(index:number)=>{if(activeTab===undefined)return;setTabs(rows=>rows.map(tab=>tab.path===activeTab.path?{...tab,preview:true}:tab));window.setTimeout(()=>window.dispatchEvent(new CustomEvent('dvw-markdown-heading',{detail:index})),50)}
680
707
  const suppressAttachmentDrop=(event:DragEvent)=>{if(!event.dataTransfer.types.includes('Files'))return;const target=event.target;const insideTree=target instanceof Element&&target.closest('.dvw-tree')!==null;if(!insideTree){event.preventDefault();event.stopPropagation()}}
681
708
  if(!snapshot.visible)return null
682
- if(sessionId===undefined)return <div className="dvw-overlay" data-dvw-theme={colorScheme}><WorkbenchTitlebar/><div className="dvw-empty" style={{gridColumn:'1/4'}}><div>请先选择或创建一个 DSH 会话。<br/><button onClick={()=>service.hide()}>返回 DSH</button></div></div></div>
709
+ if(sessionId===undefined)return <div className="dvw-overlay" data-dvw-theme={colorScheme} data-dvw-theme-name={theme}><WorkbenchTitlebar/><div className="dvw-empty" style={{gridColumn:'1/4'}}><div>请先选择或创建一个 DSH 会话。<br/><button onClick={()=>service.hide()}>返回 DSH</button></div></div></div>
683
710
  const markdown=activeTab!==undefined&&!activeTab.path.startsWith('diff:')&&/\.md$/i.test(activeTab.path)
684
711
  const diffMarkdown=activeTab?.diff!==undefined&&/\.md$/i.test(activeTab.diffSourcePath??'')
685
712
  const markdownPreview=markdown||(diffMarkdown&&activeTab?.preview===true)
686
713
  const headings=markdownPreview&&activeTab?markdownHeadings(activeTab.diff?.modified??activeTab.draft):[]
687
- return <div className="dvw-overlay" data-dvw-theme={colorScheme} data-dvw-chat={chatVisible||undefined} data-dvw-primary-side={primarySideVisible||undefined} data-dvw-status={statusVisible||undefined} data-dvw-bottom-panel={snapshot.bottomPanel||undefined} style={{'--dvw-side-width':`${sideWidth}px`,'--dvw-chat-width':`${chatWidth}px`} as CSSProperties} role="dialog" aria-label="VS Code 工作台" onDragEnterCapture={suppressAttachmentDrop} onDragOverCapture={suppressAttachmentDrop} onDropCapture={suppressAttachmentDrop}>
714
+ return <div className="dvw-overlay" data-dvw-theme={colorScheme} data-dvw-theme-name={theme} data-dvw-chat={chatVisible||undefined} data-dvw-primary-side={primarySideVisible||undefined} data-dvw-status={statusVisible||undefined} data-dvw-bottom-panel={snapshot.bottomPanel||undefined} style={{'--dvw-side-width':`${sideWidth}px`,'--dvw-chat-width':`${chatWidth}px`} as CSSProperties} role="dialog" aria-label="VS Code 工作台" onDragEnterCapture={suppressAttachmentDrop} onDragOverCapture={suppressAttachmentDrop} onDropCapture={suppressAttachmentDrop}>
688
715
  <WorkbenchTitlebar name={workspace?.name}/>
689
716
  <DshConversationPanel visible={chatVisible&&!settingsOpen} width={chatWidth} statusHeight={statusVisible?30:6}/>
690
717
  <DshConversationTheme visible={chatVisible} colorScheme={colorScheme} followDsh={false}/>
691
718
  {chatVisible&&<aside className="dvw-chat"><PanelResizer className="dvw-chat-resizer" label="拖动调整 DSH 对话栏宽度" width={chatWidth} min={DSH_CHAT_MIN_WIDTH} max={DSH_CHAT_MAX_WIDTH} defaultWidth={DSH_CHAT_DEFAULT_WIDTH} direction={-1} hideBelow={200} onHide={()=>setChatVisible(false)} onWidth={setChatWidth}/><header className="dvw-chat-head"><span><DshMark/> DSH</span><button className="dvw-icon" title="关闭 DSH 对话栏" onClick={()=>setChatVisible(false)}><Icon name="close"/></button></header><div className="dvw-chat-body"/></aside>}
692
- {switcherOpen&&<WorkspaceSwitcher choices={switcherChoices} activeSessionId={sessionId} unavailable={navigation?.available===false} busy={switcherBusy} error={switcherError} onCreate={()=>void createWorkspace()} onSwitch={switchSession} onOpenWorkspace={enterWorkspace} onClose={()=>setSwitcherOpen(false)}/>}
693
- {settingsOpen&&<div className="dvw-settings-backdrop" role="presentation" onMouseDown={()=>setSettingsOpen(false)}><section className="dvw-settings" role="dialog" aria-modal="true" aria-label="工作台设置" onMouseDown={event=>event.stopPropagation()}><header><span><Icon name="settings"/> 工作台设置</span><button className="dvw-icon" title="关闭设置" onClick={()=>setSettingsOpen(false)}><Icon name="close"/></button></header><div className="dvw-settings-body"><section><h3>外观</h3><label>颜色主题</label><div className="dvw-settings-themes">{([['dsh','跟随 DSH'],['dark','深色'],['light','浅色']] as const).map(([value,label])=><button key={value} data-active={theme===value} onClick={()=>setTheme(value)}><span className={`dvw-theme-swatch ${value}`}/><span>{label}</span>{theme===value&&<Icon name="check"/>}</button>)}</div><FontSizeControl value={fontSize} onChange={setFontSize}/></section><section><h3>布局</h3><label className="dvw-setting-check"><input type="checkbox" checked={primarySideVisible} onChange={event=>setPrimarySideVisible(event.target.checked)}/><span>显示左侧视图</span></label><label className="dvw-setting-check"><input type="checkbox" checked={chatVisible} onChange={event=>setChatVisible(event.target.checked)}/><span>显示 DSH 对话栏</span></label><label className="dvw-setting-check"><input type="checkbox" checked={statusVisible} onChange={event=>setStatusVisible(event.target.checked)}/><span>显示底部状态栏</span></label><label className="dvw-setting-range"><span>左侧视图宽度</span><input type="range" min="210" max="640" step="1" value={Math.round(sideWidth)} onChange={event=>setSideWidth(Math.round(Number(event.target.value)))}/><output>{sideWidth}px</output></label><label className="dvw-setting-range"><span>DSH 对话栏宽度</span><input type="range" min={DSH_CHAT_MIN_WIDTH} max={DSH_CHAT_MAX_WIDTH} step="1" value={Math.round(chatWidth)} onChange={event=>setChatWidth(clampChatWidth(Number(event.target.value)))}/><output>{chatWidth}px</output></label></section><section><h3>语言智能</h3><div className="dvw-setting-extension"><div><strong>TypeScript / JavaScript</strong><span>内置 · {tsIntellisense ? tsIndex.state === 'loading' ? '正在索引工作区…' : tsIndex.state === 'error' ? `索引失败:${tsIndex.error}` : tsIndex.state === 'ready' ? `已索引 ${tsIndex.files} 个文件${tsIndex.truncated ? '(已达上限)' : ''}` : '已安装,转到定义、查找引用、Peek、补全、签名帮助' : '点击安装'}</span></div><button className={tsIntellisense?undefined:'dvw-primary-button'} onClick={()=>setTsIntellisense(value=>!value)}>{tsIntellisense?'卸载':'安装'}</button></div>{lspServers.map(server=><div className="dvw-setting-extension" key={server.id}><div><strong>{server.name}</strong><span>{server.installed?'已安装':server.installable?'点击安装':'需要手动安装'}{!server.installed&&server.hint?` · ${server.hint}`:''}</span></div>{server.installable?<button disabled={lspBusy===server.id} className={server.installed?undefined:'dvw-primary-button'} onClick={()=>void changeLsp(server.id,server.installed?'uninstall':'install')}>{lspBusy===server.id?'处理中…':server.installed?'卸载':'安装'}</button>:<button onClick={()=>void refreshLsp()}>重新检测</button>}</div>)}<p className="dvw-setting-hint">每种语言使用该生态里最活跃的语言服务器,安装只写入本插件的私有目录。安装后在编辑器中 F12 转到定义、Shift+F12 查找引用、Alt+F12 速览、Ctrl+Space 补全,并显示诊断。</p>{lspError&&<p className="dvw-setting-hint">{lspError}</p>}</section></div></section></div>}
719
+ {switcherOpen&&<WorkspaceSwitcher choices={switcherChoices} activeSessionId={sessionId} unavailable={navigation?.available===false} busy={switcherBusy} error={switcherError} theme={colorScheme} onCreate={()=>void createWorkspace()} onSwitch={switchSession} onOpenWorkspace={enterWorkspace} onClose={()=>setSwitcherOpen(false)}/>}
720
+ {settingsOpen&&<div className="dvw-settings-backdrop" role="presentation" onMouseDown={()=>setSettingsOpen(false)}>
721
+ <section className="dvw-settings" role="dialog" aria-modal="true" aria-label="工作台设置" onMouseDown={event=>event.stopPropagation()}>
722
+ <header><span><Icon name="settings"/> 工作台设置</span><button className="dvw-icon" title="关闭设置" onClick={()=>setSettingsOpen(false)}><Icon name="close"/></button></header>
723
+ <div className="dvw-settings-body" data-section={settingsSection}>
724
+ <nav className="dvw-settings-nav" aria-label="设置分类">{([['appearance','外观','color-mode'],['layout','布局','layout-sidebar-right'],['commit','提交信息','sparkle'],['language','语言智能','code']] as const).map(([value,label,icon])=><button key={value} data-active={settingsSection===value} onClick={()=>setSettingsSection(value)}><Icon name={icon}/><span>{label}</span></button>)}</nav>
725
+ <section><h3>外观</h3><label>颜色主题</label><div className="dvw-settings-themes">{([['dsh','跟随 DSH'],['dark','深色'],['light','浅色'],['white','纯白'],['eyecare','护眼']] as const).map(([value,label])=><button key={value} data-active={theme===value} onClick={()=>setTheme(value)}><span className={`dvw-theme-swatch ${value}`}/><span>{label}</span>{theme===value&&<Icon name="check"/>}</button>)}</div><FontSizeControl value={fontSize} onChange={setFontSize}/></section>
726
+ <section><h3>布局</h3><label className="dvw-setting-check"><input type="checkbox" checked={primarySideVisible} onChange={event=>setPrimarySideVisible(event.target.checked)}/><span>显示左侧视图</span></label><label className="dvw-setting-check"><input type="checkbox" checked={chatVisible} onChange={event=>setChatVisible(event.target.checked)}/><span>显示 DSH 对话栏</span></label><label className="dvw-setting-check"><input type="checkbox" checked={statusVisible} onChange={event=>setStatusVisible(event.target.checked)}/><span>显示底部状态栏</span></label><label className="dvw-setting-range"><span>左侧视图宽度</span><input type="range" min="210" max="640" step="1" value={Math.round(sideWidth)} onChange={event=>setSideWidth(Math.round(Number(event.target.value)))}/><output>{sideWidth}px</output></label><label className="dvw-setting-range"><span>DSH 对话栏宽度</span><input type="range" min={DSH_CHAT_MIN_WIDTH} max={DSH_CHAT_MAX_WIDTH} step="1" value={Math.round(chatWidth)} onChange={event=>setChatWidth(clampChatWidth(Number(event.target.value)))}/><output>{chatWidth}px</output></label><div className="dvw-setting-choice"><span>Markdown 默认视图</span><div className="dvw-setting-segment">{([['preview','预览'],['source','源码']] as const).map(([value,label])=><button key={value} data-active={markdownView===value} onClick={()=>setMarkdownView(value)}>{label}</button>)}</div></div></section>
727
+ <section><h3>提交信息</h3><div className="dvw-setting-choice"><span>详细程度</span><div className="dvw-setting-segment">{([['concise','简洁'],['standard','标准'],['detailed','详细']] as const).map(([value,label])=><button key={value} data-active={commitDetail===value} onClick={()=>setCommitDetail(value)}>{label}</button>)}</div></div><div className="dvw-setting-choice"><span>语言</span><div className="dvw-setting-segment">{([['auto','跟随最近提交'],['zh','中文'],['en','English']] as const).map(([value,label])=><button key={value} data-active={commitLanguage===value} onClick={()=>setCommitLanguage(value)}>{label}</button>)}</div></div><p className="dvw-setting-hint">详细模式会生成标题和 2–4 条说明;简洁模式限制标题长度,标准模式保持单行约定式提交。</p></section>
728
+ <section><h3>语言智能</h3>
729
+ <div className="dvw-setting-extension"><div><strong>TypeScript / JavaScript</strong><span>内置 · {tsIntellisense ? tsIndex.state === 'loading' ? '正在索引工作区…' : tsIndex.state === 'error' ? `索引失败:${tsIndex.error}` : tsIndex.state === 'ready' ? `已索引 ${tsIndex.files} 个文件${tsIndex.truncated ? '(已达上限)' : ''}` : '已安装,转到定义、查找引用、Peek、补全、签名帮助' : '点击安装'}</span></div><button className={tsIntellisense?undefined:'dvw-primary-button'} onClick={()=>setTsIntellisense(value=>!value)}>{tsIntellisense?'卸载':'安装'}</button></div>
730
+ {lspServers.map(server=><div className="dvw-setting-extension" key={server.id}>
731
+ <div>
732
+ <strong>{server.name}</strong>
733
+ <div className="dvw-lsp-status">
734
+ <span className="dvw-lsp-chip" data-installed={server.language===undefined||server.language.installed} data-action={server.language!==undefined&&!server.language.installed}>{server.language===undefined?'语言:无需安装':server.language.installed?'语言:已安装':'安装语言'}</span>
735
+ <span className="dvw-lsp-chip" data-installed={server.installed}>{server.installed?'LSP:已安装':'LSP:未安装'}</span>
736
+ {!server.installed&&server.hint!==undefined&&<span className="dvw-lsp-hint">{server.hint}</span>}
737
+ </div>
738
+ </div>
739
+ {server.language!==undefined&&!server.language.installed
740
+ ? <button disabled title="先安装语言工具链,再安装 LSP">等待</button>
741
+ : server.installable&&<button disabled={lspBusy===server.id} className={server.installed?undefined:'dvw-primary-button'} onClick={()=>void changeLsp(server.id,server.installed?'uninstall':'install')}>{lspBusy===server.id?'处理中…':server.installed?'卸载 LSP':'安装 LSP'}</button>}
742
+ </div>)}
743
+ <p className="dvw-setting-hint">每种语言使用该生态里最活跃的语言服务器;能自动安装的会装进本插件的私有目录,需要手动安装的会分别检查语言工具链和服务器本身。</p>
744
+ {lspError&&<p className="dvw-setting-hint">{lspError}</p>}
745
+ </section>
746
+ </div>
747
+ </section>
748
+ </div>}
694
749
  <ActivityBar
695
750
  views={[
696
751
  {id:'explorer',label:'资源管理器',icon:<Icon name="files"/>,active:primarySideVisible&&snapshot.view==='explorer',onActivate:()=>selectView('explorer')},
@@ -706,7 +761,7 @@ export function Workbench({ service, sessionId, cwd, navigation }: { service: Vs
706
761
  ]}
707
762
  order={activityOrder} hidden={activityHidden}
708
763
  onOrderChange={setActivityOrder} onHiddenChange={setActivityHidden}/>
709
- <aside className="dvw-side" data-side-width={sideWidth}>{snapshot.view==='explorer'&&workspace!==undefined&&<ExplorerView sessionId={sessionId} cwd={workspace.cwd} selected={active} onOpen={openPath} onAddToDsh={addEntryToDsh}/>} {snapshot.view==='search'&&<SearchView sessionId={sessionId} onOpen={openPath}/>} {snapshot.view==='git'&&<GitView sessionId={sessionId} onDiff={openDiff}/>}</aside>
764
+ <aside className="dvw-side" data-side-width={sideWidth}>{snapshot.view==='explorer'&&workspace!==undefined&&<ExplorerView sessionId={sessionId} cwd={workspace.cwd} selected={active} onOpen={openPath} onAddToDsh={addEntryToDsh}/>} {snapshot.view==='search'&&<SearchView sessionId={sessionId} onOpen={openPath}/>} {snapshot.view==='git'&&<GitView sessionId={sessionId} preferences={commitPreferences} onDiff={openDiff}/>}</aside>
710
765
  <PanelResizer className="dvw-side-resizer" label="拖动调整源码管理侧边栏宽度" width={sideWidth} min={210} max={640} defaultWidth={210} direction={1} hideBelow={105} onHide={()=>setPrimarySideVisible(false)} onWidth={setSideWidth}/>
711
766
  <main className="dvw-main" onDragOver={event => { if (draggedEntry?.kind !== 'file') return; event.preventDefault(); event.dataTransfer.dropEffect = 'copy' }} onDrop={event => { const entry = draggedEntry; if (entry?.kind !== 'file') return; event.preventDefault(); endEntryDrag(); openPath(entry.path) }}><div className="dvw-tabs" data-empty={tabs.length===0||undefined}><div className="dvw-tab-list" ref={tabListRef}>{tabs.map(tab=><button className="dvw-tab" data-active={tab.path===active} key={tab.path} title={tab.diffSourcePath??tab.path} onClick={()=>setActive(tab.path)} onAuxClick={event=>{if(event.button===1){event.preventDefault();close(tab)}}} onMouseDown={event=>{if(event.button===1)event.preventDefault()}} onContextMenu={event=>{event.preventDefault();setActive(tab.path);setTabMenu({x:event.clientX,y:event.clientY,path:tab.path})}}><FileIcon name={tabLabel(tab)} className="dvw-tab-icon"/><span className="dvw-tab-name">{tab.dirty?'● ':''}{tabLabel(tab)}</span><span className="dvw-tab-close" onClick={event=>{event.stopPropagation();close(tab)}}><Icon name="close"/></span></button>)}</div></div><div className="dvw-editor" data-outline={markdownPreview&&outlineVisible&&headings.length>0}>{error&&<div className="dvw-error"><button className="dvw-icon" onClick={()=>setError(undefined)}><Icon name="close"/></button>{error}</div>}{markdownPreview&&outlineVisible&&headings.length>0&&<MarkdownOutline headings={headings} onClose={()=>setOutlineVisible(false)} onHeading={selectHeading}/>} {markdown&&activeTab&&<div className="dvw-editor-toolbar"><button className="dvw-icon dvw-outline-toggle" data-active={outlineVisible} title={outlineVisible?'关闭 Markdown 大纲':'显示 Markdown 大纲'} onClick={()=>setOutlineVisible(value=>!value)}><Icon name="list-tree"/></button><button className="dvw-mode-button" data-active={!activeTab.preview} onClick={()=>setTabs(rows=>rows.map(tab=>tab.path===activeTab.path?{...tab,preview:false}:tab))}><Icon name="code"/> 源码</button><button className="dvw-mode-button" data-active={activeTab.preview} onClick={()=>setTabs(rows=>rows.map(tab=>tab.path===activeTab.path?{...tab,preview:true}:tab))}><Icon name="preview"/> 预览</button></div>}{activeTab?.diff&&<div className="dvw-editor-toolbar dvw-diff-toolbar">{diffMarkdown&&<div className="dvw-diff-preview-switch"><button className="dvw-mode-button" data-diff-view="diff" data-active={!activeTab.preview} onClick={()=>setTabs(rows=>rows.map(tab=>tab.path===activeTab.path?{...tab,preview:false}:tab))}><Icon name="diff"/> 差异</button><button className="dvw-mode-button" data-diff-view="preview" data-active={activeTab.preview} onClick={()=>setTabs(rows=>rows.map(tab=>tab.path===activeTab.path?{...tab,preview:true}:tab))}><Icon name="preview"/> 预览</button></div>}{!activeTab.preview&&<><button className="dvw-mode-button dvw-diff-mode-button" data-mode="inline" data-active={activeTab.diffMode!=='split'} title="内联视图" onClick={()=>setTabs(rows=>rows.map(tab=>tab.path===activeTab.path?{...tab,diffMode:'inline'}:tab))}><Icon name="diff-single"/> 内联</button><button className="dvw-mode-button dvw-diff-mode-button" data-mode="split" data-active={activeTab.diffMode==='split'} title="并排视图" onClick={()=>setTabs(rows=>rows.map(tab=>tab.path===activeTab.path?{...tab,diffMode:'split'}:tab))}><Icon name="diff-sidebyside"/> 并排</button></>}</div>}<div className="dvw-editor-body" data-toolbar={markdown||activeTab?.diff!==undefined}>{activeTab===undefined?<div className="dvw-empty"><div><strong>DSH Workbench</strong><br/>从资源管理器或搜索结果打开文件</div></div>:activeTab.diff?diffMarkdown&&activeTab.preview?<MarkdownPreview source={activeTab.diff.modified} sessionId={sessionId} path={activeTab.diffSourcePath??''}/>:<MonacoDiffEditor uri={`${workspace?.cwd??''}/${activeTab.path}`} original={activeTab.diff.original} modified={activeTab.diff.modified} mode={activeTab.diffMode??'inline'} theme={colorScheme} fontSize={fontSize}/>:activeTab.imageUrl?<div className="dvw-image-preview"><img src={activeTab.imageUrl} alt={activeTab.path}/><span>{activeTab.path}</span></div>:activeTab.documentPreview&&activeTab.documentUrl?<DocumentPreview kind={activeTab.documentPreview} src={activeTab.documentUrl} path={activeTab.path}/>:markdown&&activeTab.preview?<MarkdownPreview source={activeTab.draft} sessionId={sessionId} path={activeTab.path}/>:activeTab.binary||activeTab.tooLarge?<div className="dvw-message">{activeTab.binary?'二进制文件无法在文本编辑器或差异视图中打开':`文件超过 5 MiB:${activeTab.path}`}{activeTab.binary&&activeTab.diffSourcePath!==undefined&&!activeTab.path.startsWith('diff:commit:')&&<button onClick={()=>void openPath(activeTab.diffSourcePath!)}>打开文件</button>}</div>:activeTab.loaded===false?<div className="dvw-message">正在加载文件…</div>:<MonacoEditor uri={`${workspace?.cwd??''}/${activeTab.path}`} onAddToDsh={selection=>addToDsh(dshReference(activeTab.path,{selection}))} value={activeTab.draft} theme={colorScheme} fontSize={fontSize} line={activeTab.line} column={activeTab.column} highlight={activeTab.highlight} onChange={draft=>setTabs(rows=>rows.map(tab=>tab.path===activeTab.path?{...tab,draft,dirty:draft!==tab.content}:tab))} onSave={()=>void save(activeTab)}/>}</div></div>{snapshot.bottomPanel===undefined&&<div className="dvw-bottom-reveal" title="向上拖动打开终端" onPointerDown={event=>{if(event.button!==0)return;event.preventDefault();service.toggleBottomPanel('terminal');const statusHeight=statusVisible?24:0;const heightAt=(clientY:number)=>Math.max(120,Math.min(window.innerHeight-160,window.innerHeight-clientY-statusHeight));const move=(e:PointerEvent)=>setPanelHeight(heightAt(e.clientY));const up=()=>{window.removeEventListener('pointermove',move);window.removeEventListener('pointerup',up)};window.addEventListener('pointermove',move);window.addEventListener('pointerup',up)}}/>}</main>
712
767
  {tabMenu&&menuTab&&<div className="dvw-context-menu dvw-tab-menu" role="menu" style={{left:Math.min(tabMenu.x,window.innerWidth-240),top:Math.min(tabMenu.y,window.innerHeight-180)}} onPointerDown={event=>event.stopPropagation()}>
@@ -1,14 +1,16 @@
1
1
  import { useEffect, useState } from 'react'
2
+ import { createPortal } from 'react-dom'
2
3
  import { Icon } from './Icon.tsx'
3
4
  import type { WorkspaceChoice } from './dshNavigation.ts'
4
5
 
5
6
  /** Centered switcher: existing workspaces, their sessions, and a new-workspace action. */
6
- export function WorkspaceSwitcher({ choices, activeSessionId, unavailable, busy, error, onCreate, onSwitch, onOpenWorkspace, onClose }: {
7
+ export function WorkspaceSwitcher({ choices, activeSessionId, unavailable, busy, error, theme, onCreate, onSwitch, onOpenWorkspace, onClose }: {
7
8
  choices: WorkspaceChoice[]
8
9
  activeSessionId?: string
9
10
  unavailable: boolean
10
11
  busy: boolean
11
12
  error?: string
13
+ theme?: 'dark' | 'light' | 'white' | 'eyecare'
12
14
  onCreate(): void
13
15
  onSwitch(sessionId: string): void
14
16
  onOpenWorkspace(workspaceId: string): void
@@ -21,7 +23,8 @@ export function WorkspaceSwitcher({ choices, activeSessionId, unavailable, busy,
21
23
  window.addEventListener('keydown', escape)
22
24
  return () => window.removeEventListener('keydown', escape)
23
25
  }, [onClose])
24
- return <div className="dvw-confirm-backdrop" role="presentation" onMouseDown={onClose}>
26
+ return createPortal(<div className="dvw-overlay dvw-workspace-layer" data-dvw-theme={theme}>
27
+ <div className="dvw-workspace-backdrop" role="presentation" onMouseDown={onClose}>
25
28
  <section className="dvw-workspace-switcher" role="dialog" aria-modal="true" aria-label="打开工作区" onMouseDown={event => event.stopPropagation()}>
26
29
  <header><h2>打开工作区</h2><button className="dvw-icon" title="关闭" onClick={onClose}><Icon name="close"/></button></header>
27
30
  <div className="dvw-workspace-list">
@@ -49,5 +52,6 @@ export function WorkspaceSwitcher({ choices, activeSessionId, unavailable, busy,
49
52
  {error !== undefined && <p className="dvw-workspace-error">{error}</p>}
50
53
  <footer><button className="dvw-primary-button" disabled={busy} onClick={onCreate}>{busy ? '正在打开…' : '新建工作区…'}</button><button onClick={onClose}>取消</button></footer>
51
54
  </section>
52
- </div>
55
+ </div>
56
+ </div>, document.body)
53
57
  }
package/src/client/lsp.ts CHANGED
@@ -11,6 +11,9 @@ export interface LspServerStatus {
11
11
  installable: boolean
12
12
  installed: boolean
13
13
  hint?: string
14
+ command?: string
15
+ /** Manual servers report the language toolchain next to the server so the two are not confused. */
16
+ language?: { name: string; installed: boolean }
14
17
  }
15
18
 
16
19
  interface Pending { resolve(value: unknown): void; reject(error: unknown): void }
@@ -1,7 +1,7 @@
1
1
  import type { ComponentType } from 'react'
2
2
 
3
3
  export type WorkbenchView = 'explorer' | 'search' | 'git'
4
- export interface BottomPanelProps { sessionId: string; cwd: string; visible: boolean; colorScheme: 'dark' | 'light' }
4
+ export interface BottomPanelProps { sessionId: string; cwd: string; visible: boolean; colorScheme: 'dark' | 'light' | 'white' | 'eyecare' }
5
5
  export interface BottomPanelContribution { id: string; title: string; component: ComponentType<BottomPanelProps> }
6
6
  export interface WorkbenchCommand { id: string; run(): void }
7
7
  export interface WorkbenchSnapshot { visible: boolean; view: WorkbenchView; bottomPanel?: string }
@@ -20,7 +20,7 @@ body[data-ds-dark-theme] .dvw-overlay,.dvw-overlay[data-dvw-theme=dark]{color-sc
20
20
  .dvw-main{grid-column:3;grid-row:1;position:relative;display:flex;min-width:0;min-height:0;flex-direction:column;background:var(--dvw-surface-1);border-left:1px solid var(--dvw-border);border-top:1px solid var(--dvw-border);border-right:1px solid var(--dvw-border);border-bottom:1px solid var(--dvw-border);border-radius:8px;margin-left:4px;overflow:hidden}.dvw-tabs{height:35px;background:linear-gradient(to top,var(--dvw-border) 0 1px,var(--dvw-base) 1px 100%);display:flex;overflow:hidden}.dvw-tabs[data-empty]{display:none}.dvw-tab{position:relative;height:35px;margin:0;min-width:110px;max-width:260px;border:0;border-right:1px solid var(--dvw-border-soft);border-radius:8px 8px 0 0;background:transparent;color:var(--dvw-text-2);padding:0 8px 0 12px;display:flex;align-items:center;gap:6px;cursor:pointer;flex:none;white-space:nowrap}.dvw-tab:hover{background:var(--dvw-hover)}.dvw-tab[data-active=true]{background:var(--dvw-surface-1);color:var(--dvw-text);border-top:1px solid var(--dvw-border);border-right:1px solid var(--dvw-border)}.dvw-tab-icon{width:16px;height:16px;flex:none;object-fit:contain}.dvw-tab-close{width:16px;height:16px;flex:none;display:grid;place-items:center;border-radius:3px;opacity:0}.dvw-tab-close:hover{background:var(--dvw-hover);color:var(--dvw-text)}.dvw-tab[data-active=true] .dvw-tab-close,.dvw-tab:hover .dvw-tab-close{opacity:1}.dvw-tab-name{overflow:hidden;text-overflow:ellipsis;flex:1;min-width:0;text-align:left;white-space:nowrap}.dvw-editor{position:relative;flex:1;min-height:0}.dvw-editor-toolbar{position:absolute;z-index:3;top:0;right:0;left:0;height:35px;display:flex;align-items:center;justify-content:flex-end;gap:4px;padding:0 10px;border-bottom:1px solid var(--dvw-border-soft);background:var(--dvw-surface-1)}.dvw-editor-body{position:absolute;inset:0;min-height:0}.dvw-editor-body[data-toolbar=true]{top:35px}.dvw-mode-button{height:27px;padding:0 10px;display:flex;align-items:center;gap:6px;border:1px solid transparent;border-radius:5px;background:transparent;color:var(--dvw-text-2);cursor:pointer}.dvw-mode-button:hover{background:var(--dvw-hover)}.dvw-mode-button[data-active=true]{border-color:var(--dvw-border);background:var(--dvw-active);color:var(--dvw-text)}.dvw-empty{height:100%;display:grid;place-items:center;color:var(--dvw-text-3);text-align:center}.dvw-empty strong{font-size:24px;color:var(--dvw-text-2)}.dvw-message{padding:12px;color:var(--dvw-warn)}.dvw-message button{margin-left:10px;padding:2px 10px;border:1px solid var(--dvw-border);border-radius:4px;background:var(--dvw-surface-2);color:var(--dvw-text);font:inherit;cursor:pointer}.dvw-message button:hover{background:var(--dvw-hover)}
21
21
  .dvw-bottom-resizer{height:4px;cursor:ns-resize}.dvw-bottom-resizer:hover{background:var(--dvw-accent)}.dvw-bottom-reveal{position:absolute;left:0;right:0;bottom:0;height:6px;z-index:4;cursor:ns-resize}.dvw-bottom-reveal::after{content:"";position:absolute;left:0;right:0;bottom:0;height:2px;background:transparent}.dvw-bottom-reveal:hover::after{background:var(--dvw-accent)}.dvw-bottom{grid-column:3;grid-row:2;height:260px;min-height:120px;border-top:1px solid var(--dvw-border);border-left:1px solid var(--dvw-border);border-right:1px solid var(--dvw-border);border-bottom:1px solid var(--dvw-border);border-radius:8px;margin-top:4px;margin-left:4px;background:var(--dvw-base);display:flex;flex-direction:column}.dvw-bottom-head{height:35px;display:flex;align-items:center;gap:14px;padding:0 12px}.dvw-bottom-title{border:0;border-bottom:1px solid var(--dvw-text);background:transparent;color:var(--dvw-text);height:35px}.dvw-bottom-body{flex:1;min-height:0}.dvw-status{grid-column:1/4;grid-row:3;background:var(--dvw-base);color:var(--dvw-text);display:flex;align-items:center;padding:6px 8px 0;gap:18px;font-size:12px}.dvw-theme-control{position:relative;margin-left:auto}.dvw-theme-trigger{height:22px;min-width:112px;display:flex;align-items:center;gap:7px;padding:0 8px;border:0;border-radius:5px;background:color-mix(in srgb,#000 17%,transparent);color:#fff;cursor:pointer}.dvw-theme-trigger:hover{background:color-mix(in srgb,#000 27%,transparent)}.dvw-theme-orb{width:13px;height:13px;border:2px solid #fff;border-radius:50%;background:linear-gradient(90deg,#191a1b 50%,#fff 50%)}.dvw-theme-orb.dark{background:#191a1b}.dvw-theme-orb.light{background:#fff}.dvw-theme-menu{position:absolute;right:0;bottom:29px;width:224px;padding:6px;border:1px solid var(--dvw-border);border-radius:10px;background:var(--dvw-surface-3);box-shadow:0 10px 35px rgba(0,0,0,.28);color:var(--dvw-text)}.dvw-theme-menu button{width:100%;min-height:48px;border:0;border-radius:7px;background:transparent;color:inherit;display:grid;grid-template-columns:30px 1fr 20px;align-items:center;text-align:left;padding:6px;cursor:pointer}.dvw-theme-menu button:hover,.dvw-theme-menu button[aria-checked=true]{background:var(--dvw-hover)}.dvw-theme-menu strong,.dvw-theme-menu small{display:block}.dvw-theme-menu small{color:var(--dvw-text-3);margin-top:2px}.dvw-theme-swatch{width:22px;height:22px;border-radius:6px;border:1px solid var(--dvw-border);background:linear-gradient(135deg,#191a1b 0 50%,#fff 50%)}.dvw-theme-swatch.dark{background:#191a1b}.dvw-theme-swatch.light{background:#fff}
22
22
  .dvw-search-form{padding:8px;display:grid;gap:6px}.dvw-input,.dvw-textarea,.dvw-select{width:100%;border:1px solid var(--dvw-border);border-radius:2px;background:var(--dvw-surface-2);color:var(--dvw-text);padding:3px 7px;outline:none}.dvw-input:focus,.dvw-textarea:focus,.dvw-select:focus{border-color:var(--dvw-accent)}.dvw-options{display:flex;align-items:center;gap:8px;font-size:11px}.dvw-result-file{height:22px;display:flex;align-items:center;gap:4px;padding:0 8px;font-weight:600}.dvw-result{height:22px;align-items:center;padding:0 8px 0 20px;cursor:pointer;display:flex;gap:8px}.dvw-result:hover{background:var(--dvw-hover)}.dvw-line{color:var(--dvw-text-3);min-width:30px;text-align:right}.dvw-preview{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dvw-search-hit{background:color-mix(in srgb,var(--dvw-accent) 28%,transparent);border-radius:2px}
23
- .dvw-git-head{padding:7px 10px;border-bottom:1px solid var(--dvw-border);display:flex;align-items:center;gap:8px;position:relative}.dvw-branch{min-width:0;border:0;background:transparent;color:var(--dvw-text);font:inherit;font-weight:600;display:flex;align-items:center;gap:5px;cursor:pointer}.dvw-branch-caret{margin-left:2px;font-size:14px;color:var(--dvw-text-3)}.dvw-branch-menu{position:absolute;z-index:12;top:calc(100% + 2px);left:6px;right:6px;padding:4px;border:1px solid var(--dvw-border);border-radius:8px;background:var(--dvw-surface-2);box-shadow:0 10px 28px rgba(0,0,0,.28);color:var(--dvw-text)}.dvw-branch-current{padding:4px 8px;color:var(--dvw-text-3);font-size:11px}.dvw-branch-list{max-height:min(300px,45vh);overflow:auto}.dvw-branch-list button{width:100%;height:26px;display:flex;align-items:center;gap:6px;border:0;border-radius:4px;background:transparent;color:var(--dvw-text);padding:0 8px;text-align:left;font:inherit;cursor:pointer}.dvw-branch-list button:hover{background:var(--dvw-hover)}.dvw-branch-list button[data-active=true]{background:var(--dvw-active)}.dvw-branch-list button span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dvw-branch-worktree{margin-left:auto;flex:none;padding:1px 5px;border-radius:8px;background:var(--dvw-surface-3);color:var(--dvw-text-3);font-size:10px;line-height:14px}.dvw-branch-create{display:flex;gap:4px;align-items:center;margin-top:4px;padding-top:6px;border-top:1px solid var(--dvw-border-soft)}.dvw-branch-create .dvw-input{flex:1;min-width:0}.dvw-branch-create-button{flex:none;width:24px;min-width:24px;height:24px;border:1px solid var(--dvw-border);border-radius:4px}.dvw-commit{display:grid;grid-template-columns:1fr auto;gap:6px;padding:9px}.dvw-commit .dvw-input{grid-column:1/3}.dvw-commit-button{grid-column:1/3;height:31px;border:0;border-radius:6px;background:color-mix(in srgb,var(--dvw-accent) 24%,var(--dvw-surface-2));color:var(--dvw-text);display:flex;align-items:center;justify-content:center;gap:7px;cursor:pointer}.dvw-commit-button:hover{background:color-mix(in srgb,var(--dvw-accent) 34%,var(--dvw-surface-2))}.dvw-commit-button:disabled{opacity:.45;cursor:default}.dvw-group-head{height:30px;display:flex;align-items:center;gap:5px;padding:0 6px 0 9px;font-weight:600;position:sticky;top:0;background:var(--dvw-side);z-index:1}.dvw-group-head:hover{background:var(--dvw-hover)}.dvw-group-head .dvw-count{margin-left:auto;min-width:20px;height:20px;border-radius:10px;display:grid;place-items:center;background:var(--dvw-surface-3);color:var(--dvw-text-2);font-size:11px}.dvw-change{display:flex;align-items:center;height:22px;padding:0 5px 0 19px;gap:5px;cursor:pointer}.dvw-change:hover{background:var(--dvw-hover)}.dvw-change .dvw-icon{opacity:0}.dvw-change:hover .dvw-icon,.dvw-change:focus-within .dvw-icon{opacity:1}.dvw-change-path{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}.dvw-badge{font-size:11px;color:var(--dvw-warn)}.dvw-graph{border-top:1px solid var(--dvw-border-soft);margin-top:8px;padding-top:3px}.dvw-graph-list{padding-bottom:8px}.dvw-graph-row{width:100%;height:38px;border:0;background:transparent;color:var(--dvw-text);display:flex;align-items:center;padding:0 7px;text-align:left;cursor:pointer;overflow:hidden}.dvw-graph-row:hover{background:var(--dvw-hover)}.dvw-graph-svg{flex:none;align-self:stretch}.dvw-graph-line{fill:none;stroke-width:1px;stroke-linecap:round;vector-effect:non-scaling-stroke}.dvw-graph-node{stroke:var(--dvw-side);stroke-width:2px;fill:var(--dvw-side)}.dvw-graph-copy{display:flex;min-width:0;flex:1;flex-direction:column;justify-content:center}.dvw-graph-subject{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dvw-graph-meta{font-size:11px;color:var(--dvw-text-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dvw-ref{max-width:82px;margin-left:4px;padding:1px 5px;border:1px solid color-mix(in srgb,var(--dvw-accent) 50%,transparent);border-radius:9px;color:var(--dvw-accent);font-size:10px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dvw-error{padding:8px;color:var(--dvw-error);white-space:pre-wrap}.dvw-diff{margin:0;height:100%;overflow:auto;padding:8px 0;background:var(--dvw-surface-1);font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;white-space:pre}.dvw-diff-line{display:block;min-height:18px;padding:0 14px}.dvw-diff-line.add{background:color-mix(in srgb,var(--dvw-success) 15%,transparent);color:var(--dvw-success)}.dvw-diff-line.remove{background:color-mix(in srgb,var(--dvw-error) 13%,transparent);color:var(--dvw-error)}.dvw-diff-line.hunk{background:color-mix(in srgb,var(--dvw-accent) 12%,transparent);color:var(--dvw-accent)}.dvw-diff-line.meta{color:var(--dvw-text-3)}.dvw-diff-empty{padding:24px;color:var(--dvw-text-3)}
23
+ .dvw-git-head{padding:7px 10px;border-bottom:1px solid var(--dvw-border);display:flex;align-items:center;gap:8px;position:relative}.dvw-branch{min-width:0;border:0;background:transparent;color:var(--dvw-text);font:inherit;font-weight:600;display:flex;align-items:center;gap:5px;cursor:pointer}.dvw-branch-caret{margin-left:2px;font-size:14px;color:var(--dvw-text-3)}.dvw-branch-menu{position:absolute;z-index:12;top:calc(100% + 2px);left:6px;right:6px;padding:4px;border:1px solid var(--dvw-border);border-radius:8px;background:var(--dvw-surface-2);box-shadow:0 10px 28px rgba(0,0,0,.28);color:var(--dvw-text)}.dvw-branch-current{padding:4px 8px;color:var(--dvw-text-3);font-size:11px}.dvw-branch-list{max-height:min(300px,45vh);overflow:auto}.dvw-branch-list button{width:100%;height:26px;display:flex;align-items:center;gap:6px;border:0;border-radius:4px;background:transparent;color:var(--dvw-text);padding:0 8px;text-align:left;font:inherit;cursor:pointer}.dvw-branch-list button:hover{background:var(--dvw-hover)}.dvw-branch-list button[data-active=true]{background:var(--dvw-active)}.dvw-branch-list button span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dvw-branch-worktree{margin-left:auto;flex:none;padding:1px 5px;border-radius:8px;background:var(--dvw-surface-3);color:var(--dvw-text-3);font-size:10px;line-height:14px}.dvw-branch-create{display:flex;gap:4px;align-items:center;margin-top:4px;padding-top:6px;border-top:1px solid var(--dvw-border-soft)}.dvw-branch-create .dvw-input{flex:1;min-width:0}.dvw-branch-create-button{flex:none;width:24px;min-width:24px;height:24px;border:1px solid var(--dvw-border);border-radius:4px}.dvw-commit{display:flex;flex-direction:column;gap:6px;padding:9px}.dvw-commit-actions{display:grid;grid-template-columns:1fr auto;gap:6px}.dvw-commit-button{height:31px;border:0;border-radius:6px;background:color-mix(in srgb,var(--dvw-accent) 24%,var(--dvw-surface-2));color:var(--dvw-text);display:flex;align-items:center;justify-content:center;gap:7px;cursor:pointer}.dvw-commit-button:hover{background:color-mix(in srgb,var(--dvw-accent) 34%,var(--dvw-surface-2))}.dvw-commit-button:disabled{opacity:.45;cursor:default}.dvw-commit-ai{height:31px;border:1px solid var(--dvw-border);border-radius:6px;background:var(--dvw-surface-2);color:var(--dvw-text-2);display:flex;align-items:center;justify-content:center;gap:6px;font:inherit;cursor:pointer}.dvw-commit-ai:hover:not(:disabled){background:var(--dvw-hover);color:var(--dvw-text)}.dvw-commit-ai:disabled{opacity:.45;cursor:default}.dvw-group-head{height:30px;display:flex;align-items:center;gap:5px;padding:0 6px 0 9px;font-weight:600;position:sticky;top:0;background:var(--dvw-side);z-index:1}.dvw-group-head:hover{background:var(--dvw-hover)}.dvw-group-head .dvw-count{margin-left:auto;min-width:20px;height:20px;border-radius:10px;display:grid;place-items:center;background:var(--dvw-surface-3);color:var(--dvw-text-2);font-size:11px}.dvw-change{display:flex;align-items:center;height:22px;padding:0 5px 0 19px;gap:5px;cursor:pointer}.dvw-change:hover{background:var(--dvw-hover)}.dvw-change .dvw-icon{opacity:0}.dvw-change:hover .dvw-icon,.dvw-change:focus-within .dvw-icon{opacity:1}.dvw-change-path{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}.dvw-badge{font-size:11px;color:var(--dvw-warn)}.dvw-graph{border-top:1px solid var(--dvw-border-soft);margin-top:8px;padding-top:3px}.dvw-graph-list{padding-bottom:8px}.dvw-graph-row{width:100%;height:38px;border:0;background:transparent;color:var(--dvw-text);display:flex;align-items:center;padding:0 7px;text-align:left;cursor:pointer;overflow:hidden}.dvw-graph-row:hover{background:var(--dvw-hover)}.dvw-graph-svg{flex:none;align-self:stretch}.dvw-graph-line{fill:none;stroke-width:1px;stroke-linecap:round;vector-effect:non-scaling-stroke}.dvw-graph-node{stroke:var(--dvw-side);stroke-width:2px;fill:var(--dvw-side)}.dvw-graph-copy{display:flex;min-width:0;flex:1;flex-direction:column;justify-content:center}.dvw-graph-subject{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dvw-graph-meta{font-size:11px;color:var(--dvw-text-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dvw-ref{max-width:82px;margin-left:4px;padding:1px 5px;border:1px solid color-mix(in srgb,var(--dvw-accent) 50%,transparent);border-radius:9px;color:var(--dvw-accent);font-size:10px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dvw-error{padding:8px;color:var(--dvw-error);white-space:pre-wrap}.dvw-diff{margin:0;height:100%;overflow:auto;padding:8px 0;background:var(--dvw-surface-1);font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;white-space:pre}.dvw-diff-line{display:block;min-height:18px;padding:0 14px}.dvw-diff-line.add{background:color-mix(in srgb,var(--dvw-success) 15%,transparent);color:var(--dvw-success)}.dvw-diff-line.remove{background:color-mix(in srgb,var(--dvw-error) 13%,transparent);color:var(--dvw-error)}.dvw-diff-line.hunk{background:color-mix(in srgb,var(--dvw-accent) 12%,transparent);color:var(--dvw-accent)}.dvw-diff-line.meta{color:var(--dvw-text-3)}.dvw-diff-empty{padding:24px;color:var(--dvw-text-3)}
24
24
  .dvw-tab-list{display:flex;min-width:0;flex:1;overflow-x:auto;overflow-y:hidden;overscroll-behavior-x:contain;scrollbar-width:none}.dvw-tab-list::-webkit-scrollbar{display:none}.dvw-monaco-diff{position:absolute;inset:0}.dvw-diff-toolbar{justify-content:flex-end}.dvw-group-head{gap:2px;padding-left:2px}.dvw-group-toggle{min-width:0;height:30px;flex:1;border:0;border-radius:5px;background:transparent;color:var(--dvw-text);font:inherit;font-weight:600;display:flex;align-items:center;gap:5px;padding:0 7px;cursor:pointer;text-align:left}.dvw-group-toggle:hover{background:var(--dvw-hover)}.dvw-graph-entry[data-expanded=true]{background:color-mix(in srgb,var(--dvw-accent) 5%,transparent)}.dvw-commit-files{padding:1px 0 5px 26px}.dvw-commit-file{width:100%;height:22px;border:0;background:transparent;color:var(--dvw-text);display:flex;align-items:center;gap:6px;padding:0 7px;text-align:left;cursor:pointer}.dvw-commit-file:hover{background:var(--dvw-hover)}.dvw-commit-file-name{white-space:nowrap}.dvw-commit-file-dir{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dvw-text-3);font-size:11px}
25
25
  .dvw-outline{flex:none;max-height:38%;display:flex;flex-direction:column;border-bottom:1px solid var(--dvw-border)}.dvw-outline-head{height:30px;display:flex;align-items:center;justify-content:space-between;padding:0 5px 0 10px;font-weight:600}.dvw-outline-head>span{display:flex;align-items:center;gap:6px}.dvw-outline-list{overflow:auto;padding-bottom:5px}.dvw-outline-item{display:block;width:100%;height:22px;border:0;background:transparent;color:var(--dvw-text-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:left;cursor:pointer}.dvw-outline-item:hover{background:var(--dvw-hover);color:var(--dvw-text)}.dvw-image-preview{position:absolute;inset:0;overflow:auto;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;padding:24px;background:repeating-conic-gradient(var(--dvw-surface-2) 0 25%,var(--dvw-surface-1) 0 50%) 0/20px 20px}.dvw-image-preview img{max-width:100%;max-height:calc(100% - 32px);object-fit:contain;box-shadow:0 4px 24px rgba(0,0,0,.22)}.dvw-image-preview span{max-width:100%;padding:3px 8px;border-radius:5px;background:var(--dvw-surface-1);color:var(--dvw-text-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
26
26
  .dvw-pdf-preview{position:absolute;inset:0;display:flex;min-height:0;flex-direction:column;background:var(--dvw-base);color:var(--dvw-text)}.dvw-pdf-toolbar{height:30px;flex:none;display:flex;align-items:center;justify-content:center;gap:2px;padding:0 8px;border-bottom:1px solid var(--dvw-border);background:var(--dvw-surface-1);font-variant-numeric:tabular-nums}.dvw-pdf-toolbar .dvw-icon{font-size:18px}.dvw-pdf-zoom{min-width:52px;height:24px;border:0;border-radius:4px;background:var(--dvw-active);color:var(--dvw-text);font:inherit;cursor:pointer}.dvw-pdf-zoom:hover{background:var(--dvw-hover)}.dvw-pdf-separator{width:1px;height:18px;margin:0 4px;background:var(--dvw-border)}.dvw-pdf-page{min-height:0;flex:1;overflow:auto;display:flex;justify-content:center;padding:8px;background:var(--dvw-base)}.dvw-pdf-page canvas{flex:none;background:#fff;box-shadow:0 2px 12px rgba(0,0,0,.28)}.dvw-sheet-preview{position:absolute;inset:0;display:flex;min-height:0;flex-direction:column;background:var(--dvw-base);color:var(--dvw-text)}.dvw-sheet-tabs{flex:none;display:flex;gap:2px;padding:6px 8px;overflow:auto;border-bottom:1px solid var(--dvw-border)}.dvw-sheet-tabs button{height:24px;padding:0 10px;border:0;border-radius:3px;background:transparent;color:var(--dvw-text-2);font:inherit;cursor:pointer;white-space:nowrap}.dvw-sheet-tabs button:hover{background:var(--dvw-hover)}.dvw-sheet-tabs button[data-active=true]{background:var(--dvw-active);color:var(--dvw-text)}.dvw-sheet-scroll{flex:1;min-height:0;overflow:auto;padding:10px}.dvw-sheet-table{border-collapse:collapse;font-size:12px}.dvw-sheet-table td{max-width:360px;padding:2px 8px;border:1px solid var(--dvw-border);white-space:pre;overflow:hidden;text-overflow:ellipsis}.dvw-sheet-table tr:first-child td{background:var(--dvw-hover);font-weight:600}.dvw-slides-preview{position:absolute;inset:0;overflow:auto;padding:16px;background:var(--dvw-base)}.dvw-docx-preview{position:absolute;inset:0;overflow:auto;padding:28px;background:var(--dvw-base);color:#222}.dvw-docx-preview .docx-wrapper{margin:auto;background:#fff;box-shadow:0 2px 12px rgba(0,0,0,.2)}.dvw-document-error,.dvw-document-download{height:100%;display:grid;place-content:center;justify-items:center;gap:12px;padding:24px;color:var(--dvw-text-2);text-align:center}.dvw-document-download a{color:var(--dvw-accent);text-decoration:none}.dvw-document-download a:hover{text-decoration:underline}
@@ -68,12 +68,15 @@ body[data-dvw-workbench-active] [data-sidebar-toggle]{display:none!important}
68
68
  .dvw-settings-body{overflow:auto;padding:8px 20px 20px}.dvw-settings-body section+section{margin-top:20px;padding-top:12px;border-top:1px solid var(--dvw-border-soft)}.dvw-settings-body h3{margin:8px 0 12px;font-size:13px}.dvw-settings-body>section>label:not(.dvw-setting-check):not(.dvw-setting-range){display:block;margin-bottom:7px;color:var(--dvw-text-2);font-size:12px}
69
69
  .dvw-settings-themes{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}.dvw-settings-themes button{height:56px;display:grid;grid-template-columns:28px 1fr 18px;align-items:center;gap:7px;padding:0 9px;border:1px solid var(--dvw-border);border-radius:7px;background:var(--dvw-base);color:var(--dvw-text);text-align:left;cursor:pointer}.dvw-settings-themes button:hover{background:var(--dvw-hover)}.dvw-settings-themes button[data-active=true]{border-color:var(--dvw-accent);background:color-mix(in srgb,var(--dvw-accent) 10%,var(--dvw-base))}
70
70
  .dvw-setting-check{height:36px;display:flex;align-items:center;gap:9px;border-bottom:1px solid var(--dvw-border-soft);cursor:pointer}.dvw-setting-check input{accent-color:var(--dvw-accent)}.dvw-setting-range{display:grid;grid-template-columns:145px 1fr 52px;align-items:center;gap:10px;min-height:42px}.dvw-setting-range input{accent-color:var(--dvw-accent)}.dvw-setting-range output{text-align:right;color:var(--dvw-text-3);font-variant-numeric:tabular-nums}
71
+ .dvw-setting-choice{display:grid;grid-template-columns:145px 1fr;align-items:center;gap:10px;min-height:42px}.dvw-setting-choice>span{color:var(--dvw-text-2);font-size:12px}.dvw-setting-segment{display:inline-flex;justify-self:start;padding:2px;border:1px solid var(--dvw-border);border-radius:6px;background:var(--dvw-base)}.dvw-setting-segment button{min-width:64px;height:26px;border:0;border-radius:4px;background:transparent;color:var(--dvw-text-2);font:inherit;font-size:12px;cursor:pointer}.dvw-setting-segment button:hover{background:var(--dvw-hover);color:var(--dvw-text)}.dvw-setting-segment button[data-active=true]{background:var(--dvw-active);color:var(--dvw-text)}
71
72
  @media(max-width:900px){.dvw-overlay[data-dvw-chat=true]{--dvw-chat-effective:max(440px,min(var(--dvw-chat-width),calc(100vw - 254px)));grid-template-columns:44px min(210px,var(--dvw-side-width),max(0px,calc(100vw - 164px - var(--dvw-chat-effective)))) minmax(0,1fr) var(--dvw-chat-effective)}.dvw-overlay[data-dvw-chat=true] .dvw-status{padding-right:calc(var(--dvw-chat-effective) + 8px)}.dvw-chat-resizer{right:calc(var(--dvw-chat-effective) - 5px)}[data-dvw-conversation-panel]{width:calc(max(440px,min(var(--dvw-chat-width),calc(100vw - 254px))) - 8px)!important}}
72
73
  .dvw-setting-textarea{width:100%;min-height:84px;resize:vertical;padding:8px 10px;border:1px solid var(--dvw-border);border-radius:6px;background:var(--dvw-base);color:var(--dvw-text);font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
73
74
  .dvw-setting-textarea:focus{outline:none;border-color:var(--dvw-accent)}
74
75
  .dvw-setting-extension{display:flex;align-items:center;gap:12px;padding:10px 12px;border:1px solid var(--dvw-border);border-radius:7px;background:var(--dvw-base)}.dvw-setting-extension>div{min-width:0;flex:1;display:flex;flex-direction:column;gap:2px}.dvw-setting-extension span{color:var(--dvw-text-3);font-size:12px}.dvw-setting-extension button{height:26px;padding:0 14px;border:1px solid var(--dvw-border);border-radius:4px;background:transparent;color:var(--dvw-text);font:inherit;cursor:pointer}.dvw-setting-extension button:hover{background:var(--dvw-hover)}.dvw-setting-extension button.dvw-primary-button{border-color:var(--dvw-accent);background:var(--dvw-accent);color:#fff}.dvw-setting-hint{margin:8px 0 0;color:var(--dvw-text-3);font-size:11px;line-height:1.5}
75
76
  /* Slightly larger explorer rows for comfortable reading without shrinking the file list. */
76
77
  .dvw-tree-row{height:24px;font-size:13px}
78
+ /* Keep a single focus surface: hovering another row clears the selected row's fill. */
79
+ .dvw-tree:has(.dvw-tree-row:hover) .dvw-tree-row[data-selected=true]:not(:hover){background:transparent}
77
80
  .dvw-chevron{width:18px}
78
81
  .dvw-file-icon{width:18px;height:18px}
79
82
  .dvw-tree-rename,.dvw-tree-create-input{height:22px}
@@ -94,6 +97,97 @@ body[data-dvw-workbench-active] [data-sidebar-toggle]{display:none!important}
94
97
  .dvw-tab[data-active=true]::before,.dvw-tab[data-active=true]::after{content:"";position:absolute;bottom:0;width:10px;height:10px;background:var(--dvw-surface-1);pointer-events:none}
95
98
  .dvw-tab[data-active=true]::before{left:-10px;-webkit-mask:radial-gradient(circle 10px at 0 0,transparent 9px,#000 9.6px);mask:radial-gradient(circle 10px at 0 0,transparent 9px,#000 9.6px)}
96
99
  .dvw-tab[data-active=true]::after{right:-10px;-webkit-mask:radial-gradient(circle 10px at 100% 0,transparent 9px,#000 9.6px);mask:radial-gradient(circle 10px at 100% 0,transparent 9px,#000 9.6px)}
100
+ .dvw-commit-box{position:relative;display:flex;align-items:stretch}
101
+ .dvw-commit-input{width:100%;min-height:30px;max-height:96px;field-sizing:content;resize:none;padding:5px 32px 5px 8px;line-height:18px}
102
+ .dvw-commit-ai{position:absolute;right:4px;top:50%;transform:translateY(-50%);width:24px;min-width:24px;height:24px;border:0;background:transparent;color:var(--dvw-text-3)}
103
+ .dvw-commit-ai:hover:not(:disabled){background:var(--dvw-hover);color:var(--dvw-text)}
104
+ /* VS Code-style split commit button: primary action plus a narrow dropdown for commit variants. */
105
+ .dvw-commit .dvw-commit-actions{display:flex;gap:0;position:relative}
106
+ .dvw-commit .dvw-commit-button{flex:1;min-width:0;height:30px;border:0;border-radius:6px 0 0 6px;background:var(--dvw-accent);color:#fff;display:flex;align-items:center;justify-content:center;gap:7px;font:inherit;cursor:pointer}
107
+ .dvw-commit .dvw-commit-button:hover:not(:disabled){background:color-mix(in srgb,var(--dvw-accent) 88%,#fff)}
108
+ .dvw-commit .dvw-commit-button:disabled{opacity:.45;cursor:default}
109
+ .dvw-commit .dvw-commit-more{flex:none;width:30px;height:30px;border:0;border-left:1px solid rgba(255,255,255,.3);border-radius:0 6px 6px 0;background:var(--dvw-accent);color:#fff;display:grid;place-items:center;cursor:pointer}
110
+ .dvw-commit .dvw-commit-more:hover:not(:disabled){background:color-mix(in srgb,var(--dvw-accent) 88%,#fff)}
111
+ .dvw-commit .dvw-commit-more:disabled{opacity:.45;cursor:default}
112
+ .dvw-commit .dvw-commit-menu{position:absolute;z-index:12;top:calc(100% + 4px);right:0;min-width:180px;padding:4px;border:1px solid var(--dvw-border);border-radius:8px;background:var(--dvw-surface-2);box-shadow:0 10px 28px rgba(0,0,0,.28)}
113
+ .dvw-commit .dvw-commit-menu button{width:100%;height:26px;border:0;border-radius:4px;background:transparent;color:var(--dvw-text);padding:0 8px;text-align:left;font:inherit;cursor:pointer}
114
+ .dvw-commit .dvw-commit-menu button:hover{background:var(--dvw-hover)}
115
+ /* Pure-white theme: every chrome surface uses the editor's white background. */
116
+ .dvw-overlay[data-dvw-theme=white]{--dvw-base:#fff;--dvw-surface-1:#fff;--dvw-surface-2:#fff;--dvw-surface-3:#fff;--dvw-side:#fff;--dvw-tab-active:#fff;--dvw-gutter:#fff;--dvw-graph-ref:#0063d3;--dvw-text:#171717;--dvw-text-2:#5c5c5c;--dvw-text-3:#8c8c8c;--dvw-border:#e2e2e2;--dvw-border-soft:#efefef;--dvw-hover:#0000000c;--dvw-active:#00000014;--dvw-accent:#0066cc;--dvw-error:#d13438;--dvw-success:#2e7d32;--dvw-warn:#8a6100;color-scheme:light}
117
+ .dvw-overlay[data-dvw-theme=white] .dvw-tabs{background:#fff}
118
+ .dvw-overlay[data-dvw-theme=white] .dvw-act[data-active=true]::before{background:#00000010}
119
+ /* Eye-care theme: a warm paper-yellow surface shared by every panel and the editor. */
120
+ .dvw-overlay[data-dvw-theme=eyecare]{--dvw-base:#f7ebc8;--dvw-surface-1:#f7ebc8;--dvw-surface-2:#f7ebc8;--dvw-surface-3:#f7ebc8;--dvw-side:#f7ebc8;--dvw-tab-active:#ecdcae;--dvw-gutter:#f7ebc8;--dvw-graph-ref:#9c6b1e;--dvw-text:#3b3324;--dvw-text-2:#6b604a;--dvw-text-3:#92856a;--dvw-border:#e0cf9f;--dvw-border-soft:#eadcb6;--dvw-hover:#0000000c;--dvw-active:#00000014;--dvw-accent:#9c6b1e;--dvw-error:#b03a2e;--dvw-success:#4c7a34;--dvw-warn:#8a5a00;color-scheme:light}
121
+ .dvw-overlay[data-dvw-theme=eyecare] .dvw-tabs{background:#f7ebc8}
122
+ .dvw-overlay[data-dvw-theme=eyecare] .dvw-act[data-active=true]::before{background:#00000016}
123
+ .dvw-theme-swatch.white{background:#fff}
124
+ .dvw-theme-swatch.eyecare{background:#f7ebc8}
125
+ .dvw-settings-themes{grid-template-columns:repeat(auto-fit,minmax(108px,1fr))}
126
+ body[data-dvw-palette=white] [data-dvw-conversation-panel]{--dsw-alias-bg-base:#fff;--dsw-specific-sidebar-fill:#fff;--dsw-alias-bg-layer-1:#fff;--dsw-alias-bg-layer-2:#fff;--dsw-alias-bg-layer-3:#fff;--dsw-alias-border-l2:#e2e2e2;--dsw-alias-border-l3:#efefef;background:#fff!important}
127
+ body[data-dvw-palette=eyecare] [data-dvw-conversation-panel]{--dsw-alias-bg-base:#f7ebc8;--dsw-specific-sidebar-fill:#f7ebc8;--dsw-alias-bg-layer-1:#f7ebc8;--dsw-alias-bg-layer-2:#f1e2b8;--dsw-alias-bg-layer-3:#ecdcae;--dsw-alias-bg-layer-4:#ecdcae;--dsw-specific-input-major:#f7ebc8;--dsw-specific-login-input:#f1e2b8;--dsw-menu-surface-fill:#f7ebc8;--dsw-alias-label-primary:#3b3324;--dsw-alias-label-secondary:#6b604a;--dsw-alias-label-tertiary:#92856a;--dsw-alias-border-l2:#e0cf9f;--dsw-alias-border-l3:#eadcb6;--dsw-alias-interactive-bg-hover:#0000000c;--dsw-alias-interactive-bg-active:#00000014;background:#f7ebc8!important}
128
+ /* Settings dialog: left navigation rail with aligned label/control rows, mirroring DSH settings. */
129
+ .dvw-settings{width:min(880px,calc(100vw - 40px));height:min(680px,calc(100vh - 40px));max-height:none;border-radius:18px}
130
+ .dvw-settings>header{height:52px;padding:0 14px 0 20px;font-size:15px}
131
+ .dvw-settings-body{flex:1;min-height:0;display:grid;grid-template-columns:190px minmax(0,1fr);gap:0;padding:0;overflow:hidden}
132
+ .dvw-settings-nav{grid-column:1;display:flex;flex-direction:column;gap:4px;padding:12px 10px;border-right:1px solid var(--dvw-border-soft);background:var(--dvw-surface-1);overflow:auto}
133
+ .dvw-settings-nav button{height:36px;border:0;border-radius:11px;background:transparent;color:var(--dvw-text-2);display:flex;align-items:center;gap:10px;padding:0 12px;font:inherit;text-align:left;cursor:pointer}
134
+ .dvw-settings-nav button .codicon{font-size:16px}
135
+ .dvw-settings-nav button:hover{background:var(--dvw-hover);color:var(--dvw-text)}
136
+ .dvw-settings-nav button[data-active=true]{background:var(--dvw-active);color:var(--dvw-text);font-weight:600}
137
+ .dvw-settings-body>section{grid-column:2;min-height:0;overflow:auto;padding:20px 26px 30px;border-left:0}
138
+ .dvw-settings-body>section+section{margin-top:0;padding-top:20px;border-top:0}
139
+ .dvw-settings-body[data-section=appearance]>section:not(:nth-of-type(1)){display:none}
140
+ .dvw-settings-body[data-section=layout]>section:not(:nth-of-type(2)){display:none}
141
+ .dvw-settings-body[data-section=commit]>section:not(:nth-of-type(3)){display:none}
142
+ .dvw-settings-body[data-section=language]>section:not(:nth-of-type(4)){display:none}
143
+ .dvw-settings-body h3{margin:0 0 4px;font-size:16px;font-weight:600}
144
+ .dvw-settings-body>section>label:not(.dvw-setting-check):not(.dvw-setting-range){display:block;margin:14px 0 9px;color:var(--dvw-text-2);font-size:12px}
145
+ .dvw-setting-range{display:grid;grid-template-columns:minmax(0,1fr) minmax(140px,240px) 48px;align-items:center;gap:16px;min-height:52px;border-bottom:1px solid var(--dvw-border-soft)}
146
+ .dvw-setting-range>span{color:var(--dvw-text);font-size:13px;font-weight:500}
147
+ .dvw-setting-range output{text-align:right;color:var(--dvw-text-3);font-variant-numeric:tabular-nums}
148
+ .dvw-setting-choice{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:16px;min-height:52px;border-bottom:1px solid var(--dvw-border-soft)}
149
+ .dvw-setting-choice>span{color:var(--dvw-text);font-size:13px;font-weight:500}
150
+ .dvw-setting-check{height:auto;min-height:52px;display:flex;flex-direction:row-reverse;align-items:center;justify-content:space-between;gap:16px;border-bottom:1px solid var(--dvw-border-soft);cursor:pointer;font-size:13px;font-weight:500}
151
+ .dvw-setting-check input{appearance:none;flex:none;width:38px;height:22px;margin:0;border-radius:11px;background:color-mix(in srgb,var(--dvw-text) 22%,transparent);position:relative;cursor:pointer;transition:background .15s}
152
+ .dvw-setting-check input::after{content:"";position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform .15s}
153
+ .dvw-setting-check input:checked{background:var(--dvw-accent)}
154
+ .dvw-setting-check input:checked::after{transform:translateX(16px)}
155
+ .dvw-settings-themes{grid-template-columns:repeat(5,minmax(0,1fr));gap:8px;margin:12px 0 10px}
156
+ .dvw-settings-themes button{position:relative;height:86px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:9px;padding:10px;text-align:center;border-radius:14px;background:var(--dvw-base)}
157
+ .dvw-settings-themes button[data-active=true]{border-color:var(--dvw-accent);box-shadow:inset 0 0 0 1px var(--dvw-accent);background:color-mix(in srgb,var(--dvw-accent) 8%,var(--dvw-base))}
158
+ .dvw-settings-themes .dvw-theme-swatch{width:26px;height:26px;border-radius:8px;flex:none}
159
+ .dvw-settings-themes button>.codicon{position:absolute;top:8px;right:8px;font-size:13px;color:var(--dvw-accent)}
160
+ .dvw-settings-body .dvw-setting-extension{border:0;border-bottom:1px solid var(--dvw-border-soft);border-radius:0;background:transparent;padding:14px 0}
161
+ .dvw-settings-body .dvw-setting-extension strong{font-size:13px}
162
+ .dvw-settings-body .dvw-setting-extension button{height:28px;border-radius:9px;padding:0 16px;background:var(--dvw-surface-3)}
163
+ .dvw-settings-body .dvw-setting-extension button.dvw-primary-button{border-color:var(--dvw-accent);background:var(--dvw-accent);color:#fff}
164
+ .dvw-setting-extension .dvw-lsp-status{display:flex;flex-wrap:wrap;align-items:center;gap:6px 8px;margin-top:2px}
165
+ .dvw-setting-extension .dvw-lsp-chip{padding:1px 8px;border:1px solid var(--dvw-border);border-radius:999px;font-size:11px;line-height:18px;color:var(--dvw-text-3)}
166
+ .dvw-setting-extension .dvw-lsp-chip[data-installed=true]{border-color:color-mix(in srgb,var(--dvw-success) 42%,transparent);color:var(--dvw-success)}
167
+ .dvw-setting-extension .dvw-lsp-chip[data-action=true]{border-color:color-mix(in srgb,var(--dvw-accent) 45%,transparent);color:var(--dvw-accent)}
168
+ .dvw-setting-extension .dvw-lsp-hint{flex-basis:100%;font-size:11px;line-height:1.45;color:var(--dvw-text-3)}
169
+ .dvw-setting-extension button:disabled{opacity:.55;cursor:default}
170
+ .dvw-settings .dvw-setting-segment{border-radius:10px;padding:3px}
171
+ .dvw-settings .dvw-setting-segment button{border-radius:7px}
172
+ .dvw-settings .dvw-icon{border-radius:9px}
173
+ /* Monaco owns diff scrolling; an outer scroller changes the width as scrollbars appear and makes split mode oscillate. */
174
+ .dvw-editor{min-width:0;overflow:hidden}
175
+ .dvw-editor-body{min-width:0;overflow:hidden}
176
+ .dvw-monaco-diff{height:auto;min-width:0;min-height:0;margin:0;padding:0;overflow:hidden;white-space:normal}
177
+ /* Keep the workspace switcher in the same elevated surface family as workbench settings. */
178
+ .dvw-workspace-layer{position:fixed!important;inset:0!important;z-index:1000!important;display:block!important;padding:0!important;background:transparent!important;overflow:hidden!important}
179
+ .dvw-workspace-backdrop{position:absolute;inset:0;z-index:50;display:grid;place-items:center;padding:24px;background:rgba(0,0,0,.34);backdrop-filter:blur(3px)}
180
+ .dvw-workspace-switcher{width:min(640px,calc(100vw - 48px));height:min(680px,calc(100vh - 48px));max-height:none;border-radius:18px;overflow:hidden;box-shadow:0 16px 50px rgba(0,0,0,.32)}
181
+ .dvw-workspace-switcher>header{height:52px;padding:0 12px 0 20px;font-size:15px}
182
+ .dvw-workspace-list{padding:10px}
183
+ .dvw-workspace-row{border-radius:11px}
184
+ .dvw-workspace-toggle{height:40px;padding:0 12px}
185
+ .dvw-workspace-sessions{gap:3px;padding:4px 0 8px 32px}
186
+ .dvw-workspace-session{height:34px;padding:0 12px;border-radius:11px}
187
+ .dvw-workspace-switcher>footer{padding:12px 16px}
188
+ .dvw-workspace-switcher>footer button{height:32px;border-radius:9px;padding:0 16px}
189
+ .dvw-workspace-switcher .dvw-icon{border-radius:9px}
190
+ @media(max-width:760px){.dvw-settings-body{grid-template-columns:1fr;grid-template-rows:auto minmax(0,1fr)}.dvw-settings-nav{grid-column:1;grid-row:1;flex-direction:row;border-right:0;border-bottom:1px solid var(--dvw-border-soft);padding:8px}.dvw-settings-nav button{flex:none}.dvw-settings-body>section{grid-column:1;grid-row:2;padding:16px 18px 24px}}
97
191
  `
98
192
 
99
193
  export function installStyles(): () => void {