dsh-vscode-workbench 0.2.0 → 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.
@@ -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;
@@ -1,3 +1,6 @@
1
+ /** Bundled DSH fish logo — self-contained, no dependence on host DOM. */
2
+ export declare const DSH_MARK_VIEWBOX = "0 0 23.16 17.04";
3
+ export declare const DSH_MARK_PATH = "M22.9168 1.43018C22.6713 1.31018 22.5658 1.53918 22.4223 1.65519C22.3733 1.69269 22.3318 1.74169 22.2903 1.78669C21.9317 2.1697 21.5127 2.42121 20.9657 2.39121C20.1657 2.34621 19.4827 2.59771 18.8787 3.20973C18.7502 2.45521 18.3236 2.0047 17.6746 1.71569C17.3351 1.56568 16.9916 1.41518 16.7536 1.08867C16.5876 0.856163 16.5421 0.597155 16.4591 0.341647C16.4061 0.187643 16.3536 0.0301382 16.1761 0.00363739C15.9836 -0.0263635 15.9081 0.135141 15.8326 0.270145C15.5306 0.822162 15.4136 1.43018 15.4251 2.0462C15.4516 3.43174 16.0366 4.53527 17.1991 5.3203C17.3311 5.4103 17.3651 5.5003 17.3236 5.63181C17.2441 5.90231 17.1501 6.16482 17.0671 6.43533C17.0141 6.60784 16.9351 6.64584 16.7501 6.57033C16.1121 6.30383 15.5611 5.90931 15.074 5.4328C14.2475 4.63328 13.5 3.75075 12.568 3.05973C12.349 2.89822 12.13 2.74822 11.9034 2.60522C10.9524 1.68169 12.028 0.923165 12.277 0.833162C12.5375 0.739159 12.3675 0.41615 11.5259 0.42015C10.6844 0.42365 9.91439 0.705658 8.93286 1.08117C8.78935 1.13767 8.63835 1.17867 8.48384 1.21267C7.59332 1.04367 6.66829 1.00617 5.70226 1.11517C3.88321 1.31768 2.43016 2.1777 1.36213 3.64575C0.0790928 5.4103 -0.222916 7.41536 0.146595 9.50642C0.535106 11.7105 1.66014 13.535 3.38869 14.9616C5.18125 16.4406 7.24581 17.1657 9.60138 17.0266C11.0319 16.9441 12.6245 16.7526 14.421 15.2321C14.874 15.4576 15.3496 15.5476 16.1381 15.6151C16.7456 15.6716 17.3306 15.5851 17.7836 15.4911C18.4931 15.3411 18.4441 14.6841 18.1876 14.5636C16.1081 13.595 16.5646 13.9891 16.1496 13.67C17.2061 12.42 18.8202 10.1979 19.3182 7.17235C19.3672 6.83834 19.4297 6.36783 19.4222 6.09732C19.4182 5.93231 19.4562 5.86831 19.6447 5.84931C20.1657 5.78931 20.6712 5.64681 21.1357 5.3913C22.4833 4.65528 23.0268 3.44624 23.1548 1.9972C23.1738 1.77569 23.1508 1.54668 22.9168 1.43018ZM11.1749 14.4736C9.15936 12.889 8.18184 12.3675 7.77832 12.39C7.40081 12.4125 7.46881 12.8445 7.55182 13.126C7.63882 13.404 7.75182 13.5955 7.91033 13.8396C8.01983 14.0011 8.09533 14.2411 7.80083 14.4216C7.15181 14.8231 6.02327 14.2866 5.97027 14.2601C4.65673 13.4865 3.5587 12.4655 2.78467 11.069C2.03715 9.72493 1.60314 8.28289 1.53164 6.74384C1.51264 6.37233 1.62214 6.24082 1.99215 6.17332C2.47916 6.08332 2.98118 6.06432 3.46769 6.13582C5.52476 6.43633 7.27581 7.35586 8.74385 8.8129C9.58188 9.64243 10.2159 10.634 10.8689 11.6025C11.5634 12.631 12.3105 13.611 13.262 14.4146C13.598 14.6961 13.866 14.9101 14.1225 15.0681C13.349 15.1546 12.058 15.1731 11.1749 14.4746L11.1749 14.4736ZM12.141 8.25988C12.141 8.09488 12.273 7.96338 12.439 7.96338C12.4765 7.96338 12.5105 7.97088 12.541 7.98188C12.5825 7.99688 12.6205 8.01938 12.6505 8.05338C12.7035 8.10588 12.7335 8.18088 12.7335 8.25988C12.7335 8.42489 12.6015 8.55639 12.4355 8.55639C12.2695 8.55639 12.141 8.42489 12.141 8.25988ZM15.1415 9.79893C14.949 9.87793 14.7565 9.94544 14.5715 9.95294C14.2845 9.96794 13.9715 9.85143 13.8015 9.70893C13.5375 9.48742 13.3485 9.36342 13.2695 8.97691C13.2355 8.8119 13.2545 8.55639 13.2845 8.40989C13.3525 8.09438 13.277 7.89187 13.0545 7.70787C12.8735 7.55786 12.643 7.51636 12.39 7.51636C12.2955 7.51636 12.209 7.47486 12.1445 7.44136C12.039 7.38886 11.9519 7.25735 12.035 7.09585C12.0615 7.04335 12.19 6.91584 12.22 6.89334C12.5635 6.69784 12.9595 6.76184 13.326 6.90834C13.6655 7.04735 13.9225 7.30236 14.292 7.66287C14.6695 8.09838 14.7375 8.21838 14.9525 8.54539C15.1225 8.8009 15.277 9.06341 15.3831 9.36392C15.4471 9.55142 15.3641 9.70493 15.1415 9.79893Z";
1
4
  export declare function DshMark({ size }: {
2
5
  size?: number;
3
6
  }): import("react").JSX.Element;
