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.
- package/README.en.md +14 -125
- package/README.md +14 -126
- package/lib/client.js +588 -166
- package/lib/client.js.map +1 -1
- package/lib/index.js +423 -26
- package/lib/index.js.map +1 -1
- package/lib/types/client/ActivityBar.d.ts +5 -1
- package/lib/types/client/DshConversationPanel.d.ts +1 -1
- package/lib/types/client/MonacoEditor.d.ts +6 -2
- package/lib/types/client/Workbench.d.ts +4 -0
- package/lib/types/client/WorkspaceSwitcher.d.ts +3 -2
- package/lib/types/client/lsp.d.ts +6 -0
- package/lib/types/client/service.d.ts +1 -1
- package/lib/types/commit-message.d.ts +43 -0
- package/lib/types/host.d.ts +3 -0
- package/lib/types/lsp.d.ts +21 -0
- package/package.json +3 -1
- package/src/client/ActivityBar.tsx +71 -34
- package/src/client/DshConversationPanel.tsx +1 -1
- package/src/client/MonacoEditor.tsx +42 -7
- package/src/client/TerminalPanel.tsx +2 -2
- package/src/client/Workbench.tsx +70 -15
- package/src/client/WorkspaceSwitcher.tsx +7 -3
- package/src/client/lsp.ts +3 -0
- package/src/client/service.ts +1 -1
- package/src/client/styles.ts +95 -1
- package/src/commit-message.ts +224 -0
- package/src/host.ts +47 -0
- package/src/index.ts +18 -1
- package/src/lsp.ts +85 -25
|
@@ -12,7 +12,11 @@ export declare function normalizeActivityOrder(value: unknown, known: readonly s
|
|
|
12
12
|
/** Move `id` before `before` (or to the end when absent), leaving unknown ids untouched. */
|
|
13
13
|
export declare function moveActivity(order: readonly string[], id: string, before: string | undefined): string[];
|
|
14
14
|
export declare function toggleActivityHidden(hidden: readonly string[], id: string): string[];
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* VS Code-style activity bar: every view can be dragged into a new slot while `fixed` actions stay pinned on
|
|
17
|
+
* top and `bottom` actions stay pinned at the foot. Reordering is computed from pointer position so it keeps
|
|
18
|
+
* working when the DOM shifts under a fast drag.
|
|
19
|
+
*/
|
|
16
20
|
export declare function ActivityBar({ views, fixed, bottom, order, hidden, onOrderChange, onHiddenChange }: {
|
|
17
21
|
views: ActivityView[];
|
|
18
22
|
fixed: ActivityView[];
|
|
@@ -9,6 +9,6 @@ export declare function DshConversationPanel({ visible, width, statusHeight }: {
|
|
|
9
9
|
/** Temporarily align the official DSH panel palette with a fixed workbench palette. */
|
|
10
10
|
export declare function DshConversationTheme({ visible, colorScheme, followDsh }: {
|
|
11
11
|
visible: boolean;
|
|
12
|
-
colorScheme: 'dark' | 'light';
|
|
12
|
+
colorScheme: 'dark' | 'light' | 'white' | 'eyecare';
|
|
13
13
|
followDsh: boolean;
|
|
14
14
|
}): null;
|
|
@@ -56,6 +56,10 @@ export declare function isMacDefinitionClick(event: {
|
|
|
56
56
|
}): boolean;
|
|
57
57
|
/** Monaco theme aligned with the workbench dark palette. */
|
|
58
58
|
export declare const DARK_THEME = "dvw-dark";
|
|
59
|
+
/** Eye-care palette: the editor shares the workbench's warm yellow surface. */
|
|
60
|
+
export declare const EYE_THEME = "dvw-eyecare";
|
|
61
|
+
export type MonacoColorScheme = 'dark' | 'light' | 'white' | 'eyecare';
|
|
62
|
+
export declare function monacoThemeFor(theme: MonacoColorScheme): string;
|
|
59
63
|
/**
|
|
60
64
|
* Bootstrap source for Monaco's AMD web workers. Workers resolve module paths against their own
|
|
61
65
|
* blob URL, so they need an absolute `baseUrl`; without it the TypeScript language worker fails to load.
|
|
@@ -67,7 +71,7 @@ export declare function MonacoDiffEditor(props: {
|
|
|
67
71
|
original: string;
|
|
68
72
|
modified: string;
|
|
69
73
|
mode: 'inline' | 'split';
|
|
70
|
-
theme:
|
|
74
|
+
theme: MonacoColorScheme;
|
|
71
75
|
fontSize?: number;
|
|
72
76
|
}): import("react").JSX.Element;
|
|
73
77
|
/** Start downloading Monaco in the background so the first opened file does not wait for it. */
|
|
@@ -77,7 +81,7 @@ export declare function retainEditorModels(uris: string[]): void;
|
|
|
77
81
|
export declare function MonacoEditor(props: {
|
|
78
82
|
uri: string;
|
|
79
83
|
value: string;
|
|
80
|
-
theme:
|
|
84
|
+
theme: MonacoColorScheme;
|
|
81
85
|
line?: number;
|
|
82
86
|
column?: number;
|
|
83
87
|
fontSize?: number;
|
|
@@ -20,6 +20,8 @@ interface Tab extends FileDocument {
|
|
|
20
20
|
documentPreview?: DocumentPreviewKind;
|
|
21
21
|
documentUrl?: string;
|
|
22
22
|
}
|
|
23
|
+
export type MarkdownDefaultView = 'preview' | 'source';
|
|
24
|
+
export declare const MARKDOWN_DEFAULT_VIEW_KEY = "dvw:markdown-default-view";
|
|
23
25
|
type ResizeDirection = 1 | -1;
|
|
24
26
|
export declare const DSH_CHAT_MIN_WIDTH = 440;
|
|
25
27
|
export declare const DSH_CHAT_MAX_WIDTH = 720;
|
|
@@ -28,6 +30,8 @@ export declare const DSH_CHAT_DEFAULT_WIDTH = 440;
|
|
|
28
30
|
export declare function clampChatWidth(width: number): number;
|
|
29
31
|
/** Calculate a panel width from a horizontal pointer movement. */
|
|
30
32
|
export declare function resizedPanelWidth(startWidth: number, startX: number, nextX: number, direction: ResizeDirection, min: number, max: number): number;
|
|
33
|
+
/** Read the saved Markdown default without depending on browser storage during unit tests. */
|
|
34
|
+
export declare function markdownDefaultView(): MarkdownDefaultView;
|
|
31
35
|
/** Restore tab headers without reading their file contents. */
|
|
32
36
|
export declare function restoreTabPaths(paths: string[]): Tab[];
|
|
33
37
|
/** Return whether file changes require reloading one visible directory. */
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import type { WorkspaceChoice } from './dshNavigation.ts';
|
|
2
2
|
/** Centered switcher: existing workspaces, their sessions, and a new-workspace action. */
|
|
3
|
-
export declare function WorkspaceSwitcher({ choices, activeSessionId, unavailable, busy, error, onCreate, onSwitch, onOpenWorkspace, onClose }: {
|
|
3
|
+
export declare function WorkspaceSwitcher({ choices, activeSessionId, unavailable, busy, error, theme, onCreate, onSwitch, onOpenWorkspace, onClose }: {
|
|
4
4
|
choices: WorkspaceChoice[];
|
|
5
5
|
activeSessionId?: string;
|
|
6
6
|
unavailable: boolean;
|
|
7
7
|
busy: boolean;
|
|
8
8
|
error?: string;
|
|
9
|
+
theme?: 'dark' | 'light' | 'white' | 'eyecare';
|
|
9
10
|
onCreate(): void;
|
|
10
11
|
onSwitch(sessionId: string): void;
|
|
11
12
|
onOpenWorkspace(workspaceId: string): void;
|
|
12
13
|
onClose(): void;
|
|
13
|
-
}): import("react").
|
|
14
|
+
}): import("react").ReactPortal;
|
|
@@ -7,6 +7,12 @@ export interface LspServerStatus {
|
|
|
7
7
|
installable: boolean;
|
|
8
8
|
installed: boolean;
|
|
9
9
|
hint?: string;
|
|
10
|
+
command?: string;
|
|
11
|
+
/** Manual servers report the language toolchain next to the server so the two are not confused. */
|
|
12
|
+
language?: {
|
|
13
|
+
name: string;
|
|
14
|
+
installed: boolean;
|
|
15
|
+
};
|
|
10
16
|
}
|
|
11
17
|
/** Normalize definition/reference payloads (Location, LocationLink or arrays) into Monaco locations. */
|
|
12
18
|
export declare function lspLocations(monaco: Monaco, result: unknown): import('monaco-editor').languages.Location[];
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ConfigureCall, GenerateOptions, LlmCallConfig, PreparedLlmCall, StreamChunk } from '@deepseek-ai/dsh-llm';
|
|
2
|
+
export interface CommitContext {
|
|
3
|
+
repository: string;
|
|
4
|
+
branch: string;
|
|
5
|
+
files: string[];
|
|
6
|
+
recent: string[];
|
|
7
|
+
patch: string;
|
|
8
|
+
truncated: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** The part of DSH's LLM runtime this feature needs; a fake implements it in tests. */
|
|
11
|
+
export interface CommitMessageRuntime {
|
|
12
|
+
prepareCall?(config: LlmCallConfig, signal?: AbortSignal, configure?: ConfigureCall): Promise<PreparedLlmCall>;
|
|
13
|
+
stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
14
|
+
}
|
|
15
|
+
export type CommitDetail = 'concise' | 'standard' | 'detailed';
|
|
16
|
+
export type CommitLanguage = 'auto' | 'zh' | 'en';
|
|
17
|
+
export interface CommitMessagePreferences {
|
|
18
|
+
detail: CommitDetail;
|
|
19
|
+
language: CommitLanguage;
|
|
20
|
+
}
|
|
21
|
+
export declare const DEFAULT_COMMIT_PREFERENCES: CommitMessagePreferences;
|
|
22
|
+
export declare function normalizeCommitPreferences(detail: unknown, language: unknown): CommitMessagePreferences;
|
|
23
|
+
export declare const configureCommitCall: ConfigureCall;
|
|
24
|
+
export declare function commitMessageSystemPrompt(preferences?: CommitMessagePreferences): string;
|
|
25
|
+
export declare function buildCommitMessagePrompt(context: CommitContext, preferences?: CommitMessagePreferences): string;
|
|
26
|
+
/**
|
|
27
|
+
* Keep file headers, hunk headers and changed lines, but drop unchanged context and clip very long minified or JSX
|
|
28
|
+
* lines. This lets every source file reach the model within a bounded token budget instead of a few early files
|
|
29
|
+
* consuming the entire patch.
|
|
30
|
+
*/
|
|
31
|
+
export declare function compactCommitPatch(patch: string): string;
|
|
32
|
+
/**
|
|
33
|
+
* Git prints diffs in path order, so a big README and a lockfile can swallow the whole budget before any source
|
|
34
|
+
* file is seen. Keep generated lockfiles out, put production source ahead of tests and documentation, and cut the
|
|
35
|
+
* last section that does not fit instead of bisecting the blob.
|
|
36
|
+
*/
|
|
37
|
+
export declare function prioritizeCommitPatch(patch: string, limit: number): {
|
|
38
|
+
patch: string;
|
|
39
|
+
truncated: boolean;
|
|
40
|
+
};
|
|
41
|
+
/** Strip common model wrappers so the result can go straight into the commit box. */
|
|
42
|
+
export declare function cleanCommitMessage(raw: string, detail?: CommitDetail): string;
|
|
43
|
+
export declare function generateCommitMessage(runtime: CommitMessageRuntime, config: LlmCallConfig, context: CommitContext, signal?: AbortSignal, preferences?: CommitMessagePreferences): Promise<string>;
|
package/lib/types/host.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { HostContext } from './context.ts';
|
|
2
|
+
import { type CommitContext } from './commit-message.ts';
|
|
2
3
|
import type { FileDocument, FileEntry, GitStatus, SearchMatch, SearchRequest } from './types.ts';
|
|
3
4
|
export declare function cwdFor(ctx: HostContext, sessionId: string): string;
|
|
4
5
|
/** Resolve a caller-relative path and reject traversal and symlink escape. */
|
|
@@ -67,6 +68,8 @@ export declare function searchWorkspace(cwd: string, request: SearchRequest, sig
|
|
|
67
68
|
limited: boolean;
|
|
68
69
|
}>;
|
|
69
70
|
export declare function gitStatus(cwd: string): Promise<GitStatus>;
|
|
71
|
+
/** Collect the staged (or working-tree) changes and recent subjects used to draft a commit message. */
|
|
72
|
+
export declare function gitCommitContext(cwd: string, limit?: number): Promise<CommitContext>;
|
|
70
73
|
export declare function gitAction(cwd: string, action: string, path?: string, value?: string): Promise<unknown>;
|
|
71
74
|
/** Resolve the official VS Code Codicon font shipped by @vscode/codicons. */
|
|
72
75
|
export declare function codiconAsset(): {
|
package/lib/types/lsp.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
/** Host-side registry and process bridge for language servers exposed to the workbench editor. */
|
|
2
2
|
import { type ChildProcess } from 'node:child_process';
|
|
3
|
+
/** The toolchain a manual language server belongs to, detected separately from the server itself. */
|
|
4
|
+
export type LspLanguage = {
|
|
5
|
+
name: string;
|
|
6
|
+
commands: readonly string[];
|
|
7
|
+
};
|
|
3
8
|
export type LspInstall = {
|
|
4
9
|
kind: 'npm';
|
|
5
10
|
package: string;
|
|
@@ -16,6 +21,8 @@ export interface LspDefinition {
|
|
|
16
21
|
languages: string[];
|
|
17
22
|
install: LspInstall;
|
|
18
23
|
args: string[];
|
|
24
|
+
/** Toolchain checked before the server; servers for pure data formats have nothing to check. */
|
|
25
|
+
language?: LspLanguage;
|
|
19
26
|
}
|
|
20
27
|
/** The strongest maintained server for each language, installable where the ecosystem allows it. */
|
|
21
28
|
export declare const LSP_SERVERS: readonly LspDefinition[];
|
|
@@ -26,9 +33,23 @@ export interface LspStatus {
|
|
|
26
33
|
installable: boolean;
|
|
27
34
|
installed: boolean;
|
|
28
35
|
hint?: string;
|
|
36
|
+
/** Executable name for manual servers, so the UI can name what it looked for. */
|
|
37
|
+
command?: string;
|
|
38
|
+
/** Present for manual servers: whether the language toolchain itself is on this machine. */
|
|
39
|
+
language?: {
|
|
40
|
+
name: string;
|
|
41
|
+
installed: boolean;
|
|
42
|
+
};
|
|
29
43
|
}
|
|
30
44
|
/** Private install root so language servers never touch the user's global npm state. */
|
|
31
45
|
export declare function lspRoot(): string;
|
|
46
|
+
/**
|
|
47
|
+
* GUI-launched apps inherit a trimmed PATH: Homebrew, `go install` and `cargo install` targets routinely sit
|
|
48
|
+
* outside it, so detection has to look in the usual per-user bin directories too.
|
|
49
|
+
*/
|
|
50
|
+
export declare function lspSearchDirs(env?: NodeJS.ProcessEnv, home?: string): string[];
|
|
51
|
+
/** First executable named `command` found in `dirs`; exposed for tests and for toolchain probing. */
|
|
52
|
+
export declare function findExecutable(command: string, dirs: readonly string[]): Promise<string | undefined>;
|
|
32
53
|
export declare function lspStatuses(): Promise<LspStatus[]>;
|
|
33
54
|
/** Install an npm-distributed server into the plugin's private directory. */
|
|
34
55
|
export declare function installLsp(id: string): Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-workbench",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Explorer, Monaco editor, search and local Git overlay for DSH",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -82,6 +82,7 @@
|
|
|
82
82
|
"@deepseek-ai/dsh-client-ui-sidebar": "^0.2.1-rc.5 || ^0.2.2-rc.1",
|
|
83
83
|
"@deepseek-ai/dsh-client-ui-slots": "^0.2.1-rc.5 || ^0.2.2-rc.1",
|
|
84
84
|
"@deepseek-ai/dsh-host-webserver": "^0.2.1-rc.5 || ^0.2.2-rc.1",
|
|
85
|
+
"@deepseek-ai/dsh-llm": "^0.2.1-rc.5 || ^0.2.2-rc.1",
|
|
85
86
|
"@deepseek-ai/dsh-session": "^0.2.1-rc.5 || ^0.2.2-rc.1",
|
|
86
87
|
"@deepseek-ai/dsh-working-directory": "^0.2.1-rc.5 || ^0.2.2-rc.1",
|
|
87
88
|
"react": "^18.2.0",
|
|
@@ -102,6 +103,7 @@
|
|
|
102
103
|
"@deepseek-ai/dsh-client-ui-sidebar": "0.2.2-rc.1",
|
|
103
104
|
"@deepseek-ai/dsh-client-ui-slots": "0.2.2-rc.1",
|
|
104
105
|
"@deepseek-ai/dsh-host-webserver": "0.2.2-rc.1",
|
|
106
|
+
"@deepseek-ai/dsh-llm": "0.2.2-rc.1",
|
|
105
107
|
"@deepseek-ai/dsh-session": "0.2.2-rc.1",
|
|
106
108
|
"@deepseek-ai/dsh-working-directory": "0.2.2-rc.1",
|
|
107
109
|
"@types/jsdom": "^28.0.3",
|
|
@@ -29,7 +29,11 @@ export function toggleActivityHidden(hidden: readonly string[], id: string): str
|
|
|
29
29
|
return hidden.includes(id) ? hidden.filter(value => value !== id) : [...hidden, id]
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* VS Code-style activity bar: every view can be dragged into a new slot while `fixed` actions stay pinned on
|
|
34
|
+
* top and `bottom` actions stay pinned at the foot. Reordering is computed from pointer position so it keeps
|
|
35
|
+
* working when the DOM shifts under a fast drag.
|
|
36
|
+
*/
|
|
33
37
|
export function ActivityBar({ views, fixed, bottom = [], order, hidden, onOrderChange, onHiddenChange }: {
|
|
34
38
|
views: ActivityView[]
|
|
35
39
|
fixed: ActivityView[]
|
|
@@ -41,12 +45,18 @@ export function ActivityBar({ views, fixed, bottom = [], order, hidden, onOrderC
|
|
|
41
45
|
}) {
|
|
42
46
|
const [menu, setMenu] = useState<{ x: number; y: number; target?: string }>()
|
|
43
47
|
const drag = useRef<{ id: string; x: number; y: number; active: boolean }>()
|
|
48
|
+
const draggingRef = useRef(false)
|
|
44
49
|
const nodes = useRef(new Map<string, HTMLButtonElement>())
|
|
45
50
|
const flip = useRef<Map<string, number>>()
|
|
46
|
-
const
|
|
51
|
+
const didDrag = useRef(false)
|
|
47
52
|
const [draggingId, setDraggingId] = useState<string>()
|
|
48
53
|
const [ghost, setGhost] = useState<{ x: number; y: number }>()
|
|
49
54
|
const [preview, setPreview] = useState<string[]>()
|
|
55
|
+
const previewRef = useRef<string[]>()
|
|
56
|
+
const orderRef = useRef(order)
|
|
57
|
+
const visibleRef = useRef<ActivityView[]>([])
|
|
58
|
+
previewRef.current = preview
|
|
59
|
+
orderRef.current = order
|
|
50
60
|
|
|
51
61
|
useEffect(() => {
|
|
52
62
|
if (menu === undefined) return
|
|
@@ -62,6 +72,8 @@ export function ActivityBar({ views, fixed, bottom = [], order, hidden, onOrderC
|
|
|
62
72
|
const before = flip.current
|
|
63
73
|
flip.current = undefined
|
|
64
74
|
if (before === undefined) return
|
|
75
|
+
// Reordering mid-drag must not animate: transformed rects would feed back into the next hit test.
|
|
76
|
+
if (draggingRef.current) return
|
|
65
77
|
for (const [id, element] of nodes.current) {
|
|
66
78
|
const previous = before.get(id)
|
|
67
79
|
if (previous === undefined) continue
|
|
@@ -72,49 +84,77 @@ export function ActivityBar({ views, fixed, bottom = [], order, hidden, onOrderC
|
|
|
72
84
|
})
|
|
73
85
|
|
|
74
86
|
const visible = views.filter(view => !hidden.includes(view.id))
|
|
87
|
+
visibleRef.current = visible
|
|
75
88
|
const currentOrder = preview ?? order
|
|
76
89
|
const ordered = currentOrder.flatMap(id => visible.filter(view => view.id === id))
|
|
77
90
|
const toggled = (id: string) => onHiddenChange(toggleActivityHidden(hidden, id))
|
|
78
91
|
const target = menu?.target === undefined ? undefined : views.find(view => view.id === menu.target)
|
|
79
92
|
|
|
80
93
|
const capturePositions = () => { flip.current = new Map([...nodes.current].map(([id, element]) => [id, element.getBoundingClientRect().top])) }
|
|
81
|
-
const
|
|
82
|
-
if (event.button !== 0) return
|
|
83
|
-
drag.current = { id, x: event.clientX, y: event.clientY, active: false }
|
|
84
|
-
event.currentTarget.setPointerCapture?.(event.pointerId)
|
|
85
|
-
}
|
|
86
|
-
const moveDrag = (event: ReactPointerEvent<HTMLButtonElement>, id: string) => {
|
|
94
|
+
const moveDrag = (clientX: number, clientY: number) => {
|
|
87
95
|
const state = drag.current
|
|
88
|
-
if (state === undefined
|
|
96
|
+
if (state === undefined) return
|
|
89
97
|
if (!state.active) {
|
|
90
|
-
if (Math.abs(
|
|
91
|
-
state.active = true
|
|
98
|
+
if (Math.abs(clientX - state.x) + Math.abs(clientY - state.y) < 5) return
|
|
99
|
+
state.active = true
|
|
100
|
+
draggingRef.current = true
|
|
101
|
+
setDraggingId(state.id)
|
|
92
102
|
}
|
|
93
|
-
setGhost({ x:
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
setGhost({ x: clientX, y: clientY })
|
|
104
|
+
const base = previewRef.current ?? orderRef.current
|
|
105
|
+
const visibleIds = new Set(visibleRef.current.map(view => view.id))
|
|
106
|
+
const slots = base.filter(id => visibleIds.has(id) && id !== state.id)
|
|
107
|
+
let index = 0
|
|
108
|
+
for (const id of slots) {
|
|
109
|
+
const element = nodes.current.get(id)
|
|
110
|
+
if (element === undefined) continue
|
|
111
|
+
const rect = element.getBoundingClientRect()
|
|
112
|
+
if (clientY > rect.top + rect.height / 2) index += 1
|
|
113
|
+
}
|
|
114
|
+
const next = moveActivity(base, state.id, slots[index])
|
|
115
|
+
if (next.join('|') === base.join('|')) return
|
|
104
116
|
capturePositions()
|
|
117
|
+
previewRef.current = next
|
|
105
118
|
setPreview(next)
|
|
106
119
|
}
|
|
107
|
-
const endDrag = (
|
|
108
|
-
const state = drag.current
|
|
120
|
+
const endDrag = () => {
|
|
121
|
+
const state = drag.current
|
|
122
|
+
drag.current = undefined
|
|
109
123
|
if (state?.active) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (
|
|
124
|
+
didDrag.current = true
|
|
125
|
+
const next = previewRef.current
|
|
126
|
+
if (next !== undefined) onOrderChange(next)
|
|
113
127
|
}
|
|
114
|
-
|
|
128
|
+
draggingRef.current = false
|
|
129
|
+
previewRef.current = undefined
|
|
130
|
+
setDraggingId(undefined); setGhost(undefined); setPreview(undefined)
|
|
131
|
+
}
|
|
132
|
+
const cancelDrag = () => {
|
|
133
|
+
drag.current = undefined
|
|
134
|
+
draggingRef.current = false
|
|
135
|
+
previewRef.current = undefined
|
|
115
136
|
setDraggingId(undefined); setGhost(undefined); setPreview(undefined)
|
|
116
137
|
}
|
|
117
|
-
const
|
|
138
|
+
const handlers = useRef({ moveDrag, endDrag, cancelDrag })
|
|
139
|
+
handlers.current = { moveDrag, endDrag, cancelDrag }
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
const move = (event: PointerEvent) => handlers.current.moveDrag(event.clientX, event.clientY)
|
|
142
|
+
const up = () => handlers.current.endDrag()
|
|
143
|
+
const cancel = () => handlers.current.cancelDrag()
|
|
144
|
+
window.addEventListener('pointermove', move)
|
|
145
|
+
window.addEventListener('pointerup', up)
|
|
146
|
+
window.addEventListener('pointercancel', cancel)
|
|
147
|
+
return () => {
|
|
148
|
+
window.removeEventListener('pointermove', move)
|
|
149
|
+
window.removeEventListener('pointerup', up)
|
|
150
|
+
window.removeEventListener('pointercancel', cancel)
|
|
151
|
+
}
|
|
152
|
+
}, [])
|
|
153
|
+
const beginDrag = (event: ReactPointerEvent<HTMLButtonElement>, id: string) => {
|
|
154
|
+
if (event.button !== 0) return
|
|
155
|
+
didDrag.current = false
|
|
156
|
+
drag.current = { id, x: event.clientX, y: event.clientY, active: false }
|
|
157
|
+
}
|
|
118
158
|
const renderView = (view: ActivityView) => <button
|
|
119
159
|
key={view.id}
|
|
120
160
|
ref={element => { if (element === null) nodes.current.delete(view.id); else nodes.current.set(view.id, element) }}
|
|
@@ -125,17 +165,14 @@ export function ActivityBar({ views, fixed, bottom = [], order, hidden, onOrderC
|
|
|
125
165
|
disabled={view.disabled}
|
|
126
166
|
title={view.label}
|
|
127
167
|
onPointerDown={event => beginDrag(event, view.id)}
|
|
128
|
-
|
|
129
|
-
onPointerUp={event => endDrag(event)}
|
|
130
|
-
onPointerCancel={cancelDrag}
|
|
131
|
-
onClick={() => { if (!suppressClick.current) view.onActivate() }}
|
|
168
|
+
onClick={() => { if (didDrag.current) { didDrag.current = false; return } view.onActivate() }}
|
|
132
169
|
onContextMenu={event => { event.preventDefault(); event.stopPropagation(); setMenu({ x: event.clientX, y: event.clientY, target: view.id }) }}
|
|
133
170
|
>{view.icon}</button>
|
|
134
171
|
|
|
135
172
|
return <>
|
|
136
173
|
<nav className="dvw-activity" onContextMenu={event => { event.preventDefault(); setMenu({ x: event.clientX, y: event.clientY }) }}>
|
|
137
|
-
{ordered.length === 0 ? <span className="dvw-activity-empty" aria-hidden="true"/> : ordered.map(renderView)}
|
|
138
174
|
{fixed.map(view => <button key={view.id} className="dvw-act" data-act-id={view.id} data-active={view.active} disabled={view.disabled} title={view.label} onClick={view.onActivate}>{view.icon}</button>)}
|
|
175
|
+
{ordered.length === 0 ? <span className="dvw-activity-empty" aria-hidden="true"/> : ordered.map(renderView)}
|
|
139
176
|
<div className="dvw-activity-bottom">{bottom.map(view => <button key={view.id} className="dvw-act" data-act-id={view.id} data-active={view.active} disabled={view.disabled} title={view.label} onClick={view.onActivate}>{view.icon}</button>)}</div>
|
|
140
177
|
</nav>
|
|
141
178
|
{draggingId !== undefined && ghost !== undefined && <div className="dvw-activity-ghost" style={{ left: ghost.x, top: ghost.y }} aria-hidden="true">{views.find(view => view.id === draggingId)?.icon}</div>}
|
|
@@ -46,7 +46,7 @@ export function DshConversationPanel({ visible, width, statusHeight }: { visible
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/** Temporarily align the official DSH panel palette with a fixed workbench palette. */
|
|
49
|
-
export function DshConversationTheme({ visible, colorScheme, followDsh }: { visible: boolean; colorScheme: 'dark' | 'light'; followDsh: boolean }) {
|
|
49
|
+
export function DshConversationTheme({ visible, colorScheme, followDsh }: { visible: boolean; colorScheme: 'dark' | 'light' | 'white' | 'eyecare'; followDsh: boolean }) {
|
|
50
50
|
useLayoutEffect(() => {
|
|
51
51
|
if (!visible) return
|
|
52
52
|
const body = document.body
|
|
@@ -85,8 +85,43 @@ const DARK_COLORS = {
|
|
|
85
85
|
'scrollbarSlider.hoverBackground': '#646464b3',
|
|
86
86
|
'scrollbarSlider.activeBackground': '#bfbfbf66',
|
|
87
87
|
}
|
|
88
|
+
/** Eye-care palette: the editor shares the workbench's warm yellow surface. */
|
|
89
|
+
export const EYE_THEME = 'dvw-eyecare'
|
|
90
|
+
const EYE_COLORS = {
|
|
91
|
+
'editor.background': '#f7ebc8',
|
|
92
|
+
'editor.foreground': '#3b3324',
|
|
93
|
+
'editorLineNumber.foreground': '#92856a',
|
|
94
|
+
'editorLineNumber.activeForeground': '#5c4a2a',
|
|
95
|
+
'editor.selectionBackground': '#e6d294',
|
|
96
|
+
'editor.inactiveSelectionBackground': '#ecdcae',
|
|
97
|
+
'editor.lineHighlightBackground': '#efe1b4',
|
|
98
|
+
'editorCursor.foreground': '#6b5320',
|
|
99
|
+
'editorWidget.background': '#f7ebc8',
|
|
100
|
+
'editorWidget.border': '#e0cf9f',
|
|
101
|
+
'editorSuggestWidget.background': '#f7ebc8',
|
|
102
|
+
'editorSuggestWidget.border': '#e0cf9f',
|
|
103
|
+
'editorSuggestWidget.selectedBackground': '#f0e0ae',
|
|
104
|
+
'editorHoverWidget.background': '#f7ebc8',
|
|
105
|
+
'editorHoverWidget.border': '#e0cf9f',
|
|
106
|
+
'editorGutter.background': '#f7ebc8',
|
|
107
|
+
'diffEditor.insertedTextBackground': '#b7d7a8aa',
|
|
108
|
+
'diffEditor.removedTextBackground': '#e8b4ad88',
|
|
109
|
+
'diffEditor.border': '#e0cf9f',
|
|
110
|
+
'diffEditor.diagonalFill': '#eadcb6',
|
|
111
|
+
'scrollbarSlider.background': '#92856a55',
|
|
112
|
+
'scrollbarSlider.hoverBackground': '#92856a88',
|
|
113
|
+
'scrollbarSlider.activeBackground': '#5c4a2a66',
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export type MonacoColorScheme = 'dark' | 'light' | 'white' | 'eyecare'
|
|
117
|
+
|
|
118
|
+
export function monacoThemeFor(theme: MonacoColorScheme): string {
|
|
119
|
+
return theme === 'dark' ? DARK_THEME : theme === 'eyecare' ? EYE_THEME : 'vs'
|
|
120
|
+
}
|
|
121
|
+
|
|
88
122
|
function defineWorkbenchThemes(monaco: Monaco): void {
|
|
89
123
|
monaco.editor.defineTheme(DARK_THEME, { base: 'vs-dark', inherit: true, rules: DARK_RULES, colors: DARK_COLORS })
|
|
124
|
+
monaco.editor.defineTheme(EYE_THEME, { base: 'vs', inherit: true, rules: [], colors: EYE_COLORS })
|
|
90
125
|
}
|
|
91
126
|
|
|
92
127
|
/**
|
|
@@ -119,13 +154,13 @@ export function loadMonaco(): Promise<Monaco> {
|
|
|
119
154
|
return loading
|
|
120
155
|
}
|
|
121
156
|
|
|
122
|
-
export function MonacoDiffEditor(props: { uri: string; original: string; modified: string; mode: 'inline' | 'split'; theme:
|
|
157
|
+
export function MonacoDiffEditor(props: { uri: string; original: string; modified: string; mode: 'inline' | 'split'; theme: MonacoColorScheme; fontSize?: number }) {
|
|
123
158
|
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>()
|
|
124
|
-
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
|
|
159
|
+
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',useInlineViewWhenSpaceIsLimited:false,theme:monacoThemeFor(props.theme),fontSize:props.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])
|
|
125
160
|
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])
|
|
126
|
-
useEffect(()=>{editor.current
|
|
161
|
+
useEffect(()=>{const value=editor.current;if(value===undefined)return;value.updateOptions({renderSideBySide:props.mode==='split'});const frame=requestAnimationFrame(()=>value.layout());return()=>cancelAnimationFrame(frame)},[props.mode])
|
|
127
162
|
useEffect(()=>{editor.current?.updateOptions({fontSize:props.fontSize??14})},[props.fontSize])
|
|
128
|
-
useEffect(()=>{if(window.monaco!==undefined)window.monaco.editor.setTheme(props.theme
|
|
163
|
+
useEffect(()=>{if(window.monaco!==undefined)window.monaco.editor.setTheme(monacoThemeFor(props.theme))},[props.theme])
|
|
129
164
|
return error===undefined?<div className="dvw-diff dvw-monaco-diff" data-mode={props.mode} data-render-side-by-side={props.mode==='split'} ref={root}/>:<div className="dvw-error">{error}</div>
|
|
130
165
|
}
|
|
131
166
|
|
|
@@ -166,7 +201,7 @@ function editorSelection(instance: import('monaco-editor').editor.IStandaloneCod
|
|
|
166
201
|
return { text: model.getValueInRange(range), startLine: range.startLineNumber, endLine }
|
|
167
202
|
}
|
|
168
203
|
|
|
169
|
-
export function MonacoEditor(props: { uri: string; value: string; theme:
|
|
204
|
+
export function MonacoEditor(props: { uri: string; value: string; theme: MonacoColorScheme; line?: number; column?: number; fontSize?: number; highlight?: string; onChange(value: string): void; onSave(): void; onAddToDsh?(selection?: EditorSelection): void }) {
|
|
170
205
|
const root = useRef<HTMLDivElement>(null)
|
|
171
206
|
const editor = useRef<import('monaco-editor').editor.IStandaloneCodeEditor>()
|
|
172
207
|
const decorations = useRef<import('monaco-editor').editor.IEditorDecorationsCollection>()
|
|
@@ -215,7 +250,7 @@ export function MonacoEditor(props: { uri: string; value: string; theme: 'dark'
|
|
|
215
250
|
void loadMonaco().then((monaco) => {
|
|
216
251
|
if (disposed || root.current === null) return
|
|
217
252
|
const props = latest.current
|
|
218
|
-
const instance = monaco.editor.create(root.current, { model: null, automaticLayout: true, theme: props.theme
|
|
253
|
+
const instance = monaco.editor.create(root.current, { model: null, automaticLayout: true, theme: monacoThemeFor(props.theme), fontSize: props.fontSize ?? 14, ...WORKBENCH_EDITOR_CHROME, wordWrap: 'off' })
|
|
219
254
|
editor.current = instance
|
|
220
255
|
instance.onDidChangeModelContent(() => latest.current.onChange(instance.getValue()))
|
|
221
256
|
instance.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => latest.current.onSave())
|
|
@@ -268,7 +303,7 @@ export function MonacoEditor(props: { uri: string; value: string; theme: 'dark'
|
|
|
268
303
|
useEffect(() => {
|
|
269
304
|
const model = editor.current?.getModel(); if (model !== null && model !== undefined && model.getValue() !== props.value) model.setValue(props.value)
|
|
270
305
|
}, [props.value])
|
|
271
|
-
useEffect(() => { if (window.monaco !== undefined) window.monaco.editor.setTheme(props.theme
|
|
306
|
+
useEffect(() => { if (window.monaco !== undefined) window.monaco.editor.setTheme(monacoThemeFor(props.theme)) }, [props.theme])
|
|
272
307
|
useEffect(() => { editor.current?.updateOptions({ fontSize: props.fontSize ?? 14 }) }, [props.fontSize])
|
|
273
308
|
useEffect(() => {
|
|
274
309
|
const instance = editor.current; const monaco = window.monaco; const model = instance?.getModel()
|
|
@@ -7,9 +7,9 @@ import { isTerminalProtocolReply, TerminalWorkspaceController } from './terminal
|
|
|
7
7
|
|
|
8
8
|
// VS Code Light+ ANSI palette: the default xterm colors (e.g. yellow #e5e510) vanish on a light background.
|
|
9
9
|
const LIGHT_ANSI = { black: '#000000', red: '#cd3131', green: '#107c10', yellow: '#949800', blue: '#0451a5', magenta: '#bc05bc', cyan: '#0598bc', white: '#555555', brightBlack: '#666666', brightRed: '#cd3131', brightGreen: '#14ce14', brightYellow: '#b5ba00', brightBlue: '#0451a5', brightMagenta: '#bc05bc', brightCyan: '#0598bc', brightWhite: '#a5a5a5' }
|
|
10
|
-
function terminalTheme(colorScheme: 'dark' | 'light') { return colorScheme === 'dark' ? { background: '#191a1b', foreground: '#bfbfbf', cursor: '#bfbfbf', selectionBackground: '#264f78' } : { background: '#ffffff', foreground: '#1d2025', cursor: '#4b515b', selectionBackground: '#add6ff', ...LIGHT_ANSI } }
|
|
10
|
+
function terminalTheme(colorScheme: 'dark' | 'light' | 'white' | 'eyecare') { return colorScheme === 'dark' ? { background: '#191a1b', foreground: '#bfbfbf', cursor: '#bfbfbf', selectionBackground: '#264f78' } : { background: '#ffffff', foreground: '#1d2025', cursor: '#4b515b', selectionBackground: '#add6ff', ...LIGHT_ANSI } }
|
|
11
11
|
|
|
12
|
-
function XtermView({ workspace, id, colorScheme }: { workspace: TerminalWorkspaceController; id: string; colorScheme: 'dark' | 'light' }) {
|
|
12
|
+
function XtermView({ workspace, id, colorScheme }: { workspace: TerminalWorkspaceController; id: string; colorScheme: 'dark' | 'light' | 'white' | 'eyecare' }) {
|
|
13
13
|
const root = useRef<HTMLDivElement>(null)
|
|
14
14
|
const terminalRef = useRef<Terminal>()
|
|
15
15
|
const replaying = useRef(false)
|