@fonlan/dsh-quick-commands 0.1.6 → 0.1.7

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/lib/client.js CHANGED
@@ -4,10 +4,124 @@ window.__ModuleLoader__.load({
4
4
  var module = { exports: {} };
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ //#region \0rolldown/runtime.js
8
+ var __create = Object.create;
9
+ var __defProp = Object.defineProperty;
10
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
11
+ var __getOwnPropNames = Object.getOwnPropertyNames;
12
+ var __getProtoOf = Object.getPrototypeOf;
13
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
14
+ var __copyProps = (to, from, except, desc) => {
15
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
16
+ key = keys[i];
17
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
18
+ get: ((k) => from[k]).bind(null, key),
19
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
20
+ });
21
+ }
22
+ return to;
23
+ };
24
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
25
+ value: mod,
26
+ enumerable: true
27
+ }) : target, mod));
28
+ //#endregion
7
29
  let react = require("react");
8
30
  let react_dom = require("react-dom");
9
31
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
32
+ _deepseek_ai_dsh_client_ui_primitives = __toESM(_deepseek_ai_dsh_client_ui_primitives, 1);
10
33
  let react_jsx_runtime = require("react/jsx-runtime");
34
+ //#region src/client/icons.tsx
35
+ const ICON_EXPORTS = _deepseek_ai_dsh_client_ui_primitives;
36
+ /** First export name that exists on the running host, else the bundled artwork. */
37
+ function pickIcon(exportNames, fallback) {
38
+ for (const name of exportNames) {
39
+ const candidate = ICON_EXPORTS[name];
40
+ if (typeof candidate === "function") return candidate;
41
+ }
42
+ return fallback;
43
+ }
44
+ /** Wrap one locally drawn 16×16 artwork as an icon component. */
45
+ function localIcon(children) {
46
+ return function LocalIcon({ size = 16, className }) {
47
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
48
+ width: size,
49
+ height: size,
50
+ viewBox: "0 0 16 16",
51
+ fill: "none",
52
+ className,
53
+ "aria-hidden": "true",
54
+ children
55
+ });
56
+ };
57
+ }
58
+ /** Run/play glyph for the header quick-run button. */
59
+ const PlayIcon = pickIcon([
60
+ "IconPlayOutlineRegular",
61
+ "IconPlayOutlineMedium",
62
+ "IconPlayOutline16"
63
+ ], localIcon(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
64
+ cx: "8",
65
+ cy: "8",
66
+ r: "6.5",
67
+ stroke: "currentColor"
68
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
69
+ d: "M6.6 5.9 10.4 8l-3.8 2.1Z",
70
+ fill: "currentColor"
71
+ })] })));
72
+ /** Dismiss glyph (menu header and popup close). */
73
+ const CloseIcon = pickIcon([
74
+ "IconCloseOutlineRegular",
75
+ "IconCloseOutlineMedium",
76
+ "IconCloseOutline16"
77
+ ], localIcon(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
78
+ d: "M4.2 4.2 11.8 11.8M11.8 4.2 4.2 11.8",
79
+ stroke: "currentColor",
80
+ strokeWidth: "1.2",
81
+ strokeLinecap: "round"
82
+ })));
83
+ /** Stop/kill glyph for a running command. */
84
+ const StopIcon = pickIcon([
85
+ "IconStopFillRegular",
86
+ "IconStopFillMedium",
87
+ "IconStopFill16"
88
+ ], localIcon(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
89
+ x: "4.6",
90
+ y: "4.6",
91
+ width: "6.8",
92
+ height: "6.8",
93
+ rx: "1.6",
94
+ fill: "currentColor"
95
+ })));
96
+ /** Add-row glyph in the settings page. */
97
+ const PlusIcon = pickIcon([
98
+ "IconPlusOutlineRegular",
99
+ "IconPlusOutlineMedium",
100
+ "IconPlusOutline16"
101
+ ], localIcon(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
102
+ d: "M8 3.4v9.2M3.4 8h9.2",
103
+ stroke: "currentColor",
104
+ strokeWidth: "1.2",
105
+ strokeLinecap: "round"
106
+ })));
107
+ /** Remove-command glyph in the settings page. */
108
+ const TrashIcon = pickIcon([
109
+ "IconTrashOutlineRegular",
110
+ "IconTrashOutlineMedium",
111
+ "IconTrashOutline16"
112
+ ], localIcon(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
113
+ d: "M3.4 4.4h9.2",
114
+ stroke: "currentColor",
115
+ strokeWidth: "1.2",
116
+ strokeLinecap: "round"
117
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
118
+ d: "M6.4 4.4V3.1h3.2v1.3M4.9 4.4l.6 8.5h5l.6-8.5",
119
+ stroke: "currentColor",
120
+ strokeWidth: "1.2",
121
+ strokeLinecap: "round",
122
+ strokeLinejoin: "round"
123
+ })] })));
124
+ //#endregion
11
125
  //#region src/client/api.ts
12
126
  var QuickCommandsApiError = class extends Error {
13
127
  code;
@@ -161,6 +275,17 @@ window.__ModuleLoader__.load({
161
275
  });
162
276
  }
163
277
  //#endregion
278
+ //#region src/shared/contract.ts
279
+ /**
280
+ * The document's anchor as a usable literal. Wire data is defensive by
281
+ * contract: an older host can still answer with schemastery's empty-object
282
+ * resolution (`{}`) for this field, which must never reach a CSS class name or
283
+ * a radio `checked` comparison.
284
+ */
285
+ function anchorOf(value) {
286
+ return value === "button" ? "button" : "corner";
287
+ }
288
+ //#endregion
164
289
  //#region src/client/header.tsx
165
290
  /**
166
291
  * Session-header quick-command surface: the ▶ play icon button (left of the
@@ -206,9 +331,9 @@ window.__ModuleLoader__.load({
206
331
  /** Resolve the current session's workspace from the standard kits. */
207
332
  function useCurrentWorkspace(sessionId, useSessions, useWorkspaces) {
208
333
  const row = useSessions?.((s) => s.byId[sessionId]);
209
- const workspaces = useWorkspaces?.((s) => s.items);
334
+ const items = useWorkspaces?.((s) => s.items) ?? [];
210
335
  return {
211
- workspace: matchWorkspace(row?.cwd, workspaces ?? []),
336
+ workspace: items.find((ws) => ws.sessionIds?.includes(sessionId) === true) ?? matchWorkspace(row?.cwd, items),
212
337
  cwd: row?.cwd
213
338
  };
214
339
  }
@@ -270,7 +395,7 @@ window.__ModuleLoader__.load({
270
395
  const own = entries.find((e) => e.workspaceId === workspace?.workspaceId);
271
396
  setCommands(own === void 0 ? [] : own.commands);
272
397
  setMenuRemoteHost(own?.remoteHost ?? null);
273
- setAnchor(settings.popupAnchor);
398
+ setAnchor(anchorOf(settings.popupAnchor));
274
399
  setPopupSize(settings.popupSize);
275
400
  setMenuLoadError(null);
276
401
  } catch (e) {
@@ -312,7 +437,7 @@ window.__ModuleLoader__.load({
312
437
  onClick: () => {
313
438
  popover.menuOpen ? closeMenu() : openMenu();
314
439
  },
315
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlayOutline16, { size: 15 })
440
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlayIcon, { size: 15 })
316
441
  }),
317
442
  popover.menuOpen && workspace !== void 0 && menuPosition !== null && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
318
443
  ref: menuRef,
@@ -345,7 +470,7 @@ window.__ModuleLoader__.load({
345
470
  className: "qc-menu-close",
346
471
  "aria-label": t("runClose"),
347
472
  onClick: closeMenu,
348
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutline16, { size: 12 })
473
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CloseIcon, { size: 12 })
349
474
  })
350
475
  ]
351
476
  }),
@@ -582,7 +707,7 @@ window.__ModuleLoader__.load({
582
707
  title: t("runKill"),
583
708
  "aria-label": t("runKill"),
584
709
  onClick: () => void kill(),
585
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconStopFill16, { size: 14 })
710
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StopIcon, { size: 14 })
586
711
  }),
587
712
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
588
713
  type: "button",
@@ -591,7 +716,7 @@ window.__ModuleLoader__.load({
591
716
  "aria-label": t("runClose"),
592
717
  onPointerUp: onClosePointerUp,
593
718
  onClick: closeOutput,
594
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutline16, { size: 14 })
719
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CloseIcon, { size: 14 })
595
720
  })
596
721
  ]
597
722
  }),
@@ -736,7 +861,7 @@ window.__ModuleLoader__.load({
736
861
  setState({
737
862
  loaded: true,
738
863
  entries,
739
- anchor: settings.popupAnchor
864
+ anchor: anchorOf(settings.popupAnchor)
740
865
  });
741
866
  setError(null);
742
867
  } catch (e) {
@@ -808,7 +933,7 @@ window.__ModuleLoader__.load({
808
933
  const next = await quickApi.settingsSetAnchor(anchor);
809
934
  setState((s) => ({
810
935
  ...s,
811
- anchor: next.popupAnchor
936
+ anchor: anchorOf(next.popupAnchor)
812
937
  }));
813
938
  } catch (e) {
814
939
  setError(e instanceof Error ? e.message : String(e));
@@ -914,7 +1039,7 @@ window.__ModuleLoader__.load({
914
1039
  "aria-label": t("cardRemove"),
915
1040
  title: t("cardRemove"),
916
1041
  onClick: () => removeCommand(entry.workspaceId, index),
917
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, { size: 14 })
1042
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrashIcon, { size: 14 })
918
1043
  })
919
1044
  ]
920
1045
  }, `${entry.workspaceId}-${index}`)),
@@ -922,7 +1047,7 @@ window.__ModuleLoader__.load({
922
1047
  type: "button",
923
1048
  className: "qc-settings-add-btn",
924
1049
  onClick: () => addCommand(entry.workspaceId),
925
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlusOutline16, { size: 14 }), t("cardAddCommand")]
1050
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlusIcon, { size: 14 }), t("cardAddCommand")]
926
1051
  })
927
1052
  ]
928
1053
  }, entry.workspaceId))