@@ -42,8 +42,24 @@ export declare const WORKBENCH_EDITOR_CHROME: {
42
42
  readonly horizontalScrollbarSize: 10;
43
43
  };
44
44
  };
45
+ /** macOS reports Control-click as a secondary click, so Monaco only navigates with Cmd-click there. */
46
+ export declare function isMacPlatform(userAgent?: string): boolean;
47
+ /**
48
+ * Whether a mouse press should jump to the definition. On macOS, Control-click is a common
49
+ * Windows/Linux habit but arrives as a context-menu gesture (sometimes as a right-button
50
+ * press); treat it as Cmd-click instead.
51
+ */
52
+ export declare function isMacDefinitionClick(event: {
53
+ button: number;
54
+ ctrlKey: boolean;
55
+ metaKey: boolean;
56
+ }): boolean;
45
57
  /** Monaco theme aligned with the workbench dark palette. */
46
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;
47
63
  /**
48
64
  * Bootstrap source for Monaco's AMD web workers. Workers resolve module paths against their own
49
65
  * blob URL, so they need an absolute `baseUrl`; without it the TypeScript language worker fails to load.
@@ -55,7 +71,7 @@ export declare function MonacoDiffEditor(props: {
55
71
  original: string;
56
72
  modified: string;
57
73
  mode: 'inline' | 'split';
58
- theme: 'dark' | 'light';
74
+ theme: MonacoColorScheme;
59
75
  fontSize?: number;
60
76
  }): import("react").JSX.Element;
61
77
  /** Start downloading Monaco in the background so the first opened file does not wait for it. */
