@fonlan/dsh-quick-commands 0.1.4 → 0.1.6
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 +110 -74
- package/lib/client.js.map +1 -1
- package/package.json +1 -1
- package/src/client/header.tsx +89 -34
- package/src/client/index.tsx +15 -14
- package/src/client/locales.ts +7 -7
- package/src/client/quick-commands.css +25 -190
- package/src/client/settings-section.css +163 -0
- package/src/client/settings-section.tsx +195 -0
- package/src/client/settings-card.tsx +0 -209
package/lib/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","names":["useSyncExternalStore","useState","useRef","IconPlayOutline16","IconCloseOutline16","IconStopFill16","useState","useRef","IconChevronDownOutline14","IconTrashOutline16","IconPlusOutline16"],"sources":["../../../src/client/api.ts","../../../src/client/run-state.ts","../../../src/client/header.tsx","../../../src/client/settings-card.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, useRef, useState } from 'react'\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\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\n // Click-outside dismissal: while the menu is open, a pointerdown on the\n // document that lands OUTSIDE the wrap (button / menu / run popup all live\n // inside the wrap) collapses the menu. pointerdown (not click) covers mouse,\n // touch and pen, and fires before the button's own click toggle.\n useEffect(() => {\n if (!popover.menuOpen) return\n const onPointerDown = (e: PointerEvent): void => {\n const wrap = wrapRef.current\n if (wrap === null) return\n if (e.target instanceof Node && wrap.contains(e.target)) return\n closeMenu()\n }\n document.addEventListener('pointerdown', onPointerDown)\n return () => { document.removeEventListener('pointerdown', onPointerDown) }\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 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 && (\n <div className=\"qc-menu\" role=\"menu\">\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 )}\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 [killConfirm, setKillConfirm] = useState(false)\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 const close = (): void => {\n // Already exited: there is nothing left to terminate, close directly\n // instead of asking \"terminate this command?\".\n if (state.status === 'exited') {\n onClose()\n return\n }\n setKillConfirm(true)\n }\n\n const confirmClose = (): void => {\n void kill()\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 onClick={close}\n >\n <IconCloseOutline16 size={14} />\n </button>\n </header>\n\n {killConfirm && (\n <div className=\"qc-popup-confirm\">\n <span>{t('runKillConfirm')}</span>\n <button type=\"button\" className=\"qc-popup-btn\" onClick={confirmClose}>{t('runKill')}</button>\n <button type=\"button\" className=\"qc-popup-btn qc-popup-btn-plain\" onClick={() => setKillConfirm(false)}>\n {t('runClose')}\n </button>\n </div>\n )}\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 card (设置 → 插件配置): an expandable plugin card —\n * header (title + description + chevron) collapsed by default, body reveals\n * the workspace × command-set editor and the popup anchor preference. Body\n * edits instant-save per change; partial rows (empty name/command) persist\n * while the user types.\n *\n * Mirrors the official PluginCard disclosure pattern (header button with\n * aria-expanded, rotate-180 chevron, body separated by a top border).\n */\nimport { useEffect, useRef, useState } from 'react'\nimport {\n IconChevronDownOutline14,\n IconPlusOutline16,\n IconTrashOutline16,\n} from '@deepseek-ai/dsh-client-ui-primitives'\nimport { quickApi } from './api'\nimport type { QuickRosterEntry, QuickCommandsSettings } from '../shared/contract'\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 QuickCommandsSettingsCard({ t }: Props): JSX.Element {\n // Card-local disclosure state (a reading gesture, like the official card).\n const [open, setOpen] = useState(false)\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('cardDescription')\n\n return (\n <li className={'qc-settings-card' + (open ? ' qc-settings-card-open' : '')}>\n <button\n type=\"button\"\n className=\"qc-settings-header\"\n aria-expanded={open}\n aria-label={`${t('collapseOrExpand')}: ${title}`}\n onClick={() => setOpen((v) => !v)}\n >\n <span className=\"qc-settings-head-text\">\n <span className=\"qc-settings-name\">{title}</span>\n <span className=\"qc-settings-description\">{description}</span>\n </span>\n <IconChevronDownOutline14 className={'qc-settings-chevron' + (open ? ' qc-settings-chevron-open' : '')} />\n </button>\n\n {open && (\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 )}\n </li>\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 card\n cardTitle: '快速命令',\n cardDescription: '为每个工作区配置常用命令,会话头部一键运行',\n collapseOrExpand: '展开 / 收起',\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}\n\nexport const en = {\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 cardTitle: 'Quick commands',\n cardDescription: 'Configure common commands per workspace, run them from the session header',\n collapseOrExpand: 'Expand / collapse',\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.plugin.item` card (keyed by the `quick-commands` settings\n * namespace) — per-workspace command CRUD + popup anchor preference.\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 type {} from '@deepseek-ai/dsh-client-ui-settings-plugins/client'\nimport { QuickCommandsHeaderAction } from './header'\nimport { QuickCommandsSettingsCard } from './settings-card'\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; key?: string; id?: string; order?: number; 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 Card (设置 → 插件配置) rides the `quick-commands`\n // settings namespace: registering into the keyed `settings.plugin.item` slot\n // with the namespace string makes the configurable-plugins tab dispatch the\n // card next to the built-in ones (bash / agent loop / web search).\n slots.inject('settings.plugin.item', () =>\n slots.register({\n name: 'settings.plugin.item',\n key: LOCALE_NS,\n locale: LOCALE_NS,\n }, QuickCommandsSettingsCard 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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"}
|
|
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":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fonlan/dsh-quick-commands",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
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": {
|
package/src/client/header.tsx
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* Props are the standard session/global kit of `conversation.session.header.utilities`
|
|
7
7
|
* plus this plugin's injected hooks (anchor preference + resolver).
|
|
8
8
|
*/
|
|
9
|
-
import { useEffect, useRef, useState } from 'react'
|
|
9
|
+
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
|
10
|
+
import { createPortal } from 'react-dom'
|
|
10
11
|
import {
|
|
11
12
|
IconPlayOutline16,
|
|
12
13
|
IconCloseOutline16,
|
|
@@ -36,6 +37,38 @@ interface HeaderProps {
|
|
|
36
37
|
useWorkspaces?: (selector: (s: { items: WorkspaceRow[] }) => unknown) => unknown
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
interface MenuPosition {
|
|
41
|
+
left: number
|
|
42
|
+
top: number
|
|
43
|
+
width: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const PHONE_MENU_RAIL_WIDTH = 56
|
|
47
|
+
const MENU_VIEWPORT_GAP = 12
|
|
48
|
+
const MENU_PREFERRED_WIDTH = 280
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Position the portal menu beside its button without ever entering the phone
|
|
52
|
+
* nav rail. Calculating actual pixels (rather than only CSS max-width) also
|
|
53
|
+
* means it remains correct when the header itself sits in a clipped stacking
|
|
54
|
+
* context created by a shell/mobile plugin.
|
|
55
|
+
*/
|
|
56
|
+
function positionMenu(button: HTMLButtonElement): MenuPosition {
|
|
57
|
+
const rect = button.getBoundingClientRect()
|
|
58
|
+
const narrow = window.matchMedia('(max-width: 767px)').matches
|
|
59
|
+
const minLeft = narrow ? PHONE_MENU_RAIL_WIDTH + MENU_VIEWPORT_GAP : MENU_VIEWPORT_GAP
|
|
60
|
+
const availableWidth = Math.max(0, window.innerWidth - minLeft - MENU_VIEWPORT_GAP)
|
|
61
|
+
const width = Math.min(MENU_PREFERRED_WIDTH, availableWidth)
|
|
62
|
+
const maxLeft = Math.max(minLeft, window.innerWidth - MENU_VIEWPORT_GAP - width)
|
|
63
|
+
return {
|
|
64
|
+
// The old in-header popover used `right:12px`; retain that visual anchor
|
|
65
|
+
// but clamp its left edge to the usable side of the rail.
|
|
66
|
+
left: Math.max(minLeft, Math.min(rect.right - MENU_VIEWPORT_GAP - width, maxLeft)),
|
|
67
|
+
top: Math.min(rect.bottom + 6, Math.max(MENU_VIEWPORT_GAP, window.innerHeight - 72)),
|
|
68
|
+
width,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
39
72
|
/** Longest-path workspace match for a cwd (subdirectory allowed). */
|
|
40
73
|
function matchWorkspace(cwd: string | undefined, workspaces: WorkspaceRow[]): WorkspaceRow | undefined {
|
|
41
74
|
if (cwd === undefined) return undefined
|
|
@@ -73,23 +106,48 @@ export function QuickCommandsHeaderAction(props: HeaderProps): JSX.Element {
|
|
|
73
106
|
const [anchor, setAnchor] = useState<'corner' | 'button'>('corner')
|
|
74
107
|
const [popupSize, setPopupSize] = useState<QuickPopupSize | undefined>(undefined)
|
|
75
108
|
const wrapRef = useRef<HTMLDivElement | null>(null)
|
|
109
|
+
const buttonRef = useRef<HTMLButtonElement | null>(null)
|
|
110
|
+
const menuRef = useRef<HTMLDivElement | null>(null)
|
|
111
|
+
const [menuPosition, setMenuPosition] = useState<MenuPosition | null>(null)
|
|
76
112
|
|
|
77
|
-
// Click-outside dismissal:
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
// touch and pen, and fires before the button's own click toggle.
|
|
113
|
+
// Click-outside dismissal: the command menu is portaled to document.body so
|
|
114
|
+
// it escapes the mobile shell's clipped/z-indexed header context. Treat both
|
|
115
|
+
// its portal node and its original header wrap as inside targets.
|
|
81
116
|
useEffect(() => {
|
|
82
117
|
if (!popover.menuOpen) return
|
|
83
118
|
const onPointerDown = (e: PointerEvent): void => {
|
|
84
119
|
const wrap = wrapRef.current
|
|
85
|
-
|
|
86
|
-
if (e.target instanceof Node
|
|
120
|
+
const menu = menuRef.current
|
|
121
|
+
if (!(e.target instanceof Node)) return
|
|
122
|
+
if (wrap?.contains(e.target) || menu?.contains(e.target)) return
|
|
87
123
|
closeMenu()
|
|
88
124
|
}
|
|
89
125
|
document.addEventListener('pointerdown', onPointerDown)
|
|
90
126
|
return () => { document.removeEventListener('pointerdown', onPointerDown) }
|
|
91
127
|
}, [popover.menuOpen])
|
|
92
128
|
|
|
129
|
+
// Recalculate against the actual header button whenever the menu opens and
|
|
130
|
+
// whenever a viewport change moves it (rotation, browser chrome resize).
|
|
131
|
+
useLayoutEffect(() => {
|
|
132
|
+
if (!popover.menuOpen) {
|
|
133
|
+
setMenuPosition(null)
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
const update = (): void => {
|
|
137
|
+
const button = buttonRef.current
|
|
138
|
+
if (button !== null) setMenuPosition(positionMenu(button))
|
|
139
|
+
}
|
|
140
|
+
update()
|
|
141
|
+
window.addEventListener('resize', update)
|
|
142
|
+
window.addEventListener('scroll', update, true)
|
|
143
|
+
window.visualViewport?.addEventListener('resize', update)
|
|
144
|
+
return () => {
|
|
145
|
+
window.removeEventListener('resize', update)
|
|
146
|
+
window.removeEventListener('scroll', update, true)
|
|
147
|
+
window.visualViewport?.removeEventListener('resize', update)
|
|
148
|
+
}
|
|
149
|
+
}, [popover.menuOpen])
|
|
150
|
+
|
|
93
151
|
// Load the workspace's command list + anchor when the menu opens.
|
|
94
152
|
useEffect(() => {
|
|
95
153
|
if (!popover.menuOpen) return
|
|
@@ -135,6 +193,7 @@ export function QuickCommandsHeaderAction(props: HeaderProps): JSX.Element {
|
|
|
135
193
|
return (
|
|
136
194
|
<div ref={wrapRef} className="qc-head-wrap">
|
|
137
195
|
<button
|
|
196
|
+
ref={buttonRef}
|
|
138
197
|
type="button"
|
|
139
198
|
className="qc-head-btn"
|
|
140
199
|
title={workspace === undefined ? t('noWorkspace') : t('buttonTooltip')}
|
|
@@ -145,8 +204,13 @@ export function QuickCommandsHeaderAction(props: HeaderProps): JSX.Element {
|
|
|
145
204
|
<IconPlayOutline16 size={15} />
|
|
146
205
|
</button>
|
|
147
206
|
|
|
148
|
-
{popover.menuOpen && workspace !== undefined && (
|
|
149
|
-
<div
|
|
207
|
+
{popover.menuOpen && workspace !== undefined && menuPosition !== null && createPortal(
|
|
208
|
+
<div
|
|
209
|
+
ref={menuRef}
|
|
210
|
+
className="qc-menu qc-menu-portal"
|
|
211
|
+
role="menu"
|
|
212
|
+
style={{ left: menuPosition.left, top: menuPosition.top, width: menuPosition.width }}
|
|
213
|
+
>
|
|
150
214
|
<div className="qc-menu-head">
|
|
151
215
|
<span className="qc-menu-title">{t('menuTitle')}</span>
|
|
152
216
|
{menuRemoteHost !== null && <span className="qc-menu-remote" title={`SSH · ${menuRemoteHost}`}>SSH</span>}
|
|
@@ -172,7 +236,7 @@ export function QuickCommandsHeaderAction(props: HeaderProps): JSX.Element {
|
|
|
172
236
|
</button>
|
|
173
237
|
))}
|
|
174
238
|
</div>
|
|
175
|
-
)}
|
|
239
|
+
, document.body)}
|
|
176
240
|
|
|
177
241
|
{popover.runId !== null && (
|
|
178
242
|
<RunPopup
|
|
@@ -279,7 +343,6 @@ function RunPopup(props: {
|
|
|
279
343
|
}))
|
|
280
344
|
const [view, setView] = useState<'stdout' | 'stderr'>('stdout')
|
|
281
345
|
const [autoFollow, setAutoFollow] = useState(true)
|
|
282
|
-
const [killConfirm, setKillConfirm] = useState(false)
|
|
283
346
|
const [pollError, setPollError] = useState<string | null>(null)
|
|
284
347
|
const [closing, setClosing] = useState(false)
|
|
285
348
|
const outputRef = useRef<HTMLDivElement | null>(null)
|
|
@@ -342,18 +405,19 @@ function RunPopup(props: {
|
|
|
342
405
|
}
|
|
343
406
|
}
|
|
344
407
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
408
|
+
// Closing the output surface must not terminate a still-running command:
|
|
409
|
+
// the dedicated stop control owns that destructive action. In particular,
|
|
410
|
+
// an X in a compact touch header is expected to dismiss immediately rather
|
|
411
|
+
// than reveal a second confirmation row below the visible viewport.
|
|
412
|
+
const closeOutput = (): void => { onClose() }
|
|
413
|
+
|
|
414
|
+
// Some mobile webviews occasionally fail to synthesize `click` for compact
|
|
415
|
+
// header controls after a touch gesture. Handle touch pointer-up directly;
|
|
416
|
+
// click remains the keyboard and mouse activation path.
|
|
417
|
+
const onClosePointerUp = (e: React.PointerEvent<HTMLButtonElement>): void => {
|
|
418
|
+
if (e.pointerType !== 'touch') return
|
|
419
|
+
e.preventDefault()
|
|
420
|
+
closeOutput()
|
|
357
421
|
}
|
|
358
422
|
|
|
359
423
|
// ── drag-resize: pointer capture on the handles keeps moving/cancelling
|
|
@@ -438,22 +502,13 @@ function RunPopup(props: {
|
|
|
438
502
|
className="qc-popup-icon qc-popup-close"
|
|
439
503
|
title={t('runClose')}
|
|
440
504
|
aria-label={t('runClose')}
|
|
441
|
-
|
|
505
|
+
onPointerUp={onClosePointerUp}
|
|
506
|
+
onClick={closeOutput}
|
|
442
507
|
>
|
|
443
508
|
<IconCloseOutline16 size={14} />
|
|
444
509
|
</button>
|
|
445
510
|
</header>
|
|
446
511
|
|
|
447
|
-
{killConfirm && (
|
|
448
|
-
<div className="qc-popup-confirm">
|
|
449
|
-
<span>{t('runKillConfirm')}</span>
|
|
450
|
-
<button type="button" className="qc-popup-btn" onClick={confirmClose}>{t('runKill')}</button>
|
|
451
|
-
<button type="button" className="qc-popup-btn qc-popup-btn-plain" onClick={() => setKillConfirm(false)}>
|
|
452
|
-
{t('runClose')}
|
|
453
|
-
</button>
|
|
454
|
-
</div>
|
|
455
|
-
)}
|
|
456
|
-
|
|
457
512
|
<div className="qc-popup-tabs">
|
|
458
513
|
<button
|
|
459
514
|
type="button"
|
package/src/client/index.tsx
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* @fonlan/dsh-quick-commands client half.
|
|
3
3
|
*
|
|
4
4
|
* Registers:
|
|
5
|
-
* -
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* - "settings.section" page (id "quick-commands", order 330) — per-workspace
|
|
6
|
+
* command CRUD + popup anchor preference, rendered as its own page in the
|
|
7
|
+
* settings sidebar.
|
|
8
|
+
* - "conversation.session.header.utilities" entry (id "quick-commands",
|
|
8
9
|
* order -1 → left of the Session log pill) — the quick-run ▶ button, command
|
|
9
10
|
* menu, and live output popup.
|
|
10
11
|
*/
|
|
@@ -12,9 +13,8 @@ import type { Context } from '@deepseek-ai/cordis'
|
|
|
12
13
|
type ClientContext = Context
|
|
13
14
|
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
14
15
|
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
15
|
-
import type {} from '@deepseek-ai/dsh-client-ui-settings-plugins/client'
|
|
16
16
|
import { QuickCommandsHeaderAction } from './header'
|
|
17
|
-
import {
|
|
17
|
+
import { QuickCommandsSettingsSection } from './settings-section'
|
|
18
18
|
import { LOCALE_NS, zh, en } from './locales'
|
|
19
19
|
import './quick-commands.css'
|
|
20
20
|
|
|
@@ -22,7 +22,7 @@ import './quick-commands.css'
|
|
|
22
22
|
interface Slots {
|
|
23
23
|
inject(name: string, callback: () => unknown): unknown
|
|
24
24
|
register(
|
|
25
|
-
def: { name: string;
|
|
25
|
+
def: { name: string; id?: string; order?: number; label?: string | (() => string); locale?: string },
|
|
26
26
|
component: unknown,
|
|
27
27
|
): unknown
|
|
28
28
|
}
|
|
@@ -36,16 +36,17 @@ export function apply(ctx: ClientContext): void {
|
|
|
36
36
|
const t = ctx.locale.bind(LOCALE_NS)
|
|
37
37
|
const slots = (ctx as unknown as { slots: Slots }).slots
|
|
38
38
|
|
|
39
|
-
// The plugin's own
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
slots.inject('settings.plugin.item', () =>
|
|
39
|
+
// The plugin's own settings page (设置 → 侧栏「快捷命令」) rides the
|
|
40
|
+
// "quick-commands" locale namespace: a "settings.section" list entry gives it
|
|
41
|
+
// one row in the settings sidebar and renders the page in the content column.
|
|
42
|
+
slots.inject('settings.section', () =>
|
|
44
43
|
slots.register({
|
|
45
|
-
name: 'settings.
|
|
46
|
-
|
|
44
|
+
name: 'settings.section',
|
|
45
|
+
id: 'quick-commands',
|
|
46
|
+
order: 330,
|
|
47
|
+
label: () => t('settingsTitle'),
|
|
47
48
|
locale: LOCALE_NS,
|
|
48
|
-
},
|
|
49
|
+
}, QuickCommandsSettingsSection as never),
|
|
49
50
|
)
|
|
50
51
|
|
|
51
52
|
// Session-header util: order -1 renders LEFT of the Session log pill
|