package/lib/client.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":["useSyncExternalStore","useState","useRef","IconPlayOutline16","createPortal","IconCloseOutline16","IconStopFill16","useState","useRef","IconTrashOutline16","IconPlusOutline16"],"sources":["../../../src/client/api.ts","../../../src/client/run-state.ts","../../../src/client/header.tsx","../../../src/client/settings-section.tsx","../../../src/client/locales.ts","../../../src/client/index.tsx"],"sourcesContent":["/**\n * Browser client for the plugin's fenced JSON API\n * (/plugins/@fonlan/dsh-quick-commands/api/<method>). Same-origin fetch with\n * the task-kanban envelope shape: { ok, value } | { ok:false, error }.\n */\nimport type {\n ApiEnvelope,\n QuickCommandsSettings,\n QuickPopupSize,\n QuickRosterEntry,\n QuickRunView,\n} from '../shared/contract'\n\nexport class QuickCommandsApiError extends Error {\n constructor(readonly code: string, message: string) {\n super(message)\n }\n}\n\nasync function call<T>(method: string, payload: Record<string, unknown> = {}): Promise<T> {\n let response: Response\n try {\n response = await fetch(`/plugins/@fonlan/dsh-quick-commands/api/${method}`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n })\n } catch (error) {\n throw new QuickCommandsApiError('network', error instanceof Error ? error.message : String(error))\n }\n const parsed = (await response.json().catch(() => null)) as ApiEnvelope<T> | null\n if (!response.ok || parsed === null || parsed.ok !== true || parsed.value === undefined) {\n throw new QuickCommandsApiError(\n (parsed?.error?.code as string | undefined) ?? 'http',\n parsed?.error?.message ?? `HTTP ${response.status}`,\n )\n }\n return parsed.value as T\n}\n\n/** Current session cwd of a session row (SessionSummary carries cwd). */\nexport interface SessionRow {\n id: string\n cwd?: string\n}\n\n/** One delta read result (mirrors the host RunPollResult). */\nexport interface RunPoll {\n status: 'running' | 'exited'\n exitCode: number | null\n signal: string | null\n stdoutEnd: number\n stderrEnd: number\n stdoutDelta: string\n stderrDelta: string\n stdoutLossy: boolean\n stderrLossy: boolean\n}\n\nexport const quickApi = {\n settingsGet: () => call<QuickCommandsSettings>('settings.get'),\n settingsSetCommands: (workspaceId: string, commands: Array<{ name: string; command: string }>) =>\n call<QuickCommandsSettings>('settings.setCommands', { workspaceId, commands }),\n settingsSetAnchor: (anchor: QuickCommandsSettings['popupAnchor']) =>\n call<QuickCommandsSettings>('settings.setAnchor', { anchor }),\n settingsSetPopupSize: (size: QuickPopupSize) =>\n call<QuickCommandsSettings>('settings.setPopupSize', { width: size.width, height: size.height }),\n workspacesList: () => call<QuickRosterEntry[]>('workspaces.list'),\n runStart: (workspaceId: string, commandName: string, sessionCwd: string) =>\n call<QuickRunView>('run.start', { workspaceId, commandName, sessionCwd }),\n runPoll: (runId: string, stdoutFrom: number, stderrFrom: number) =>\n call<RunPoll>('run.poll', { runId, stdoutFrom, stderrFrom }),\n runKill: (runId: string) => call<{ killed: boolean }>('run.kill', { runId }),\n runList: (workspaceId: string) =>\n call<Array<{ runId: string; commandName: string; startedAt: number; status: 'running' | 'exited' }>>('run.list', { workspaceId }),\n}\n","/**\n * HMR-safe shared run state for the quick-commands header button and output\n * popup. Module-level fields live on one globalThis-backed record keyed with\n * Symbol.for so every client-bundle copy (client-hmr hot swap re-evaluates the\n * bundle without reloading the page) reads/writes the same state cell.\n *\n * The popup handles ONE run at a time (serial per workspace by design); a\n * second workspace can run in parallel with its own popup, so the store keys\n * popups by runId.\n */\nimport { useSyncExternalStore } from 'react'\n\nconst STATE_KEY = Symbol.for('@fonlan/dsh-quick-commands/run-state')\n\n/** One popup slot: menu open flag + the live run the popup streams. */\nexport interface RunPopoverState {\n /** Whether the header command menu is open. */\n menuOpen: boolean\n /** Active run id (undefined when no run is open). */\n runId: string | null\n /** Workspace id owning the active run (for kill/cwd display). */\n workspaceId: string | null\n /** Command display name. */\n commandName: string | null\n /** SSH host id when the run executes on a remote workspace (else null). */\n remoteHost: string | null\n /** Whether the run popup should stream (false while minimized). */\n streaming: boolean\n}\n\ninterface RunPopoverStateInternal extends RunPopoverState {\n listeners: Set<() => void>\n}\n\ninterface QuickCommandsState {\n popover: RunPopoverStateInternal\n /** Cached published snapshot — STABLE reference; rebuilt only after a patch. */\n snapshot: RunPopoverState | null\n}\n\nfunction getState(): QuickCommandsState {\n const g = globalThis as unknown as Record<symbol, QuickCommandsState | undefined>\n let state = g[STATE_KEY]\n if (state === undefined) {\n state = {\n popover: {\n menuOpen: false,\n runId: null,\n workspaceId: null,\n commandName: null,\n remoteHost: null,\n streaming: true,\n listeners: new Set(),\n },\n // Seed the cache with values our equality check will see as unchanged.\n snapshot: {\n menuOpen: false,\n runId: null,\n workspaceId: null,\n commandName: null,\n remoteHost: null,\n streaming: true,\n },\n }\n g[STATE_KEY] = state\n }\n return state\n}\n\nfunction notify(): void {\n // Invalidate the cached snapshot FIRST: useSyncExternalStore reads\n // getSnapshot after notification, so a new stable object must be ready.\n getState().snapshot = null\n for (const listener of getState().popover.listeners) listener()\n}\n\nfunction subscribe(cb: () => void): () => void {\n const listeners = getState().popover.listeners\n listeners.add(cb)\n return () => { listeners.delete(cb) }\n}\n\n/**\n * getSnapshot for useSyncExternalStore. MUST return a stable reference while\n * the underlying fields are unchanged (React compares with Object.is) — return\n * the CACHED object; only rebuild it when a patch invalidated it.\n */\nfunction snapshot(): RunPopoverState {\n const state = getState()\n if (state.snapshot !== null) return state.snapshot\n const p = state.popover\n state.snapshot = {\n menuOpen: p.menuOpen,\n runId: p.runId,\n workspaceId: p.workspaceId,\n commandName: p.commandName,\n remoteHost: p.remoteHost,\n streaming: p.streaming,\n }\n return state.snapshot\n}\n\nfunction patch(partial: Partial<RunPopoverState>): void {\n const state = getState()\n Object.assign(state.popover, partial)\n // Invalidate the cached snapshot so equality flips exactly on real changes.\n state.snapshot = null\n notify()\n}\n\nexport function useRunPopover(): RunPopoverState {\n return useSyncExternalStore(subscribe, snapshot, snapshot)\n}\n\nexport function openMenu(): void { patch({ menuOpen: true }) }\nexport function closeMenu(): void { patch({ menuOpen: false }) }\nexport function toggleMenu(open: boolean): void { patch({ menuOpen: open }) }\n\nexport function openRun(runId: string, workspaceId: string, commandName: string, remoteHost?: string): void {\n patch({ runId, workspaceId, commandName, remoteHost: remoteHost ?? null, menuOpen: false, streaming: true })\n}\n\nexport function closeRun(): void {\n patch({ runId: null, workspaceId: null, commandName: null, remoteHost: null, streaming: false })\n}\n","/**\n * Session-header quick-command surface: the ▶ play icon button (left of the\n * Session log pill), the command menu popover (current workspace's commands),\n * and the live output popup that streams stdout/stderr at ~200ms cadence.\n *\n * Props are the standard session/global kit of `conversation.session.header.utilities`\n * plus this plugin's injected hooks (anchor preference + resolver).\n */\nimport { useEffect, useLayoutEffect, useRef, useState } from 'react'\nimport { createPortal } from 'react-dom'\nimport {\n IconPlayOutline16,\n IconCloseOutline16,\n IconStopFill16,\n} from '@deepseek-ai/dsh-client-ui-primitives'\nimport { quickApi } from './api'\nimport { useRunPopover, openMenu, closeMenu, openRun, closeRun } from './run-state'\nimport type { QuickPopupSize } from '../shared/contract'\n\n/** Workspace row shape the standard useWorkspaces hook exposes. */\ninterface WorkspaceRow {\n workspaceId: string\n path: string\n title: string\n}\n\n/** Session list state subset we read (cwd + id). */\ninterface SessionRow {\n id: string\n cwd?: string\n}\n\ninterface HeaderProps {\n sessionId: string\n t: (key: string) => string\n useSessions?: (selector: (s: { byId: Record<string, SessionRow>; current?: string }) => unknown) => unknown\n useWorkspaces?: (selector: (s: { items: WorkspaceRow[] }) => unknown) => unknown\n}\n\ninterface MenuPosition {\n left: number\n top: number\n width: number\n}\n\nconst PHONE_MENU_RAIL_WIDTH = 56\nconst MENU_VIEWPORT_GAP = 12\nconst MENU_PREFERRED_WIDTH = 280\n\n/**\n * Position the portal menu beside its button without ever entering the phone\n * nav rail. Calculating actual pixels (rather than only CSS max-width) also\n * means it remains correct when the header itself sits in a clipped stacking\n * context created by a shell/mobile plugin.\n */\nfunction positionMenu(button: HTMLButtonElement): MenuPosition {\n const rect = button.getBoundingClientRect()\n const narrow = window.matchMedia('(max-width: 767px)').matches\n const minLeft = narrow ? PHONE_MENU_RAIL_WIDTH + MENU_VIEWPORT_GAP : MENU_VIEWPORT_GAP\n const availableWidth = Math.max(0, window.innerWidth - minLeft - MENU_VIEWPORT_GAP)\n const width = Math.min(MENU_PREFERRED_WIDTH, availableWidth)\n const maxLeft = Math.max(minLeft, window.innerWidth - MENU_VIEWPORT_GAP - width)\n return {\n // The old in-header popover used `right:12px`; retain that visual anchor\n // but clamp its left edge to the usable side of the rail.\n left: Math.max(minLeft, Math.min(rect.right - MENU_VIEWPORT_GAP - width, maxLeft)),\n top: Math.min(rect.bottom + 6, Math.max(MENU_VIEWPORT_GAP, window.innerHeight - 72)),\n width,\n }\n}\n\n/** Longest-path workspace match for a cwd (subdirectory allowed). */\nfunction matchWorkspace(cwd: string | undefined, workspaces: WorkspaceRow[]): WorkspaceRow | undefined {\n if (cwd === undefined) return undefined\n let best: WorkspaceRow | undefined\n for (const ws of workspaces) {\n const norm = ws.path.replace(/\\\\/g, '/').replace(/\\/+$/, '')\n const nc = cwd.replace(/\\\\/g, '/')\n if (nc === norm || nc.startsWith(norm + '/')) {\n if (best === undefined || norm.length > best.path.length) best = ws\n }\n }\n return best\n}\n\n/** Resolve the current session's workspace from the standard kits. */\nfunction useCurrentWorkspace(\n sessionId: string,\n useSessions?: HeaderProps['useSessions'],\n useWorkspaces?: HeaderProps['useWorkspaces'],\n): { workspace: WorkspaceRow | undefined; cwd: string | undefined } {\n const row = useSessions?.((s) => s.byId[sessionId]) as SessionRow | undefined\n const workspaces = useWorkspaces?.((s) => s.items) as WorkspaceRow[] | undefined\n return { workspace: matchWorkspace(row?.cwd, workspaces ?? []), cwd: row?.cwd }\n}\n\n/** Header icon button + command menu + live output popup. */\nexport function QuickCommandsHeaderAction(props: HeaderProps): JSX.Element {\n const { sessionId, t } = props\n const { workspace, cwd } = useCurrentWorkspace(sessionId, props.useSessions, props.useWorkspaces)\n const popover = useRunPopover()\n const [commands, setCommands] = useState<Array<{ name: string; command: string }>>([])\n const [menuLoadError, setMenuLoadError] = useState<string | null>(null)\n const [menuRemoteHost, setMenuRemoteHost] = useState<string | null>(null)\n const [busy, setBusy] = useState(false)\n const [anchor, setAnchor] = useState<'corner' | 'button'>('corner')\n const [popupSize, setPopupSize] = useState<QuickPopupSize | undefined>(undefined)\n const wrapRef = useRef<HTMLDivElement | null>(null)\n const buttonRef = useRef<HTMLButtonElement | null>(null)\n const menuRef = useRef<HTMLDivElement | null>(null)\n const [menuPosition, setMenuPosition] = useState<MenuPosition | null>(null)\n\n // Click-outside dismissal: the command menu is portaled to document.body so\n // it escapes the mobile shell's clipped/z-indexed header context. Treat both\n // its portal node and its original header wrap as inside targets.\n useEffect(() => {\n if (!popover.menuOpen) return\n const onPointerDown = (e: PointerEvent): void => {\n const wrap = wrapRef.current\n const menu = menuRef.current\n if (!(e.target instanceof Node)) return\n if (wrap?.contains(e.target) || menu?.contains(e.target)) return\n closeMenu()\n }\n document.addEventListener('pointerdown', onPointerDown)\n return () => { document.removeEventListener('pointerdown', onPointerDown) }\n }, [popover.menuOpen])\n\n // Recalculate against the actual header button whenever the menu opens and\n // whenever a viewport change moves it (rotation, browser chrome resize).\n useLayoutEffect(() => {\n if (!popover.menuOpen) {\n setMenuPosition(null)\n return\n }\n const update = (): void => {\n const button = buttonRef.current\n if (button !== null) setMenuPosition(positionMenu(button))\n }\n update()\n window.addEventListener('resize', update)\n window.addEventListener('scroll', update, true)\n window.visualViewport?.addEventListener('resize', update)\n return () => {\n window.removeEventListener('resize', update)\n window.removeEventListener('scroll', update, true)\n window.visualViewport?.removeEventListener('resize', update)\n }\n }, [popover.menuOpen])\n\n // Load the workspace's command list + anchor when the menu opens.\n useEffect(() => {\n if (!popover.menuOpen) return\n let cancelled = false\n void (async () => {\n try {\n const [entries, settings] = await Promise.all([quickApi.workspacesList(), quickApi.settingsGet()])\n if (cancelled) return\n const own = entries.find((e) => e.workspaceId === workspace?.workspaceId)\n setCommands(own === undefined ? [] : own.commands)\n setMenuRemoteHost(own?.remoteHost ?? null)\n setAnchor(settings.popupAnchor)\n setPopupSize(settings.popupSize)\n setMenuLoadError(null)\n } catch (e) {\n if (cancelled) return\n setCommands([])\n setMenuLoadError(e instanceof Error ? e.message : String(e))\n }\n })()\n return () => { cancelled = true }\n }, [popover.menuOpen, workspace?.workspaceId])\n\n const startRun = async (commandName: string): Promise<void> => {\n setBusy(true)\n closeMenu()\n try {\n const run = await quickApi.runStart(workspace!.workspaceId, commandName, cwd ?? workspace!.path)\n openRun(run.runId, run.workspaceId, run.commandName, run.remoteHost)\n } catch (e) {\n const message = e instanceof Error ? e.message : String(e)\n if (message.includes('run-busy') || message.includes('already running')) {\n setMenuLoadError(t('runBusy'))\n } else {\n setMenuLoadError(t('runStartFailed') + ': ' + message)\n }\n openMenu()\n } finally {\n setBusy(false)\n }\n }\n\n return (\n <div ref={wrapRef} className=\"qc-head-wrap\">\n <button\n ref={buttonRef}\n type=\"button\"\n className=\"qc-head-btn\"\n title={workspace === undefined ? t('noWorkspace') : t('buttonTooltip')}\n aria-label={t('buttonTitle')}\n disabled={workspace === undefined}\n onClick={() => { popover.menuOpen ? closeMenu() : openMenu() }}\n >\n <IconPlayOutline16 size={15} />\n </button>\n\n {popover.menuOpen && workspace !== undefined && menuPosition !== null && createPortal(\n <div\n ref={menuRef}\n className=\"qc-menu qc-menu-portal\"\n role=\"menu\"\n style={{ left: menuPosition.left, top: menuPosition.top, width: menuPosition.width }}\n >\n <div className=\"qc-menu-head\">\n <span className=\"qc-menu-title\">{t('menuTitle')}</span>\n {menuRemoteHost !== null && <span className=\"qc-menu-remote\" title={`SSH · ${menuRemoteHost}`}>SSH</span>}\n <span className=\"qc-menu-ws\">{workspace.title}</span>\n <button type=\"button\" className=\"qc-menu-close\" aria-label={t('runClose')} onClick={closeMenu}>\n <IconCloseOutline16 size={12} />\n </button>\n </div>\n {commands.length === 0 && (\n <p className=\"qc-menu-empty\">{menuLoadError ?? t('noCommands')}</p>\n )}\n {commands.filter((c) => c.name.trim() !== '').map((command) => (\n <button\n key={command.name}\n type=\"button\"\n role=\"menuitem\"\n className=\"qc-menu-item\"\n disabled={busy}\n onClick={() => void startRun(command.name)}\n >\n <span className=\"qc-menu-item-name\">{command.name}</span>\n <span className=\"qc-menu-item-cmd\">{command.command}</span>\n </button>\n ))}\n </div>\n , document.body)}\n\n {popover.runId !== null && (\n <RunPopup\n runId={popover.runId}\n workspaceId={popover.workspaceId ?? ''}\n commandName={popover.commandName ?? ''}\n remoteHost={popover.remoteHost ?? undefined}\n t={t}\n anchor={anchor}\n popupSize={popupSize}\n sessionId={sessionId}\n onClose={closeRun}\n />\n )}\n </div>\n )\n}\n\n// ── popup drag-resize ───────────────────────────────────────────────────────\n// The popup pins its RIGHT edge (both anchors), plus its BOTTOM edge (corner\n// anchor) or TOP edge (button anchor, pinned below the button). So the MOVABLE\n// borders are the LEFT border (width) and the TOP border (corner anchor) /\n// BOTTOM border (button anchor) (height); the free corner (top-left /\n// bottom-left) carries a diagonal grip for simultaneous two-axis resizing.\n// Every handle follows the cursor edge-for-edge — pulling the left border LEFT\n// widens, pulling the top border UP grows taller — nothing feels inverted.\n// The size is clamped to viewport bounds and persisted to settings.\n\nconst POPUP_MIN_W = 320\nconst POPUP_MIN_H = 200\n/** Total horizontal margin kept around the popup (both anchors). */\nconst POPUP_VIEWPORT_MARGIN = 40\n/** Vertical space reserved for header/footer chrome (both anchors). */\nconst POPUP_VIEWPORT_RESERVED = 120\n\nfunction popupDefaultSize(anchor: 'corner' | 'button'): QuickPopupSize {\n return anchor === 'corner' ? { width: 520, height: 340 } : { width: 440, height: 300 }\n}\n\nfunction popupClamp(size: QuickPopupSize): QuickPopupSize {\n const maxW = Math.max(POPUP_MIN_W, window.innerWidth - POPUP_VIEWPORT_MARGIN)\n const maxH = Math.max(POPUP_MIN_H, window.innerHeight - POPUP_VIEWPORT_RESERVED)\n return {\n width: Math.min(Math.max(POPUP_MIN_W, Math.round(size.width)), maxW),\n height: Math.min(Math.max(POPUP_MIN_H, Math.round(size.height)), maxH),\n }\n}\n\n/**\n * Initial popup size: the persisted one when it holds sane numbers, else the\n * per-anchor default. Defensive because schemastery resolves a MISSING object\n * field to `{}` (object schemas carry an implicit `default: {}`), so a\n * never-resized popup can arrive as `popupSize: {}` from settings.get().\n */\nfunction popupSizeOrDefault(size: QuickPopupSize | undefined, anchor: 'corner' | 'button'): QuickPopupSize {\n if (\n size === undefined\n || !Number.isFinite(size.width)\n || !Number.isFinite(size.height)\n || size.width <= 0\n || size.height <= 0\n ) {\n return popupDefaultSize(anchor)\n }\n return popupClamp(size)\n}\n\n/** The streaming output popup. Polls stdout/stderr deltas every ~200ms. */\nfunction RunPopup(props: {\n runId: string\n workspaceId: string\n commandName: string\n remoteHost?: string\n t: (key: string) => string\n anchor: 'corner' | 'button'\n popupSize?: QuickPopupSize\n sessionId: string\n onClose: () => void\n}): JSX.Element {\n const { runId, t, anchor, remoteHost, onClose } = props\n const [size, setSize] = useState<QuickPopupSize>(() => popupSizeOrDefault(props.popupSize, anchor))\n // Mirrors `size` for drag handlers: pointer events can arrive faster than\n // React commits, so pointerup must read the latest value, not the render's.\n const sizeRef = useRef<QuickPopupSize>(size)\n const sizeDragRef = useRef<{\n pointerId: number\n startX: number\n startY: number\n startW: number\n startH: number\n /** Which handle started the drag (controls which axes move). */\n mode: 'left' | 'height' | 'corner'\n } | null>(null)\n const [state, setState] = useState(() => ({\n status: 'running' as 'running' | 'exited',\n exitCode: null as number | null,\n signal: null as string | null,\n stdoutEnd: 0,\n stderrEnd: 0,\n stdout: '',\n stderr: '',\n stdoutLossy: false,\n stderrLossy: false,\n }))\n const [view, setView] = useState<'stdout' | 'stderr'>('stdout')\n const [autoFollow, setAutoFollow] = useState(true)\n const [pollError, setPollError] = useState<string | null>(null)\n const [closing, setClosing] = useState(false)\n const outputRef = useRef<HTMLDivElement | null>(null)\n const autoFollowRef = useRef(true)\n\n // Auto-follow: scroll the output pane to the bottom on new output while the\n // user has not scrolled up (autoFollow mirror keeps the effect cheap).\n useEffect(() => {\n autoFollowRef.current = autoFollow\n }, [autoFollow])\n useEffect(() => {\n if (!autoFollowRef.current) return\n const el = outputRef.current\n if (el === null) return\n el.scrollTop = el.scrollHeight\n }, [state.stdout, state.stderr, view])\n\n // Poll deltas; stop polling at exit (the run stays readable in memory).\n useEffect(() => {\n let cancelled = false\n let outEnd = 0\n let errEnd = 0\n const tick = async (): Promise<void> => {\n try {\n const poll = await quickApi.runPoll(runId, outEnd, errEnd)\n if (cancelled) return\n outEnd = poll.stdoutEnd\n errEnd = poll.stderrEnd\n setState((s) => ({\n status: poll.status,\n exitCode: poll.exitCode,\n signal: poll.signal,\n stdoutEnd: poll.stdoutEnd,\n stderrEnd: poll.stderrEnd,\n stdout: poll.stdoutLossy ? poll.stdoutDelta : s.stdout + poll.stdoutDelta,\n stderr: poll.stderrLossy ? poll.stderrDelta : s.stderr + poll.stderrDelta,\n stdoutLossy: poll.stdoutLossy,\n stderrLossy: poll.stderrLossy,\n }))\n setPollError(null)\n if (poll.status === 'exited') return\n timer = window.setTimeout(() => void tick(), 200)\n } catch (e) {\n if (cancelled) return\n setPollError(e instanceof Error ? e.message : String(e))\n timer = window.setTimeout(() => void tick(), 500)\n }\n }\n let timer: number\n void tick()\n return () => { cancelled = true; window.clearTimeout(timer) }\n }, [runId])\n\n const kill = async (): Promise<void> => {\n setClosing(true)\n try {\n await quickApi.runKill(runId)\n } finally {\n onClose()\n }\n }\n\n // Closing the output surface must not terminate a still-running command:\n // the dedicated stop control owns that destructive action. In particular,\n // an X in a compact touch header is expected to dismiss immediately rather\n // than reveal a second confirmation row below the visible viewport.\n const closeOutput = (): void => { onClose() }\n\n // Some mobile webviews occasionally fail to synthesize `click` for compact\n // header controls after a touch gesture. Handle touch pointer-up directly;\n // click remains the keyboard and mouse activation path.\n const onClosePointerUp = (e: React.PointerEvent<HTMLButtonElement>): void => {\n if (e.pointerType !== 'touch') return\n e.preventDefault()\n closeOutput()\n }\n\n // ── drag-resize: pointer capture on the handles keeps moving/cancelling\n // events flowing even when the cursor leaves the handle or the window.\n // Which border is free to move depends on the anchor: the corner anchor\n // pins bottom+right (top border moves for height), the button anchor pins\n // top+right (bottom border moves for height).\n const heightFree: 'top' | 'bottom' = anchor === 'corner' ? 'top' : 'bottom'\n\n const onResizePointerDown = (e: React.PointerEvent<HTMLDivElement>): void => {\n e.preventDefault() // keep the handle from starting a text-selection gesture\n e.currentTarget.setPointerCapture(e.pointerId)\n sizeDragRef.current = {\n pointerId: e.pointerId,\n startX: e.clientX,\n startY: e.clientY,\n startW: sizeRef.current.width,\n startH: sizeRef.current.height,\n mode: e.currentTarget.dataset.resize as 'left' | 'height' | 'corner',\n }\n }\n\n const onResizePointerMove = (e: React.PointerEvent<HTMLDivElement>): void => {\n const drag = sizeDragRef.current\n if (drag === null || drag.pointerId !== e.pointerId) return\n const dx = e.clientX - drag.startX\n const dy = e.clientY - drag.startY\n const controlsW = drag.mode === 'left' || drag.mode === 'corner'\n const controlsH = drag.mode === 'height' || drag.mode === 'corner'\n const next = popupClamp({\n // Left border follows the cursor: dragging LEFT widens, RIGHT narrows.\n width: controlsW ? drag.startW - dx : drag.startW,\n // Top-free (corner): dragging UP grows. Bottom-free (button): DOWN grows.\n height: controlsH ? drag.startH + (heightFree === 'top' ? -dy : dy) : drag.startH,\n })\n sizeRef.current = next\n setSize(next)\n }\n\n const onResizePointerUp = (e: React.PointerEvent<HTMLDivElement>): void => {\n const drag = sizeDragRef.current\n if (drag === null || drag.pointerId !== e.pointerId) return\n sizeDragRef.current = null\n if (e.currentTarget.hasPointerCapture(e.pointerId)) {\n e.currentTarget.releasePointerCapture(e.pointerId)\n }\n // Persist best-effort: a failed save just means the next popup reopens at\n // the default size (or the last successfully saved one).\n void quickApi.settingsSetPopupSize(sizeRef.current).catch(() => undefined)\n }\n\n /** Pointer-handler trio for one resize handle (mode = which axes it drives). */\n const resizeProps = (mode: 'left' | 'height' | 'corner') => ({\n 'data-resize': mode,\n onPointerDown: onResizePointerDown,\n onPointerMove: onResizePointerMove,\n onPointerUp: onResizePointerUp,\n onPointerCancel: onResizePointerUp,\n })\n\n const active = state.status === 'running'\n const title = active ? `${props.commandName} · ${t('runStateRunning')}` : `${props.commandName} · ${t('runStateExited')} (${state.exitCode ?? '?'})`\n\n return (\n <div className={`qc-popup qc-popup-${anchor}`} role=\"dialog\" aria-label={title} style={{ width: size.width, height: size.height }}>\n <header className=\"qc-popup-head\">\n <span className=\"qc-popup-title\" title={title}>{title}</span>\n {remoteHost !== undefined && <span className=\"qc-popup-remote\" title={`SSH · ${remoteHost}`}>SSH</span>}\n <span className=\"qc-popup-cmd\">{props.commandName}</span>\n <button\n type=\"button\"\n className=\"qc-popup-icon qc-popup-kill\"\n disabled={!active || closing}\n title={t('runKill')}\n aria-label={t('runKill')}\n onClick={() => void kill()}\n >\n <IconStopFill16 size={14} />\n </button>\n <button\n type=\"button\"\n className=\"qc-popup-icon qc-popup-close\"\n title={t('runClose')}\n aria-label={t('runClose')}\n onPointerUp={onClosePointerUp}\n onClick={closeOutput}\n >\n <IconCloseOutline16 size={14} />\n </button>\n </header>\n\n <div className=\"qc-popup-tabs\">\n <button\n type=\"button\"\n className={'qc-popup-tab' + (view === 'stdout' ? ' qc-popup-tab-active' : '')}\n onClick={() => setView('stdout')}\n >\n {t('runStdout')}\n </button>\n <button\n type=\"button\"\n className={'qc-popup-tab' + (view === 'stderr' ? ' qc-popup-tab-active' : '')}\n onClick={() => setView('stderr')}\n >\n {t('runStderr')}\n </button>\n </div>\n\n <div\n ref={outputRef}\n className=\"qc-popup-output\"\n onScroll={(e) => {\n const el = e.currentTarget\n const atEnd = Math.abs(el.scrollHeight - el.scrollTop - el.clientHeight) < 24\n setAutoFollow(atEnd)\n }}\n >\n {pollError !== null && <p className=\"qc-popup-error\">{t('runPollFailed')}: {pollError}</p>}\n {view === 'stdout' ? (\n state.stdout === '' && !active ? <p className=\"qc-popup-empty\">{t('runEmpty')}</p>\n : <pre className=\"qc-popup-pre\">{state.stdout}</pre>\n ) : (\n state.stderr === '' && !active ? <p className=\"qc-popup-empty\">{t('runEmpty')}</p>\n : <pre className=\"qc-popup-pre\">{state.stderr}</pre>\n )}\n </div>\n\n <footer className=\"qc-popup-foot\">\n <span className=\"qc-popup-state\">\n {active ? t('runStateRunning') : `${t('runStateExited')} · ${t('runExitCode')} ${state.exitCode ?? '-'}${state.signal !== null ? ` · ${state.signal}` : ''}`}\n </span>\n </footer>\n\n {/* Resize handles: left border = width; top (corner anchor) / bottom\n (button anchor) border = height; free corner = diagonal grip. */}\n <div className=\"qc-popup-resize qc-popup-resize-left\" title={t('runResize')} {...resizeProps('left')} />\n {heightFree === 'top'\n ? <div className=\"qc-popup-resize qc-popup-resize-top\" title={t('runResize')} {...resizeProps('height')} />\n : <div className=\"qc-popup-resize qc-popup-resize-bottom\" title={t('runResize')} {...resizeProps('height')} />}\n {heightFree === 'top' ? (\n <div className=\"qc-popup-resize qc-popup-resize-corner qc-popup-resize-corner-tl\" title={t('runResize')} {...resizeProps('corner')}>\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 12 12\" aria-hidden=\"true\">\n <path\n d=\"M9 9 3 3 M9 5.5 5.5 3 M5.5 9 3 5.5\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.4\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </div>\n ) : (\n <div className=\"qc-popup-resize qc-popup-resize-corner qc-popup-resize-corner-bl\" title={t('runResize')} {...resizeProps('corner')}>\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 12 12\" aria-hidden=\"true\">\n <path\n d=\"M9 3 3 9 M9 6.5 6.5 9 M5.5 3 3 5.5\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.4\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </div>\n )}\n </div>\n )\n}\n","/**\n * Quick-commands settings page (设置 → 侧栏「快捷命令」).\n *\n * Registers into the \"settings.section\" list slot, so the page owns one entry\n * in the settings sidebar and renders in the settings panel's content column.\n * The header is static (title + one-line summary) over the always-visible\n * body: the workspace / command-set editor and the popup anchor preference.\n * Body edits instant-save per change; partial rows (empty name/command)\n * persist while the user types.\n */\nimport { useEffect, useRef, useState } from 'react'\nimport {\n IconPlusOutline16,\n IconTrashOutline16,\n} from '@deepseek-ai/dsh-client-ui-primitives'\nimport { quickApi } from './api'\nimport type { QuickRosterEntry, QuickCommandsSettings } from '../shared/contract'\nimport './settings-section.css'\n\ninterface Props {\n t: (key: string) => string\n}\n\ninterface RosterState {\n loaded: boolean\n entries: QuickRosterEntry[]\n anchor: QuickCommandsSettings['popupAnchor']\n}\n\ntype CommandDraft = { name: string; command: string }\n\nexport function QuickCommandsSettingsSection({ t }: Props): JSX.Element {\n const [state, setState] = useState<RosterState>({ loaded: false, entries: [], anchor: 'corner' })\n const [error, setError] = useState<string | null>(null)\n // Guard concurrent saves: the last write wins via a monotonically bumped token.\n const saveToken = useRef(0)\n\n const load = async (): Promise<void> => {\n try {\n const [entries, settings] = await Promise.all([quickApi.workspacesList(), quickApi.settingsGet()])\n setState({ loaded: true, entries, anchor: settings.popupAnchor })\n setError(null)\n } catch (e) {\n setState((s) => ({ ...s, loaded: true }))\n setError(e instanceof Error ? e.message : String(e))\n }\n }\n\n useEffect(() => {\n void load()\n }, [])\n\n /** Instant-save the workspace's whole command set; optimistic UI. */\n const saveCommands = async (workspaceId: string, commands: CommandDraft[]): Promise<void> => {\n const token = ++saveToken.current\n try {\n const next = await quickApi.settingsSetCommands(workspaceId, commands)\n if (token !== saveToken.current) return\n setState((s) => ({\n ...s,\n anchor: next.popupAnchor,\n entries: s.entries.map((e) => (e.workspaceId === workspaceId ? { ...e, commands } : e)),\n }))\n setError(null)\n } catch (e) {\n setError(e instanceof Error ? e.message : String(e))\n // Re-sync to server truth on failure.\n void load()\n }\n }\n\n /** Compute the next command list for a workspace, THEN save + update state. */\n const mutateCommands = (workspaceId: string, next: (current: CommandDraft[]) => CommandDraft[]): void => {\n const entry = state.entries.find((e) => e.workspaceId === workspaceId)\n if (entry === undefined) return\n const commands = next(entry.commands)\n // Optimistic update first (row stays visible immediately), then persist.\n setState((s) => ({\n ...s,\n entries: s.entries.map((e) => (e.workspaceId === workspaceId ? { ...e, commands } : e)),\n }))\n void saveCommands(workspaceId, commands)\n }\n\n const patchEntry = (workspaceId: string, index: number, field: 'name' | 'command', value: string): void => {\n mutateCommands(workspaceId, (current) =>\n current.map((c, i) => (i === index ? { ...c, [field]: value } : c)),\n )\n }\n\n const addCommand = (workspaceId: string): void => {\n mutateCommands(workspaceId, (current) => [...current, { name: '', command: '' }])\n }\n\n const removeCommand = (workspaceId: string, index: number): void => {\n mutateCommands(workspaceId, (current) => current.filter((_, i) => i !== index))\n }\n\n const setAnchor = async (anchor: QuickCommandsSettings['popupAnchor']): Promise<void> => {\n setState((s) => ({ ...s, anchor }))\n try {\n const next = await quickApi.settingsSetAnchor(anchor)\n setState((s) => ({ ...s, anchor: next.popupAnchor }))\n } catch (e) {\n setError(e instanceof Error ? e.message : String(e))\n void load()\n }\n }\n\n const title = t('cardTitle')\n const description = t('sectionSub')\n\n return (\n <div className=\"qc-page\">\n <header className=\"qc-page-head\">\n <h3 className=\"qc-page-title\">{title}</h3>\n <p className=\"qc-page-sub\">{description}</p>\n </header>\n <div className=\"qc-settings-body\">\n <p className=\"qc-settings-intro\">{t('cardIntro')}</p>\n\n <div className=\"qc-settings-anchor\">\n <span className=\"qc-settings-anchor-label\">{t('cardAnchor')}</span>\n <label className=\"qc-settings-radio\">\n <input\n type=\"radio\"\n name=\"qc-anchor\"\n checked={state.anchor === 'corner'}\n onChange={() => void setAnchor('corner')}\n />\n {t('cardAnchorCorner')}\n </label>\n <label className=\"qc-settings-radio\">\n <input\n type=\"radio\"\n name=\"qc-anchor\"\n checked={state.anchor === 'button'}\n onChange={() => void setAnchor('button')}\n />\n {t('cardAnchorButton')}\n </label>\n </div>\n\n {error !== null && <div className=\"qc-settings-error\">{t('cardSaveError')}: {error}</div>}\n\n {state.loaded && state.entries.length === 0 && (\n <p className=\"qc-settings-empty\">{t('cardNoWorkspaces')}</p>\n )}\n\n {state.entries.map((entry) => (\n <section key={entry.workspaceId} className=\"qc-settings-workspace\">\n <header className=\"qc-settings-workspace-head\">\n <span className=\"qc-settings-workspace-title\">{entry.title || entry.path || entry.workspaceId}</span>\n <span className=\"qc-settings-workspace-path\">{entry.path}</span>\n </header>\n {entry.commands.length === 0 && (\n <p className=\"qc-settings-no-commands\">{t('cardNoCommands')}</p>\n )}\n {entry.commands.map((command, index) => (\n <div key={`${entry.workspaceId}-${index}`} className=\"qc-settings-command\">\n <input\n className=\"qc-settings-field qc-settings-field-name\"\n placeholder={t('cardName')}\n value={command.name}\n aria-label={t('cardName')}\n onChange={(e) => patchEntry(entry.workspaceId, index, 'name', e.target.value)}\n />\n <input\n className=\"qc-settings-field qc-settings-field-command\"\n placeholder={t('cardCommand')}\n value={command.command}\n aria-label={t('cardCommand')}\n onChange={(e) => patchEntry(entry.workspaceId, index, 'command', e.target.value)}\n />\n <button\n type=\"button\"\n className=\"qc-settings-icon-btn\"\n aria-label={t('cardRemove')}\n title={t('cardRemove')}\n onClick={() => removeCommand(entry.workspaceId, index)}\n >\n <IconTrashOutline16 size={14} />\n </button>\n </div>\n ))}\n <button type=\"button\" className=\"qc-settings-add-btn\" onClick={() => addCommand(entry.workspaceId)}>\n <IconPlusOutline16 size={14} />\n {t('cardAddCommand')}\n </button>\n </section>\n ))}\n </div>\n </div>\n )\n}\n","/**\n * Client dictionary namespace for @fonlan/dsh-quick-commands.\n */\nexport const LOCALE_NS = 'quick-commands'\n\nexport const zh = {\n // header button\n buttonTitle: '快速命令',\n buttonTooltip: '快速命令(当前工作区)',\n noWorkspace: '当前会话不在任何已配置的工作区中',\n noCommands: '当前工作区未配置命令',\n openSettings: '去设置配置命令',\n\n // command popup menu\n menuTitle: '快速命令',\n menuEmpty: '(无命令)',\n\n // run popup\n runTitle: '运行中',\n runExited: '已退出',\n runExitCode: '退出码',\n runSignal: '信号',\n runStdout: '输出 (stdout)',\n runStderr: '错误 (stderr)',\n runEmpty: '(无输出)',\n runCommand: '命令',\n runCwd: '工作区',\n runStateRunning: '运行中…',\n runStateExited: '已退出',\n runKill: '终止',\n runClose: '关闭',\n runResize: '拖动调整弹窗大小',\n runKillConfirm: '终止后输出弹窗将关闭,确定终止该命令?',\n runBusy: '该工作区已有命令在运行',\n runStartFailed: '命令启动失败',\n runPollFailed: '输出读取失败',\n runLost: '运行记录已失效',\n runNotFound: '工作区或命令未找到',\n\n // settings page (设置 → 侧栏「快捷命令」)\n settingsTitle: '快捷命令',\n cardTitle: '快速命令',\n sectionSub: '为每个工作区配置常用命令,会话头部一键运行',\n cardIntro: '为每个工作区配置常用命令,运行于工作区目录。支持占位符:{workspace} 工作区路径,{cwd} 会话目录,{title} 工作区标题。',\n cardWorkspaces: '工作区',\n cardNoWorkspaces: '没有工作区。请在侧边栏先添加工作区。',\n cardCommands: '命令',\n cardNoCommands: '未配置命令',\n cardAddCommand: '添加命令',\n cardName: '名称',\n cardCommand: '命令',\n cardRemove: '删除',\n cardSaveError: '保存失败',\n cardAnchor: '输出弹窗位置',\n cardAnchorCorner: '右下角',\n cardAnchorButton: '锚定按钮下方',\n} as const\n\nexport const en: Record<keyof typeof zh, string> = {\n buttonTitle: 'Quick commands',\n buttonTooltip: 'Quick commands (current workspace)',\n noWorkspace: 'This session is not in any configured workspace',\n noCommands: 'No commands configured for this workspace',\n openSettings: 'Configure commands in Settings',\n\n menuTitle: 'Quick commands',\n menuEmpty: '(no commands)',\n\n runTitle: 'Running',\n runExited: 'Exited',\n runExitCode: 'Exit code',\n runSignal: 'Signal',\n runStdout: 'Output (stdout)',\n runStderr: 'Errors (stderr)',\n runEmpty: '(no output)',\n runCommand: 'Command',\n runCwd: 'Workspace',\n runStateRunning: 'Running…',\n runStateExited: 'Exited',\n runKill: 'Terminate',\n runClose: 'Close',\n runResize: 'Drag to resize the popup',\n runKillConfirm: 'Terminating closes the output popup. Terminate this command?',\n runBusy: 'A command is already running for this workspace',\n runStartFailed: 'Command failed to start',\n runPollFailed: 'Failed to read output',\n runLost: 'Run record expired',\n runNotFound: 'Workspace or command not found',\n\n settingsTitle: 'Quick Commands',\n cardTitle: 'Quick commands',\n sectionSub: 'Configure common commands per workspace, run them from the session header',\n cardIntro: 'Configure common commands per workspace; they run in the workspace directory. Placeholders: {workspace} workspace path, {cwd} session cwd, {title} workspace title.',\n cardWorkspaces: 'Workspaces',\n cardNoWorkspaces: 'No workspaces. Add one in the sidebar first.',\n cardCommands: 'Commands',\n cardNoCommands: 'No commands configured',\n cardAddCommand: 'Add command',\n cardName: 'Name',\n cardCommand: 'Command',\n cardRemove: 'Remove',\n cardSaveError: 'Save failed',\n cardAnchor: 'Output popup position',\n cardAnchorCorner: 'Bottom right',\n cardAnchorButton: 'Anchored at button',\n}\n","/**\n * @fonlan/dsh-quick-commands client half.\n *\n * Registers:\n * - \"settings.section\" page (id \"quick-commands\", order 330) — per-workspace\n * command CRUD + popup anchor preference, rendered as its own page in the\n * settings sidebar.\n * - \"conversation.session.header.utilities\" entry (id \"quick-commands\",\n * order -1 → left of the Session log pill) — the quick-run ▶ button, command\n * menu, and live output popup.\n */\nimport type { Context } from '@deepseek-ai/cordis'\ntype ClientContext = Context\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\nimport { QuickCommandsHeaderAction } from './header'\nimport { QuickCommandsSettingsSection } from './settings-section'\nimport { LOCALE_NS, zh, en } from './locales'\nimport './quick-commands.css'\n\n/** Slots face (local, erased at build). */\ninterface Slots {\n inject(name: string, callback: () => unknown): unknown\n register(\n def: { name: string; id?: string; order?: number; label?: string | (() => string); locale?: string },\n component: unknown,\n ): unknown\n}\n\n/** Services required before mounting (provided by the client runtime). */\nexport const inject = ['slots', 'locale']\n\n/** Client plugin body. */\nexport function apply(ctx: ClientContext): void {\n ctx.effect(() => ctx.locale.register(LOCALE_NS, { zh, en }), 'quick-commands: dictionaries')\n const t = ctx.locale.bind(LOCALE_NS)\n const slots = (ctx as unknown as { slots: Slots }).slots\n\n // The plugin's own settings page (设置 → 侧栏「快捷命令」) rides the\n // \"quick-commands\" locale namespace: a \"settings.section\" list entry gives it\n // one row in the settings sidebar and renders the page in the content column.\n slots.inject('settings.section', () =>\n slots.register({\n name: 'settings.section',\n id: 'quick-commands',\n order: 330,\n label: () => t('settingsTitle'),\n locale: LOCALE_NS,\n }, QuickCommandsSettingsSection as never),\n )\n\n // Session-header util: order -1 renders LEFT of the Session log pill\n // (session-log-download registers with default order 0).\n slots.inject('conversation.session.header.utilities', () =>\n slots.register({\n name: 'conversation.session.header.utilities',\n id: 'quick-commands',\n order: -1,\n locale: LOCALE_NS,\n }, QuickCommandsHeaderAction as never),\n )\n}\n"],"mappings":";;;;;;;;;;;EAaA,IAAa,wBAAb,cAA2C,MAAM;GAC1B;GAArB,YAAY,MAAuB,SAAiB;IAClD,MAAM,OAAO;IADM,KAAA,OAAA;GAErB;EACF;EAEA,eAAe,KAAQ,QAAgB,UAAmC,CAAC,GAAe;GACxF,IAAI;GACJ,IAAI;IACF,WAAW,MAAM,MAAM,2CAA2C,UAAU;KAC1E,QAAQ;KACR,SAAS,EAAE,gBAAgB,mBAAmB;KAC9C,MAAM,KAAK,UAAU,OAAO;IAC9B,CAAC;GACH,SAAS,OAAO;IACd,MAAM,IAAI,sBAAsB,WAAW,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;GACnG;GACA,MAAM,SAAU,MAAM,SAAS,KAAK,CAAC,CAAC,YAAY,IAAI;GACtD,IAAI,CAAC,SAAS,MAAM,WAAW,QAAQ,OAAO,OAAO,QAAQ,OAAO,UAAU,KAAA,GAC5E,MAAM,IAAI,sBACP,QAAQ,OAAO,QAA+B,QAC/C,QAAQ,OAAO,WAAW,QAAQ,SAAS,QAC7C;GAEF,OAAO,OAAO;EAChB;EAqBA,MAAa,WAAW;GACtB,mBAAmB,KAA4B,cAAc;GAC7D,sBAAsB,aAAqB,aACzC,KAA4B,wBAAwB;IAAE;IAAa;GAAS,CAAC;GAC/E,oBAAoB,WAClB,KAA4B,sBAAsB,EAAE,OAAO,CAAC;GAC9D,uBAAuB,SACrB,KAA4B,yBAAyB;IAAE,OAAO,KAAK;IAAO,QAAQ,KAAK;GAAO,CAAC;GACjG,sBAAsB,KAAyB,iBAAiB;GAChE,WAAW,aAAqB,aAAqB,eACnD,KAAmB,aAAa;IAAE;IAAa;IAAa;GAAW,CAAC;GAC1E,UAAU,OAAe,YAAoB,eAC3C,KAAc,YAAY;IAAE;IAAO;IAAY;GAAW,CAAC;GAC7D,UAAU,UAAkB,KAA0B,YAAY,EAAE,MAAM,CAAC;GAC3E,UAAU,gBACR,KAAqG,YAAY,EAAE,YAAY,CAAC;EACpI;;;;;;;;;;;;;EC/DA,MAAM,YAAY,OAAO,IAAI,sCAAsC;EA4BnE,SAAS,WAA+B;GACtC,MAAM,IAAI;GACV,IAAI,QAAQ,EAAE;GACd,IAAI,UAAU,KAAA,GAAW;IACvB,QAAQ;KACN,SAAS;MACP,UAAU;MACV,OAAO;MACP,aAAa;MACb,aAAa;MACb,YAAY;MACZ,WAAW;MACX,2BAAW,IAAI,IAAI;KACrB;KAEA,UAAU;MACR,UAAU;MACV,OAAO;MACP,aAAa;MACb,aAAa;MACb,YAAY;MACZ,WAAW;KACb;IACF;IACA,EAAE,aAAa;GACjB;GACA,OAAO;EACT;EAEA,SAAS,SAAe;GAGtB,SAAS,CAAC,CAAC,WAAW;GACtB,KAAK,MAAM,YAAY,SAAS,CAAC,CAAC,QAAQ,WAAW,SAAS;EAChE;EAEA,SAAS,UAAU,IAA4B;GAC7C,MAAM,YAAY,SAAS,CAAC,CAAC,QAAQ;GACrC,UAAU,IAAI,EAAE;GAChB,aAAa;IAAE,UAAU,OAAO,EAAE;GAAE;EACtC;;;;;;EAOA,SAAS,WAA4B;GACnC,MAAM,QAAQ,SAAS;GACvB,IAAI,MAAM,aAAa,MAAM,OAAO,MAAM;GAC1C,MAAM,IAAI,MAAM;GAChB,MAAM,WAAW;IACf,UAAU,EAAE;IACZ,OAAO,EAAE;IACT,aAAa,EAAE;IACf,aAAa,EAAE;IACf,YAAY,EAAE;IACd,WAAW,EAAE;GACf;GACA,OAAO,MAAM;EACf;EAEA,SAAS,MAAM,SAAyC;GACtD,MAAM,QAAQ,SAAS;GACvB,OAAO,OAAO,MAAM,SAAS,OAAO;GAEpC,MAAM,WAAW;GACjB,OAAO;EACT;EAEA,SAAgB,gBAAiC;GAC/C,QAAA,GAAOA,MAAAA,qBAAAA,CAAqB,WAAW,UAAU,QAAQ;EAC3D;EAEA,SAAgB,WAAiB;GAAE,MAAM,EAAE,UAAU,KAAK,CAAC;EAAE;EAC7D,SAAgB,YAAkB;GAAE,MAAM,EAAE,UAAU,MAAM,CAAC;EAAE;EAG/D,SAAgB,QAAQ,OAAe,aAAqB,aAAqB,YAA2B;GAC1G,MAAM;IAAE;IAAO;IAAa;IAAa,YAAY,cAAc;IAAM,UAAU;IAAO,WAAW;GAAK,CAAC;EAC7G;EAEA,SAAgB,WAAiB;GAC/B,MAAM;IAAE,OAAO;IAAM,aAAa;IAAM,aAAa;IAAM,YAAY;IAAM,WAAW;GAAM,CAAC;EACjG;;;;;;;;;;;EC9EA,MAAM,oBAAoB;EAC1B,MAAM,uBAAuB;;;;;;;EAQ7B,SAAS,aAAa,QAAyC;GAC7D,MAAM,OAAO,OAAO,sBAAsB;GAE1C,MAAM,UADS,OAAO,WAAW,oBAAoB,CAAC,CAAC,UAC9B,KAA4C;GACrE,MAAM,iBAAiB,KAAK,IAAI,GAAG,OAAO,aAAa,UAAU,iBAAiB;GAClF,MAAM,QAAQ,KAAK,IAAI,sBAAsB,cAAc;GAC3D,MAAM,UAAU,KAAK,IAAI,SAAS,OAAO,aAAa,oBAAoB,KAAK;GAC/E,OAAO;IAGL,MAAM,KAAK,IAAI,SAAS,KAAK,IAAI,KAAK,QAAQ,oBAAoB,OAAO,OAAO,CAAC;IACjF,KAAK,KAAK,IAAI,KAAK,SAAS,GAAG,KAAK,IAAI,mBAAmB,OAAO,cAAc,EAAE,CAAC;IACnF;GACF;EACF;;EAGA,SAAS,eAAe,KAAyB,YAAsD;GACrG,IAAI,QAAQ,KAAA,GAAW,OAAO,KAAA;GAC9B,IAAI;GACJ,KAAK,MAAM,MAAM,YAAY;IAC3B,MAAM,OAAO,GAAG,KAAK,QAAQ,OAAO,GAAG,CAAC,CAAC,QAAQ,QAAQ,EAAE;IAC3D,MAAM,KAAK,IAAI,QAAQ,OAAO,GAAG;IACjC,IAAI,OAAO,QAAQ,GAAG,WAAW,OAAO,GAAG,GACrC;SAAA,SAAS,KAAA,KAAa,KAAK,SAAS,KAAK,KAAK,QAAQ,OAAO;IAAA;GAErE;GACA,OAAO;EACT;;EAGA,SAAS,oBACP,WACA,aACA,eACkE;GAClE,MAAM,MAAM,eAAe,MAAM,EAAE,KAAK,UAAU;GAClD,MAAM,aAAa,iBAAiB,MAAM,EAAE,KAAK;GACjD,OAAO;IAAE,WAAW,eAAe,KAAK,KAAK,cAAc,CAAC,CAAC;IAAG,KAAK,KAAK;GAAI;EAChF;;EAGA,SAAgB,0BAA0B,OAAiC;GACzE,MAAM,EAAE,WAAW,MAAM;GACzB,MAAM,EAAE,WAAW,QAAQ,oBAAoB,WAAW,MAAM,aAAa,MAAM,aAAa;GAChG,MAAM,UAAU,cAAc;GAC9B,MAAM,CAAC,UAAU,gBAAA,GAAeC,MAAAA,SAAAA,CAAmD,CAAC,CAAC;GACrF,MAAM,CAAC,eAAe,qBAAA,GAAoBA,MAAAA,SAAAA,CAAwB,IAAI;GACtE,MAAM,CAAC,gBAAgB,sBAAA,GAAqBA,MAAAA,SAAAA,CAAwB,IAAI;GACxE,MAAM,CAAC,MAAM,YAAA,GAAWA,MAAAA,SAAAA,CAAS,KAAK;GACtC,MAAM,CAAC,QAAQ,cAAA,GAAaA,MAAAA,SAAAA,CAA8B,QAAQ;GAClE,MAAM,CAAC,WAAW,iBAAA,GAAgBA,MAAAA,SAAAA,CAAqC,KAAA,CAAS;GAChF,MAAM,WAAA,GAAUC,MAAAA,OAAAA,CAA8B,IAAI;GAClD,MAAM,aAAA,GAAYA,MAAAA,OAAAA,CAAiC,IAAI;GACvD,MAAM,WAAA,GAAUA,MAAAA,OAAAA,CAA8B,IAAI;GAClD,MAAM,CAAC,cAAc,oBAAA,GAAmBD,MAAAA,SAAAA,CAA8B,IAAI;GAK1E,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,IAAI,CAAC,QAAQ,UAAU;IACvB,MAAM,iBAAiB,MAA0B;KAC/C,MAAM,OAAO,QAAQ;KACrB,MAAM,OAAO,QAAQ;KACrB,IAAI,EAAE,EAAE,kBAAkB,OAAO;KACjC,IAAI,MAAM,SAAS,EAAE,MAAM,KAAK,MAAM,SAAS,EAAE,MAAM,GAAG;KAC1D,UAAU;IACZ;IACA,SAAS,iBAAiB,eAAe,aAAa;IACtD,aAAa;KAAE,SAAS,oBAAoB,eAAe,aAAa;IAAE;GAC5E,GAAG,CAAC,QAAQ,QAAQ,CAAC;GAIrB,CAAA,GAAA,MAAA,gBAAA,OAAsB;IACpB,IAAI,CAAC,QAAQ,UAAU;KACrB,gBAAgB,IAAI;KACpB;IACF;IACA,MAAM,eAAqB;KACzB,MAAM,SAAS,UAAU;KACzB,IAAI,WAAW,MAAM,gBAAgB,aAAa,MAAM,CAAC;IAC3D;IACA,OAAO;IACP,OAAO,iBAAiB,UAAU,MAAM;IACxC,OAAO,iBAAiB,UAAU,QAAQ,IAAI;IAC9C,OAAO,gBAAgB,iBAAiB,UAAU,MAAM;IACxD,aAAa;KACX,OAAO,oBAAoB,UAAU,MAAM;KAC3C,OAAO,oBAAoB,UAAU,QAAQ,IAAI;KACjD,OAAO,gBAAgB,oBAAoB,UAAU,MAAM;IAC7D;GACF,GAAG,CAAC,QAAQ,QAAQ,CAAC;GAGrB,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,IAAI,CAAC,QAAQ,UAAU;IACvB,IAAI,YAAY;IAChB,CAAM,YAAY;KAChB,IAAI;MACF,MAAM,CAAC,SAAS,YAAY,MAAM,QAAQ,IAAI,CAAC,SAAS,eAAe,GAAG,SAAS,YAAY,CAAC,CAAC;MACjG,IAAI,WAAW;MACf,MAAM,MAAM,QAAQ,MAAM,MAAM,EAAE,gBAAgB,WAAW,WAAW;MACxE,YAAY,QAAQ,KAAA,IAAY,CAAC,IAAI,IAAI,QAAQ;MACjD,kBAAkB,KAAK,cAAc,IAAI;MACzC,UAAU,SAAS,WAAW;MAC9B,aAAa,SAAS,SAAS;MAC/B,iBAAiB,IAAI;KACvB,SAAS,GAAG;MACV,IAAI,WAAW;MACf,YAAY,CAAC,CAAC;MACd,iBAAiB,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;KAC7D;IACF,EAAA,CAAG;IACH,aAAa;KAAE,YAAY;IAAK;GAClC,GAAG,CAAC,QAAQ,UAAU,WAAW,WAAW,CAAC;GAE7C,MAAM,WAAW,OAAO,gBAAuC;IAC7D,QAAQ,IAAI;IACZ,UAAU;IACV,IAAI;KACF,MAAM,MAAM,MAAM,SAAS,SAAS,UAAW,aAAa,aAAa,OAAO,UAAW,IAAI;KAC/F,QAAQ,IAAI,OAAO,IAAI,aAAa,IAAI,aAAa,IAAI,UAAU;IACrE,SAAS,GAAG;KACV,MAAM,UAAU,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC;KACzD,IAAI,QAAQ,SAAS,UAAU,KAAK,QAAQ,SAAS,iBAAiB,GACpE,iBAAiB,EAAE,SAAS,CAAC;UAE7B,iBAAiB,EAAE,gBAAgB,IAAI,OAAO,OAAO;KAEvD,SAAS;IACX,UAAU;KACR,QAAQ,KAAK;IACf;GACF;GAEA,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;IAAK,KAAK;IAAS,WAAU;IAA7B,UAAA;KACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;MACE,KAAK;MACL,MAAK;MACL,WAAU;MACV,OAAO,cAAc,KAAA,IAAY,EAAE,aAAa,IAAI,EAAE,eAAe;MACrE,cAAY,EAAE,aAAa;MAC3B,UAAU,cAAc,KAAA;MACxB,eAAe;OAAE,QAAQ,WAAW,UAAU,IAAI,SAAS;MAAE;MAE7D,UAAA,iBAAA,GAAA,kBAAA,IAAA,CAACE,sCAAAA,mBAAD,EAAmB,MAAM,GAAK,CAAA;KACxB,CAAA;KAEP,QAAQ,YAAY,cAAc,KAAA,KAAa,iBAAiB,SAAA,GAAQC,UAAAA,aAAAA,CACvE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;MACE,KAAK;MACL,WAAU;MACV,MAAK;MACL,OAAO;OAAE,MAAM,aAAa;OAAM,KAAK,aAAa;OAAK,OAAO,aAAa;MAAM;MAJrF,UAAA;OAME,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;QAAK,WAAU;QAAf,UAAA;SACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;UAAM,WAAU;UAAiB,UAAA,EAAE,WAAW;SAAQ,CAAA;SACrD,mBAAmB,QAAQ,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;UAAM,WAAU;UAAiB,OAAO,SAAS;UAAkB,UAAA;SAAS,CAAA;SACxG,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;UAAM,WAAU;UAAc,UAAA,UAAU;SAAY,CAAA;SACpD,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;UAAQ,MAAK;UAAS,WAAU;UAAgB,cAAY,EAAE,UAAU;UAAG,SAAS;UAClF,UAAA,iBAAA,GAAA,kBAAA,IAAA,CAACC,sCAAAA,oBAAD,EAAoB,MAAM,GAAK,CAAA;SACzB,CAAA;QACL;;OACJ,SAAS,WAAW,KACnB,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD;QAAG,WAAU;QAAiB,UAAA,iBAAiB,EAAE,YAAY;OAAK,CAAA;OAEnE,SAAS,QAAQ,MAAM,EAAE,KAAK,KAAK,MAAM,EAAE,CAAC,CAAC,KAAK,YACjD,iBAAA,GAAA,kBAAA,KAAA,CAAC,UAAD;QAEE,MAAK;QACL,MAAK;QACL,WAAU;QACV,UAAU;QACV,eAAe,KAAK,SAAS,QAAQ,IAAI;QAN3C,UAAA,CAQE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;SAAM,WAAU;SAAqB,UAAA,QAAQ;QAAW,CAAA,GACxD,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;SAAM,WAAU;SAAoB,UAAA,QAAQ;QAAc,CAAA,CACpD;OATD,GAAA,QAAQ,IASP,CACT;MACE;KACL,CAAA,GAAA,SAAS,IAAI;KAEd,QAAQ,UAAU,QACjB,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;MACE,OAAO,QAAQ;MACf,aAAa,QAAQ,eAAe;MACpC,aAAa,QAAQ,eAAe;MACpC,YAAY,QAAQ,cAAc,KAAA;MAC/B;MACK;MACG;MACA;MACX,SAAS;KACV,CAAA;IAEA;;EAET;EAYA,MAAM,cAAc;EACpB,MAAM,cAAc;;EAEpB,MAAM,wBAAwB;;EAE9B,MAAM,0BAA0B;EAEhC,SAAS,iBAAiB,QAA6C;GACrE,OAAO,WAAW,WAAW;IAAE,OAAO;IAAK,QAAQ;GAAI,IAAI;IAAE,OAAO;IAAK,QAAQ;GAAI;EACvF;EAEA,SAAS,WAAW,MAAsC;GACxD,MAAM,OAAO,KAAK,IAAI,aAAa,OAAO,aAAa,qBAAqB;GAC5E,MAAM,OAAO,KAAK,IAAI,aAAa,OAAO,cAAc,uBAAuB;GAC/E,OAAO;IACL,OAAO,KAAK,IAAI,KAAK,IAAI,aAAa,KAAK,MAAM,KAAK,KAAK,CAAC,GAAG,IAAI;IACnE,QAAQ,KAAK,IAAI,KAAK,IAAI,aAAa,KAAK,MAAM,KAAK,MAAM,CAAC,GAAG,IAAI;GACvE;EACF;;;;;;;EAQA,SAAS,mBAAmB,MAAkC,QAA6C;GACzG,IACE,SAAS,KAAA,KACN,CAAC,OAAO,SAAS,KAAK,KAAK,KAC3B,CAAC,OAAO,SAAS,KAAK,MAAM,KAC5B,KAAK,SAAS,KACd,KAAK,UAAU,GAElB,OAAO,iBAAiB,MAAM;GAEhC,OAAO,WAAW,IAAI;EACxB;;EAGA,SAAS,SAAS,OAUF;GACd,MAAM,EAAE,OAAO,GAAG,QAAQ,YAAY,YAAY;GAClD,MAAM,CAAC,MAAM,YAAA,GAAWJ,MAAAA,SAAAA,OAA+B,mBAAmB,MAAM,WAAW,MAAM,CAAC;GAGlG,MAAM,WAAA,GAAUC,MAAAA,OAAAA,CAAuB,IAAI;GAC3C,MAAM,eAAA,GAAcA,MAAAA,OAAAA,CAQV,IAAI;GACd,MAAM,CAAC,OAAO,aAAA,GAAYD,MAAAA,SAAAA,QAAgB;IACxC,QAAQ;IACR,UAAU;IACV,QAAQ;IACR,WAAW;IACX,WAAW;IACX,QAAQ;IACR,QAAQ;IACR,aAAa;IACb,aAAa;GACf,EAAE;GACF,MAAM,CAAC,MAAM,YAAA,GAAWA,MAAAA,SAAAA,CAA8B,QAAQ;GAC9D,MAAM,CAAC,YAAY,kBAAA,GAAiBA,MAAAA,SAAAA,CAAS,IAAI;GACjD,MAAM,CAAC,WAAW,iBAAA,GAAgBA,MAAAA,SAAAA,CAAwB,IAAI;GAC9D,MAAM,CAAC,SAAS,eAAA,GAAcA,MAAAA,SAAAA,CAAS,KAAK;GAC5C,MAAM,aAAA,GAAYC,MAAAA,OAAAA,CAA8B,IAAI;GACpD,MAAM,iBAAA,GAAgBA,MAAAA,OAAAA,CAAO,IAAI;GAIjC,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,cAAc,UAAU;GAC1B,GAAG,CAAC,UAAU,CAAC;GACf,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,IAAI,CAAC,cAAc,SAAS;IAC5B,MAAM,KAAK,UAAU;IACrB,IAAI,OAAO,MAAM;IACjB,GAAG,YAAY,GAAG;GACpB,GAAG;IAAC,MAAM;IAAQ,MAAM;IAAQ;GAAI,CAAC;GAGrC,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,IAAI,YAAY;IAChB,IAAI,SAAS;IACb,IAAI,SAAS;IACb,MAAM,OAAO,YAA2B;KACtC,IAAI;MACF,MAAM,OAAO,MAAM,SAAS,QAAQ,OAAO,QAAQ,MAAM;MACzD,IAAI,WAAW;MACf,SAAS,KAAK;MACd,SAAS,KAAK;MACd,UAAU,OAAO;OACf,QAAQ,KAAK;OACb,UAAU,KAAK;OACf,QAAQ,KAAK;OACb,WAAW,KAAK;OAChB,WAAW,KAAK;OAChB,QAAQ,KAAK,cAAc,KAAK,cAAc,EAAE,SAAS,KAAK;OAC9D,QAAQ,KAAK,cAAc,KAAK,cAAc,EAAE,SAAS,KAAK;OAC9D,aAAa,KAAK;OAClB,aAAa,KAAK;MACpB,EAAE;MACF,aAAa,IAAI;MACjB,IAAI,KAAK,WAAW,UAAU;MAC9B,QAAQ,OAAO,iBAAiB,KAAK,KAAK,GAAG,GAAG;KAClD,SAAS,GAAG;MACV,IAAI,WAAW;MACf,aAAa,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;MACvD,QAAQ,OAAO,iBAAiB,KAAK,KAAK,GAAG,GAAG;KAClD;IACF;IACA,IAAI;IACJ,KAAU;IACV,aAAa;KAAE,YAAY;KAAM,OAAO,aAAa,KAAK;IAAE;GAC9D,GAAG,CAAC,KAAK,CAAC;GAEV,MAAM,OAAO,YAA2B;IACtC,WAAW,IAAI;IACf,IAAI;KACF,MAAM,SAAS,QAAQ,KAAK;IAC9B,UAAU;KACR,QAAQ;IACV;GACF;GAMA,MAAM,oBAA0B;IAAE,QAAQ;GAAE;GAK5C,MAAM,oBAAoB,MAAmD;IAC3E,IAAI,EAAE,gBAAgB,SAAS;IAC/B,EAAE,eAAe;IACjB,YAAY;GACd;GAOA,MAAM,aAA+B,WAAW,WAAW,QAAQ;GAEnE,MAAM,uBAAuB,MAAgD;IAC3E,EAAE,eAAe;IACjB,EAAE,cAAc,kBAAkB,EAAE,SAAS;IAC7C,YAAY,UAAU;KACpB,WAAW,EAAE;KACb,QAAQ,EAAE;KACV,QAAQ,EAAE;KACV,QAAQ,QAAQ,QAAQ;KACxB,QAAQ,QAAQ,QAAQ;KACxB,MAAM,EAAE,cAAc,QAAQ;IAChC;GACF;GAEA,MAAM,uBAAuB,MAAgD;IAC3E,MAAM,OAAO,YAAY;IACzB,IAAI,SAAS,QAAQ,KAAK,cAAc,EAAE,WAAW;IACrD,MAAM,KAAK,EAAE,UAAU,KAAK;IAC5B,MAAM,KAAK,EAAE,UAAU,KAAK;IAC5B,MAAM,YAAY,KAAK,SAAS,UAAU,KAAK,SAAS;IACxD,MAAM,YAAY,KAAK,SAAS,YAAY,KAAK,SAAS;IAC1D,MAAM,OAAO,WAAW;KAEtB,OAAO,YAAY,KAAK,SAAS,KAAK,KAAK;KAE3C,QAAQ,YAAY,KAAK,UAAU,eAAe,QAAQ,CAAC,KAAK,MAAM,KAAK;IAC7E,CAAC;IACD,QAAQ,UAAU;IAClB,QAAQ,IAAI;GACd;GAEA,MAAM,qBAAqB,MAAgD;IACzE,MAAM,OAAO,YAAY;IACzB,IAAI,SAAS,QAAQ,KAAK,cAAc,EAAE,WAAW;IACrD,YAAY,UAAU;IACtB,IAAI,EAAE,cAAc,kBAAkB,EAAE,SAAS,GAC/C,EAAE,cAAc,sBAAsB,EAAE,SAAS;IAInD,SAAc,qBAAqB,QAAQ,OAAO,CAAC,CAAC,YAAY,KAAA,CAAS;GAC3E;;GAGA,MAAM,eAAe,UAAwC;IAC3D,eAAe;IACf,eAAe;IACf,eAAe;IACf,aAAa;IACb,iBAAiB;GACnB;GAEA,MAAM,SAAS,MAAM,WAAW;GAChC,MAAM,QAAQ,SAAS,GAAG,MAAM,YAAY,KAAK,EAAE,iBAAiB,MAAM,GAAG,MAAM,YAAY,KAAK,EAAE,gBAAgB,EAAE,IAAI,MAAM,YAAY,IAAI;GAElJ,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;IAAK,WAAW,qBAAqB;IAAU,MAAK;IAAS,cAAY;IAAO,OAAO;KAAE,OAAO,KAAK;KAAO,QAAQ,KAAK;IAAO;IAAhI,UAAA;KACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,UAAD;MAAQ,WAAU;MAAlB,UAAA;OACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;QAAM,WAAU;QAAwB;QAAQ,UAAA;OAAY,CAAA;OAC3D,eAAe,KAAA,KAAa,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;QAAM,WAAU;QAAkB,OAAO,SAAS;QAAc,UAAA;OAAS,CAAA;OACtG,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;QAAM,WAAU;QAAgB,UAAA,MAAM;OAAkB,CAAA;OACxD,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;QACE,MAAK;QACL,WAAU;QACV,UAAU,CAAC,UAAU;QACrB,OAAO,EAAE,SAAS;QAClB,cAAY,EAAE,SAAS;QACvB,eAAe,KAAK,KAAK;QAEzB,UAAA,iBAAA,GAAA,kBAAA,IAAA,CAACI,sCAAAA,gBAAD,EAAgB,MAAM,GAAK,CAAA;OACrB,CAAA;OACR,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;QACE,MAAK;QACL,WAAU;QACV,OAAO,EAAE,UAAU;QACnB,cAAY,EAAE,UAAU;QACxB,aAAa;QACb,SAAS;QAET,UAAA,iBAAA,GAAA,kBAAA,IAAA,CAACD,sCAAAA,oBAAD,EAAoB,MAAM,GAAK,CAAA;OACzB,CAAA;MACF;;KAER,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;MAAK,WAAU;MAAf,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;OACE,MAAK;OACL,WAAW,kBAAkB,SAAS,WAAW,yBAAyB;OAC1E,eAAe,QAAQ,QAAQ;OAE9B,UAAA,EAAE,WAAW;MACR,CAAA,GACR,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;OACE,MAAK;OACL,WAAW,kBAAkB,SAAS,WAAW,yBAAyB;OAC1E,eAAe,QAAQ,QAAQ;OAE9B,UAAA,EAAE,WAAW;MACR,CAAA,CACL;;KAEL,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;MACE,KAAK;MACL,WAAU;MACV,WAAW,MAAM;OACf,MAAM,KAAK,EAAE;OACb,MAAM,QAAQ,KAAK,IAAI,GAAG,eAAe,GAAG,YAAY,GAAG,YAAY,IAAI;OAC3E,cAAc,KAAK;MACrB;MAPF,UAAA,CASG,cAAc,QAAQ,iBAAA,GAAA,kBAAA,KAAA,CAAC,KAAD;OAAG,WAAU;OAAb,UAAA;QAA+B,EAAE,eAAe;QAAE;QAAG;OAAa;MACxF,CAAA,GAAA,SAAS,WACR,MAAM,WAAW,MAAM,CAAC,SAAS,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD;OAAG,WAAU;OAAkB,UAAA,EAAE,UAAU;MAAK,CAAA,IAC7E,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;OAAK,WAAU;OAAgB,UAAA,MAAM;MAAY,CAAA,IAErD,MAAM,WAAW,MAAM,CAAC,SAAS,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD;OAAG,WAAU;OAAkB,UAAA,EAAE,UAAU;MAAK,CAAA,IAC7E,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;OAAK,WAAU;OAAgB,UAAA,MAAM;MAAY,CAAA,CAEpD;;KAEL,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;MAAQ,WAAU;MAChB,UAAA,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;OAAM,WAAU;OACb,UAAA,SAAS,EAAE,iBAAiB,IAAI,GAAG,EAAE,gBAAgB,EAAE,KAAK,EAAE,aAAa,EAAE,GAAG,MAAM,YAAY,MAAM,MAAM,WAAW,OAAO,MAAM,MAAM,WAAW;MACpJ,CAAA;KACA,CAAA;KAIR,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;MAAK,WAAU;MAAuC,OAAO,EAAE,WAAW;MAAG,GAAI,YAAY,MAAM;KAAI,CAAA;KACtG,eAAe,QACZ,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;MAAK,WAAU;MAAsC,OAAO,EAAE,WAAW;MAAG,GAAI,YAAY,QAAQ;KAAI,CAAA,IACxG,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;MAAK,WAAU;MAAyC,OAAO,EAAE,WAAW;MAAG,GAAI,YAAY,QAAQ;KAAI,CAAA;KAC9G,eAAe,QACd,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;MAAK,WAAU;MAAmE,OAAO,EAAE,WAAW;MAAG,GAAI,YAAY,QAAQ;MAC/H,UAAA,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;OAAK,OAAM;OAAK,QAAO;OAAK,SAAQ;OAAY,eAAY;OAC1D,UAAA,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;QACE,GAAE;QACF,MAAK;QACL,QAAO;QACP,aAAY;QACZ,eAAc;QACd,gBAAe;OAChB,CAAA;MACE,CAAA;KACF,CAAA,IAEL,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;MAAK,WAAU;MAAmE,OAAO,EAAE,WAAW;MAAG,GAAI,YAAY,QAAQ;MAC/H,UAAA,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;OAAK,OAAM;OAAK,QAAO;OAAK,SAAQ;OAAY,eAAY;OAC1D,UAAA,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;QACE,GAAE;QACF,MAAK;QACL,QAAO;QACP,aAAY;QACZ,eAAc;QACd,gBAAe;OAChB,CAAA;MACE,CAAA;KACF,CAAA;IAEJ;;EAET;;;;;;;;;;;;;;;;;;;;;;;;EC7iBA,SAAgB,6BAA6B,EAAE,KAAyB;GACtE,MAAM,CAAC,OAAO,aAAA,GAAYE,MAAAA,SAAAA,CAAsB;IAAE,QAAQ;IAAO,SAAS,CAAC;IAAG,QAAQ;GAAS,CAAC;GAChG,MAAM,CAAC,OAAO,aAAA,GAAYA,MAAAA,SAAAA,CAAwB,IAAI;GAEtD,MAAM,aAAA,GAAYC,MAAAA,OAAAA,CAAO,CAAC;GAE1B,MAAM,OAAO,YAA2B;IACtC,IAAI;KACF,MAAM,CAAC,SAAS,YAAY,MAAM,QAAQ,IAAI,CAAC,SAAS,eAAe,GAAG,SAAS,YAAY,CAAC,CAAC;KACjG,SAAS;MAAE,QAAQ;MAAM;MAAS,QAAQ,SAAS;KAAY,CAAC;KAChE,SAAS,IAAI;IACf,SAAS,GAAG;KACV,UAAU,OAAO;MAAE,GAAG;MAAG,QAAQ;KAAK,EAAE;KACxC,SAAS,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;IACrD;GACF;GAEA,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,KAAU;GACZ,GAAG,CAAC,CAAC;;GAGL,MAAM,eAAe,OAAO,aAAqB,aAA4C;IAC3F,MAAM,QAAQ,EAAE,UAAU;IAC1B,IAAI;KACF,MAAM,OAAO,MAAM,SAAS,oBAAoB,aAAa,QAAQ;KACrE,IAAI,UAAU,UAAU,SAAS;KACjC,UAAU,OAAO;MACf,GAAG;MACH,QAAQ,KAAK;MACb,SAAS,EAAE,QAAQ,KAAK,MAAO,EAAE,gBAAgB,cAAc;OAAE,GAAG;OAAG;MAAS,IAAI,CAAE;KACxF,EAAE;KACF,SAAS,IAAI;IACf,SAAS,GAAG;KACV,SAAS,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;KAEnD,KAAU;IACZ;GACF;;GAGA,MAAM,kBAAkB,aAAqB,SAA4D;IACvG,MAAM,QAAQ,MAAM,QAAQ,MAAM,MAAM,EAAE,gBAAgB,WAAW;IACrE,IAAI,UAAU,KAAA,GAAW;IACzB,MAAM,WAAW,KAAK,MAAM,QAAQ;IAEpC,UAAU,OAAO;KACf,GAAG;KACH,SAAS,EAAE,QAAQ,KAAK,MAAO,EAAE,gBAAgB,cAAc;MAAE,GAAG;MAAG;KAAS,IAAI,CAAE;IACxF,EAAE;IACF,aAAkB,aAAa,QAAQ;GACzC;GAEA,MAAM,cAAc,aAAqB,OAAe,OAA2B,UAAwB;IACzG,eAAe,cAAc,YAC3B,QAAQ,KAAK,GAAG,MAAO,MAAM,QAAQ;KAAE,GAAG;MAAI,QAAQ;IAAM,IAAI,CAAE,CACpE;GACF;GAEA,MAAM,cAAc,gBAA8B;IAChD,eAAe,cAAc,YAAY,CAAC,GAAG,SAAS;KAAE,MAAM;KAAI,SAAS;IAAG,CAAC,CAAC;GAClF;GAEA,MAAM,iBAAiB,aAAqB,UAAwB;IAClE,eAAe,cAAc,YAAY,QAAQ,QAAQ,GAAG,MAAM,MAAM,KAAK,CAAC;GAChF;GAEA,MAAM,YAAY,OAAO,WAAgE;IACvF,UAAU,OAAO;KAAE,GAAG;KAAG;IAAO,EAAE;IAClC,IAAI;KACF,MAAM,OAAO,MAAM,SAAS,kBAAkB,MAAM;KACpD,UAAU,OAAO;MAAE,GAAG;MAAG,QAAQ,KAAK;KAAY,EAAE;IACtD,SAAS,GAAG;KACV,SAAS,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;KACnD,KAAU;IACZ;GACF;GAEA,MAAM,QAAQ,EAAE,WAAW;GAC3B,MAAM,cAAc,EAAE,YAAY;GAElC,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;IAAK,WAAU;IAAf,UAAA,CACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,UAAD;KAAQ,WAAU;KAAlB,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,MAAD;MAAI,WAAU;MAAiB,UAAA;KAAU,CAAA,GACzC,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD;MAAG,WAAU;MAAe,UAAA;KAAe,CAAA,CACrC;IACR,CAAA,GAAA,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;KAAK,WAAU;KAAf,UAAA;MACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD;OAAG,WAAU;OAAqB,UAAA,EAAE,WAAW;MAAK,CAAA;MAEpD,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;OAAK,WAAU;OAAf,UAAA;QACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;SAAM,WAAU;SAA4B,UAAA,EAAE,YAAY;QAAQ,CAAA;QAClE,iBAAA,GAAA,kBAAA,KAAA,CAAC,SAAD;SAAO,WAAU;SAAjB,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;UACE,MAAK;UACL,MAAK;UACL,SAAS,MAAM,WAAW;UAC1B,gBAAgB,KAAK,UAAU,QAAQ;SACxC,CAAA,GACA,EAAE,kBAAkB,CAChB;;QACP,iBAAA,GAAA,kBAAA,KAAA,CAAC,SAAD;SAAO,WAAU;SAAjB,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;UACE,MAAK;UACL,MAAK;UACL,SAAS,MAAM,WAAW;UAC1B,gBAAgB,KAAK,UAAU,QAAQ;SACxC,CAAA,GACA,EAAE,kBAAkB,CAChB;;OACJ;;MAEJ,UAAU,QAAQ,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;OAAK,WAAU;OAAf,UAAA;QAAoC,EAAE,eAAe;QAAE;QAAG;OAAW;;MAEvF,MAAM,UAAU,MAAM,QAAQ,WAAW,KACxC,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD;OAAG,WAAU;OAAqB,UAAA,EAAE,kBAAkB;MAAK,CAAA;MAG5D,MAAM,QAAQ,KAAK,UAClB,iBAAA,GAAA,kBAAA,KAAA,CAAC,WAAD;OAAiC,WAAU;OAA3C,UAAA;QACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,UAAD;SAAQ,WAAU;SAAlB,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;UAAM,WAAU;UAA+B,UAAA,MAAM,SAAS,MAAM,QAAQ,MAAM;SAAkB,CAAA,GACpG,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;UAAM,WAAU;UAA8B,UAAA,MAAM;SAAW,CAAA,CACzD;;QACP,MAAM,SAAS,WAAW,KACzB,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD;SAAG,WAAU;SAA2B,UAAA,EAAE,gBAAgB;QAAK,CAAA;QAEhE,MAAM,SAAS,KAAK,SAAS,UAC5B,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;SAA2C,WAAU;SAArD,UAAA;UACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;WACE,WAAU;WACV,aAAa,EAAE,UAAU;WACzB,OAAO,QAAQ;WACf,cAAY,EAAE,UAAU;WACxB,WAAW,MAAM,WAAW,MAAM,aAAa,OAAO,QAAQ,EAAE,OAAO,KAAK;UAC7E,CAAA;UACD,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;WACE,WAAU;WACV,aAAa,EAAE,aAAa;WAC5B,OAAO,QAAQ;WACf,cAAY,EAAE,aAAa;WAC3B,WAAW,MAAM,WAAW,MAAM,aAAa,OAAO,WAAW,EAAE,OAAO,KAAK;UAChF,CAAA;UACD,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;WACE,MAAK;WACL,WAAU;WACV,cAAY,EAAE,YAAY;WAC1B,OAAO,EAAE,YAAY;WACrB,eAAe,cAAc,MAAM,aAAa,KAAK;WAErD,UAAA,iBAAA,GAAA,kBAAA,IAAA,CAACC,sCAAAA,oBAAD,EAAoB,MAAM,GAAK,CAAA;UACzB,CAAA;SACL;QAxBK,GAAA,GAAG,MAAM,YAAY,GAAG,OAwB7B,CACN;QACD,iBAAA,GAAA,kBAAA,KAAA,CAAC,UAAD;SAAQ,MAAK;SAAS,WAAU;SAAsB,eAAe,WAAW,MAAM,WAAW;SAAjG,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAACC,sCAAAA,mBAAD,EAAmB,MAAM,GAAK,CAAA,GAC7B,EAAE,gBAAgB,CACb;;OACD;MAvCK,GAAA,MAAM,WAuCX,CACV;KACE;IACF,CAAA,CAAA;;EAET;;;;;;EC/LA,MAAa,YAAY;EAEzB,MAAa,KAAK;GAEhB,aAAa;GACb,eAAe;GACf,aAAa;GACb,YAAY;GACZ,cAAc;GAGd,WAAW;GACX,WAAW;GAGX,UAAU;GACV,WAAW;GACX,aAAa;GACb,WAAW;GACX,WAAW;GACX,WAAW;GACX,UAAU;GACV,YAAY;GACZ,QAAQ;GACR,iBAAiB;GACjB,gBAAgB;GAChB,SAAS;GACT,UAAU;GACV,WAAW;GACX,gBAAgB;GAChB,SAAS;GACT,gBAAgB;GAChB,eAAe;GACf,SAAS;GACT,aAAa;GAGb,eAAe;GACf,WAAW;GACX,YAAY;GACZ,WAAW;GACX,gBAAgB;GAChB,kBAAkB;GAClB,cAAc;GACd,gBAAgB;GAChB,gBAAgB;GAChB,UAAU;GACV,aAAa;GACb,YAAY;GACZ,eAAe;GACf,YAAY;GACZ,kBAAkB;GAClB,kBAAkB;EACpB;EAEA,MAAa,KAAsC;GACjD,aAAa;GACb,eAAe;GACf,aAAa;GACb,YAAY;GACZ,cAAc;GAEd,WAAW;GACX,WAAW;GAEX,UAAU;GACV,WAAW;GACX,aAAa;GACb,WAAW;GACX,WAAW;GACX,WAAW;GACX,UAAU;GACV,YAAY;GACZ,QAAQ;GACR,iBAAiB;GACjB,gBAAgB;GAChB,SAAS;GACT,UAAU;GACV,WAAW;GACX,gBAAgB;GAChB,SAAS;GACT,gBAAgB;GAChB,eAAe;GACf,SAAS;GACT,aAAa;GAEb,eAAe;GACf,WAAW;GACX,YAAY;GACZ,WAAW;GACX,gBAAgB;GAChB,kBAAkB;GAClB,cAAc;GACd,gBAAgB;GAChB,gBAAgB;GAChB,UAAU;GACV,aAAa;GACb,YAAY;GACZ,eAAe;GACf,YAAY;GACZ,kBAAkB;GAClB,kBAAkB;EACpB;;;;;;;;;;;;;;;EC3EA,MAAa,SAAS,CAAC,SAAS,QAAQ;;EAGxC,SAAgB,MAAM,KAA0B;GAC9C,IAAI,aAAa,IAAI,OAAO,SAAS,WAAW;IAAE;IAAI;GAAG,CAAC,GAAG,8BAA8B;GAC3F,MAAM,IAAI,IAAI,OAAO,KAAK,SAAS;GACnC,MAAM,QAAS,IAAoC;GAKnD,MAAM,OAAO,0BACX,MAAM,SAAS;IACb,MAAM;IACN,IAAI;IACJ,OAAO;IACP,aAAa,EAAE,eAAe;IAC9B,QAAQ;GACV,GAAG,4BAAqC,CAC1C;GAIA,MAAM,OAAO,+CACX,MAAM,SAAS;IACb,MAAM;IACN,IAAI;IACJ,OAAO;IACP,QAAQ;GACV,GAAG,yBAAkC,CACvC;EACF"}
1
+ {"version":3,"file":"client.js","names":["primitives","useSyncExternalStore","useState","useRef","createPortal","useState","useRef"],"sources":["../../../src/client/icons.tsx","../../../src/client/api.ts","../../../src/client/run-state.ts","../../../src/shared/contract.ts","../../../src/client/header.tsx","../../../src/client/settings-section.tsx","../../../src/client/locales.ts","../../../src/client/index.tsx"],"sourcesContent":["/**\n * Icon compatibility layer over `@deepseek-ai/dsh-client-ui-primitives`.\n *\n * DSH renamed this package's icon exports in 0.1.7: the size-suffixed names\n * (`IconPlayOutline16`) became weight-suffixed ones (`IconPlayOutlineRegular` /\n * `IconPlayOutlineMedium`, matching what the official header plugins use).\n *\n * A client bundle that statically imports a name the host no longer exports\n * receives `undefined`; rendering that element throws, the slot's per-entry\n * error boundary swallows the whole entry, and the surface silently vanishes —\n * exactly how the header ▶ button disappeared on 0.1.7.\n *\n * Resolving by name at runtime (new scheme → legacy scheme → bundled artwork)\n * keeps every surface rendering across DSH alphas. The bundled artwork is only\n * a last resort, so a future rename can never blank a control again.\n */\nimport type { ReactElement, ReactNode } from 'react'\nimport * as primitives from '@deepseek-ai/dsh-client-ui-primitives'\n\n/** Props both naming schemes accept (the legacy exports ignore extra fields). */\nexport interface IconProps {\n /** Rendered square size in CSS pixels. */\n size?: number\n className?: string\n}\n\ntype IconComponent = (props: IconProps) => ReactElement\n\nconst ICON_EXPORTS = primitives as unknown as Record<string, IconComponent | undefined>\n\n/** First export name that exists on the running host, else the bundled artwork. */\nfunction pickIcon(exportNames: readonly string[], fallback: IconComponent): IconComponent {\n for (const name of exportNames) {\n const candidate = ICON_EXPORTS[name]\n if (typeof candidate === 'function') return candidate\n }\n return fallback\n}\n\n/** Wrap one locally drawn 16×16 artwork as an icon component. */\nfunction localIcon(children: ReactNode): IconComponent {\n return function LocalIcon({ size = 16, className }: IconProps): ReactElement {\n return (\n <svg\n width={size}\n height={size}\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n className={className}\n aria-hidden=\"true\"\n >\n {children}\n </svg>\n )\n }\n}\n\n/** Run/play glyph for the header quick-run button. */\nexport const PlayIcon = pickIcon(\n ['IconPlayOutlineRegular', 'IconPlayOutlineMedium', 'IconPlayOutline16'],\n localIcon(\n <>\n <circle cx=\"8\" cy=\"8\" r=\"6.5\" stroke=\"currentColor\" />\n <path d=\"M6.6 5.9 10.4 8l-3.8 2.1Z\" fill=\"currentColor\" />\n </>,\n ),\n)\n\n/** Dismiss glyph (menu header and popup close). */\nexport const CloseIcon = pickIcon(\n ['IconCloseOutlineRegular', 'IconCloseOutlineMedium', 'IconCloseOutline16'],\n localIcon(\n <path d=\"M4.2 4.2 11.8 11.8M11.8 4.2 4.2 11.8\" stroke=\"currentColor\" strokeWidth=\"1.2\" strokeLinecap=\"round\" />,\n ),\n)\n\n/** Stop/kill glyph for a running command. */\nexport const StopIcon = pickIcon(\n ['IconStopFillRegular', 'IconStopFillMedium', 'IconStopFill16'],\n localIcon(<rect x=\"4.6\" y=\"4.6\" width=\"6.8\" height=\"6.8\" rx=\"1.6\" fill=\"currentColor\" />),\n)\n\n/** Add-row glyph in the settings page. */\nexport const PlusIcon = pickIcon(\n ['IconPlusOutlineRegular', 'IconPlusOutlineMedium', 'IconPlusOutline16'],\n localIcon(\n <path d=\"M8 3.4v9.2M3.4 8h9.2\" stroke=\"currentColor\" strokeWidth=\"1.2\" strokeLinecap=\"round\" />,\n ),\n)\n\n/** Remove-command glyph in the settings page. */\nexport const TrashIcon = pickIcon(\n ['IconTrashOutlineRegular', 'IconTrashOutlineMedium', 'IconTrashOutline16'],\n localIcon(\n <>\n <path d=\"M3.4 4.4h9.2\" stroke=\"currentColor\" strokeWidth=\"1.2\" strokeLinecap=\"round\" />\n <path\n d=\"M6.4 4.4V3.1h3.2v1.3M4.9 4.4l.6 8.5h5l.6-8.5\"\n stroke=\"currentColor\"\n strokeWidth=\"1.2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </>,\n ),\n)\n","/**\n * Browser client for the plugin's fenced JSON API\n * (/plugins/@fonlan/dsh-quick-commands/api/<method>). Same-origin fetch with\n * the task-kanban envelope shape: { ok, value } | { ok:false, error }.\n */\nimport type {\n ApiEnvelope,\n QuickCommandsSettings,\n QuickPopupSize,\n QuickRosterEntry,\n QuickRunView,\n} from '../shared/contract'\n\nexport class QuickCommandsApiError extends Error {\n constructor(readonly code: string, message: string) {\n super(message)\n }\n}\n\nasync function call<T>(method: string, payload: Record<string, unknown> = {}): Promise<T> {\n let response: Response\n try {\n response = await fetch(`/plugins/@fonlan/dsh-quick-commands/api/${method}`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n })\n } catch (error) {\n throw new QuickCommandsApiError('network', error instanceof Error ? error.message : String(error))\n }\n const parsed = (await response.json().catch(() => null)) as ApiEnvelope<T> | null\n if (!response.ok || parsed === null || parsed.ok !== true || parsed.value === undefined) {\n throw new QuickCommandsApiError(\n (parsed?.error?.code as string | undefined) ?? 'http',\n parsed?.error?.message ?? `HTTP ${response.status}`,\n )\n }\n return parsed.value as T\n}\n\n/** Current session cwd of a session row (SessionSummary carries cwd). */\nexport interface SessionRow {\n id: string\n cwd?: string\n}\n\n/** One delta read result (mirrors the host RunPollResult). */\nexport interface RunPoll {\n status: 'running' | 'exited'\n exitCode: number | null\n signal: string | null\n stdoutEnd: number\n stderrEnd: number\n stdoutDelta: string\n stderrDelta: string\n stdoutLossy: boolean\n stderrLossy: boolean\n}\n\nexport const quickApi = {\n settingsGet: () => call<QuickCommandsSettings>('settings.get'),\n settingsSetCommands: (workspaceId: string, commands: Array<{ name: string; command: string }>) =>\n call<QuickCommandsSettings>('settings.setCommands', { workspaceId, commands }),\n settingsSetAnchor: (anchor: QuickCommandsSettings['popupAnchor']) =>\n call<QuickCommandsSettings>('settings.setAnchor', { anchor }),\n settingsSetPopupSize: (size: QuickPopupSize) =>\n call<QuickCommandsSettings>('settings.setPopupSize', { width: size.width, height: size.height }),\n workspacesList: () => call<QuickRosterEntry[]>('workspaces.list'),\n runStart: (workspaceId: string, commandName: string, sessionCwd: string) =>\n call<QuickRunView>('run.start', { workspaceId, commandName, sessionCwd }),\n runPoll: (runId: string, stdoutFrom: number, stderrFrom: number) =>\n call<RunPoll>('run.poll', { runId, stdoutFrom, stderrFrom }),\n runKill: (runId: string) => call<{ killed: boolean }>('run.kill', { runId }),\n runList: (workspaceId: string) =>\n call<Array<{ runId: string; commandName: string; startedAt: number; status: 'running' | 'exited' }>>('run.list', { workspaceId }),\n}\n","/**\n * HMR-safe shared run state for the quick-commands header button and output\n * popup. Module-level fields live on one globalThis-backed record keyed with\n * Symbol.for so every client-bundle copy (client-hmr hot swap re-evaluates the\n * bundle without reloading the page) reads/writes the same state cell.\n *\n * The popup handles ONE run at a time (serial per workspace by design); a\n * second workspace can run in parallel with its own popup, so the store keys\n * popups by runId.\n */\nimport { useSyncExternalStore } from 'react'\n\nconst STATE_KEY = Symbol.for('@fonlan/dsh-quick-commands/run-state')\n\n/** One popup slot: menu open flag + the live run the popup streams. */\nexport interface RunPopoverState {\n /** Whether the header command menu is open. */\n menuOpen: boolean\n /** Active run id (undefined when no run is open). */\n runId: string | null\n /** Workspace id owning the active run (for kill/cwd display). */\n workspaceId: string | null\n /** Command display name. */\n commandName: string | null\n /** SSH host id when the run executes on a remote workspace (else null). */\n remoteHost: string | null\n /** Whether the run popup should stream (false while minimized). */\n streaming: boolean\n}\n\ninterface RunPopoverStateInternal extends RunPopoverState {\n listeners: Set<() => void>\n}\n\ninterface QuickCommandsState {\n popover: RunPopoverStateInternal\n /** Cached published snapshot — STABLE reference; rebuilt only after a patch. */\n snapshot: RunPopoverState | null\n}\n\nfunction getState(): QuickCommandsState {\n const g = globalThis as unknown as Record<symbol, QuickCommandsState | undefined>\n let state = g[STATE_KEY]\n if (state === undefined) {\n state = {\n popover: {\n menuOpen: false,\n runId: null,\n workspaceId: null,\n commandName: null,\n remoteHost: null,\n streaming: true,\n listeners: new Set(),\n },\n // Seed the cache with values our equality check will see as unchanged.\n snapshot: {\n menuOpen: false,\n runId: null,\n workspaceId: null,\n commandName: null,\n remoteHost: null,\n streaming: true,\n },\n }\n g[STATE_KEY] = state\n }\n return state\n}\n\nfunction notify(): void {\n // Invalidate the cached snapshot FIRST: useSyncExternalStore reads\n // getSnapshot after notification, so a new stable object must be ready.\n getState().snapshot = null\n for (const listener of getState().popover.listeners) listener()\n}\n\nfunction subscribe(cb: () => void): () => void {\n const listeners = getState().popover.listeners\n listeners.add(cb)\n return () => { listeners.delete(cb) }\n}\n\n/**\n * getSnapshot for useSyncExternalStore. MUST return a stable reference while\n * the underlying fields are unchanged (React compares with Object.is) — return\n * the CACHED object; only rebuild it when a patch invalidated it.\n */\nfunction snapshot(): RunPopoverState {\n const state = getState()\n if (state.snapshot !== null) return state.snapshot\n const p = state.popover\n state.snapshot = {\n menuOpen: p.menuOpen,\n runId: p.runId,\n workspaceId: p.workspaceId,\n commandName: p.commandName,\n remoteHost: p.remoteHost,\n streaming: p.streaming,\n }\n return state.snapshot\n}\n\nfunction patch(partial: Partial<RunPopoverState>): void {\n const state = getState()\n Object.assign(state.popover, partial)\n // Invalidate the cached snapshot so equality flips exactly on real changes.\n state.snapshot = null\n notify()\n}\n\nexport function useRunPopover(): RunPopoverState {\n return useSyncExternalStore(subscribe, snapshot, snapshot)\n}\n\nexport function openMenu(): void { patch({ menuOpen: true }) }\nexport function closeMenu(): void { patch({ menuOpen: false }) }\nexport function toggleMenu(open: boolean): void { patch({ menuOpen: open }) }\n\nexport function openRun(runId: string, workspaceId: string, commandName: string, remoteHost?: string): void {\n patch({ runId, workspaceId, commandName, remoteHost: remoteHost ?? null, menuOpen: false, streaming: true })\n}\n\nexport function closeRun(): void {\n patch({ runId: null, workspaceId: null, commandName: null, remoteHost: null, streaming: false })\n}\n","/**\n * Shared wire contracts for @fonlan/dsh-quick-commands.\n *\n * Browser-safe: no host-only imports live here. The client bundle imports\n * these shapes (type-only + runtime constants), so anything that must cross\n * the fenced JSON API keeps its plain-Object shape here.\n */\n\n/** One configured quick command inside a workspace. */\nexport interface QuickCommandEntry {\n /** Human-facing command name (unique within the workspace). */\n name: string\n /** Shell command line, executed through bash -c / pwsh -c. */\n command: string\n}\n\n/** Config slice bound to one workspace (keyed by workspaceId). */\nexport interface WorkspaceCommands {\n /** DSH workspace registry id this command set belongs to. */\n workspaceId: string\n /** Ordered command entries. */\n commands: QuickCommandEntry[]\n}\n\n/** Output popup dimensions in CSS pixels (user drag-resize state). */\nexport interface QuickPopupSize {\n width: number\n height: number\n}\n\n/** Full user-facing plugin settings document (settings namespace value). */\nexport interface QuickCommandsSettings {\n /** Command sets; a workspace may be absent (no commands configured). */\n workspaces: WorkspaceCommands[]\n /** Output popup anchor preference: fixed bottom-right or anchored at the button. */\n popupAnchor: 'corner' | 'button'\n /** Output popup size; absent until the user drags the resize handle (then per-anchor defaults apply). */\n popupSize?: QuickPopupSize\n}\n\n/**\n * The document's anchor as a usable literal. Wire data is defensive by\n * contract: an older host can still answer with schemastery's empty-object\n * resolution (`{}`) for this field, which must never reach a CSS class name or\n * a radio `checked` comparison.\n */\nexport function anchorOf(value: unknown): 'corner' | 'button' {\n return value === 'button' ? 'button' : 'corner'\n}\n\n/** Placeholder substitution values resolved for one execution. */\nexport interface QuickRunPlaceholders {\n /** Workspace root path (also the execution cwd). */\n workspace: string\n /** Session actual cwd (may be a subdirectory of the workspace). */\n cwd: string\n /** Workspace display title. */\n title: string\n}\n\n/** One live run view returned by `run.start`. */\nexport interface QuickRunView {\n runId: string\n workspaceId: string\n commandName: string\n /** Command line after placeholder substitution (display only). */\n resolvedCommand: string\n /** Non-empty when the command executes over SSH on a remote workspace host. */\n remoteHost?: string\n status: 'running' | 'exited'\n exitCode: number | null\n signal: string | null\n startedAt: number\n /** Total stdout bytes captured so far (offset currency). */\n stdoutEnd: number\n /** Total stderr bytes captured so far (offset currency). */\n stderrEnd: number\n /** stdout tail appended since `stdoutFrom` (whole tail when lossy). */\n stdoutDelta: string\n /** stderr tail appended since `stderrFrom` (whole tail when lossy). */\n stderrDelta: string\n /** True when the in-memory tail lost its head for this stream. */\n stdoutLossy: boolean\n stderrLossy: boolean\n}\n\n/** API response envelope (mirrors task-kanban's fenced fetch wrapper). */\nexport interface ApiEnvelope<T> {\n ok: boolean\n value?: T\n error?: { code: string; message: string }\n}\n\n/** Settings card view: command sets plus the workspace roster for display. */\nexport interface QuickRosterEntry {\n workspaceId: string\n path: string\n title: string\n /** SSH host id when the workspace runs remotely through dsh-ssh (else absent). */\n remoteHost?: string\n commands: QuickCommandEntry[]\n}\n","/**\n * Session-header quick-command surface: the ▶ play icon button (left of the\n * Session log pill), the command menu popover (current workspace's commands),\n * and the live output popup that streams stdout/stderr at ~200ms cadence.\n *\n * Props are the standard session/global kit of `conversation.session.header.utilities`\n * plus this plugin's injected hooks (anchor preference + resolver).\n */\nimport { useEffect, useLayoutEffect, useRef, useState } from 'react'\nimport { createPortal } from 'react-dom'\nimport { PlayIcon, CloseIcon, StopIcon } from './icons'\nimport { quickApi } from './api'\nimport { useRunPopover, openMenu, closeMenu, openRun, closeRun } from './run-state'\nimport { anchorOf, type QuickPopupSize } from '../shared/contract'\n\n/** Workspace row shape the standard useWorkspaces hook exposes. */\ninterface WorkspaceRow {\n workspaceId: string\n path: string\n title: string\n /** Session membership; the canonical link DSH itself resolves Workspaces by. */\n sessionIds?: string[]\n}\n\n/** Session list state subset we read (cwd + id). */\ninterface SessionRow {\n id: string\n cwd?: string\n}\n\ninterface HeaderProps {\n sessionId: string\n t: (key: string) => string\n useSessions?: (selector: (s: { byId: Record<string, SessionRow>; current?: string }) => unknown) => unknown\n useWorkspaces?: (selector: (s: { items: WorkspaceRow[] }) => unknown) => unknown\n}\n\ninterface MenuPosition {\n left: number\n top: number\n width: number\n}\n\nconst PHONE_MENU_RAIL_WIDTH = 56\nconst MENU_VIEWPORT_GAP = 12\nconst MENU_PREFERRED_WIDTH = 280\n\n/**\n * Position the portal menu beside its button without ever entering the phone\n * nav rail. Calculating actual pixels (rather than only CSS max-width) also\n * means it remains correct when the header itself sits in a clipped stacking\n * context created by a shell/mobile plugin.\n */\nfunction positionMenu(button: HTMLButtonElement): MenuPosition {\n const rect = button.getBoundingClientRect()\n const narrow = window.matchMedia('(max-width: 767px)').matches\n const minLeft = narrow ? PHONE_MENU_RAIL_WIDTH + MENU_VIEWPORT_GAP : MENU_VIEWPORT_GAP\n const availableWidth = Math.max(0, window.innerWidth - minLeft - MENU_VIEWPORT_GAP)\n const width = Math.min(MENU_PREFERRED_WIDTH, availableWidth)\n const maxLeft = Math.max(minLeft, window.innerWidth - MENU_VIEWPORT_GAP - width)\n return {\n // The old in-header popover used `right:12px`; retain that visual anchor\n // but clamp its left edge to the usable side of the rail.\n left: Math.max(minLeft, Math.min(rect.right - MENU_VIEWPORT_GAP - width, maxLeft)),\n top: Math.min(rect.bottom + 6, Math.max(MENU_VIEWPORT_GAP, window.innerHeight - 72)),\n width,\n }\n}\n\n/** Longest-path workspace match for a cwd (subdirectory allowed). */\nfunction matchWorkspace(cwd: string | undefined, workspaces: WorkspaceRow[]): WorkspaceRow | undefined {\n if (cwd === undefined) return undefined\n let best: WorkspaceRow | undefined\n for (const ws of workspaces) {\n const norm = ws.path.replace(/\\\\/g, '/').replace(/\\/+$/, '')\n const nc = cwd.replace(/\\\\/g, '/')\n if (nc === norm || nc.startsWith(norm + '/')) {\n if (best === undefined || norm.length > best.path.length) best = ws\n }\n }\n return best\n}\n\n/** Resolve the current session's workspace from the standard kits. */\nfunction useCurrentWorkspace(\n sessionId: string,\n useSessions?: HeaderProps['useSessions'],\n useWorkspaces?: HeaderProps['useWorkspaces'],\n): { workspace: WorkspaceRow | undefined; cwd: string | undefined } {\n const row = useSessions?.((s) => s.byId[sessionId]) as SessionRow | undefined\n const workspaces = useWorkspaces?.((s) => s.items) as WorkspaceRow[] | undefined\n const items = workspaces ?? []\n // Membership first (how DSH core resolves a session's Workspace, and the only\n // link that survives when a session runs outside the Workspace root), then\n // the longest cwd prefix as the fallback for hosts without membership data.\n const byMembership = items.find((ws) => ws.sessionIds?.includes(sessionId) === true)\n return { workspace: byMembership ?? matchWorkspace(row?.cwd, items), cwd: row?.cwd }\n}\n\n/** Header icon button + command menu + live output popup. */\nexport function QuickCommandsHeaderAction(props: HeaderProps): JSX.Element {\n const { sessionId, t } = props\n const { workspace, cwd } = useCurrentWorkspace(sessionId, props.useSessions, props.useWorkspaces)\n const popover = useRunPopover()\n const [commands, setCommands] = useState<Array<{ name: string; command: string }>>([])\n const [menuLoadError, setMenuLoadError] = useState<string | null>(null)\n const [menuRemoteHost, setMenuRemoteHost] = useState<string | null>(null)\n const [busy, setBusy] = useState(false)\n const [anchor, setAnchor] = useState<'corner' | 'button'>('corner')\n const [popupSize, setPopupSize] = useState<QuickPopupSize | undefined>(undefined)\n const wrapRef = useRef<HTMLDivElement | null>(null)\n const buttonRef = useRef<HTMLButtonElement | null>(null)\n const menuRef = useRef<HTMLDivElement | null>(null)\n const [menuPosition, setMenuPosition] = useState<MenuPosition | null>(null)\n\n // Click-outside dismissal: the command menu is portaled to document.body so\n // it escapes the mobile shell's clipped/z-indexed header context. Treat both\n // its portal node and its original header wrap as inside targets.\n useEffect(() => {\n if (!popover.menuOpen) return\n const onPointerDown = (e: PointerEvent): void => {\n const wrap = wrapRef.current\n const menu = menuRef.current\n if (!(e.target instanceof Node)) return\n if (wrap?.contains(e.target) || menu?.contains(e.target)) return\n closeMenu()\n }\n document.addEventListener('pointerdown', onPointerDown)\n return () => { document.removeEventListener('pointerdown', onPointerDown) }\n }, [popover.menuOpen])\n\n // Recalculate against the actual header button whenever the menu opens and\n // whenever a viewport change moves it (rotation, browser chrome resize).\n useLayoutEffect(() => {\n if (!popover.menuOpen) {\n setMenuPosition(null)\n return\n }\n const update = (): void => {\n const button = buttonRef.current\n if (button !== null) setMenuPosition(positionMenu(button))\n }\n update()\n window.addEventListener('resize', update)\n window.addEventListener('scroll', update, true)\n window.visualViewport?.addEventListener('resize', update)\n return () => {\n window.removeEventListener('resize', update)\n window.removeEventListener('scroll', update, true)\n window.visualViewport?.removeEventListener('resize', update)\n }\n }, [popover.menuOpen])\n\n // Load the workspace's command list + anchor when the menu opens.\n useEffect(() => {\n if (!popover.menuOpen) return\n let cancelled = false\n void (async () => {\n try {\n const [entries, settings] = await Promise.all([quickApi.workspacesList(), quickApi.settingsGet()])\n if (cancelled) return\n const own = entries.find((e) => e.workspaceId === workspace?.workspaceId)\n setCommands(own === undefined ? [] : own.commands)\n setMenuRemoteHost(own?.remoteHost ?? null)\n setAnchor(anchorOf(settings.popupAnchor))\n setPopupSize(settings.popupSize)\n setMenuLoadError(null)\n } catch (e) {\n if (cancelled) return\n setCommands([])\n setMenuLoadError(e instanceof Error ? e.message : String(e))\n }\n })()\n return () => { cancelled = true }\n }, [popover.menuOpen, workspace?.workspaceId])\n\n const startRun = async (commandName: string): Promise<void> => {\n setBusy(true)\n closeMenu()\n try {\n const run = await quickApi.runStart(workspace!.workspaceId, commandName, cwd ?? workspace!.path)\n openRun(run.runId, run.workspaceId, run.commandName, run.remoteHost)\n } catch (e) {\n const message = e instanceof Error ? e.message : String(e)\n if (message.includes('run-busy') || message.includes('already running')) {\n setMenuLoadError(t('runBusy'))\n } else {\n setMenuLoadError(t('runStartFailed') + ': ' + message)\n }\n openMenu()\n } finally {\n setBusy(false)\n }\n }\n\n return (\n <div ref={wrapRef} className=\"qc-head-wrap\">\n <button\n ref={buttonRef}\n type=\"button\"\n className=\"qc-head-btn\"\n title={workspace === undefined ? t('noWorkspace') : t('buttonTooltip')}\n aria-label={t('buttonTitle')}\n disabled={workspace === undefined}\n onClick={() => { popover.menuOpen ? closeMenu() : openMenu() }}\n >\n <PlayIcon size={15} />\n </button>\n\n {popover.menuOpen && workspace !== undefined && menuPosition !== null && createPortal(\n <div\n ref={menuRef}\n className=\"qc-menu qc-menu-portal\"\n role=\"menu\"\n style={{ left: menuPosition.left, top: menuPosition.top, width: menuPosition.width }}\n >\n <div className=\"qc-menu-head\">\n <span className=\"qc-menu-title\">{t('menuTitle')}</span>\n {menuRemoteHost !== null && <span className=\"qc-menu-remote\" title={`SSH · ${menuRemoteHost}`}>SSH</span>}\n <span className=\"qc-menu-ws\">{workspace.title}</span>\n <button type=\"button\" className=\"qc-menu-close\" aria-label={t('runClose')} onClick={closeMenu}>\n <CloseIcon size={12} />\n </button>\n </div>\n {commands.length === 0 && (\n <p className=\"qc-menu-empty\">{menuLoadError ?? t('noCommands')}</p>\n )}\n {commands.filter((c) => c.name.trim() !== '').map((command) => (\n <button\n key={command.name}\n type=\"button\"\n role=\"menuitem\"\n className=\"qc-menu-item\"\n disabled={busy}\n onClick={() => void startRun(command.name)}\n >\n <span className=\"qc-menu-item-name\">{command.name}</span>\n <span className=\"qc-menu-item-cmd\">{command.command}</span>\n </button>\n ))}\n </div>\n , document.body)}\n\n {popover.runId !== null && (\n <RunPopup\n runId={popover.runId}\n workspaceId={popover.workspaceId ?? ''}\n commandName={popover.commandName ?? ''}\n remoteHost={popover.remoteHost ?? undefined}\n t={t}\n anchor={anchor}\n popupSize={popupSize}\n sessionId={sessionId}\n onClose={closeRun}\n />\n )}\n </div>\n )\n}\n\n// ── popup drag-resize ───────────────────────────────────────────────────────\n// The popup pins its RIGHT edge (both anchors), plus its BOTTOM edge (corner\n// anchor) or TOP edge (button anchor, pinned below the button). So the MOVABLE\n// borders are the LEFT border (width) and the TOP border (corner anchor) /\n// BOTTOM border (button anchor) (height); the free corner (top-left /\n// bottom-left) carries a diagonal grip for simultaneous two-axis resizing.\n// Every handle follows the cursor edge-for-edge — pulling the left border LEFT\n// widens, pulling the top border UP grows taller — nothing feels inverted.\n// The size is clamped to viewport bounds and persisted to settings.\n\nconst POPUP_MIN_W = 320\nconst POPUP_MIN_H = 200\n/** Total horizontal margin kept around the popup (both anchors). */\nconst POPUP_VIEWPORT_MARGIN = 40\n/** Vertical space reserved for header/footer chrome (both anchors). */\nconst POPUP_VIEWPORT_RESERVED = 120\n\nfunction popupDefaultSize(anchor: 'corner' | 'button'): QuickPopupSize {\n return anchor === 'corner' ? { width: 520, height: 340 } : { width: 440, height: 300 }\n}\n\nfunction popupClamp(size: QuickPopupSize): QuickPopupSize {\n const maxW = Math.max(POPUP_MIN_W, window.innerWidth - POPUP_VIEWPORT_MARGIN)\n const maxH = Math.max(POPUP_MIN_H, window.innerHeight - POPUP_VIEWPORT_RESERVED)\n return {\n width: Math.min(Math.max(POPUP_MIN_W, Math.round(size.width)), maxW),\n height: Math.min(Math.max(POPUP_MIN_H, Math.round(size.height)), maxH),\n }\n}\n\n/**\n * Initial popup size: the persisted one when it holds sane numbers, else the\n * per-anchor default. Defensive because schemastery resolves a MISSING object\n * field to `{}` (object schemas carry an implicit `default: {}`), so a\n * never-resized popup can arrive as `popupSize: {}` from settings.get().\n */\nfunction popupSizeOrDefault(size: QuickPopupSize | undefined, anchor: 'corner' | 'button'): QuickPopupSize {\n if (\n size === undefined\n || !Number.isFinite(size.width)\n || !Number.isFinite(size.height)\n || size.width <= 0\n || size.height <= 0\n ) {\n return popupDefaultSize(anchor)\n }\n return popupClamp(size)\n}\n\n/** The streaming output popup. Polls stdout/stderr deltas every ~200ms. */\nfunction RunPopup(props: {\n runId: string\n workspaceId: string\n commandName: string\n remoteHost?: string\n t: (key: string) => string\n anchor: 'corner' | 'button'\n popupSize?: QuickPopupSize\n sessionId: string\n onClose: () => void\n}): JSX.Element {\n const { runId, t, anchor, remoteHost, onClose } = props\n const [size, setSize] = useState<QuickPopupSize>(() => popupSizeOrDefault(props.popupSize, anchor))\n // Mirrors `size` for drag handlers: pointer events can arrive faster than\n // React commits, so pointerup must read the latest value, not the render's.\n const sizeRef = useRef<QuickPopupSize>(size)\n const sizeDragRef = useRef<{\n pointerId: number\n startX: number\n startY: number\n startW: number\n startH: number\n /** Which handle started the drag (controls which axes move). */\n mode: 'left' | 'height' | 'corner'\n } | null>(null)\n const [state, setState] = useState(() => ({\n status: 'running' as 'running' | 'exited',\n exitCode: null as number | null,\n signal: null as string | null,\n stdoutEnd: 0,\n stderrEnd: 0,\n stdout: '',\n stderr: '',\n stdoutLossy: false,\n stderrLossy: false,\n }))\n const [view, setView] = useState<'stdout' | 'stderr'>('stdout')\n const [autoFollow, setAutoFollow] = useState(true)\n const [pollError, setPollError] = useState<string | null>(null)\n const [closing, setClosing] = useState(false)\n const outputRef = useRef<HTMLDivElement | null>(null)\n const autoFollowRef = useRef(true)\n\n // Auto-follow: scroll the output pane to the bottom on new output while the\n // user has not scrolled up (autoFollow mirror keeps the effect cheap).\n useEffect(() => {\n autoFollowRef.current = autoFollow\n }, [autoFollow])\n useEffect(() => {\n if (!autoFollowRef.current) return\n const el = outputRef.current\n if (el === null) return\n el.scrollTop = el.scrollHeight\n }, [state.stdout, state.stderr, view])\n\n // Poll deltas; stop polling at exit (the run stays readable in memory).\n useEffect(() => {\n let cancelled = false\n let outEnd = 0\n let errEnd = 0\n const tick = async (): Promise<void> => {\n try {\n const poll = await quickApi.runPoll(runId, outEnd, errEnd)\n if (cancelled) return\n outEnd = poll.stdoutEnd\n errEnd = poll.stderrEnd\n setState((s) => ({\n status: poll.status,\n exitCode: poll.exitCode,\n signal: poll.signal,\n stdoutEnd: poll.stdoutEnd,\n stderrEnd: poll.stderrEnd,\n stdout: poll.stdoutLossy ? poll.stdoutDelta : s.stdout + poll.stdoutDelta,\n stderr: poll.stderrLossy ? poll.stderrDelta : s.stderr + poll.stderrDelta,\n stdoutLossy: poll.stdoutLossy,\n stderrLossy: poll.stderrLossy,\n }))\n setPollError(null)\n if (poll.status === 'exited') return\n timer = window.setTimeout(() => void tick(), 200)\n } catch (e) {\n if (cancelled) return\n setPollError(e instanceof Error ? e.message : String(e))\n timer = window.setTimeout(() => void tick(), 500)\n }\n }\n let timer: number\n void tick()\n return () => { cancelled = true; window.clearTimeout(timer) }\n }, [runId])\n\n const kill = async (): Promise<void> => {\n setClosing(true)\n try {\n await quickApi.runKill(runId)\n } finally {\n onClose()\n }\n }\n\n // Closing the output surface must not terminate a still-running command:\n // the dedicated stop control owns that destructive action. In particular,\n // an X in a compact touch header is expected to dismiss immediately rather\n // than reveal a second confirmation row below the visible viewport.\n const closeOutput = (): void => { onClose() }\n\n // Some mobile webviews occasionally fail to synthesize `click` for compact\n // header controls after a touch gesture. Handle touch pointer-up directly;\n // click remains the keyboard and mouse activation path.\n const onClosePointerUp = (e: React.PointerEvent<HTMLButtonElement>): void => {\n if (e.pointerType !== 'touch') return\n e.preventDefault()\n closeOutput()\n }\n\n // ── drag-resize: pointer capture on the handles keeps moving/cancelling\n // events flowing even when the cursor leaves the handle or the window.\n // Which border is free to move depends on the anchor: the corner anchor\n // pins bottom+right (top border moves for height), the button anchor pins\n // top+right (bottom border moves for height).\n const heightFree: 'top' | 'bottom' = anchor === 'corner' ? 'top' : 'bottom'\n\n const onResizePointerDown = (e: React.PointerEvent<HTMLDivElement>): void => {\n e.preventDefault() // keep the handle from starting a text-selection gesture\n e.currentTarget.setPointerCapture(e.pointerId)\n sizeDragRef.current = {\n pointerId: e.pointerId,\n startX: e.clientX,\n startY: e.clientY,\n startW: sizeRef.current.width,\n startH: sizeRef.current.height,\n mode: e.currentTarget.dataset.resize as 'left' | 'height' | 'corner',\n }\n }\n\n const onResizePointerMove = (e: React.PointerEvent<HTMLDivElement>): void => {\n const drag = sizeDragRef.current\n if (drag === null || drag.pointerId !== e.pointerId) return\n const dx = e.clientX - drag.startX\n const dy = e.clientY - drag.startY\n const controlsW = drag.mode === 'left' || drag.mode === 'corner'\n const controlsH = drag.mode === 'height' || drag.mode === 'corner'\n const next = popupClamp({\n // Left border follows the cursor: dragging LEFT widens, RIGHT narrows.\n width: controlsW ? drag.startW - dx : drag.startW,\n // Top-free (corner): dragging UP grows. Bottom-free (button): DOWN grows.\n height: controlsH ? drag.startH + (heightFree === 'top' ? -dy : dy) : drag.startH,\n })\n sizeRef.current = next\n setSize(next)\n }\n\n const onResizePointerUp = (e: React.PointerEvent<HTMLDivElement>): void => {\n const drag = sizeDragRef.current\n if (drag === null || drag.pointerId !== e.pointerId) return\n sizeDragRef.current = null\n if (e.currentTarget.hasPointerCapture(e.pointerId)) {\n e.currentTarget.releasePointerCapture(e.pointerId)\n }\n // Persist best-effort: a failed save just means the next popup reopens at\n // the default size (or the last successfully saved one).\n void quickApi.settingsSetPopupSize(sizeRef.current).catch(() => undefined)\n }\n\n /** Pointer-handler trio for one resize handle (mode = which axes it drives). */\n const resizeProps = (mode: 'left' | 'height' | 'corner') => ({\n 'data-resize': mode,\n onPointerDown: onResizePointerDown,\n onPointerMove: onResizePointerMove,\n onPointerUp: onResizePointerUp,\n onPointerCancel: onResizePointerUp,\n })\n\n const active = state.status === 'running'\n const title = active ? `${props.commandName} · ${t('runStateRunning')}` : `${props.commandName} · ${t('runStateExited')} (${state.exitCode ?? '?'})`\n\n return (\n <div className={`qc-popup qc-popup-${anchor}`} role=\"dialog\" aria-label={title} style={{ width: size.width, height: size.height }}>\n <header className=\"qc-popup-head\">\n <span className=\"qc-popup-title\" title={title}>{title}</span>\n {remoteHost !== undefined && <span className=\"qc-popup-remote\" title={`SSH · ${remoteHost}`}>SSH</span>}\n <span className=\"qc-popup-cmd\">{props.commandName}</span>\n <button\n type=\"button\"\n className=\"qc-popup-icon qc-popup-kill\"\n disabled={!active || closing}\n title={t('runKill')}\n aria-label={t('runKill')}\n onClick={() => void kill()}\n >\n <StopIcon size={14} />\n </button>\n <button\n type=\"button\"\n className=\"qc-popup-icon qc-popup-close\"\n title={t('runClose')}\n aria-label={t('runClose')}\n onPointerUp={onClosePointerUp}\n onClick={closeOutput}\n >\n <CloseIcon size={14} />\n </button>\n </header>\n\n <div className=\"qc-popup-tabs\">\n <button\n type=\"button\"\n className={'qc-popup-tab' + (view === 'stdout' ? ' qc-popup-tab-active' : '')}\n onClick={() => setView('stdout')}\n >\n {t('runStdout')}\n </button>\n <button\n type=\"button\"\n className={'qc-popup-tab' + (view === 'stderr' ? ' qc-popup-tab-active' : '')}\n onClick={() => setView('stderr')}\n >\n {t('runStderr')}\n </button>\n </div>\n\n <div\n ref={outputRef}\n className=\"qc-popup-output\"\n onScroll={(e) => {\n const el = e.currentTarget\n const atEnd = Math.abs(el.scrollHeight - el.scrollTop - el.clientHeight) < 24\n setAutoFollow(atEnd)\n }}\n >\n {pollError !== null && <p className=\"qc-popup-error\">{t('runPollFailed')}: {pollError}</p>}\n {view === 'stdout' ? (\n state.stdout === '' && !active ? <p className=\"qc-popup-empty\">{t('runEmpty')}</p>\n : <pre className=\"qc-popup-pre\">{state.stdout}</pre>\n ) : (\n state.stderr === '' && !active ? <p className=\"qc-popup-empty\">{t('runEmpty')}</p>\n : <pre className=\"qc-popup-pre\">{state.stderr}</pre>\n )}\n </div>\n\n <footer className=\"qc-popup-foot\">\n <span className=\"qc-popup-state\">\n {active ? t('runStateRunning') : `${t('runStateExited')} · ${t('runExitCode')} ${state.exitCode ?? '-'}${state.signal !== null ? ` · ${state.signal}` : ''}`}\n </span>\n </footer>\n\n {/* Resize handles: left border = width; top (corner anchor) / bottom\n (button anchor) border = height; free corner = diagonal grip. */}\n <div className=\"qc-popup-resize qc-popup-resize-left\" title={t('runResize')} {...resizeProps('left')} />\n {heightFree === 'top'\n ? <div className=\"qc-popup-resize qc-popup-resize-top\" title={t('runResize')} {...resizeProps('height')} />\n : <div className=\"qc-popup-resize qc-popup-resize-bottom\" title={t('runResize')} {...resizeProps('height')} />}\n {heightFree === 'top' ? (\n <div className=\"qc-popup-resize qc-popup-resize-corner qc-popup-resize-corner-tl\" title={t('runResize')} {...resizeProps('corner')}>\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 12 12\" aria-hidden=\"true\">\n <path\n d=\"M9 9 3 3 M9 5.5 5.5 3 M5.5 9 3 5.5\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.4\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </div>\n ) : (\n <div className=\"qc-popup-resize qc-popup-resize-corner qc-popup-resize-corner-bl\" title={t('runResize')} {...resizeProps('corner')}>\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 12 12\" aria-hidden=\"true\">\n <path\n d=\"M9 3 3 9 M9 6.5 6.5 9 M5.5 3 3 5.5\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.4\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </div>\n )}\n </div>\n )\n}\n","/**\n * Quick-commands settings page (设置 → 侧栏「快捷命令」).\n *\n * Registers into the \"settings.section\" list slot, so the page owns one entry\n * in the settings sidebar and renders in the settings panel's content column.\n * The header is static (title + one-line summary) over the always-visible\n * body: the workspace / command-set editor and the popup anchor preference.\n * Body edits instant-save per change; partial rows (empty name/command)\n * persist while the user types.\n */\nimport { useEffect, useRef, useState } from 'react'\nimport { PlusIcon, TrashIcon } from './icons'\nimport { quickApi } from './api'\nimport { anchorOf, type QuickRosterEntry, type QuickCommandsSettings } from '../shared/contract'\nimport './settings-section.css'\n\ninterface Props {\n t: (key: string) => string\n}\n\ninterface RosterState {\n loaded: boolean\n entries: QuickRosterEntry[]\n anchor: QuickCommandsSettings['popupAnchor']\n}\n\ntype CommandDraft = { name: string; command: string }\n\nexport function QuickCommandsSettingsSection({ t }: Props): JSX.Element {\n const [state, setState] = useState<RosterState>({ loaded: false, entries: [], anchor: 'corner' })\n const [error, setError] = useState<string | null>(null)\n // Guard concurrent saves: the last write wins via a monotonically bumped token.\n const saveToken = useRef(0)\n\n const load = async (): Promise<void> => {\n try {\n const [entries, settings] = await Promise.all([quickApi.workspacesList(), quickApi.settingsGet()])\n setState({ loaded: true, entries, anchor: anchorOf(settings.popupAnchor) })\n setError(null)\n } catch (e) {\n setState((s) => ({ ...s, loaded: true }))\n setError(e instanceof Error ? e.message : String(e))\n }\n }\n\n useEffect(() => {\n void load()\n }, [])\n\n /** Instant-save the workspace's whole command set; optimistic UI. */\n const saveCommands = async (workspaceId: string, commands: CommandDraft[]): Promise<void> => {\n const token = ++saveToken.current\n try {\n const next = await quickApi.settingsSetCommands(workspaceId, commands)\n if (token !== saveToken.current) return\n setState((s) => ({\n ...s,\n anchor: next.popupAnchor,\n entries: s.entries.map((e) => (e.workspaceId === workspaceId ? { ...e, commands } : e)),\n }))\n setError(null)\n } catch (e) {\n setError(e instanceof Error ? e.message : String(e))\n // Re-sync to server truth on failure.\n void load()\n }\n }\n\n /** Compute the next command list for a workspace, THEN save + update state. */\n const mutateCommands = (workspaceId: string, next: (current: CommandDraft[]) => CommandDraft[]): void => {\n const entry = state.entries.find((e) => e.workspaceId === workspaceId)\n if (entry === undefined) return\n const commands = next(entry.commands)\n // Optimistic update first (row stays visible immediately), then persist.\n setState((s) => ({\n ...s,\n entries: s.entries.map((e) => (e.workspaceId === workspaceId ? { ...e, commands } : e)),\n }))\n void saveCommands(workspaceId, commands)\n }\n\n const patchEntry = (workspaceId: string, index: number, field: 'name' | 'command', value: string): void => {\n mutateCommands(workspaceId, (current) =>\n current.map((c, i) => (i === index ? { ...c, [field]: value } : c)),\n )\n }\n\n const addCommand = (workspaceId: string): void => {\n mutateCommands(workspaceId, (current) => [...current, { name: '', command: '' }])\n }\n\n const removeCommand = (workspaceId: string, index: number): void => {\n mutateCommands(workspaceId, (current) => current.filter((_, i) => i !== index))\n }\n\n const setAnchor = async (anchor: QuickCommandsSettings['popupAnchor']): Promise<void> => {\n setState((s) => ({ ...s, anchor }))\n try {\n const next = await quickApi.settingsSetAnchor(anchor)\n setState((s) => ({ ...s, anchor: anchorOf(next.popupAnchor) }))\n } catch (e) {\n setError(e instanceof Error ? e.message : String(e))\n void load()\n }\n }\n\n const title = t('cardTitle')\n const description = t('sectionSub')\n\n return (\n <div className=\"qc-page\">\n <header className=\"qc-page-head\">\n <h3 className=\"qc-page-title\">{title}</h3>\n <p className=\"qc-page-sub\">{description}</p>\n </header>\n <div className=\"qc-settings-body\">\n <p className=\"qc-settings-intro\">{t('cardIntro')}</p>\n\n <div className=\"qc-settings-anchor\">\n <span className=\"qc-settings-anchor-label\">{t('cardAnchor')}</span>\n <label className=\"qc-settings-radio\">\n <input\n type=\"radio\"\n name=\"qc-anchor\"\n checked={state.anchor === 'corner'}\n onChange={() => void setAnchor('corner')}\n />\n {t('cardAnchorCorner')}\n </label>\n <label className=\"qc-settings-radio\">\n <input\n type=\"radio\"\n name=\"qc-anchor\"\n checked={state.anchor === 'button'}\n onChange={() => void setAnchor('button')}\n />\n {t('cardAnchorButton')}\n </label>\n </div>\n\n {error !== null && <div className=\"qc-settings-error\">{t('cardSaveError')}: {error}</div>}\n\n {state.loaded && state.entries.length === 0 && (\n <p className=\"qc-settings-empty\">{t('cardNoWorkspaces')}</p>\n )}\n\n {state.entries.map((entry) => (\n <section key={entry.workspaceId} className=\"qc-settings-workspace\">\n <header className=\"qc-settings-workspace-head\">\n <span className=\"qc-settings-workspace-title\">{entry.title || entry.path || entry.workspaceId}</span>\n <span className=\"qc-settings-workspace-path\">{entry.path}</span>\n </header>\n {entry.commands.length === 0 && (\n <p className=\"qc-settings-no-commands\">{t('cardNoCommands')}</p>\n )}\n {entry.commands.map((command, index) => (\n <div key={`${entry.workspaceId}-${index}`} className=\"qc-settings-command\">\n <input\n className=\"qc-settings-field qc-settings-field-name\"\n placeholder={t('cardName')}\n value={command.name}\n aria-label={t('cardName')}\n onChange={(e) => patchEntry(entry.workspaceId, index, 'name', e.target.value)}\n />\n <input\n className=\"qc-settings-field qc-settings-field-command\"\n placeholder={t('cardCommand')}\n value={command.command}\n aria-label={t('cardCommand')}\n onChange={(e) => patchEntry(entry.workspaceId, index, 'command', e.target.value)}\n />\n <button\n type=\"button\"\n className=\"qc-settings-icon-btn\"\n aria-label={t('cardRemove')}\n title={t('cardRemove')}\n onClick={() => removeCommand(entry.workspaceId, index)}\n >\n <TrashIcon size={14} />\n </button>\n </div>\n ))}\n <button type=\"button\" className=\"qc-settings-add-btn\" onClick={() => addCommand(entry.workspaceId)}>\n <PlusIcon size={14} />\n {t('cardAddCommand')}\n </button>\n </section>\n ))}\n </div>\n </div>\n )\n}\n","/**\n * Client dictionary namespace for @fonlan/dsh-quick-commands.\n */\nexport const LOCALE_NS = 'quick-commands'\n\nexport const zh = {\n // header button\n buttonTitle: '快速命令',\n buttonTooltip: '快速命令(当前工作区)',\n noWorkspace: '当前会话不在任何已配置的工作区中',\n noCommands: '当前工作区未配置命令',\n openSettings: '去设置配置命令',\n\n // command popup menu\n menuTitle: '快速命令',\n menuEmpty: '(无命令)',\n\n // run popup\n runTitle: '运行中',\n runExited: '已退出',\n runExitCode: '退出码',\n runSignal: '信号',\n runStdout: '输出 (stdout)',\n runStderr: '错误 (stderr)',\n runEmpty: '(无输出)',\n runCommand: '命令',\n runCwd: '工作区',\n runStateRunning: '运行中…',\n runStateExited: '已退出',\n runKill: '终止',\n runClose: '关闭',\n runResize: '拖动调整弹窗大小',\n runKillConfirm: '终止后输出弹窗将关闭,确定终止该命令?',\n runBusy: '该工作区已有命令在运行',\n runStartFailed: '命令启动失败',\n runPollFailed: '输出读取失败',\n runLost: '运行记录已失效',\n runNotFound: '工作区或命令未找到',\n\n // settings page (设置 → 侧栏「快捷命令」)\n settingsTitle: '快捷命令',\n cardTitle: '快速命令',\n sectionSub: '为每个工作区配置常用命令,会话头部一键运行',\n cardIntro: '为每个工作区配置常用命令,运行于工作区目录。支持占位符:{workspace} 工作区路径,{cwd} 会话目录,{title} 工作区标题。',\n cardWorkspaces: '工作区',\n cardNoWorkspaces: '没有工作区。请在侧边栏先添加工作区。',\n cardCommands: '命令',\n cardNoCommands: '未配置命令',\n cardAddCommand: '添加命令',\n cardName: '名称',\n cardCommand: '命令',\n cardRemove: '删除',\n cardSaveError: '保存失败',\n cardAnchor: '输出弹窗位置',\n cardAnchorCorner: '右下角',\n cardAnchorButton: '锚定按钮下方',\n} as const\n\nexport const en: Record<keyof typeof zh, string> = {\n buttonTitle: 'Quick commands',\n buttonTooltip: 'Quick commands (current workspace)',\n noWorkspace: 'This session is not in any configured workspace',\n noCommands: 'No commands configured for this workspace',\n openSettings: 'Configure commands in Settings',\n\n menuTitle: 'Quick commands',\n menuEmpty: '(no commands)',\n\n runTitle: 'Running',\n runExited: 'Exited',\n runExitCode: 'Exit code',\n runSignal: 'Signal',\n runStdout: 'Output (stdout)',\n runStderr: 'Errors (stderr)',\n runEmpty: '(no output)',\n runCommand: 'Command',\n runCwd: 'Workspace',\n runStateRunning: 'Running…',\n runStateExited: 'Exited',\n runKill: 'Terminate',\n runClose: 'Close',\n runResize: 'Drag to resize the popup',\n runKillConfirm: 'Terminating closes the output popup. Terminate this command?',\n runBusy: 'A command is already running for this workspace',\n runStartFailed: 'Command failed to start',\n runPollFailed: 'Failed to read output',\n runLost: 'Run record expired',\n runNotFound: 'Workspace or command not found',\n\n settingsTitle: 'Quick Commands',\n cardTitle: 'Quick commands',\n sectionSub: 'Configure common commands per workspace, run them from the session header',\n cardIntro: 'Configure common commands per workspace; they run in the workspace directory. Placeholders: {workspace} workspace path, {cwd} session cwd, {title} workspace title.',\n cardWorkspaces: 'Workspaces',\n cardNoWorkspaces: 'No workspaces. Add one in the sidebar first.',\n cardCommands: 'Commands',\n cardNoCommands: 'No commands configured',\n cardAddCommand: 'Add command',\n cardName: 'Name',\n cardCommand: 'Command',\n cardRemove: 'Remove',\n cardSaveError: 'Save failed',\n cardAnchor: 'Output popup position',\n cardAnchorCorner: 'Bottom right',\n cardAnchorButton: 'Anchored at button',\n}\n","/**\n * @fonlan/dsh-quick-commands client half.\n *\n * Registers:\n * - \"settings.section\" page (id \"quick-commands\", order 330) — per-workspace\n * command CRUD + popup anchor preference, rendered as its own page in the\n * settings sidebar.\n * - \"conversation.session.header.utilities\" entry (id \"quick-commands\",\n * order -1 → left of the Session log pill) — the quick-run ▶ button, command\n * menu, and live output popup.\n */\nimport type { Context } from '@deepseek-ai/cordis'\ntype ClientContext = Context\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\nimport { QuickCommandsHeaderAction } from './header'\nimport { QuickCommandsSettingsSection } from './settings-section'\nimport { LOCALE_NS, zh, en } from './locales'\nimport './quick-commands.css'\n\n/** Slots face (local, erased at build). */\ninterface Slots {\n inject(name: string, callback: () => unknown): unknown\n register(\n def: { name: string; id?: string; order?: number; label?: string | (() => string); locale?: string },\n component: unknown,\n ): unknown\n}\n\n/** Services required before mounting (provided by the client runtime). */\nexport const inject = ['slots', 'locale']\n\n/** Client plugin body. */\nexport function apply(ctx: ClientContext): void {\n ctx.effect(() => ctx.locale.register(LOCALE_NS, { zh, en }), 'quick-commands: dictionaries')\n const t = ctx.locale.bind(LOCALE_NS)\n const slots = (ctx as unknown as { slots: Slots }).slots\n\n // The plugin's own settings page (设置 → 侧栏「快捷命令」) rides the\n // \"quick-commands\" locale namespace: a \"settings.section\" list entry gives it\n // one row in the settings sidebar and renders the page in the content column.\n slots.inject('settings.section', () =>\n slots.register({\n name: 'settings.section',\n id: 'quick-commands',\n order: 330,\n label: () => t('settingsTitle'),\n locale: LOCALE_NS,\n }, QuickCommandsSettingsSection as never),\n )\n\n // Session-header util: order -1 renders LEFT of the Session log pill\n // (session-log-download registers with default order 0).\n slots.inject('conversation.session.header.utilities', () =>\n slots.register({\n name: 'conversation.session.header.utilities',\n id: 'quick-commands',\n order: -1,\n locale: LOCALE_NS,\n }, QuickCommandsHeaderAction as never),\n )\n}\n"],"mappings":"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"}
package/lib/index.js CHANGED
@@ -556,43 +556,67 @@ const WorkspaceCommandsSchema = z.object({
556
556
  commands: z.array(CommandSchema).max(200)
557
557
  });
558
558
  const QuickCommandsSettingsSchema = z.object({
559
- workspaces: z.array(WorkspaceCommandsSchema).max(500),
560
- popupAnchor: z.union([z.const("corner"), z.const("button")]).default("corner"),
559
+ workspaces: z.array(WorkspaceCommandsSchema).max(500).volatile(),
560
+ popupAnchor: z.union([z.const("corner"), z.const("button")]).default("corner").volatile(),
561
561
  popupSize: z.object({
562
562
  width: z.number().min(320).max(8192),
563
563
  height: z.number().min(200).max(8192)
564
- })
564
+ }).volatile()
565
565
  });
566
- const DEFAULT_SETTINGS = {
567
- workspaces: [],
568
- popupAnchor: "corner"
569
- };
570
- function registerSettings(ctx) {
571
- let current = {
572
- ...DEFAULT_SETTINGS,
573
- workspaces: []
566
+ /**
567
+ * Read one config field. A volatile field arrives as a handle (dsh >= 0.1.7)
568
+ * and is read through `.get()` on EVERY access, so edits made through the
569
+ * settings plane are visible immediately; hosts that hand the plain value keep
570
+ * working unchanged.
571
+ */
572
+ function readField(field, fallback) {
573
+ if (field === void 0 || field === null) return fallback;
574
+ const handle = field;
575
+ const value = typeof handle.get === "function" ? handle.get() : field;
576
+ return value === void 0 || value === null ? fallback : value;
577
+ }
578
+ function registerSettings(ctx, config) {
579
+ const readWorkspaces = () => {
580
+ const value = readField(config?.workspaces, []);
581
+ return Array.isArray(value) ? value : [];
582
+ };
583
+ const readAnchor = () => readField(config?.popupAnchor, "corner") === "button" ? "button" : "corner";
584
+ const readPopupSize = () => {
585
+ const value = readField(config?.popupSize, void 0);
586
+ if (typeof value !== "object" || value === null) return void 0;
587
+ const { width, height } = value;
588
+ if (typeof width !== "number" || typeof height !== "number") return void 0;
589
+ if (!Number.isFinite(width) || !Number.isFinite(height)) return void 0;
590
+ return {
591
+ width: Math.round(width),
592
+ height: Math.round(height)
593
+ };
594
+ };
595
+ /** Current document, rebuilt from the live config fields on every read. */
596
+ const readDocument = () => {
597
+ const size = readPopupSize();
598
+ return {
599
+ workspaces: readWorkspaces(),
600
+ popupAnchor: readAnchor(),
601
+ ...size === void 0 ? {} : { popupSize: size }
602
+ };
574
603
  };
575
604
  let service;
576
605
  ctx.inject(["settings"], (sctx) => {
577
606
  service = sctx.settings;
578
- const scope = service.register(QUICK_COMMANDS_NS, QuickCommandsSettingsSchema);
579
- current = scope.get();
580
- scope.watch((next) => {
581
- current = next;
582
- });
583
607
  });
584
608
  async function persist(patch) {
585
609
  if (service === void 0) throw new Error("@fonlan/dsh-quick-commands: settings service is not available in this profile");
586
610
  await service.update(QUICK_COMMANDS_NS, patch);
587
611
  }
588
612
  return {
589
- get: () => current,
613
+ get: readDocument,
590
614
  commandsOf: (workspaceId) => {
591
- const set = current.workspaces.find((w) => w.workspaceId === workspaceId);
615
+ const set = readWorkspaces().find((w) => w.workspaceId === workspaceId);
592
616
  return set === void 0 ? [] : set.commands;
593
617
  },
594
618
  setCommands: async (workspaceId, commands) => {
595
- const next = current.workspaces.filter((w) => w.workspaceId !== workspaceId);
619
+ const next = readWorkspaces().filter((w) => w.workspaceId !== workspaceId);
596
620
  if (commands.length > 0) next.push({
597
621
  workspaceId,
598
622
  commands
@@ -871,9 +895,10 @@ function registerApiRoutes(ctx, runner, settings) {
871
895
  //#region src/index.ts
872
896
  const name = "@fonlan/dsh-quick-commands";
873
897
  const inject = ["settings"];
874
- /** No plugin-level config; all configuration lives in the settings namespace. */
875
- function apply(ctx) {
876
- const settings = registerSettings(ctx);
898
+ const Config = QuickCommandsSettingsSchema;
899
+ /** Configuration rides the entry config; edits persist through the settings service. */
900
+ function apply(ctx, config) {
901
+ const settings = registerSettings(ctx, config);
877
902
  const subprocess = ctx.get("subprocess");
878
903
  if (subprocess === void 0) {
879
904
  ctx.logger?.warn?.("@fonlan/dsh-quick-commands: subprocess service unavailable; commands cannot run");
@@ -900,4 +925,4 @@ function apply(ctx) {
900
925
  }, "quick-commands: process cleanup");
901
926
  }
902
927
  //#endregion
903
- export { apply, inject, name };
928
+ export { Config, apply, inject, name };
@@ -13,7 +13,9 @@
13
13
  * absence of dsh-ssh degrades cleanly to local-only behaviour.
14
14
  */
15
15
  import type { Context } from '@deepseek-ai/cordis';
16
+ import type { QuickCommandsSettings } from './shared/contract.js';
16
17
  export declare const name = "@fonlan/dsh-quick-commands";
17
18
  export declare const inject: string[];
18
- /** No plugin-level config; all configuration lives in the settings namespace. */
19
- export declare function apply(ctx: Context): void;
19
+ export declare const Config: import("@deepseek-ai/schemastery").default<QuickCommandsSettings>;
20
+ /** Configuration rides the entry config; edits persist through the settings service. */
21
+ export declare function apply(ctx: Context, config: QuickCommandsSettings): void;
@@ -1,8 +1,18 @@
1
1
  /**
2
2
  * User-facing plugin settings (per-workspace command sets + popup anchor),
3
- * persisted through the settings service namespace `quick-commands`.
4
- * The DSH settings service requires a lowercase kebab-case namespace
5
- * (/^[a-z][a-z0-9-]*$/), so the scoped package name cannot be used here.
3
+ * carried by this bundle's entry config: dsh >= 0.1.7 owns plugin settings as
4
+ * entry config (the plugin's `Config` schema) and persists edits through
5
+ * `settings.update`.
6
+ *
7
+ * All three fields are declared `volatile()` — that is what makes them
8
+ * writable by the settings plane — and schemastery hands a volatile field to
9
+ * `apply()` as a LIVE HANDLE (`{ get() }`), not as the value. Reading the raw
10
+ * field therefore yields the handle object, which silently destroys the whole
11
+ * document: `Array.isArray(handle)` is false, JSON renders `{}`, and a
12
+ * `.default('corner')` union arrives as the handle rather than a literal.
13
+ * Every read goes through {@link readField} instead, which unwraps handles and
14
+ * still accepts plain values (hosts that resolve the config without the
15
+ * volatile wrapper).
6
16
  */
7
17
  import type { Context } from '@deepseek-ai/cordis';
8
18
  import z from '@deepseek-ai/schemastery';
@@ -21,4 +31,4 @@ export interface QuickCommandsSettingsFace {
21
31
  /** Persist the output popup size (drag-resize). */
22
32
  setPopupSize(size: QuickPopupSize): Promise<void>;
23
33
  }
24
- export declare function registerSettings(ctx: Context): QuickCommandsSettingsFace;
34
+ export declare function registerSettings(ctx: Context, config: QuickCommandsSettings): QuickCommandsSettingsFace;
@@ -33,6 +33,13 @@ export interface QuickCommandsSettings {
33
33
  /** Output popup size; absent until the user drags the resize handle (then per-anchor defaults apply). */
34
34
  popupSize?: QuickPopupSize;
35
35
  }
36
+ /**
37
+ * The document's anchor as a usable literal. Wire data is defensive by
38
+ * contract: an older host can still answer with schemastery's empty-object
39
+ * resolution (`{}`) for this field, which must never reach a CSS class name or
40
+ * a radio `checked` comparison.
41
+ */
42
+ export declare function anchorOf(value: unknown): 'corner' | 'button';
36
43
  /** Placeholder substitution values resolved for one execution. */
37
44
  export interface QuickRunPlaceholders {
38
45
  /** Workspace root path (also the execution cwd). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fonlan/dsh-quick-commands",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "DSH web plugin: per-workspace quick commands. Configure named shell commands per DSH workspace, run one from the session header with live stdout/stderr in a floating popup.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -63,38 +63,39 @@
63
63
  },
64
64
  "peerDependencies": {
65
65
  "@deepseek-ai/cordis": "^4.0.1",
66
+ "@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.2",
67
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.2-alpha.2",
68
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.2",
69
+ "@deepseek-ai/dsh-host-webserver": "^0.1.2-alpha.2",
66
70
  "@deepseek-ai/dsh-settings": "^0.1.2-alpha.2",
67
71
  "@deepseek-ai/dsh-subprocess": "^0.1.2-alpha.2",
68
72
  "@deepseek-ai/dsh-workspace": "^0.1.2-alpha.2",
69
- "@deepseek-ai/dsh-host-webserver": "^0.1.2-alpha.2",
70
73
  "@deepseek-ai/schemastery": "^3.18.2",
71
- "@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.2",
72
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.2",
73
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.2-alpha.2",
74
74
  "react": "^18.2.0",
75
75
  "react-dom": "^18.2.0"
76
76
  },
77
77
  "devDependencies": {
78
+ "@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.2",
79
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.2-alpha.2",
80
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.2-alpha.2",
81
+ "@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.2-alpha.2",
82
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.2",
83
+ "@deepseek-ai/dsh-host-webserver": "^0.1.2-alpha.2",
84
+ "@deepseek-ai/dsh-settings": "^0.1.2-alpha.2",
85
+ "@deepseek-ai/dsh-subprocess": "^0.1.2-alpha.2",
86
+ "@deepseek-ai/dsh-workspace": "^0.1.2-alpha.2",
87
+ "@deepseek-ai/schemastery": "^3.18.3",
78
88
  "@types/node": "^22.0.0",
79
89
  "@types/react": "~18.3.1",
80
90
  "@types/react-dom": "~18.3.0",
81
91
  "tsdown": "^0.22.14",
82
- "typescript": "^5.9.0",
83
- "@deepseek-ai/dsh-settings": "^0.1.2-alpha.2",
84
- "@deepseek-ai/dsh-subprocess": "^0.1.2-alpha.2",
85
- "@deepseek-ai/dsh-workspace": "^0.1.2-alpha.2",
86
- "@deepseek-ai/dsh-host-webserver": "^0.1.2-alpha.2",
87
- "@deepseek-ai/schemastery": "^3.18.2",
88
- "@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.2",
89
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.2",
90
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.2-alpha.2",
91
- "@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.2-alpha.2",
92
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.2-alpha.2"
92
+ "typescript": "^5.9.0"
93
93
  },
94
94
  "scripts": {
95
95
  "build": "rm -rf lib && tsc -p tsconfig.build.json && tsdown",
96
96
  "build:client": "tsdown",
97
97
  "typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.client.json",
98
+ "test": "node --experimental-transform-types --test test/remote.test.ts test/settings-config.test.ts && node test/header-icons.test.mjs",
98
99
  "bundle": "tsdown",
99
100
  "watch": "tsdown --watch",
100
101
  "release:patch": "npm version patch && git push && git push --tags",
@@ -8,20 +8,18 @@
8
8
  */
9
9
  import { useEffect, useLayoutEffect, useRef, useState } from 'react'
10
10
  import { createPortal } from 'react-dom'
11
- import {
12
- IconPlayOutline16,
13
- IconCloseOutline16,
14
- IconStopFill16,
15
- } from '@deepseek-ai/dsh-client-ui-primitives'
11
+ import { PlayIcon, CloseIcon, StopIcon } from './icons'
16
12
  import { quickApi } from './api'
17
13
  import { useRunPopover, openMenu, closeMenu, openRun, closeRun } from './run-state'
18
- import type { QuickPopupSize } from '../shared/contract'
14
+ import { anchorOf, type QuickPopupSize } from '../shared/contract'
19
15
 
20
16
  /** Workspace row shape the standard useWorkspaces hook exposes. */
21
17
  interface WorkspaceRow {
22
18
  workspaceId: string
23
19
  path: string
24
20
  title: string
21
+ /** Session membership; the canonical link DSH itself resolves Workspaces by. */
22
+ sessionIds?: string[]
25
23
  }
26
24
 
27
25
  /** Session list state subset we read (cwd + id). */
@@ -91,7 +89,12 @@ function useCurrentWorkspace(
91
89
  ): { workspace: WorkspaceRow | undefined; cwd: string | undefined } {
92
90
  const row = useSessions?.((s) => s.byId[sessionId]) as SessionRow | undefined
93
91
  const workspaces = useWorkspaces?.((s) => s.items) as WorkspaceRow[] | undefined
94
- return { workspace: matchWorkspace(row?.cwd, workspaces ?? []), cwd: row?.cwd }
92
+ const items = workspaces ?? []
93
+ // Membership first (how DSH core resolves a session's Workspace, and the only
94
+ // link that survives when a session runs outside the Workspace root), then
95
+ // the longest cwd prefix as the fallback for hosts without membership data.
96
+ const byMembership = items.find((ws) => ws.sessionIds?.includes(sessionId) === true)
97
+ return { workspace: byMembership ?? matchWorkspace(row?.cwd, items), cwd: row?.cwd }
95
98
  }
96
99
 
97
100
  /** Header icon button + command menu + live output popup. */
@@ -159,7 +162,7 @@ export function QuickCommandsHeaderAction(props: HeaderProps): JSX.Element {
159
162
  const own = entries.find((e) => e.workspaceId === workspace?.workspaceId)
160
163
  setCommands(own === undefined ? [] : own.commands)
161
164
  setMenuRemoteHost(own?.remoteHost ?? null)
162
- setAnchor(settings.popupAnchor)
165
+ setAnchor(anchorOf(settings.popupAnchor))
163
166
  setPopupSize(settings.popupSize)
164
167
  setMenuLoadError(null)
165
168
  } catch (e) {
@@ -201,7 +204,7 @@ export function QuickCommandsHeaderAction(props: HeaderProps): JSX.Element {
201
204
  disabled={workspace === undefined}
202
205
  onClick={() => { popover.menuOpen ? closeMenu() : openMenu() }}
203
206
  >
204
- <IconPlayOutline16 size={15} />
207
+ <PlayIcon size={15} />
205
208
  </button>
206
209
 
207
210
  {popover.menuOpen && workspace !== undefined && menuPosition !== null && createPortal(
@@ -216,7 +219,7 @@ export function QuickCommandsHeaderAction(props: HeaderProps): JSX.Element {
216
219
  {menuRemoteHost !== null && <span className="qc-menu-remote" title={`SSH · ${menuRemoteHost}`}>SSH</span>}
217
220
  <span className="qc-menu-ws">{workspace.title}</span>
218
221
  <button type="button" className="qc-menu-close" aria-label={t('runClose')} onClick={closeMenu}>
219
- <IconCloseOutline16 size={12} />
222
+ <CloseIcon size={12} />
220
223
  </button>
221
224
  </div>
222
225
  {commands.length === 0 && (
@@ -495,7 +498,7 @@ function RunPopup(props: {
495
498
  aria-label={t('runKill')}
496
499
  onClick={() => void kill()}
497
500
  >
498
- <IconStopFill16 size={14} />
501
+ <StopIcon size={14} />
499
502
  </button>
500
503
  <button
501
504
  type="button"
@@ -505,7 +508,7 @@ function RunPopup(props: {
505
508
  onPointerUp={onClosePointerUp}
506
509
  onClick={closeOutput}
507
510
  >
508
- <IconCloseOutline16 size={14} />
511
+ <CloseIcon size={14} />
509
512
  </button>
510
513
  </header>
511
514
 
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Icon compatibility layer over `@deepseek-ai/dsh-client-ui-primitives`.
3
+ *
4
+ * DSH renamed this package's icon exports in 0.1.7: the size-suffixed names
5
+ * (`IconPlayOutline16`) became weight-suffixed ones (`IconPlayOutlineRegular` /
6
+ * `IconPlayOutlineMedium`, matching what the official header plugins use).
7
+ *
8
+ * A client bundle that statically imports a name the host no longer exports
9
+ * receives `undefined`; rendering that element throws, the slot's per-entry
10
+ * error boundary swallows the whole entry, and the surface silently vanishes —
11
+ * exactly how the header ▶ button disappeared on 0.1.7.
12
+ *
13
+ * Resolving by name at runtime (new scheme → legacy scheme → bundled artwork)
14
+ * keeps every surface rendering across DSH alphas. The bundled artwork is only
15
+ * a last resort, so a future rename can never blank a control again.
16
+ */
17
+ import type { ReactElement, ReactNode } from 'react'
18
+ import * as primitives from '@deepseek-ai/dsh-client-ui-primitives'
19
+
20
+ /** Props both naming schemes accept (the legacy exports ignore extra fields). */
21
+ export interface IconProps {
22
+ /** Rendered square size in CSS pixels. */
23
+ size?: number
24
+ className?: string
25
+ }
26
+
27
+ type IconComponent = (props: IconProps) => ReactElement
28
+
29
+ const ICON_EXPORTS = primitives as unknown as Record<string, IconComponent | undefined>
30
+
31
+ /** First export name that exists on the running host, else the bundled artwork. */
32
+ function pickIcon(exportNames: readonly string[], fallback: IconComponent): IconComponent {
33
+ for (const name of exportNames) {
34
+ const candidate = ICON_EXPORTS[name]
35
+ if (typeof candidate === 'function') return candidate
36
+ }
37
+ return fallback
38
+ }
39
+
40
+ /** Wrap one locally drawn 16×16 artwork as an icon component. */
41
+ function localIcon(children: ReactNode): IconComponent {
42
+ return function LocalIcon({ size = 16, className }: IconProps): ReactElement {
43
+ return (
44
+ <svg
45
+ width={size}
46
+ height={size}
47
+ viewBox="0 0 16 16"
48
+ fill="none"
49
+ className={className}
50
+ aria-hidden="true"
51
+ >
52
+ {children}
53
+ </svg>
54
+ )
55
+ }
56
+ }
57
+
58
+ /** Run/play glyph for the header quick-run button. */
59
+ export const PlayIcon = pickIcon(
60
+ ['IconPlayOutlineRegular', 'IconPlayOutlineMedium', 'IconPlayOutline16'],
61
+ localIcon(
62
+ <>
63
+ <circle cx="8" cy="8" r="6.5" stroke="currentColor" />
64
+ <path d="M6.6 5.9 10.4 8l-3.8 2.1Z" fill="currentColor" />
65
+ </>,
66
+ ),
67
+ )
68
+
69
+ /** Dismiss glyph (menu header and popup close). */
70
+ export const CloseIcon = pickIcon(
71
+ ['IconCloseOutlineRegular', 'IconCloseOutlineMedium', 'IconCloseOutline16'],
72
+ localIcon(
73
+ <path d="M4.2 4.2 11.8 11.8M11.8 4.2 4.2 11.8" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" />,
74
+ ),
75
+ )
76
+
77
+ /** Stop/kill glyph for a running command. */
78
+ export const StopIcon = pickIcon(
79
+ ['IconStopFillRegular', 'IconStopFillMedium', 'IconStopFill16'],
80
+ localIcon(<rect x="4.6" y="4.6" width="6.8" height="6.8" rx="1.6" fill="currentColor" />),
81
+ )
82
+
83
+ /** Add-row glyph in the settings page. */
84
+ export const PlusIcon = pickIcon(
85
+ ['IconPlusOutlineRegular', 'IconPlusOutlineMedium', 'IconPlusOutline16'],
86
+ localIcon(
87
+ <path d="M8 3.4v9.2M3.4 8h9.2" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" />,
88
+ ),
89
+ )
90
+
91
+ /** Remove-command glyph in the settings page. */
92
+ export const TrashIcon = pickIcon(
93
+ ['IconTrashOutlineRegular', 'IconTrashOutlineMedium', 'IconTrashOutline16'],
94
+ localIcon(
95
+ <>
96
+ <path d="M3.4 4.4h9.2" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" />
97
+ <path
98
+ d="M6.4 4.4V3.1h3.2v1.3M4.9 4.4l.6 8.5h5l.6-8.5"
99
+ stroke="currentColor"
100
+ strokeWidth="1.2"
101
+ strokeLinecap="round"
102
+ strokeLinejoin="round"
103
+ />
104
+ </>,
105
+ ),
106
+ )
@@ -9,12 +9,9 @@
9
9
  * persist while the user types.
10
10
  */
11
11
  import { useEffect, useRef, useState } from 'react'
12
- import {
13
- IconPlusOutline16,
14
- IconTrashOutline16,
15
- } from '@deepseek-ai/dsh-client-ui-primitives'
12
+ import { PlusIcon, TrashIcon } from './icons'
16
13
  import { quickApi } from './api'
17
- import type { QuickRosterEntry, QuickCommandsSettings } from '../shared/contract'
14
+ import { anchorOf, type QuickRosterEntry, type QuickCommandsSettings } from '../shared/contract'
18
15
  import './settings-section.css'
19
16
 
20
17
  interface Props {
@@ -38,7 +35,7 @@ export function QuickCommandsSettingsSection({ t }: Props): JSX.Element {
38
35
  const load = async (): Promise<void> => {
39
36
  try {
40
37
  const [entries, settings] = await Promise.all([quickApi.workspacesList(), quickApi.settingsGet()])
41
- setState({ loaded: true, entries, anchor: settings.popupAnchor })
38
+ setState({ loaded: true, entries, anchor: anchorOf(settings.popupAnchor) })
42
39
  setError(null)
43
40
  } catch (e) {
44
41
  setState((s) => ({ ...s, loaded: true }))
@@ -100,7 +97,7 @@ export function QuickCommandsSettingsSection({ t }: Props): JSX.Element {
100
97
  setState((s) => ({ ...s, anchor }))
101
98
  try {
102
99
  const next = await quickApi.settingsSetAnchor(anchor)
103
- setState((s) => ({ ...s, anchor: next.popupAnchor }))
100
+ setState((s) => ({ ...s, anchor: anchorOf(next.popupAnchor) }))
104
101
  } catch (e) {
105
102
  setError(e instanceof Error ? e.message : String(e))
106
103
  void load()
@@ -179,12 +176,12 @@ export function QuickCommandsSettingsSection({ t }: Props): JSX.Element {
179
176
  title={t('cardRemove')}
180
177
  onClick={() => removeCommand(entry.workspaceId, index)}
181
178
  >
182
- <IconTrashOutline16 size={14} />
179
+ <TrashIcon size={14} />
183
180
  </button>
184
181
  </div>
185
182
  ))}
186
183
  <button type="button" className="qc-settings-add-btn" onClick={() => addCommand(entry.workspaceId)}>
187
- <IconPlusOutline16 size={14} />
184
+ <PlusIcon size={14} />
188
185
  {t('cardAddCommand')}
189
186
  </button>
190
187
  </section>
package/src/index.ts CHANGED
@@ -15,16 +15,23 @@
15
15
  import type { Context } from '@deepseek-ai/cordis'
16
16
  import { QuickCommandRunner, type RemoteBackend } from './server/runner.js'
17
17
  import { readHostConfig } from './server/remote.js'
18
- import { registerSettings } from './server/settings.js'
18
+ import { QuickCommandsSettingsSchema, registerSettings } from './server/settings.js'
19
19
  import { registerApiRoutes } from './server/rpc.js'
20
+ import type { QuickCommandsSettings } from './shared/contract.js'
20
21
 
21
22
  export const name = '@fonlan/dsh-quick-commands'
22
23
 
23
24
  export const inject = ['settings']
24
25
 
25
- /** No plugin-level config; all configuration lives in the settings namespace. */
26
- export function apply(ctx: Context): void {
27
- const settings = registerSettings(ctx)
26
+ // The entry config IS this plugin's settings document (workspace command sets
27
+ // + popup preferences). Its fields are declared volatile, so the settings
28
+ // plane edits them without remounting the entry and `apply` receives live
29
+ // handles rather than snapshots — see server/settings.ts for the reads.
30
+ export const Config = QuickCommandsSettingsSchema
31
+
32
+ /** Configuration rides the entry config; edits persist through the settings service. */
33
+ export function apply(ctx: Context, config: QuickCommandsSettings): void {
34
+ const settings = registerSettings(ctx, config)
28
35
  const subprocess = ctx.get('subprocess')
29
36
  if (subprocess === undefined) {
30
37
  ctx.logger?.warn?.('@fonlan/dsh-quick-commands: subprocess service unavailable; commands cannot run')
@@ -1,13 +1,22 @@
1
1
  /**
2
2
  * User-facing plugin settings (per-workspace command sets + popup anchor),
3
- * persisted through the settings service namespace `quick-commands`.
4
- * The DSH settings service requires a lowercase kebab-case namespace
5
- * (/^[a-z][a-z0-9-]*$/), so the scoped package name cannot be used here.
3
+ * carried by this bundle's entry config: dsh >= 0.1.7 owns plugin settings as
4
+ * entry config (the plugin's `Config` schema) and persists edits through
5
+ * `settings.update`.
6
+ *
7
+ * All three fields are declared `volatile()` — that is what makes them
8
+ * writable by the settings plane — and schemastery hands a volatile field to
9
+ * `apply()` as a LIVE HANDLE (`{ get() }`), not as the value. Reading the raw
10
+ * field therefore yields the handle object, which silently destroys the whole
11
+ * document: `Array.isArray(handle)` is false, JSON renders `{}`, and a
12
+ * `.default('corner')` union arrives as the handle rather than a literal.
13
+ * Every read goes through {@link readField} instead, which unwraps handles and
14
+ * still accepts plain values (hosts that resolve the config without the
15
+ * volatile wrapper).
6
16
  */
7
17
  import type { Context } from '@deepseek-ai/cordis'
8
18
  import z from '@deepseek-ai/schemastery'
9
19
  import type { QuickCommandsSettings, QuickCommandEntry, QuickPopupSize, WorkspaceCommands } from '../shared/contract.js'
10
- import type {} from '@deepseek-ai/dsh-settings'
11
20
 
12
21
  export const QUICK_COMMANDS_NS = 'quick-commands'
13
22
 
@@ -28,27 +37,41 @@ const WorkspaceCommandsSchema = z.object({
28
37
  })
29
38
 
30
39
  export const QuickCommandsSettingsSchema: z<QuickCommandsSettings> = z.object({
31
- workspaces: z.array(WorkspaceCommandsSchema).max(500),
32
- popupAnchor: z.union([z.const('corner'), z.const('button')]).default('corner'),
40
+ // Values are addressed by index (array edits), so the whole array is the
41
+ // volatile handle the settings plane swaps in place.
42
+ workspaces: z.array(WorkspaceCommandsSchema).max(500).volatile(),
43
+ popupAnchor: z.union([z.const('corner'), z.const('button')]).default('corner').volatile(),
33
44
  // No default on purpose: a missing popupSize stays ABSENT in the resolved
34
45
  // document, so the client can distinguish "never resized" (per-anchor
35
- // default size) from a user-chosen size.
46
+ // default size) from a user-chosen size. Semantics match the runtime value:
47
+ // never resized resolves to `{}` (schemastery's implicit object default).
36
48
  popupSize: z.object({
37
49
  width: z.number().min(320).max(8192),
38
50
  height: z.number().min(200).max(8192),
39
- }),
51
+ }).volatile(),
40
52
  })
41
53
 
42
- const DEFAULT_SETTINGS: QuickCommandsSettings = {
43
- workspaces: [],
44
- popupAnchor: 'corner',
54
+ /** Live handle schemastery wraps around one `volatile()` config field. */
55
+ interface VolatileField<T> {
56
+ get(): T
45
57
  }
46
58
 
47
- /** Structural settings-service subset; the real service resolves the schema. */
48
- interface SettingsServiceLike {
49
- register(ns: unknown, schema: unknown): unknown
50
- update(ns: unknown, patch: Record<string, unknown>, expectedRevision?: number): Promise<unknown>
51
- mutate?(ns: unknown, ops: readonly unknown[], expectedRevision?: number): Promise<unknown>
59
+ /**
60
+ * Read one config field. A volatile field arrives as a handle (dsh >= 0.1.7)
61
+ * and is read through `.get()` on EVERY access, so edits made through the
62
+ * settings plane are visible immediately; hosts that hand the plain value keep
63
+ * working unchanged.
64
+ */
65
+ function readField<T>(field: unknown, fallback: T): T {
66
+ if (field === undefined || field === null) return fallback
67
+ const handle = field as Partial<VolatileField<T>>
68
+ const value = typeof handle.get === 'function' ? handle.get() : (field as T)
69
+ return value === undefined || value === null ? fallback : value
70
+ }
71
+
72
+ /** Structural settings-service subset used for persistence. */
73
+ interface SettingsUpdateFace {
74
+ update(ns: string, patch: Record<string, unknown>, expectedRevision?: number): Promise<unknown>
52
75
  }
53
76
 
54
77
  /** Settings service face for the quick-commands engine. */
@@ -64,35 +87,57 @@ export interface QuickCommandsSettingsFace {
64
87
  setPopupSize(size: QuickPopupSize): Promise<void>
65
88
  }
66
89
 
67
- export function registerSettings(ctx: Context): QuickCommandsSettingsFace {
68
- let current: QuickCommandsSettings = { ...DEFAULT_SETTINGS, workspaces: [] }
69
- let service: SettingsServiceLike | undefined
90
+ export function registerSettings(ctx: Context, config: QuickCommandsSettings): QuickCommandsSettingsFace {
91
+ const readWorkspaces = (): WorkspaceCommands[] => {
92
+ const value = readField<unknown>(config?.workspaces, [])
93
+ return Array.isArray(value) ? value as WorkspaceCommands[] : []
94
+ }
95
+ const readAnchor = (): QuickCommandsSettings['popupAnchor'] =>
96
+ (readField<unknown>(config?.popupAnchor, 'corner') === 'button' ? 'button' : 'corner')
97
+ const readPopupSize = (): QuickPopupSize | undefined => {
98
+ const value = readField<unknown>(config?.popupSize, undefined)
99
+ if (typeof value !== 'object' || value === null) return undefined
100
+ const { width, height } = value as { width?: unknown; height?: unknown }
101
+ // `{}` (never resized) and any partially written size read as absent.
102
+ if (typeof width !== 'number' || typeof height !== 'number') return undefined
103
+ if (!Number.isFinite(width) || !Number.isFinite(height)) return undefined
104
+ return { width: Math.round(width), height: Math.round(height) }
105
+ }
106
+ /** Current document, rebuilt from the live config fields on every read. */
107
+ const readDocument = (): QuickCommandsSettings => {
108
+ const size = readPopupSize()
109
+ return {
110
+ workspaces: readWorkspaces(),
111
+ popupAnchor: readAnchor(),
112
+ ...(size === undefined ? {} : { popupSize: size }),
113
+ }
114
+ }
115
+ let service: SettingsUpdateFace | undefined
70
116
 
117
+ // Optional settings service: without one the commands still run from the
118
+ // entry config; they just cannot persist edits.
71
119
  ctx.inject(['settings'], (sctx) => {
72
- service = sctx.settings as unknown as SettingsServiceLike
73
- const scope = service.register(QUICK_COMMANDS_NS, QuickCommandsSettingsSchema) as {
74
- get(): QuickCommandsSettings
75
- watch(callback: (next: QuickCommandsSettings) => void): () => void
76
- }
77
- current = scope.get()
78
- scope.watch((next) => { current = next })
120
+ service = (sctx as unknown as { settings?: SettingsUpdateFace }).settings
79
121
  })
80
122
 
81
123
  async function persist(patch: Record<string, unknown>): Promise<void> {
82
124
  if (service === undefined) {
83
125
  throw new Error('@fonlan/dsh-quick-commands: settings service is not available in this profile')
84
126
  }
127
+ // The write lands in this entry's config: the settings plane either swaps
128
+ // the volatile handles in place or re-applies the entry — both paths are
129
+ // picked up by the next read, which is why no local mirror is kept here.
85
130
  await service.update(QUICK_COMMANDS_NS, patch)
86
131
  }
87
132
 
88
133
  return {
89
- get: () => current,
134
+ get: readDocument,
90
135
  commandsOf: (workspaceId) => {
91
- const set = current.workspaces.find((w) => w.workspaceId === workspaceId)
136
+ const set = readWorkspaces().find((w) => w.workspaceId === workspaceId)
92
137
  return set === undefined ? [] : set.commands
93
138
  },
94
139
  setCommands: async (workspaceId, commands) => {
95
- const next: WorkspaceCommands[] = current.workspaces.filter((w) => w.workspaceId !== workspaceId)
140
+ const next = readWorkspaces().filter((w) => w.workspaceId !== workspaceId)
96
141
  if (commands.length > 0) next.push({ workspaceId, commands })
97
142
  await persist({ workspaces: next })
98
143
  },
@@ -38,6 +38,16 @@ export interface QuickCommandsSettings {
38
38
  popupSize?: QuickPopupSize
39
39
  }
40
40
 
41
+ /**
42
+ * The document's anchor as a usable literal. Wire data is defensive by
43
+ * contract: an older host can still answer with schemastery's empty-object
44
+ * resolution (`{}`) for this field, which must never reach a CSS class name or
45
+ * a radio `checked` comparison.
46
+ */
47
+ export function anchorOf(value: unknown): 'corner' | 'button' {
48
+ return value === 'button' ? 'button' : 'corner'
49
+ }
50
+
41
51
  /** Placeholder substitution values resolved for one execution. */
42
52
  export interface QuickRunPlaceholders {
43
53
  /** Workspace root path (also the execution cwd). */