@@ -65,7 +81,7 @@ export declare function retainEditorModels(uris: string[]): void;
65
81
  export declare function MonacoEditor(props: {
66
82
  uri: string;
67
83
  value: string;
68
- theme: 'dark' | 'light';
84
+ theme: MonacoColorScheme;
69
85
  line?: number;
70
86
  column?: number;
71
87
  fontSize?: number;
@@ -20,9 +20,18 @@ 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;
26
+ export declare const DSH_CHAT_MIN_WIDTH = 440;
27
+ export declare const DSH_CHAT_MAX_WIDTH = 720;
28
+ export declare const DSH_CHAT_DEFAULT_WIDTH = 440;
29
+ /** Keep persisted or dragged widths inside the range the DSH composer can lay out cleanly. */
30
+ export declare function clampChatWidth(width: number): number;
24
31
  /** Calculate a panel width from a horizontal pointer movement. */
25
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;
26
35
  /** Restore tab headers without reading their file contents. */
27
36
  export declare function restoreTabPaths(paths: string[]): Tab[];
28
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;
@@ -5,3 +5,5 @@ export declare class ApiError extends Error {
5
5
  /** Back off while DSH publishes the current session after startup; every view shares this retry. */
6
6
  export declare function sessionRetryDelay(error: unknown, attempt: number): number | undefined;
7
7
  export declare function api<T>(method: string, body: Record<string, unknown>, signal?: AbortSignal): Promise<T>;
8
+ /** Retry an operation while DSH is still publishing the selected session. */
9
+ export declare function withSessionRetry<T>(operation: () => Promise<T>, signal?: AbortSignal): Promise<T>;
@@ -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;
@@ -17,7 +17,7 @@ export interface TerminalWorkspaceSnapshot {
17
17
  }
18
18
  /** Browser client for the workbench's local terminal transport. */
19
19
  export interface TerminalWorkspace {
20
- refresh(): Promise<void>;
20
+ refresh(): Promise<boolean>;
21
21
  create(title?: string): Promise<TerminalInfo>;
22
22
  close(id: string): Promise<void>;
23
23
  rename(id: string, title: string): Promise<void>;
@@ -33,7 +33,7 @@ export declare class TerminalWorkspaceController implements TerminalWorkspace {
33
33
  private snapshot;
34
34
  private readonly listeners;
35
35
  constructor(sessionId: string);
36
- refresh(): Promise<void>;
36
+ refresh(): Promise<boolean>;
37
37
  create(title?: string): Promise<TerminalInfo>;
38
38
  close(id: string): Promise<void>;
39
39
  rename(id: string, title: string): Promise<void>;
@@ -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.0",
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
- /** 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
@@ -1,11 +1,11 @@
1
1
  /** Bundled DSH fish logo — self-contained, no dependence on host DOM. */
2
- const WHALE_VIEWBOX = '0 0 23.16 17.04'
3
- const WHALE_PATH = 'M22.9168 1.43018C22.6713 1.31018 22.5658 1.53918 22.4223 1.65519C22.3733 1.69269 22.3318 1.74169 22.2903 1.78669C21.9317 2.1697 21.5127 2.42121 20.9657 2.39121C20.1657 2.34621 19.4827 2.59771 18.8787 3.20973C18.7502 2.45521 18.3236 2.0047 17.6746 1.71569C17.3351 1.56568 16.9916 1.41518 16.7536 1.08867C16.5876 0.856163 16.5421 0.597155 16.4591 0.341647C16.4061 0.187643 16.3536 0.0301382 16.1761 0.00363739C15.9836 -0.0263635 15.9081 0.135141 15.8326 0.270145C15.5306 0.822162 15.4136 1.43018 15.4251 2.0462C15.4516 3.43174 16.0366 4.53527 17.1991 5.3203C17.3311 5.4103 17.3651 5.5003 17.3236 5.63181C17.2441 5.90231 17.1501 6.16482 17.0671 6.43533C17.0141 6.60784 16.9351 6.64584 16.7501 6.57033C16.1121 6.30383 15.5611 5.90931 15.074 5.4328C14.2475 4.63328 13.5 3.75075 12.568 3.05973C12.349 2.89822 12.13 2.74822 11.9034 2.60522C10.9524 1.68169 12.028 0.923165 12.277 0.833162C12.5375 0.739159 12.3675 0.41615 11.5259 0.42015C10.6844 0.42365 9.91439 0.705658 8.93286 1.08117C8.78935 1.13767 8.63835 1.17867 8.48384 1.21267C7.59332 1.04367 6.66829 1.00617 5.70226 1.11517C3.88321 1.31768 2.43016 2.1777 1.36213 3.64575C0.0790928 5.4103 -0.222916 7.41536 0.146595 9.50642C0.535106 11.7105 1.66014 13.535 3.38869 14.9616C5.18125 16.4406 7.24581 17.1657 9.60138 17.0266C11.0319 16.9441 12.6245 16.7526 14.421 15.2321C14.874 15.4576 15.3496 15.5476 16.1381 15.6151C16.7456 15.6716 17.3306 15.5851 17.7836 15.4911C18.4931 15.3411 18.4441 14.6841 18.1876 14.5636C16.1081 13.595 16.5646 13.9891 16.1496 13.67C17.2061 12.42 18.8202 10.1979 19.3182 7.17235C19.3672 6.83834 19.4297 6.36783 19.4222 6.09732C19.4182 5.93231 19.4562 5.86831 19.6447 5.84931C20.1657 5.78931 20.6712 5.64681 21.1357 5.3913C22.4833 4.65528 23.0268 3.44624 23.1548 1.9972C23.1738 1.77569 23.1508 1.54668 22.9168 1.43018ZM11.1749 14.4736C9.15936 12.889 8.18184 12.3675 7.77832 12.39C7.40081 12.4125 7.46881 12.8445 7.55182 13.126C7.63882 13.404 7.75182 13.5955 7.91033 13.8396C8.01983 14.0011 8.09533 14.2411 7.80083 14.4216C7.15181 14.8231 6.02327 14.2866 5.97027 14.2601C4.65673 13.4865 3.5587 12.4655 2.78467 11.069C2.03715 9.72493 1.60314 8.28289 1.53164 6.74384C1.51264 6.37233 1.62214 6.24082 1.99215 6.17332C2.47916 6.08332 2.98118 6.06432 3.46769 6.13582C5.52476 6.43633 7.27581 7.35586 8.74385 8.8129C9.58188 9.64243 10.2159 10.634 10.8689 11.6025C11.5634 12.631 12.3105 13.611 13.262 14.4146C13.598 14.6961 13.866 14.9101 14.1225 15.0681C13.349 15.1546 12.058 15.1731 11.1749 14.4746L11.1749 14.4736ZM12.141 8.25988C12.141 8.09488 12.273 7.96338 12.439 7.96338C12.4765 7.96338 12.5105 7.97088 12.541 7.98188C12.5825 7.99688 12.6205 8.01938 12.6505 8.05338C12.7035 8.10588 12.7335 8.18088 12.7335 8.25988C12.7335 8.42489 12.6015 8.55639 12.4355 8.55639C12.2695 8.55639 12.141 8.42489 12.141 8.25988ZM15.1415 9.79893C14.949 9.87793 14.7565 9.94544 14.5715 9.95294C14.2845 9.96794 13.9715 9.85143 13.8015 9.70893C13.5375 9.48742 13.3485 9.36342 13.2695 8.97691C13.2355 8.8119 13.2545 8.55639 13.2845 8.40989C13.3525 8.09438 13.277 7.89187 13.0545 7.70787C12.8735 7.55786 12.643 7.51636 12.39 7.51636C12.2955 7.51636 12.209 7.47486 12.1445 7.44136C12.039 7.38886 11.9519 7.25735 12.035 7.09585C12.0615 7.04335 12.19 6.91584 12.22 6.89334C12.5635 6.69784 12.9595 6.76184 13.326 6.90834C13.6655 7.04735 13.9225 7.30236 14.292 7.66287C14.6695 8.09838 14.7375 8.21838 14.9525 8.54539C15.1225 8.8009 15.277 9.06341 15.3831 9.36392C15.4471 9.55142 15.3641 9.70493 15.1415 9.79893Z'
2
+ export const DSH_MARK_VIEWBOX = '0 0 23.16 17.04'
3
+ export const DSH_MARK_PATH = 'M22.9168 1.43018C22.6713 1.31018 22.5658 1.53918 22.4223 1.65519C22.3733 1.69269 22.3318 1.74169 22.2903 1.78669C21.9317 2.1697 21.5127 2.42121 20.9657 2.39121C20.1657 2.34621 19.4827 2.59771 18.8787 3.20973C18.7502 2.45521 18.3236 2.0047 17.6746 1.71569C17.3351 1.56568 16.9916 1.41518 16.7536 1.08867C16.5876 0.856163 16.5421 0.597155 16.4591 0.341647C16.4061 0.187643 16.3536 0.0301382 16.1761 0.00363739C15.9836 -0.0263635 15.9081 0.135141 15.8326 0.270145C15.5306 0.822162 15.4136 1.43018 15.4251 2.0462C15.4516 3.43174 16.0366 4.53527 17.1991 5.3203C17.3311 5.4103 17.3651 5.5003 17.3236 5.63181C17.2441 5.90231 17.1501 6.16482 17.0671 6.43533C17.0141 6.60784 16.9351 6.64584 16.7501 6.57033C16.1121 6.30383 15.5611 5.90931 15.074 5.4328C14.2475 4.63328 13.5 3.75075 12.568 3.05973C12.349 2.89822 12.13 2.74822 11.9034 2.60522C10.9524 1.68169 12.028 0.923165 12.277 0.833162C12.5375 0.739159 12.3675 0.41615 11.5259 0.42015C10.6844 0.42365 9.91439 0.705658 8.93286 1.08117C8.78935 1.13767 8.63835 1.17867 8.48384 1.21267C7.59332 1.04367 6.66829 1.00617 5.70226 1.11517C3.88321 1.31768 2.43016 2.1777 1.36213 3.64575C0.0790928 5.4103 -0.222916 7.41536 0.146595 9.50642C0.535106 11.7105 1.66014 13.535 3.38869 14.9616C5.18125 16.4406 7.24581 17.1657 9.60138 17.0266C11.0319 16.9441 12.6245 16.7526 14.421 15.2321C14.874 15.4576 15.3496 15.5476 16.1381 15.6151C16.7456 15.6716 17.3306 15.5851 17.7836 15.4911C18.4931 15.3411 18.4441 14.6841 18.1876 14.5636C16.1081 13.595 16.5646 13.9891 16.1496 13.67C17.2061 12.42 18.8202 10.1979 19.3182 7.17235C19.3672 6.83834 19.4297 6.36783 19.4222 6.09732C19.4182 5.93231 19.4562 5.86831 19.6447 5.84931C20.1657 5.78931 20.6712 5.64681 21.1357 5.3913C22.4833 4.65528 23.0268 3.44624 23.1548 1.9972C23.1738 1.77569 23.1508 1.54668 22.9168 1.43018ZM11.1749 14.4736C9.15936 12.889 8.18184 12.3675 7.77832 12.39C7.40081 12.4125 7.46881 12.8445 7.55182 13.126C7.63882 13.404 7.75182 13.5955 7.91033 13.8396C8.01983 14.0011 8.09533 14.2411 7.80083 14.4216C7.15181 14.8231 6.02327 14.2866 5.97027 14.2601C4.65673 13.4865 3.5587 12.4655 2.78467 11.069C2.03715 9.72493 1.60314 8.28289 1.53164 6.74384C1.51264 6.37233 1.62214 6.24082 1.99215 6.17332C2.47916 6.08332 2.98118 6.06432 3.46769 6.13582C5.52476 6.43633 7.27581 7.35586 8.74385 8.8129C9.58188 9.64243 10.2159 10.634 10.8689 11.6025C11.5634 12.631 12.3105 13.611 13.262 14.4146C13.598 14.6961 13.866 14.9101 14.1225 15.0681C13.349 15.1546 12.058 15.1731 11.1749 14.4746L11.1749 14.4736ZM12.141 8.25988C12.141 8.09488 12.273 7.96338 12.439 7.96338C12.4765 7.96338 12.5105 7.97088 12.541 7.98188C12.5825 7.99688 12.6205 8.01938 12.6505 8.05338C12.7035 8.10588 12.7335 8.18088 12.7335 8.25988C12.7335 8.42489 12.6015 8.55639 12.4355 8.55639C12.2695 8.55639 12.141 8.42489 12.141 8.25988ZM15.1415 9.79893C14.949 9.87793 14.7565 9.94544 14.5715 9.95294C14.2845 9.96794 13.9715 9.85143 13.8015 9.70893C13.5375 9.48742 13.3485 9.36342 13.2695 8.97691C13.2355 8.8119 13.2545 8.55639 13.2845 8.40989C13.3525 8.09438 13.277 7.89187 13.0545 7.70787C12.8735 7.55786 12.643 7.51636 12.39 7.51636C12.2955 7.51636 12.209 7.47486 12.1445 7.44136C12.039 7.38886 11.9519 7.25735 12.035 7.09585C12.0615 7.04335 12.19 6.91584 12.22 6.89334C12.5635 6.69784 12.9595 6.76184 13.326 6.90834C13.6655 7.04735 13.9225 7.30236 14.292 7.66287C14.6695 8.09838 14.7375 8.21838 14.9525 8.54539C15.1225 8.8009 15.277 9.06341 15.3831 9.36392C15.4471 9.55142 15.3641 9.70493 15.1415 9.79893Z'
4
4
 
5
5
  export function DshMark({ size = 18 }: { size?: number }) {
6
6
  return (
7
- <svg className="dvw-dsh-mark" width={size} height={(size * 17.04) / 23.16} viewBox={WHALE_VIEWBOX} fill="none" aria-hidden="true">
8
- <path d={WHALE_PATH} fill="currentColor" />
7
+ <svg className="dvw-dsh-mark" width={size} height={(size * 17.04) / 23.16} viewBox={DSH_MARK_VIEWBOX} fill="none" aria-hidden="true">
8
+ <path d={DSH_MARK_PATH} fill="currentColor" />
9
9
  </svg>
10
10
  )
11
11
  }