dsh-vscode-workbench 0.2.1 → 0.2.4

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.
@@ -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
- /** VS Code-style activity bar: pointer drag with a floating ghost and live reordering, right-click hide. */
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: 'dark' | 'light';
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: 'dark' | 'light';
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").JSX.Element;
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[];
@@ -4,7 +4,7 @@ export interface BottomPanelProps {
4
4
  sessionId: string;
5
5
  cwd: string;
6
6
  visible: boolean;
7
- colorScheme: 'dark' | 'light';
7
+ colorScheme: 'dark' | 'light' | 'white' | 'eyecare';
8
8
  }
9
9
  export interface BottomPanelContribution {
10
10
  id: string;
@@ -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>;
@@ -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(): {
@@ -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.1",
3
+ "version": "0.2.4",
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
- /** VS Code-style activity bar: pointer drag with a floating ghost and live reordering, right-click hide. */
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 suppressClick = useRef(false)
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 beginDrag = (event: ReactPointerEvent<HTMLButtonElement>, id: string) => {
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 || state.id !== id) return
96
+ if (state === undefined) return
89
97
  if (!state.active) {
90
- if (Math.abs(event.clientX - state.x) + Math.abs(event.clientY - state.y) < 5) return
91
- state.active = true; setDraggingId(id)
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: event.clientX, y: event.clientY })
94
- const element = (document.elementFromPoint(event.clientX, event.clientY) as HTMLElement | null)?.closest<HTMLElement>('[data-act-id]')
95
- if (element === null || element === undefined) return
96
- const targetId = element.dataset.actId
97
- if (targetId === undefined || targetId === id || !visible.some(view => view.id === targetId)) return
98
- const rect = element.getBoundingClientRect()
99
- const ids = currentOrder.filter(value => visible.some(view => view.id === value))
100
- const index = ids.indexOf(targetId)
101
- if (index < 0) return
102
- const next = moveActivity(currentOrder, id, event.clientY > rect.top + rect.height / 2 ? ids[index + 1] : targetId)
103
- if (next.join('|') === currentOrder.join('|')) return
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 = (event: ReactPointerEvent<HTMLButtonElement>) => {
108
- const state = drag.current; drag.current = undefined
120
+ const endDrag = () => {
121
+ const state = drag.current
122
+ drag.current = undefined
109
123
  if (state?.active) {
110
- suppressClick.current = true
111
- window.setTimeout(() => { suppressClick.current = false }, 0)
112
- if (preview !== undefined) onOrderChange(preview)
124
+ didDrag.current = true
125
+ const next = previewRef.current
126
+ if (next !== undefined) onOrderChange(next)
113
127
  }
114
- event.currentTarget.releasePointerCapture?.(event.pointerId)
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 cancelDrag = () => { drag.current = undefined; setDraggingId(undefined); setGhost(undefined); setPreview(undefined) }
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
- onPointerMove={event => moveDrag(event, view.id)}
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: 'dark' | 'light'; fontSize?: number }) {
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==='dark'?DARK_THEME:'vs',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])
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?.updateOptions({renderSideBySide:props.mode==='split'})},[props.mode])
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==='dark'?DARK_THEME:'vs')},[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: 'dark' | 'light'; line?: number; column?: number; fontSize?: number; highlight?: string; onChange(value: string): void; onSave(): void; onAddToDsh?(selection?: EditorSelection): void }) {
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 === 'dark' ? DARK_THEME : 'vs', fontSize: props.fontSize ?? 14, ...WORKBENCH_EDITOR_CHROME, wordWrap: 'off' })
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 === 'dark' ? DARK_THEME : 'vs') }, [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)