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.
- package/README.en.md +0 -10
- package/README.md +0 -11
- package/lib/client.js +124 -56
- package/lib/client.js.map +1 -1
- package/lib/types/client/MonacoEditor.d.ts +13 -0
- package/lib/types/client/workspaceState.d.ts +20 -0
- package/package.json +1 -1
- package/src/client/MonacoEditor.tsx +11 -2
- package/src/client/Workbench.tsx +6 -2
- package/src/client/index.tsx +4 -1
- package/src/client/workspaceState.ts +37 -0
|
@@ -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
|
@@ -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,
|
|
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,
|
|
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())
|
package/src/client/Workbench.tsx
CHANGED
|
@@ -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
|
-
|
|
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;
|
|
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)}
|
package/src/client/index.tsx
CHANGED
|
@@ -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 }) =>
|
|
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
|
+
}
|