dsh-vscode-workbench 0.1.1 → 0.1.2

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,6 +8,19 @@ declare global {
8
8
  monaco?: Monaco;
9
9
  }
10
10
  }
11
+ /** Monaco chrome used by both editors so only the narrow native scrollbar remains. */
12
+ export declare const WORKBENCH_EDITOR_CHROME: {
13
+ readonly minimap: {
14
+ readonly enabled: false;
15
+ };
16
+ readonly renderOverviewRuler: false;
17
+ readonly overviewRulerLanes: 0;
18
+ readonly hideCursorInOverviewRuler: true;
19
+ readonly scrollbar: {
20
+ readonly verticalScrollbarSize: 10;
21
+ readonly horizontalScrollbarSize: 10;
22
+ };
23
+ };
11
24
  export declare function loadMonaco(): Promise<Monaco>;
12
25
  export declare function MonacoDiffEditor(props: {
13
26
  uri: string;
@@ -0,0 +1,20 @@
1
+ /** Values cleared before another workspace starts loading. */
2
+ export interface EmptyWorkspaceDocuments<T> {
3
+ tabs: T[];
4
+ active: undefined;
5
+ error: undefined;
6
+ }
7
+ /** Remove editor state that belongs to the previous workspace. */
8
+ export declare function resetWorkspaceDocuments<T>(): EmptyWorkspaceDocuments<T>;
9
+ /** Identifies responses that still belong to the active workspace request. */
10
+ export declare class WorkspaceRequestScope {
11
+ private epoch;
12
+ /** Begin loading another workspace and invalidate earlier work. */
13
+ begin(): number;
14
+ /** Return the active workspace request identifier. */
15
+ current(): number;
16
+ /** Invalidate an active request while preserving any newer request. */
17
+ invalidate(epoch: number): void;
18
+ /** Return whether a response may update the current workspace. */
19
+ isCurrent(epoch: number): boolean;
20
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vscode-workbench",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Explorer, Monaco editor, search and local Git overlay for DSH",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -3,6 +3,15 @@ import { useEffect, useRef, useState } from 'react'
3
3
  type Monaco = typeof import('monaco-editor')
4
4
  declare global { interface Window { require?: { (ids: string[], done: () => void): void; config(value: unknown): void }; monaco?: Monaco } }
5
5
  let loading: Promise<Monaco> | undefined
6
+ /** Monaco chrome used by both editors so only the narrow native scrollbar remains. */
7
+ export const WORKBENCH_EDITOR_CHROME = {
8
+ minimap: { enabled: false },
9
+ renderOverviewRuler: false,
10
+ overviewRulerLanes: 0,
11
+ hideCursorInOverviewRuler: true,
12
+ scrollbar: { verticalScrollbarSize: 10, horizontalScrollbarSize: 10 },
13
+ } as const
14
+
6
15
  export function loadMonaco(): Promise<Monaco> {
7
16
  loading ??= new Promise((resolve, reject) => {
8
17
  const start = () => {
@@ -19,7 +28,7 @@ export function loadMonaco(): Promise<Monaco> {
19
28
 
20
29
  export function MonacoDiffEditor(props: { uri: string; original: string; modified: string; mode: 'inline' | 'split'; theme: 'dark' | 'light' }) {
21
30
  const root=useRef<HTMLDivElement>(null);const editor=useRef<import('monaco-editor').editor.IStandaloneDiffEditor>();const models=useRef<{original:import('monaco-editor').editor.ITextModel;modified:import('monaco-editor').editor.ITextModel}>();const [error,setError]=useState<string>()
22
- useEffect(()=>{let disposed=false;void loadMonaco().then(monaco=>{if(disposed||root.current===null)return;const safe=props.uri.replace(/^\/+/, '');const original=monaco.editor.createModel(props.original,undefined,monaco.Uri.parse(`dvw-diff-original:///${safe}`));const modified=monaco.editor.createModel(props.modified,undefined,monaco.Uri.parse(`dvw-diff-modified:///${safe}`));models.current={original,modified};const instance=monaco.editor.createDiffEditor(root.current,{automaticLayout:true,readOnly:true,renderSideBySide:props.mode==='split',theme:props.theme==='dark'?'vs-dark':'vs',fontSize:14,minimap:{enabled:false},originalEditable:false,renderOverviewRuler:true,ignoreTrimWhitespace:false});instance.setModel({original,modified});editor.current=instance}).catch(error=>setError(error instanceof Error?error.message:String(error)));return()=>{disposed=true;editor.current?.dispose();editor.current=undefined;models.current?.original.dispose();models.current?.modified.dispose();models.current=undefined}},[props.uri])
31
+ useEffect(()=>{let disposed=false;void loadMonaco().then(monaco=>{if(disposed||root.current===null)return;const safe=props.uri.replace(/^\/+/, '');const original=monaco.editor.createModel(props.original,undefined,monaco.Uri.parse(`dvw-diff-original:///${safe}`));const modified=monaco.editor.createModel(props.modified,undefined,monaco.Uri.parse(`dvw-diff-modified:///${safe}`));models.current={original,modified};const instance=monaco.editor.createDiffEditor(root.current,{automaticLayout:true,readOnly:true,renderSideBySide:props.mode==='split',theme:props.theme==='dark'?'vs-dark':'vs',fontSize:14,...WORKBENCH_EDITOR_CHROME,originalEditable:false,ignoreTrimWhitespace:false});instance.setModel({original,modified});editor.current=instance}).catch(error=>setError(error instanceof Error?error.message:String(error)));return()=>{disposed=true;editor.current?.dispose();editor.current=undefined;models.current?.original.dispose();models.current?.modified.dispose();models.current=undefined}},[props.uri])
23
32
  useEffect(()=>{const value=models.current;if(value===undefined)return;if(value.original.getValue()!==props.original)value.original.setValue(props.original);if(value.modified.getValue()!==props.modified)value.modified.setValue(props.modified)},[props.original,props.modified])
24
33
  useEffect(()=>{editor.current?.updateOptions({renderSideBySide:props.mode==='split'})},[props.mode])
25
34
  useEffect(()=>{if(window.monaco!==undefined)window.monaco.editor.setTheme(props.theme==='dark'?'vs-dark':'vs')},[props.theme])
@@ -39,7 +48,7 @@ export function MonacoEditor(props: { uri: string; value: string; theme: 'dark'
39
48
  const uri = monaco.Uri.parse(`file:///${props.uri.replace(/^\/+/, '')}`)
40
49
  model = monaco.editor.getModel(uri) ?? monaco.editor.createModel(props.value, undefined, uri)
41
50
  if (model.getValue() !== props.value) model.setValue(props.value)
42
- const instance = monaco.editor.create(root.current, { model, automaticLayout: true, theme: props.theme === 'dark' ? 'vs-dark' : 'vs', fontSize: 14, minimap: { enabled: true }, wordWrap: 'off' })
51
+ const instance = monaco.editor.create(root.current, { model, automaticLayout: true, theme: props.theme === 'dark' ? 'vs-dark' : 'vs', fontSize: 14, ...WORKBENCH_EDITOR_CHROME, wordWrap: 'off' })
43
52
  editor.current = instance
44
53
  const change = instance.onDidChangeModelContent(() => callbacks.current.onChange(instance.getValue()))
45
54
  instance.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => callbacks.current.onSave())
@@ -9,6 +9,7 @@ import type { MarkdownHeading } from './MarkdownPreview.tsx'
9
9
  import { MonacoDiffEditor, MonacoEditor } from './MonacoEditor.tsx'
10
10
  import { DshConversationPanel, DshConversationTheme } from './DshConversationPanel.tsx'
11
11
  import { DshMark } from './DshMark.tsx'
12
+ import { resetWorkspaceDocuments, WorkspaceRequestScope } from './workspaceState.ts'
12
13
 
13
14
  interface Tab extends FileDocument { dirty: boolean; draft: string; line?: number; column?: number; external?: boolean; preview?: boolean; title?: string; diff?: GitDiff; diffMode?: 'inline' | 'split'; diffSourcePath?:string; imageUrl?:string }
14
15
  type WorkbenchTheme = 'dsh' | 'dark' | 'light'
@@ -142,6 +143,8 @@ export function Workbench({ service, useSessions }: { service: VscodeWorkbench;
142
143
  useEffect(() => service.subscribe(() => { setSnapshot(service.getSnapshot()); setPanels(service.getPanels()) }), [service])
143
144
  const sessionId = useSessions(state=>state.current) as string|undefined; const cwd = useSessions(state=>sessionId===undefined?undefined:state.byId[sessionId]?.cwd) as string|undefined
144
145
  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-visible')!=='closed');const [settingsOpen,setSettingsOpen]=useState(false);const [chatWidth,setChatWidth]=useState(()=>Math.max(400,Math.min(720,Math.round(Number(localStorage.getItem('dvw:dsh-chat-width'))||420))));const [chatVisible,setChatVisible]=useState(()=>localStorage.getItem('dvw:dsh-chat-visible')!=='closed');const [outlineVisible,setOutlineVisible]=useState(()=>localStorage.getItem('dvw:outline')!=='closed')
146
+ const tabsRef=useRef(tabs);tabsRef.current=tabs
147
+ const workspaceRequests=useRef(new WorkspaceRequestScope())
145
148
  const dshDark = useDshDarkTheme()
146
149
  const [theme,setTheme]=useState<WorkbenchTheme>(()=>{const saved=localStorage.getItem('dvw:theme');return saved==='dark'||saved==='light'?saved:'dsh'})
147
150
  const colorScheme: 'dark' | 'light' = theme === 'dsh' ? dshDark ? 'dark' : 'light' : theme
@@ -152,10 +155,11 @@ export function Workbench({ service, useSessions }: { service: VscodeWorkbench;
152
155
  useEffect(()=>localStorage.setItem('dvw:primary-side-visible',primarySideVisible?'open':'closed'),[primarySideVisible])
153
156
  useEffect(()=>localStorage.setItem('dvw:status-visible',statusVisible?'open':'closed'),[statusVisible])
154
157
  useEffect(()=>{if(!snapshot.visible)return;const onKey=(event:KeyboardEvent)=>{if(event.isComposing||!event.ctrlKey||!event.altKey||event.shiftKey||event.code!=='KeyB')return;event.preventDefault();setChatVisible(value=>!value)};window.addEventListener('keydown',onKey,true);return()=>window.removeEventListener('keydown',onKey,true)},[snapshot.visible])
155
- useEffect(()=>{if(!snapshot.visible||sessionId===undefined)return;void api<{cwd:string;name:string}>('workspace',{sessionId}).then(value=>{setWorkspace(value);const saved=localStorage.getItem(`dvw:${value.cwd}`);if(saved){try{const parsed=JSON.parse(saved) as {view?:WorkbenchView;paths?:string[];panelHeight?:number;sideWidth?:number};if(parsed.view)service.show(parsed.view);if(typeof parsed.panelHeight==='number')setPanelHeight(parsed.panelHeight);if(typeof parsed.sideWidth==='number')setSideWidth(Math.max(210,Math.min(640,Math.round(parsed.sideWidth))));for(const path of parsed.paths??[])void open(path)}catch{}}}).catch(error=>setError(String(error)))},[snapshot.visible,sessionId])
158
+ const openForWorkspace=async(targetSessionId:string,epoch:number,path:string,line?:number,column?:number)=>{if(!workspaceRequests.current.isCurrent(epoch))return;const existing=tabsRef.current.find(tab=>tab.path===path);if(existing){setActive(path);setTabs(rows=>rows.map(tab=>tab.path===path?{...tab,line,column}:tab));return}if(/\.(?:avif|bmp|gif|ico|jpe?g|png|svg|webp)$/i.test(path)){const imageUrl=`/dsh-vscode/file?sessionId=${encodeURIComponent(targetSessionId)}&path=${encodeURIComponent(path)}`;if(!workspaceRequests.current.isCurrent(epoch))return;setTabs(rows=>workspaceRequests.current.isCurrent(epoch)?[...rows.filter(tab=>tab.path!==path),{path,content:'',draft:'',revision:'',size:0,binary:false,tooLarge:false,dirty:false,imageUrl}]:rows);setActive(path);return}try{const doc=await api<FileDocument>('fs.read',{sessionId:targetSessionId,path});if(!workspaceRequests.current.isCurrent(epoch))return;if(doc.binary||doc.tooLarge){setError(doc.binary?'二进制文件不能在文本编辑器中打开':`文件超过 5 MiB:${path}`);return}setTabs(rows=>workspaceRequests.current.isCurrent(epoch)?[...rows.filter(tab=>tab.path!==path),{...doc,draft:doc.content,dirty:false,line,column,preview:/\.md$/i.test(path)}]:rows);setActive(path)}catch(error){if(workspaceRequests.current.isCurrent(epoch))setError(String(error))}}
159
+ useEffect(()=>{if(!snapshot.visible||sessionId===undefined)return;const epoch=workspaceRequests.current.begin();const cleared=resetWorkspaceDocuments<Tab>();setWorkspace(undefined);setTabs(cleared.tabs);tabsRef.current=cleared.tabs;setActive(cleared.active);setError(cleared.error);const controller=new AbortController();void api<{cwd:string;name:string}>('workspace',{sessionId},controller.signal).then(value=>{if(!workspaceRequests.current.isCurrent(epoch))return;setWorkspace(value);const saved=localStorage.getItem(`dvw:${value.cwd}`);if(saved){try{const parsed=JSON.parse(saved) as {view?:WorkbenchView;paths?:string[];panelHeight?:number;sideWidth?:number};if(parsed.view)service.show(parsed.view);if(typeof parsed.panelHeight==='number')setPanelHeight(parsed.panelHeight);if(typeof parsed.sideWidth==='number')setSideWidth(Math.max(210,Math.min(640,Math.round(parsed.sideWidth))));for(const path of parsed.paths??[])void openForWorkspace(sessionId,epoch,path)}catch{}}}).catch(error=>{if(!controller.signal.aborted&&workspaceRequests.current.isCurrent(epoch))setError(String(error))});return()=>{controller.abort();workspaceRequests.current.invalidate(epoch)}},[snapshot.visible,sessionId])
156
160
  useEffect(()=>{if(workspace) localStorage.setItem(`dvw:${workspace.cwd}`,JSON.stringify({view:snapshot.view,paths:tabs.filter(t=>!t.path.startsWith('diff:')).map(t=>t.path),panelHeight,sideWidth}))},[workspace,snapshot.view,tabs,panelHeight,sideWidth])
157
161
  useEffect(()=>{if(!snapshot.visible||sessionId===undefined)return;const protocol=location.protocol==='https:'?'wss':'ws';const ws=new WebSocket(`${protocol}://${location.host}/dsh-vscode/ws/files?sessionId=${encodeURIComponent(sessionId)}`);ws.onmessage=()=>{window.dispatchEvent(new Event('dvw-files-changed'));setTabs(current=>current.map(tab=>tab.dirty?{...tab,external:true}:tab))};return()=>ws.close()},[snapshot.visible,sessionId])
158
- const open=async(path:string,line?:number,column?:number)=>{if(sessionId===undefined)return;const existing=tabs.find(t=>t.path===path);if(existing){setActive(path);setTabs(rows=>rows.map(t=>t.path===path?{...t,line,column}:t));return}if(/\.(?:avif|bmp|gif|ico|jpe?g|png|svg|webp)$/i.test(path)){const imageUrl=`/dsh-vscode/file?sessionId=${encodeURIComponent(sessionId)}&path=${encodeURIComponent(path)}`;setTabs(rows=>[...rows,{path,content:'',draft:'',revision:'',size:0,binary:false,tooLarge:false,dirty:false,imageUrl}]);setActive(path);return}try{const doc=await api<FileDocument>('fs.read',{sessionId,path});if(doc.binary||doc.tooLarge){setError(doc.binary?'二进制文件不能在文本编辑器中打开':`文件超过 5 MiB:${path}`);return}setTabs(rows=>[...rows,{...doc,draft:doc.content,dirty:false,line,column,preview:/\.md$/i.test(path)}]);setActive(path)}catch(error){setError(String(error))}}
162
+ const open=async(path:string,line?:number,column?:number)=>{if(sessionId===undefined)return;await openForWorkspace(sessionId,workspaceRequests.current.current(),path,line,column)}
159
163
  useEffect(()=>{const listener=(event:Event)=>void open((event as CustomEvent<string>).detail);window.addEventListener('dvw-open-workspace-path',listener);return()=>window.removeEventListener('dvw-open-workspace-path',listener)},[sessionId,tabs])
160
164
  const save=async(tab:Tab)=>{if(sessionId===undefined||!tab.dirty)return;try{const doc=await api<FileDocument>('fs.save',{sessionId,path:tab.path,content:tab.draft,revision:tab.revision});setTabs(rows=>rows.map(row=>row.path===tab.path?{...row,...doc,draft:doc.content,dirty:false,external:false}:row))}catch(error){setError(String(error))}}
161
165
  const close=(tab:Tab)=>{if(tab.dirty&&!confirm(`${tab.path} 有未保存更改,确定关闭?`))return;setTabs(rows=>rows.filter(row=>row.path!==tab.path));if(active===tab.path)setActive(tabs.filter(row=>row.path!==tab.path).at(-1)?.path)}
@@ -14,7 +14,10 @@ export type { BottomPanelContribution, BottomPanelProps, VscodeWorkbench, Workbe
14
14
  declare module '@deepseek-ai/cordis' { interface Context { vscodeWorkbench: VscodeWorkbench } }
15
15
  declare module '@deepseek-ai/dsh-client-ui-slots' { interface SlotInjectMap { 'dsh-vscode-workbench.overlay': { service: VscodeWorkbench }; 'dsh-vscode-workbench.launcher': { service: VscodeWorkbench } } }
16
16
 
17
- const Overlay = ({ service, useSessions }: PropsRuntime<'shell.overlay'> & { service: VscodeWorkbench }) => <Workbench service={service} useSessions={useSessions as never}/>
17
+ const Overlay = ({ service, useSessions }: PropsRuntime<'shell.overlay'> & { service: VscodeWorkbench }) => {
18
+ const sessionId = useSessions(state => state.current) as string | undefined
19
+ return <Workbench key={sessionId ?? 'no-session'} service={service} useSessions={useSessions as never}/>
20
+ }
18
21
  const Footer = ({ service, wide }: PropsRuntime<'sidebar.footer.action'> & { service: VscodeWorkbench }) => {
19
22
  const marker=useRef<HTMLSpanElement>(null);const [target,setTarget]=useState<HTMLElement|null>()
20
23
  useLayoutEffect(()=>{let container=marker.current?.parentElement;while(container?.previousElementSibling===null)container=container.parentElement;const region=container?.previousElementSibling;if(!(region instanceof HTMLElement)){setTarget(null);return}const mount=document.createElement('div');mount.className='dvw-sidebar-launcher-mount';region.prepend(mount);setTarget(mount);return()=>mount.remove()},[])
@@ -0,0 +1,37 @@
1
+ /** Values cleared before another workspace starts loading. */
2
+ export interface EmptyWorkspaceDocuments<T> {
3
+ tabs: T[]
4
+ active: undefined
5
+ error: undefined
6
+ }
7
+
8
+ /** Remove editor state that belongs to the previous workspace. */
9
+ export function resetWorkspaceDocuments<T>(): EmptyWorkspaceDocuments<T> {
10
+ return { tabs: [], active: undefined, error: undefined }
11
+ }
12
+
13
+ /** Identifies responses that still belong to the active workspace request. */
14
+ export class WorkspaceRequestScope {
15
+ private epoch = 0
16
+
17
+ /** Begin loading another workspace and invalidate earlier work. */
18
+ begin(): number {
19
+ this.epoch += 1
20
+ return this.epoch
21
+ }
22
+
23
+ /** Return the active workspace request identifier. */
24
+ current(): number {
25
+ return this.epoch
26
+ }
27
+
28
+ /** Invalidate an active request while preserving any newer request. */
29
+ invalidate(epoch: number): void {
30
+ if (this.epoch === epoch) this.epoch += 1
31
+ }
32
+
33
+ /** Return whether a response may update the current workspace. */
34
+ isCurrent(epoch: number): boolean {
35
+ return epoch === this.epoch
36
+ }
37
+ }