@fonlan/dsh-quick-commands 0.1.0
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/LICENSE +21 -0
- package/README.en.md +149 -0
- package/README.md +108 -0
- package/cordis.patch.yml +3 -0
- package/lib/client.js +1036 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +904 -0
- package/lib/types/index.d.ts +19 -0
- package/lib/types/server/remote.d.ts +117 -0
- package/lib/types/server/rpc.d.ts +4 -0
- package/lib/types/server/runner.d.ts +114 -0
- package/lib/types/server/settings.d.ts +24 -0
- package/lib/types/shared/contract.d.ts +87 -0
- package/package.json +108 -0
- package/src/client/api.ts +76 -0
- package/src/client/header.tsx +534 -0
- package/src/client/index.tsx +50 -0
- package/src/client/locales.ts +106 -0
- package/src/client/quick-commands.css +580 -0
- package/src/client/run-state.ts +125 -0
- package/src/client/settings-card.tsx +209 -0
- package/src/client/slot-augment.d.ts +14 -0
- package/src/index.ts +61 -0
- package/src/server/remote.ts +304 -0
- package/src/server/rpc.ts +286 -0
- package/src/server/runner.ts +471 -0
- package/src/server/settings.ts +106 -0
- package/src/shared/contract.ts +92 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,1036 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@fonlan/dsh-quick-commands",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
9
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
10
|
+
//#region src/client/api.ts
|
|
11
|
+
var QuickCommandsApiError = class extends Error {
|
|
12
|
+
code;
|
|
13
|
+
constructor(code, message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.code = code;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
async function call(method, payload = {}) {
|
|
19
|
+
let response;
|
|
20
|
+
try {
|
|
21
|
+
response = await fetch(`/plugins/@fonlan/dsh-quick-commands/api/${method}`, {
|
|
22
|
+
method: "POST",
|
|
23
|
+
headers: { "content-type": "application/json" },
|
|
24
|
+
body: JSON.stringify(payload)
|
|
25
|
+
});
|
|
26
|
+
} catch (error) {
|
|
27
|
+
throw new QuickCommandsApiError("network", error instanceof Error ? error.message : String(error));
|
|
28
|
+
}
|
|
29
|
+
const parsed = await response.json().catch(() => null);
|
|
30
|
+
if (!response.ok || parsed === null || parsed.ok !== true || parsed.value === void 0) throw new QuickCommandsApiError(parsed?.error?.code ?? "http", parsed?.error?.message ?? `HTTP ${response.status}`);
|
|
31
|
+
return parsed.value;
|
|
32
|
+
}
|
|
33
|
+
const quickApi = {
|
|
34
|
+
settingsGet: () => call("settings.get"),
|
|
35
|
+
settingsSetCommands: (workspaceId, commands) => call("settings.setCommands", {
|
|
36
|
+
workspaceId,
|
|
37
|
+
commands
|
|
38
|
+
}),
|
|
39
|
+
settingsSetAnchor: (anchor) => call("settings.setAnchor", { anchor }),
|
|
40
|
+
settingsSetPopupSize: (size) => call("settings.setPopupSize", {
|
|
41
|
+
width: size.width,
|
|
42
|
+
height: size.height
|
|
43
|
+
}),
|
|
44
|
+
workspacesList: () => call("workspaces.list"),
|
|
45
|
+
runStart: (workspaceId, commandName, sessionCwd) => call("run.start", {
|
|
46
|
+
workspaceId,
|
|
47
|
+
commandName,
|
|
48
|
+
sessionCwd
|
|
49
|
+
}),
|
|
50
|
+
runPoll: (runId, stdoutFrom, stderrFrom) => call("run.poll", {
|
|
51
|
+
runId,
|
|
52
|
+
stdoutFrom,
|
|
53
|
+
stderrFrom
|
|
54
|
+
}),
|
|
55
|
+
runKill: (runId) => call("run.kill", { runId }),
|
|
56
|
+
runList: (workspaceId) => call("run.list", { workspaceId })
|
|
57
|
+
};
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region src/client/run-state.ts
|
|
60
|
+
/**
|
|
61
|
+
* HMR-safe shared run state for the quick-commands header button and output
|
|
62
|
+
* popup. Module-level fields live on one globalThis-backed record keyed with
|
|
63
|
+
* Symbol.for so every client-bundle copy (client-hmr hot swap re-evaluates the
|
|
64
|
+
* bundle without reloading the page) reads/writes the same state cell.
|
|
65
|
+
*
|
|
66
|
+
* The popup handles ONE run at a time (serial per workspace by design); a
|
|
67
|
+
* second workspace can run in parallel with its own popup, so the store keys
|
|
68
|
+
* popups by runId.
|
|
69
|
+
*/
|
|
70
|
+
const STATE_KEY = Symbol.for("@fonlan/dsh-quick-commands/run-state");
|
|
71
|
+
function getState() {
|
|
72
|
+
const g = globalThis;
|
|
73
|
+
let state = g[STATE_KEY];
|
|
74
|
+
if (state === void 0) {
|
|
75
|
+
state = {
|
|
76
|
+
popover: {
|
|
77
|
+
menuOpen: false,
|
|
78
|
+
runId: null,
|
|
79
|
+
workspaceId: null,
|
|
80
|
+
commandName: null,
|
|
81
|
+
remoteHost: null,
|
|
82
|
+
streaming: true,
|
|
83
|
+
listeners: /* @__PURE__ */ new Set()
|
|
84
|
+
},
|
|
85
|
+
snapshot: {
|
|
86
|
+
menuOpen: false,
|
|
87
|
+
runId: null,
|
|
88
|
+
workspaceId: null,
|
|
89
|
+
commandName: null,
|
|
90
|
+
remoteHost: null,
|
|
91
|
+
streaming: true
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
g[STATE_KEY] = state;
|
|
95
|
+
}
|
|
96
|
+
return state;
|
|
97
|
+
}
|
|
98
|
+
function notify() {
|
|
99
|
+
getState().snapshot = null;
|
|
100
|
+
for (const listener of getState().popover.listeners) listener();
|
|
101
|
+
}
|
|
102
|
+
function subscribe(cb) {
|
|
103
|
+
const listeners = getState().popover.listeners;
|
|
104
|
+
listeners.add(cb);
|
|
105
|
+
return () => {
|
|
106
|
+
listeners.delete(cb);
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* getSnapshot for useSyncExternalStore. MUST return a stable reference while
|
|
111
|
+
* the underlying fields are unchanged (React compares with Object.is) — return
|
|
112
|
+
* the CACHED object; only rebuild it when a patch invalidated it.
|
|
113
|
+
*/
|
|
114
|
+
function snapshot() {
|
|
115
|
+
const state = getState();
|
|
116
|
+
if (state.snapshot !== null) return state.snapshot;
|
|
117
|
+
const p = state.popover;
|
|
118
|
+
state.snapshot = {
|
|
119
|
+
menuOpen: p.menuOpen,
|
|
120
|
+
runId: p.runId,
|
|
121
|
+
workspaceId: p.workspaceId,
|
|
122
|
+
commandName: p.commandName,
|
|
123
|
+
remoteHost: p.remoteHost,
|
|
124
|
+
streaming: p.streaming
|
|
125
|
+
};
|
|
126
|
+
return state.snapshot;
|
|
127
|
+
}
|
|
128
|
+
function patch(partial) {
|
|
129
|
+
const state = getState();
|
|
130
|
+
Object.assign(state.popover, partial);
|
|
131
|
+
state.snapshot = null;
|
|
132
|
+
notify();
|
|
133
|
+
}
|
|
134
|
+
function useRunPopover() {
|
|
135
|
+
return (0, react.useSyncExternalStore)(subscribe, snapshot, snapshot);
|
|
136
|
+
}
|
|
137
|
+
function openMenu() {
|
|
138
|
+
patch({ menuOpen: true });
|
|
139
|
+
}
|
|
140
|
+
function closeMenu() {
|
|
141
|
+
patch({ menuOpen: false });
|
|
142
|
+
}
|
|
143
|
+
function openRun(runId, workspaceId, commandName, remoteHost) {
|
|
144
|
+
patch({
|
|
145
|
+
runId,
|
|
146
|
+
workspaceId,
|
|
147
|
+
commandName,
|
|
148
|
+
remoteHost: remoteHost ?? null,
|
|
149
|
+
menuOpen: false,
|
|
150
|
+
streaming: true
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
function closeRun() {
|
|
154
|
+
patch({
|
|
155
|
+
runId: null,
|
|
156
|
+
workspaceId: null,
|
|
157
|
+
commandName: null,
|
|
158
|
+
remoteHost: null,
|
|
159
|
+
streaming: false
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region src/client/header.tsx
|
|
164
|
+
/**
|
|
165
|
+
* Session-header quick-command surface: the ▶ play icon button (left of the
|
|
166
|
+
* Session log pill), the command menu popover (current workspace's commands),
|
|
167
|
+
* and the live output popup that streams stdout/stderr at ~200ms cadence.
|
|
168
|
+
*
|
|
169
|
+
* Props are the standard session/global kit of `conversation.session.header.utilities`
|
|
170
|
+
* plus this plugin's injected hooks (anchor preference + resolver).
|
|
171
|
+
*/
|
|
172
|
+
/** Longest-path workspace match for a cwd (subdirectory allowed). */
|
|
173
|
+
function matchWorkspace(cwd, workspaces) {
|
|
174
|
+
if (cwd === void 0) return void 0;
|
|
175
|
+
let best;
|
|
176
|
+
for (const ws of workspaces) {
|
|
177
|
+
const norm = ws.path.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
178
|
+
const nc = cwd.replace(/\\/g, "/");
|
|
179
|
+
if (nc === norm || nc.startsWith(norm + "/")) {
|
|
180
|
+
if (best === void 0 || norm.length > best.path.length) best = ws;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return best;
|
|
184
|
+
}
|
|
185
|
+
/** Resolve the current session's workspace from the standard kits. */
|
|
186
|
+
function useCurrentWorkspace(sessionId, useSessions, useWorkspaces) {
|
|
187
|
+
const row = useSessions?.((s) => s.byId[sessionId]);
|
|
188
|
+
const workspaces = useWorkspaces?.((s) => s.items);
|
|
189
|
+
return {
|
|
190
|
+
workspace: matchWorkspace(row?.cwd, workspaces ?? []),
|
|
191
|
+
cwd: row?.cwd
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/** Header icon button + command menu + live output popup. */
|
|
195
|
+
function QuickCommandsHeaderAction(props) {
|
|
196
|
+
const { sessionId, t } = props;
|
|
197
|
+
const { workspace, cwd } = useCurrentWorkspace(sessionId, props.useSessions, props.useWorkspaces);
|
|
198
|
+
const popover = useRunPopover();
|
|
199
|
+
const [commands, setCommands] = (0, react.useState)([]);
|
|
200
|
+
const [menuLoadError, setMenuLoadError] = (0, react.useState)(null);
|
|
201
|
+
const [menuRemoteHost, setMenuRemoteHost] = (0, react.useState)(null);
|
|
202
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
203
|
+
const [anchor, setAnchor] = (0, react.useState)("corner");
|
|
204
|
+
const [popupSize, setPopupSize] = (0, react.useState)(void 0);
|
|
205
|
+
const wrapRef = (0, react.useRef)(null);
|
|
206
|
+
(0, react.useEffect)(() => {
|
|
207
|
+
if (!popover.menuOpen) return;
|
|
208
|
+
const onPointerDown = (e) => {
|
|
209
|
+
const wrap = wrapRef.current;
|
|
210
|
+
if (wrap === null) return;
|
|
211
|
+
if (e.target instanceof Node && wrap.contains(e.target)) return;
|
|
212
|
+
closeMenu();
|
|
213
|
+
};
|
|
214
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
215
|
+
return () => {
|
|
216
|
+
document.removeEventListener("pointerdown", onPointerDown);
|
|
217
|
+
};
|
|
218
|
+
}, [popover.menuOpen]);
|
|
219
|
+
(0, react.useEffect)(() => {
|
|
220
|
+
if (!popover.menuOpen) return;
|
|
221
|
+
let cancelled = false;
|
|
222
|
+
(async () => {
|
|
223
|
+
try {
|
|
224
|
+
const [entries, settings] = await Promise.all([quickApi.workspacesList(), quickApi.settingsGet()]);
|
|
225
|
+
if (cancelled) return;
|
|
226
|
+
const own = entries.find((e) => e.workspaceId === workspace?.workspaceId);
|
|
227
|
+
setCommands(own === void 0 ? [] : own.commands);
|
|
228
|
+
setMenuRemoteHost(own?.remoteHost ?? null);
|
|
229
|
+
setAnchor(settings.popupAnchor);
|
|
230
|
+
setPopupSize(settings.popupSize);
|
|
231
|
+
setMenuLoadError(null);
|
|
232
|
+
} catch (e) {
|
|
233
|
+
if (cancelled) return;
|
|
234
|
+
setCommands([]);
|
|
235
|
+
setMenuLoadError(e instanceof Error ? e.message : String(e));
|
|
236
|
+
}
|
|
237
|
+
})();
|
|
238
|
+
return () => {
|
|
239
|
+
cancelled = true;
|
|
240
|
+
};
|
|
241
|
+
}, [popover.menuOpen, workspace?.workspaceId]);
|
|
242
|
+
const startRun = async (commandName) => {
|
|
243
|
+
setBusy(true);
|
|
244
|
+
closeMenu();
|
|
245
|
+
try {
|
|
246
|
+
const run = await quickApi.runStart(workspace.workspaceId, commandName, cwd ?? workspace.path);
|
|
247
|
+
openRun(run.runId, run.workspaceId, run.commandName, run.remoteHost);
|
|
248
|
+
} catch (e) {
|
|
249
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
250
|
+
if (message.includes("run-busy") || message.includes("already running")) setMenuLoadError(t("runBusy"));
|
|
251
|
+
else setMenuLoadError(t("runStartFailed") + ": " + message);
|
|
252
|
+
openMenu();
|
|
253
|
+
} finally {
|
|
254
|
+
setBusy(false);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
258
|
+
ref: wrapRef,
|
|
259
|
+
className: "qc-head-wrap",
|
|
260
|
+
children: [
|
|
261
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
262
|
+
type: "button",
|
|
263
|
+
className: "qc-head-btn",
|
|
264
|
+
title: workspace === void 0 ? t("noWorkspace") : t("buttonTooltip"),
|
|
265
|
+
"aria-label": t("buttonTitle"),
|
|
266
|
+
disabled: workspace === void 0,
|
|
267
|
+
onClick: () => {
|
|
268
|
+
popover.menuOpen ? closeMenu() : openMenu();
|
|
269
|
+
},
|
|
270
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlayOutline16, { size: 16 })
|
|
271
|
+
}),
|
|
272
|
+
popover.menuOpen && workspace !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
273
|
+
className: "qc-menu",
|
|
274
|
+
role: "menu",
|
|
275
|
+
children: [
|
|
276
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
277
|
+
className: "qc-menu-head",
|
|
278
|
+
children: [
|
|
279
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
280
|
+
className: "qc-menu-title",
|
|
281
|
+
children: t("menuTitle")
|
|
282
|
+
}),
|
|
283
|
+
menuRemoteHost !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
284
|
+
className: "qc-menu-remote",
|
|
285
|
+
title: `SSH · ${menuRemoteHost}`,
|
|
286
|
+
children: "SSH"
|
|
287
|
+
}),
|
|
288
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
289
|
+
className: "qc-menu-ws",
|
|
290
|
+
children: workspace.title
|
|
291
|
+
}),
|
|
292
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
293
|
+
type: "button",
|
|
294
|
+
className: "qc-menu-close",
|
|
295
|
+
"aria-label": t("runClose"),
|
|
296
|
+
onClick: closeMenu,
|
|
297
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutline16, { size: 12 })
|
|
298
|
+
})
|
|
299
|
+
]
|
|
300
|
+
}),
|
|
301
|
+
commands.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
302
|
+
className: "qc-menu-empty",
|
|
303
|
+
children: menuLoadError ?? t("noCommands")
|
|
304
|
+
}),
|
|
305
|
+
commands.filter((c) => c.name.trim() !== "").map((command) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
306
|
+
type: "button",
|
|
307
|
+
role: "menuitem",
|
|
308
|
+
className: "qc-menu-item",
|
|
309
|
+
disabled: busy,
|
|
310
|
+
onClick: () => void startRun(command.name),
|
|
311
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
312
|
+
className: "qc-menu-item-name",
|
|
313
|
+
children: command.name
|
|
314
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
315
|
+
className: "qc-menu-item-cmd",
|
|
316
|
+
children: command.command
|
|
317
|
+
})]
|
|
318
|
+
}, command.name))
|
|
319
|
+
]
|
|
320
|
+
}),
|
|
321
|
+
popover.runId !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RunPopup, {
|
|
322
|
+
runId: popover.runId,
|
|
323
|
+
workspaceId: popover.workspaceId ?? "",
|
|
324
|
+
commandName: popover.commandName ?? "",
|
|
325
|
+
remoteHost: popover.remoteHost ?? void 0,
|
|
326
|
+
t,
|
|
327
|
+
anchor,
|
|
328
|
+
popupSize,
|
|
329
|
+
sessionId,
|
|
330
|
+
onClose: closeRun
|
|
331
|
+
})
|
|
332
|
+
]
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
const POPUP_MIN_W = 320;
|
|
336
|
+
const POPUP_MIN_H = 200;
|
|
337
|
+
/** Total horizontal margin kept around the popup (both anchors). */
|
|
338
|
+
const POPUP_VIEWPORT_MARGIN = 40;
|
|
339
|
+
/** Vertical space reserved for header/footer chrome (both anchors). */
|
|
340
|
+
const POPUP_VIEWPORT_RESERVED = 120;
|
|
341
|
+
function popupDefaultSize(anchor) {
|
|
342
|
+
return anchor === "corner" ? {
|
|
343
|
+
width: 520,
|
|
344
|
+
height: 340
|
|
345
|
+
} : {
|
|
346
|
+
width: 440,
|
|
347
|
+
height: 300
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
function popupClamp(size) {
|
|
351
|
+
const maxW = Math.max(POPUP_MIN_W, window.innerWidth - POPUP_VIEWPORT_MARGIN);
|
|
352
|
+
const maxH = Math.max(POPUP_MIN_H, window.innerHeight - POPUP_VIEWPORT_RESERVED);
|
|
353
|
+
return {
|
|
354
|
+
width: Math.min(Math.max(POPUP_MIN_W, Math.round(size.width)), maxW),
|
|
355
|
+
height: Math.min(Math.max(POPUP_MIN_H, Math.round(size.height)), maxH)
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Initial popup size: the persisted one when it holds sane numbers, else the
|
|
360
|
+
* per-anchor default. Defensive because schemastery resolves a MISSING object
|
|
361
|
+
* field to `{}` (object schemas carry an implicit `default: {}`), so a
|
|
362
|
+
* never-resized popup can arrive as `popupSize: {}` from settings.get().
|
|
363
|
+
*/
|
|
364
|
+
function popupSizeOrDefault(size, anchor) {
|
|
365
|
+
if (size === void 0 || !Number.isFinite(size.width) || !Number.isFinite(size.height) || size.width <= 0 || size.height <= 0) return popupDefaultSize(anchor);
|
|
366
|
+
return popupClamp(size);
|
|
367
|
+
}
|
|
368
|
+
/** The streaming output popup. Polls stdout/stderr deltas every ~200ms. */
|
|
369
|
+
function RunPopup(props) {
|
|
370
|
+
const { runId, t, anchor, remoteHost, onClose } = props;
|
|
371
|
+
const [size, setSize] = (0, react.useState)(() => popupSizeOrDefault(props.popupSize, anchor));
|
|
372
|
+
const sizeRef = (0, react.useRef)(size);
|
|
373
|
+
const sizeDragRef = (0, react.useRef)(null);
|
|
374
|
+
const [state, setState] = (0, react.useState)(() => ({
|
|
375
|
+
status: "running",
|
|
376
|
+
exitCode: null,
|
|
377
|
+
signal: null,
|
|
378
|
+
stdoutEnd: 0,
|
|
379
|
+
stderrEnd: 0,
|
|
380
|
+
stdout: "",
|
|
381
|
+
stderr: "",
|
|
382
|
+
stdoutLossy: false,
|
|
383
|
+
stderrLossy: false
|
|
384
|
+
}));
|
|
385
|
+
const [view, setView] = (0, react.useState)("stdout");
|
|
386
|
+
const [autoFollow, setAutoFollow] = (0, react.useState)(true);
|
|
387
|
+
const [killConfirm, setKillConfirm] = (0, react.useState)(false);
|
|
388
|
+
const [pollError, setPollError] = (0, react.useState)(null);
|
|
389
|
+
const [closing, setClosing] = (0, react.useState)(false);
|
|
390
|
+
const outputRef = (0, react.useRef)(null);
|
|
391
|
+
const autoFollowRef = (0, react.useRef)(true);
|
|
392
|
+
(0, react.useEffect)(() => {
|
|
393
|
+
autoFollowRef.current = autoFollow;
|
|
394
|
+
}, [autoFollow]);
|
|
395
|
+
(0, react.useEffect)(() => {
|
|
396
|
+
if (!autoFollowRef.current) return;
|
|
397
|
+
const el = outputRef.current;
|
|
398
|
+
if (el === null) return;
|
|
399
|
+
el.scrollTop = el.scrollHeight;
|
|
400
|
+
}, [
|
|
401
|
+
state.stdout,
|
|
402
|
+
state.stderr,
|
|
403
|
+
view
|
|
404
|
+
]);
|
|
405
|
+
(0, react.useEffect)(() => {
|
|
406
|
+
let cancelled = false;
|
|
407
|
+
let outEnd = 0;
|
|
408
|
+
let errEnd = 0;
|
|
409
|
+
const tick = async () => {
|
|
410
|
+
try {
|
|
411
|
+
const poll = await quickApi.runPoll(runId, outEnd, errEnd);
|
|
412
|
+
if (cancelled) return;
|
|
413
|
+
outEnd = poll.stdoutEnd;
|
|
414
|
+
errEnd = poll.stderrEnd;
|
|
415
|
+
setState((s) => ({
|
|
416
|
+
status: poll.status,
|
|
417
|
+
exitCode: poll.exitCode,
|
|
418
|
+
signal: poll.signal,
|
|
419
|
+
stdoutEnd: poll.stdoutEnd,
|
|
420
|
+
stderrEnd: poll.stderrEnd,
|
|
421
|
+
stdout: poll.stdoutLossy ? poll.stdoutDelta : s.stdout + poll.stdoutDelta,
|
|
422
|
+
stderr: poll.stderrLossy ? poll.stderrDelta : s.stderr + poll.stderrDelta,
|
|
423
|
+
stdoutLossy: poll.stdoutLossy,
|
|
424
|
+
stderrLossy: poll.stderrLossy
|
|
425
|
+
}));
|
|
426
|
+
setPollError(null);
|
|
427
|
+
if (poll.status === "exited") return;
|
|
428
|
+
timer = window.setTimeout(() => void tick(), 200);
|
|
429
|
+
} catch (e) {
|
|
430
|
+
if (cancelled) return;
|
|
431
|
+
setPollError(e instanceof Error ? e.message : String(e));
|
|
432
|
+
timer = window.setTimeout(() => void tick(), 500);
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
let timer;
|
|
436
|
+
tick();
|
|
437
|
+
return () => {
|
|
438
|
+
cancelled = true;
|
|
439
|
+
window.clearTimeout(timer);
|
|
440
|
+
};
|
|
441
|
+
}, [runId]);
|
|
442
|
+
const kill = async () => {
|
|
443
|
+
setClosing(true);
|
|
444
|
+
try {
|
|
445
|
+
await quickApi.runKill(runId);
|
|
446
|
+
} finally {
|
|
447
|
+
onClose();
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
const close = () => {
|
|
451
|
+
if (state.status === "exited") {
|
|
452
|
+
onClose();
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
setKillConfirm(true);
|
|
456
|
+
};
|
|
457
|
+
const confirmClose = () => {
|
|
458
|
+
kill();
|
|
459
|
+
};
|
|
460
|
+
const heightFree = anchor === "corner" ? "top" : "bottom";
|
|
461
|
+
const onResizePointerDown = (e) => {
|
|
462
|
+
e.preventDefault();
|
|
463
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
464
|
+
sizeDragRef.current = {
|
|
465
|
+
pointerId: e.pointerId,
|
|
466
|
+
startX: e.clientX,
|
|
467
|
+
startY: e.clientY,
|
|
468
|
+
startW: sizeRef.current.width,
|
|
469
|
+
startH: sizeRef.current.height,
|
|
470
|
+
mode: e.currentTarget.dataset.resize
|
|
471
|
+
};
|
|
472
|
+
};
|
|
473
|
+
const onResizePointerMove = (e) => {
|
|
474
|
+
const drag = sizeDragRef.current;
|
|
475
|
+
if (drag === null || drag.pointerId !== e.pointerId) return;
|
|
476
|
+
const dx = e.clientX - drag.startX;
|
|
477
|
+
const dy = e.clientY - drag.startY;
|
|
478
|
+
const controlsW = drag.mode === "left" || drag.mode === "corner";
|
|
479
|
+
const controlsH = drag.mode === "height" || drag.mode === "corner";
|
|
480
|
+
const next = popupClamp({
|
|
481
|
+
width: controlsW ? drag.startW - dx : drag.startW,
|
|
482
|
+
height: controlsH ? drag.startH + (heightFree === "top" ? -dy : dy) : drag.startH
|
|
483
|
+
});
|
|
484
|
+
sizeRef.current = next;
|
|
485
|
+
setSize(next);
|
|
486
|
+
};
|
|
487
|
+
const onResizePointerUp = (e) => {
|
|
488
|
+
const drag = sizeDragRef.current;
|
|
489
|
+
if (drag === null || drag.pointerId !== e.pointerId) return;
|
|
490
|
+
sizeDragRef.current = null;
|
|
491
|
+
if (e.currentTarget.hasPointerCapture(e.pointerId)) e.currentTarget.releasePointerCapture(e.pointerId);
|
|
492
|
+
quickApi.settingsSetPopupSize(sizeRef.current).catch(() => void 0);
|
|
493
|
+
};
|
|
494
|
+
/** Pointer-handler trio for one resize handle (mode = which axes it drives). */
|
|
495
|
+
const resizeProps = (mode) => ({
|
|
496
|
+
"data-resize": mode,
|
|
497
|
+
onPointerDown: onResizePointerDown,
|
|
498
|
+
onPointerMove: onResizePointerMove,
|
|
499
|
+
onPointerUp: onResizePointerUp,
|
|
500
|
+
onPointerCancel: onResizePointerUp
|
|
501
|
+
});
|
|
502
|
+
const active = state.status === "running";
|
|
503
|
+
const title = active ? `${props.commandName} · ${t("runStateRunning")}` : `${props.commandName} · ${t("runStateExited")} (${state.exitCode ?? "?"})`;
|
|
504
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
505
|
+
className: `qc-popup qc-popup-${anchor}`,
|
|
506
|
+
role: "dialog",
|
|
507
|
+
"aria-label": title,
|
|
508
|
+
style: {
|
|
509
|
+
width: size.width,
|
|
510
|
+
height: size.height
|
|
511
|
+
},
|
|
512
|
+
children: [
|
|
513
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
514
|
+
className: "qc-popup-head",
|
|
515
|
+
children: [
|
|
516
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
517
|
+
className: "qc-popup-title",
|
|
518
|
+
title,
|
|
519
|
+
children: title
|
|
520
|
+
}),
|
|
521
|
+
remoteHost !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
522
|
+
className: "qc-popup-remote",
|
|
523
|
+
title: `SSH · ${remoteHost}`,
|
|
524
|
+
children: "SSH"
|
|
525
|
+
}),
|
|
526
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
527
|
+
className: "qc-popup-cmd",
|
|
528
|
+
children: props.commandName
|
|
529
|
+
}),
|
|
530
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
531
|
+
type: "button",
|
|
532
|
+
className: "qc-popup-icon qc-popup-kill",
|
|
533
|
+
disabled: !active || closing,
|
|
534
|
+
title: t("runKill"),
|
|
535
|
+
"aria-label": t("runKill"),
|
|
536
|
+
onClick: () => void kill(),
|
|
537
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconStopFill16, { size: 14 })
|
|
538
|
+
}),
|
|
539
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
540
|
+
type: "button",
|
|
541
|
+
className: "qc-popup-icon qc-popup-close",
|
|
542
|
+
title: t("runClose"),
|
|
543
|
+
"aria-label": t("runClose"),
|
|
544
|
+
onClick: close,
|
|
545
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutline16, { size: 14 })
|
|
546
|
+
})
|
|
547
|
+
]
|
|
548
|
+
}),
|
|
549
|
+
killConfirm && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
550
|
+
className: "qc-popup-confirm",
|
|
551
|
+
children: [
|
|
552
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("runKillConfirm") }),
|
|
553
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
554
|
+
type: "button",
|
|
555
|
+
className: "qc-popup-btn",
|
|
556
|
+
onClick: confirmClose,
|
|
557
|
+
children: t("runKill")
|
|
558
|
+
}),
|
|
559
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
560
|
+
type: "button",
|
|
561
|
+
className: "qc-popup-btn qc-popup-btn-plain",
|
|
562
|
+
onClick: () => setKillConfirm(false),
|
|
563
|
+
children: t("runClose")
|
|
564
|
+
})
|
|
565
|
+
]
|
|
566
|
+
}),
|
|
567
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
568
|
+
className: "qc-popup-tabs",
|
|
569
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
570
|
+
type: "button",
|
|
571
|
+
className: "qc-popup-tab" + (view === "stdout" ? " qc-popup-tab-active" : ""),
|
|
572
|
+
onClick: () => setView("stdout"),
|
|
573
|
+
children: t("runStdout")
|
|
574
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
575
|
+
type: "button",
|
|
576
|
+
className: "qc-popup-tab" + (view === "stderr" ? " qc-popup-tab-active" : ""),
|
|
577
|
+
onClick: () => setView("stderr"),
|
|
578
|
+
children: t("runStderr")
|
|
579
|
+
})]
|
|
580
|
+
}),
|
|
581
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
582
|
+
ref: outputRef,
|
|
583
|
+
className: "qc-popup-output",
|
|
584
|
+
onScroll: (e) => {
|
|
585
|
+
const el = e.currentTarget;
|
|
586
|
+
const atEnd = Math.abs(el.scrollHeight - el.scrollTop - el.clientHeight) < 24;
|
|
587
|
+
setAutoFollow(atEnd);
|
|
588
|
+
},
|
|
589
|
+
children: [pollError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
590
|
+
className: "qc-popup-error",
|
|
591
|
+
children: [
|
|
592
|
+
t("runPollFailed"),
|
|
593
|
+
": ",
|
|
594
|
+
pollError
|
|
595
|
+
]
|
|
596
|
+
}), view === "stdout" ? state.stdout === "" && !active ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
597
|
+
className: "qc-popup-empty",
|
|
598
|
+
children: t("runEmpty")
|
|
599
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
600
|
+
className: "qc-popup-pre",
|
|
601
|
+
children: state.stdout
|
|
602
|
+
}) : state.stderr === "" && !active ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
603
|
+
className: "qc-popup-empty",
|
|
604
|
+
children: t("runEmpty")
|
|
605
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
606
|
+
className: "qc-popup-pre",
|
|
607
|
+
children: state.stderr
|
|
608
|
+
})]
|
|
609
|
+
}),
|
|
610
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("footer", {
|
|
611
|
+
className: "qc-popup-foot",
|
|
612
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
613
|
+
className: "qc-popup-state",
|
|
614
|
+
children: active ? t("runStateRunning") : `${t("runStateExited")} · ${t("runExitCode")} ${state.exitCode ?? "-"}${state.signal !== null ? ` · ${state.signal}` : ""}`
|
|
615
|
+
})
|
|
616
|
+
}),
|
|
617
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
618
|
+
className: "qc-popup-resize qc-popup-resize-left",
|
|
619
|
+
title: t("runResize"),
|
|
620
|
+
...resizeProps("left")
|
|
621
|
+
}),
|
|
622
|
+
heightFree === "top" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
623
|
+
className: "qc-popup-resize qc-popup-resize-top",
|
|
624
|
+
title: t("runResize"),
|
|
625
|
+
...resizeProps("height")
|
|
626
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
627
|
+
className: "qc-popup-resize qc-popup-resize-bottom",
|
|
628
|
+
title: t("runResize"),
|
|
629
|
+
...resizeProps("height")
|
|
630
|
+
}),
|
|
631
|
+
heightFree === "top" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
632
|
+
className: "qc-popup-resize qc-popup-resize-corner qc-popup-resize-corner-tl",
|
|
633
|
+
title: t("runResize"),
|
|
634
|
+
...resizeProps("corner"),
|
|
635
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
636
|
+
width: "12",
|
|
637
|
+
height: "12",
|
|
638
|
+
viewBox: "0 0 12 12",
|
|
639
|
+
"aria-hidden": "true",
|
|
640
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
641
|
+
d: "M9 9 3 3 M9 5.5 5.5 3 M5.5 9 3 5.5",
|
|
642
|
+
fill: "none",
|
|
643
|
+
stroke: "currentColor",
|
|
644
|
+
strokeWidth: "1.4",
|
|
645
|
+
strokeLinecap: "round",
|
|
646
|
+
strokeLinejoin: "round"
|
|
647
|
+
})
|
|
648
|
+
})
|
|
649
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
650
|
+
className: "qc-popup-resize qc-popup-resize-corner qc-popup-resize-corner-bl",
|
|
651
|
+
title: t("runResize"),
|
|
652
|
+
...resizeProps("corner"),
|
|
653
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
654
|
+
width: "12",
|
|
655
|
+
height: "12",
|
|
656
|
+
viewBox: "0 0 12 12",
|
|
657
|
+
"aria-hidden": "true",
|
|
658
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
659
|
+
d: "M9 3 3 9 M9 6.5 6.5 9 M5.5 3 3 5.5",
|
|
660
|
+
fill: "none",
|
|
661
|
+
stroke: "currentColor",
|
|
662
|
+
strokeWidth: "1.4",
|
|
663
|
+
strokeLinecap: "round",
|
|
664
|
+
strokeLinejoin: "round"
|
|
665
|
+
})
|
|
666
|
+
})
|
|
667
|
+
})
|
|
668
|
+
]
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
//#endregion
|
|
672
|
+
//#region src/client/settings-card.tsx
|
|
673
|
+
/**
|
|
674
|
+
* Quick-commands settings card (设置 → 插件配置): an expandable plugin card —
|
|
675
|
+
* header (title + description + chevron) collapsed by default, body reveals
|
|
676
|
+
* the workspace × command-set editor and the popup anchor preference. Body
|
|
677
|
+
* edits instant-save per change; partial rows (empty name/command) persist
|
|
678
|
+
* while the user types.
|
|
679
|
+
*
|
|
680
|
+
* Mirrors the official PluginCard disclosure pattern (header button with
|
|
681
|
+
* aria-expanded, rotate-180 chevron, body separated by a top border).
|
|
682
|
+
*/
|
|
683
|
+
function QuickCommandsSettingsCard({ t }) {
|
|
684
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
685
|
+
const [state, setState] = (0, react.useState)({
|
|
686
|
+
loaded: false,
|
|
687
|
+
entries: [],
|
|
688
|
+
anchor: "corner"
|
|
689
|
+
});
|
|
690
|
+
const [error, setError] = (0, react.useState)(null);
|
|
691
|
+
const saveToken = (0, react.useRef)(0);
|
|
692
|
+
const load = async () => {
|
|
693
|
+
try {
|
|
694
|
+
const [entries, settings] = await Promise.all([quickApi.workspacesList(), quickApi.settingsGet()]);
|
|
695
|
+
setState({
|
|
696
|
+
loaded: true,
|
|
697
|
+
entries,
|
|
698
|
+
anchor: settings.popupAnchor
|
|
699
|
+
});
|
|
700
|
+
setError(null);
|
|
701
|
+
} catch (e) {
|
|
702
|
+
setState((s) => ({
|
|
703
|
+
...s,
|
|
704
|
+
loaded: true
|
|
705
|
+
}));
|
|
706
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
(0, react.useEffect)(() => {
|
|
710
|
+
load();
|
|
711
|
+
}, []);
|
|
712
|
+
/** Instant-save the workspace's whole command set; optimistic UI. */
|
|
713
|
+
const saveCommands = async (workspaceId, commands) => {
|
|
714
|
+
const token = ++saveToken.current;
|
|
715
|
+
try {
|
|
716
|
+
const next = await quickApi.settingsSetCommands(workspaceId, commands);
|
|
717
|
+
if (token !== saveToken.current) return;
|
|
718
|
+
setState((s) => ({
|
|
719
|
+
...s,
|
|
720
|
+
anchor: next.popupAnchor,
|
|
721
|
+
entries: s.entries.map((e) => e.workspaceId === workspaceId ? {
|
|
722
|
+
...e,
|
|
723
|
+
commands
|
|
724
|
+
} : e)
|
|
725
|
+
}));
|
|
726
|
+
setError(null);
|
|
727
|
+
} catch (e) {
|
|
728
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
729
|
+
load();
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
/** Compute the next command list for a workspace, THEN save + update state. */
|
|
733
|
+
const mutateCommands = (workspaceId, next) => {
|
|
734
|
+
const entry = state.entries.find((e) => e.workspaceId === workspaceId);
|
|
735
|
+
if (entry === void 0) return;
|
|
736
|
+
const commands = next(entry.commands);
|
|
737
|
+
setState((s) => ({
|
|
738
|
+
...s,
|
|
739
|
+
entries: s.entries.map((e) => e.workspaceId === workspaceId ? {
|
|
740
|
+
...e,
|
|
741
|
+
commands
|
|
742
|
+
} : e)
|
|
743
|
+
}));
|
|
744
|
+
saveCommands(workspaceId, commands);
|
|
745
|
+
};
|
|
746
|
+
const patchEntry = (workspaceId, index, field, value) => {
|
|
747
|
+
mutateCommands(workspaceId, (current) => current.map((c, i) => i === index ? {
|
|
748
|
+
...c,
|
|
749
|
+
[field]: value
|
|
750
|
+
} : c));
|
|
751
|
+
};
|
|
752
|
+
const addCommand = (workspaceId) => {
|
|
753
|
+
mutateCommands(workspaceId, (current) => [...current, {
|
|
754
|
+
name: "",
|
|
755
|
+
command: ""
|
|
756
|
+
}]);
|
|
757
|
+
};
|
|
758
|
+
const removeCommand = (workspaceId, index) => {
|
|
759
|
+
mutateCommands(workspaceId, (current) => current.filter((_, i) => i !== index));
|
|
760
|
+
};
|
|
761
|
+
const setAnchor = async (anchor) => {
|
|
762
|
+
setState((s) => ({
|
|
763
|
+
...s,
|
|
764
|
+
anchor
|
|
765
|
+
}));
|
|
766
|
+
try {
|
|
767
|
+
const next = await quickApi.settingsSetAnchor(anchor);
|
|
768
|
+
setState((s) => ({
|
|
769
|
+
...s,
|
|
770
|
+
anchor: next.popupAnchor
|
|
771
|
+
}));
|
|
772
|
+
} catch (e) {
|
|
773
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
774
|
+
load();
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
const title = t("cardTitle");
|
|
778
|
+
const description = t("cardDescription");
|
|
779
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
780
|
+
className: "qc-settings-card" + (open ? " qc-settings-card-open" : ""),
|
|
781
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
782
|
+
type: "button",
|
|
783
|
+
className: "qc-settings-header",
|
|
784
|
+
"aria-expanded": open,
|
|
785
|
+
"aria-label": `${t("collapseOrExpand")}: ${title}`,
|
|
786
|
+
onClick: () => setOpen((v) => !v),
|
|
787
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
788
|
+
className: "qc-settings-head-text",
|
|
789
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
790
|
+
className: "qc-settings-name",
|
|
791
|
+
children: title
|
|
792
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
793
|
+
className: "qc-settings-description",
|
|
794
|
+
children: description
|
|
795
|
+
})]
|
|
796
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { className: "qc-settings-chevron" + (open ? " qc-settings-chevron-open" : "") })]
|
|
797
|
+
}), open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
798
|
+
className: "qc-settings-body",
|
|
799
|
+
children: [
|
|
800
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
801
|
+
className: "qc-settings-intro",
|
|
802
|
+
children: t("cardIntro")
|
|
803
|
+
}),
|
|
804
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
805
|
+
className: "qc-settings-anchor",
|
|
806
|
+
children: [
|
|
807
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
808
|
+
className: "qc-settings-anchor-label",
|
|
809
|
+
children: t("cardAnchor")
|
|
810
|
+
}),
|
|
811
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
812
|
+
className: "qc-settings-radio",
|
|
813
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
814
|
+
type: "radio",
|
|
815
|
+
name: "qc-anchor",
|
|
816
|
+
checked: state.anchor === "corner",
|
|
817
|
+
onChange: () => void setAnchor("corner")
|
|
818
|
+
}), t("cardAnchorCorner")]
|
|
819
|
+
}),
|
|
820
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
821
|
+
className: "qc-settings-radio",
|
|
822
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
823
|
+
type: "radio",
|
|
824
|
+
name: "qc-anchor",
|
|
825
|
+
checked: state.anchor === "button",
|
|
826
|
+
onChange: () => void setAnchor("button")
|
|
827
|
+
}), t("cardAnchorButton")]
|
|
828
|
+
})
|
|
829
|
+
]
|
|
830
|
+
}),
|
|
831
|
+
error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
832
|
+
className: "qc-settings-error",
|
|
833
|
+
children: [
|
|
834
|
+
t("cardSaveError"),
|
|
835
|
+
": ",
|
|
836
|
+
error
|
|
837
|
+
]
|
|
838
|
+
}),
|
|
839
|
+
state.loaded && state.entries.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
840
|
+
className: "qc-settings-empty",
|
|
841
|
+
children: t("cardNoWorkspaces")
|
|
842
|
+
}),
|
|
843
|
+
state.entries.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
844
|
+
className: "qc-settings-workspace",
|
|
845
|
+
children: [
|
|
846
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
847
|
+
className: "qc-settings-workspace-head",
|
|
848
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
849
|
+
className: "qc-settings-workspace-title",
|
|
850
|
+
children: entry.title || entry.path || entry.workspaceId
|
|
851
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
852
|
+
className: "qc-settings-workspace-path",
|
|
853
|
+
children: entry.path
|
|
854
|
+
})]
|
|
855
|
+
}),
|
|
856
|
+
entry.commands.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
857
|
+
className: "qc-settings-no-commands",
|
|
858
|
+
children: t("cardNoCommands")
|
|
859
|
+
}),
|
|
860
|
+
entry.commands.map((command, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
861
|
+
className: "qc-settings-command",
|
|
862
|
+
children: [
|
|
863
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
864
|
+
className: "qc-settings-field qc-settings-field-name",
|
|
865
|
+
placeholder: t("cardName"),
|
|
866
|
+
value: command.name,
|
|
867
|
+
"aria-label": t("cardName"),
|
|
868
|
+
onChange: (e) => patchEntry(entry.workspaceId, index, "name", e.target.value)
|
|
869
|
+
}),
|
|
870
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
871
|
+
className: "qc-settings-field qc-settings-field-command",
|
|
872
|
+
placeholder: t("cardCommand"),
|
|
873
|
+
value: command.command,
|
|
874
|
+
"aria-label": t("cardCommand"),
|
|
875
|
+
onChange: (e) => patchEntry(entry.workspaceId, index, "command", e.target.value)
|
|
876
|
+
}),
|
|
877
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
878
|
+
type: "button",
|
|
879
|
+
className: "qc-settings-icon-btn",
|
|
880
|
+
"aria-label": t("cardRemove"),
|
|
881
|
+
title: t("cardRemove"),
|
|
882
|
+
onClick: () => removeCommand(entry.workspaceId, index),
|
|
883
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, { size: 14 })
|
|
884
|
+
})
|
|
885
|
+
]
|
|
886
|
+
}, `${entry.workspaceId}-${index}`)),
|
|
887
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
888
|
+
type: "button",
|
|
889
|
+
className: "qc-settings-add-btn",
|
|
890
|
+
onClick: () => addCommand(entry.workspaceId),
|
|
891
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlusOutline16, { size: 14 }), t("cardAddCommand")]
|
|
892
|
+
})
|
|
893
|
+
]
|
|
894
|
+
}, entry.workspaceId))
|
|
895
|
+
]
|
|
896
|
+
})]
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
//#endregion
|
|
900
|
+
//#region src/client/locales.ts
|
|
901
|
+
/**
|
|
902
|
+
* Client dictionary namespace for @fonlan/dsh-quick-commands.
|
|
903
|
+
*/
|
|
904
|
+
const LOCALE_NS = "quick-commands";
|
|
905
|
+
const zh = {
|
|
906
|
+
buttonTitle: "快速命令",
|
|
907
|
+
buttonTooltip: "快速命令(当前工作区)",
|
|
908
|
+
noWorkspace: "当前会话不在任何已配置的工作区中",
|
|
909
|
+
noCommands: "当前工作区未配置命令",
|
|
910
|
+
openSettings: "去设置配置命令",
|
|
911
|
+
menuTitle: "快速命令",
|
|
912
|
+
menuEmpty: "(无命令)",
|
|
913
|
+
runTitle: "运行中",
|
|
914
|
+
runExited: "已退出",
|
|
915
|
+
runExitCode: "退出码",
|
|
916
|
+
runSignal: "信号",
|
|
917
|
+
runStdout: "输出 (stdout)",
|
|
918
|
+
runStderr: "错误 (stderr)",
|
|
919
|
+
runEmpty: "(无输出)",
|
|
920
|
+
runCommand: "命令",
|
|
921
|
+
runCwd: "工作区",
|
|
922
|
+
runStateRunning: "运行中…",
|
|
923
|
+
runStateExited: "已退出",
|
|
924
|
+
runKill: "终止",
|
|
925
|
+
runClose: "关闭",
|
|
926
|
+
runResize: "拖动调整弹窗大小",
|
|
927
|
+
runKillConfirm: "终止后输出弹窗将关闭,确定终止该命令?",
|
|
928
|
+
runBusy: "该工作区已有命令在运行",
|
|
929
|
+
runStartFailed: "命令启动失败",
|
|
930
|
+
runPollFailed: "输出读取失败",
|
|
931
|
+
runLost: "运行记录已失效",
|
|
932
|
+
runNotFound: "工作区或命令未找到",
|
|
933
|
+
cardTitle: "快速命令",
|
|
934
|
+
cardDescription: "为每个工作区配置常用命令,会话头部一键运行",
|
|
935
|
+
collapseOrExpand: "展开 / 收起",
|
|
936
|
+
cardIntro: "为每个工作区配置常用命令,运行于工作区目录。支持占位符:{workspace} 工作区路径,{cwd} 会话目录,{title} 工作区标题。",
|
|
937
|
+
cardWorkspaces: "工作区",
|
|
938
|
+
cardNoWorkspaces: "没有工作区。请在侧边栏先添加工作区。",
|
|
939
|
+
cardCommands: "命令",
|
|
940
|
+
cardNoCommands: "未配置命令",
|
|
941
|
+
cardAddCommand: "添加命令",
|
|
942
|
+
cardName: "名称",
|
|
943
|
+
cardCommand: "命令",
|
|
944
|
+
cardRemove: "删除",
|
|
945
|
+
cardSaveError: "保存失败",
|
|
946
|
+
cardAnchor: "输出弹窗位置",
|
|
947
|
+
cardAnchorCorner: "右下角",
|
|
948
|
+
cardAnchorButton: "锚定按钮下方"
|
|
949
|
+
};
|
|
950
|
+
const en = {
|
|
951
|
+
buttonTitle: "Quick commands",
|
|
952
|
+
buttonTooltip: "Quick commands (current workspace)",
|
|
953
|
+
noWorkspace: "This session is not in any configured workspace",
|
|
954
|
+
noCommands: "No commands configured for this workspace",
|
|
955
|
+
openSettings: "Configure commands in Settings",
|
|
956
|
+
menuTitle: "Quick commands",
|
|
957
|
+
menuEmpty: "(no commands)",
|
|
958
|
+
runTitle: "Running",
|
|
959
|
+
runExited: "Exited",
|
|
960
|
+
runExitCode: "Exit code",
|
|
961
|
+
runSignal: "Signal",
|
|
962
|
+
runStdout: "Output (stdout)",
|
|
963
|
+
runStderr: "Errors (stderr)",
|
|
964
|
+
runEmpty: "(no output)",
|
|
965
|
+
runCommand: "Command",
|
|
966
|
+
runCwd: "Workspace",
|
|
967
|
+
runStateRunning: "Running…",
|
|
968
|
+
runStateExited: "Exited",
|
|
969
|
+
runKill: "Terminate",
|
|
970
|
+
runClose: "Close",
|
|
971
|
+
runResize: "Drag to resize the popup",
|
|
972
|
+
runKillConfirm: "Terminating closes the output popup. Terminate this command?",
|
|
973
|
+
runBusy: "A command is already running for this workspace",
|
|
974
|
+
runStartFailed: "Command failed to start",
|
|
975
|
+
runPollFailed: "Failed to read output",
|
|
976
|
+
runLost: "Run record expired",
|
|
977
|
+
runNotFound: "Workspace or command not found",
|
|
978
|
+
cardTitle: "Quick commands",
|
|
979
|
+
cardDescription: "Configure common commands per workspace, run them from the session header",
|
|
980
|
+
collapseOrExpand: "Expand / collapse",
|
|
981
|
+
cardIntro: "Configure common commands per workspace; they run in the workspace directory. Placeholders: {workspace} workspace path, {cwd} session cwd, {title} workspace title.",
|
|
982
|
+
cardWorkspaces: "Workspaces",
|
|
983
|
+
cardNoWorkspaces: "No workspaces. Add one in the sidebar first.",
|
|
984
|
+
cardCommands: "Commands",
|
|
985
|
+
cardNoCommands: "No commands configured",
|
|
986
|
+
cardAddCommand: "Add command",
|
|
987
|
+
cardName: "Name",
|
|
988
|
+
cardCommand: "Command",
|
|
989
|
+
cardRemove: "Remove",
|
|
990
|
+
cardSaveError: "Save failed",
|
|
991
|
+
cardAnchor: "Output popup position",
|
|
992
|
+
cardAnchorCorner: "Bottom right",
|
|
993
|
+
cardAnchorButton: "Anchored at button"
|
|
994
|
+
};
|
|
995
|
+
//#endregion
|
|
996
|
+
//#region \0dsh-css:/Users/fonlan/Repos/dsh-quick-commands/src/client/quick-commands.css.mjs
|
|
997
|
+
const css = "/* @fonlan/dsh-quick-commands client styles.\n Uses theme CSS variables (--dsw-alias-*); falls back to neutral literals.\n Constraint: this deployment's theme defines only these text tokens —\n label-primary, label-secondary, label-tertiary, label-dimmed. READABLE\n description copy uses label-secondary / label-tertiary (never label-dimmed,\n which is intentionally near-invisible and reserved for disabled states). */\n\n/* ── header button (icon-only, left of Session log pill) ─────────────────── */\n.qc-head-wrap {\n position: relative;\n display: inline-flex;\n align-items: center;\n}\n.qc-head-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 32px;\n height: 32px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35));\n border-radius: 18px;\n background: transparent;\n color: var(--dsw-alias-label-primary, #f2f2f2);\n cursor: pointer;\n padding: 0;\n}\n.qc-head-btn:hover:not(:disabled) {\n background: color-mix(in srgb, var(--dsw-alias-label-primary, #f2f2f2) 10%, transparent);\n}\n.qc-head-btn:disabled {\n color: var(--dsw-alias-label-dimmed, #666);\n cursor: not-allowed;\n}\n.qc-head-btn svg {\n flex: none;\n}\n\n/* ── command menu popover (anchored under the button) ────────────────────── */\n.qc-menu {\n position: absolute;\n top: calc(100% + 6px);\n right: 12px;\n min-width: 280px;\n max-height: 420px;\n overflow: auto;\n background: var(--dsw-alias-bg-overlay, #1b1b1c);\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35));\n border-radius: 10px;\n box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);\n z-index: 50;\n padding: 6px;\n display: flex;\n flex-direction: column;\n}\n.qc-menu-head {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 4px 8px 6px;\n border-bottom: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.25));\n}\n.qc-menu-title {\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-label-primary, #f2f2f2);\n}\n.qc-menu-ws {\n flex: 1;\n font-size: 11px;\n color: var(--dsw-alias-label-tertiary, #adb2b8);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n text-align: right;\n margin-right: 2px;\n}\n.qc-menu-remote {\n flex: 0 0 auto;\n font-size: 10px;\n font-weight: 600;\n line-height: 1;\n padding: 2px 5px;\n border-radius: 5px;\n color: var(--dsw-alias-fg-on-accent, #fff);\n background: var(--dsw-alias-accent, #4a8dff);\n letter-spacing: 0.04em;\n}\n.qc-menu-close {\n border: 0;\n background: transparent;\n color: var(--dsw-alias-label-tertiary, #adb2b8);\n cursor: pointer;\n padding: 2px;\n display: inline-flex;\n align-items: center;\n}\n.qc-menu-empty {\n font-size: 12px;\n color: var(--dsw-alias-label-tertiary, #adb2b8);\n padding: 10px 8px;\n margin: 0;\n}\n.qc-menu-item {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 2px;\n text-align: left;\n border: 0;\n background: transparent;\n border-radius: 8px;\n padding: 6px 8px;\n cursor: pointer;\n width: 100%;\n}\n.qc-menu-item:hover:not(:disabled) {\n background: color-mix(in srgb, var(--dsw-alias-label-primary, #f2f2f2) 10%, transparent);\n}\n.qc-menu-item:disabled {\n opacity: 0.6;\n cursor: wait;\n}\n.qc-menu-item-name {\n font-size: 13px;\n font-weight: 500;\n color: var(--dsw-alias-label-primary, #f2f2f2);\n}\n.qc-menu-item-cmd {\n font-family: var(--dsw-font-family-mono, monospace);\n font-size: 11px;\n color: var(--dsw-alias-label-secondary, #cfd3d6);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n max-width: 100%;\n}\n\n/* ── live output popup ───────────────────────────────────────────────────── */\n.qc-popup {\n position: relative; /* anchors the absolutely-positioned resize grip */\n display: flex;\n flex-direction: column;\n background: var(--dsw-alias-bg-overlay, #1b1b1c);\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35));\n border-radius: 12px;\n box-shadow: 0 12px 36px rgba(0, 0, 0, 0.45);\n z-index: 60;\n overflow: hidden;\n}\n.qc-popup-corner {\n position: fixed;\n right: 20px;\n bottom: 20px;\n width: 520px;\n max-width: calc(100vw - 40px);\n height: 340px;\n max-height: calc(100vh - 120px);\n}\n.qc-popup-button {\n position: absolute;\n top: calc(100% + 6px);\n right: 12px;\n width: 440px;\n max-width: calc(100vw - 40px);\n height: 300px;\n max-height: calc(100vh - 120px);\n}\n.qc-popup-head {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 10px;\n border-bottom: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.25));\n}\n.qc-popup-title {\n flex: 1;\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-label-primary, #f2f2f2);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.qc-popup-cmd {\n font-family: var(--dsw-font-family-mono, monospace);\n font-size: 11px;\n color: var(--dsw-alias-label-secondary, #cfd3d6);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n max-width: 40%;\n}\n.qc-popup-remote {\n flex: 0 0 auto;\n font-size: 10px;\n font-weight: 600;\n line-height: 1;\n padding: 3px 6px;\n border-radius: 6px;\n color: var(--dsw-alias-fg-on-accent, #fff);\n background: var(--dsw-alias-accent, #4a8dff);\n letter-spacing: 0.04em;\n}\n.qc-popup-icon {\n border: 0;\n background: transparent;\n color: var(--dsw-alias-label-tertiary, #adb2b8);\n cursor: pointer;\n padding: 4px;\n display: inline-flex;\n align-items: center;\n border-radius: 6px;\n}\n.qc-popup-icon:hover:not(:disabled) {\n background: color-mix(in srgb, var(--dsw-alias-label-primary, #f2f2f2) 10%, transparent);\n}\n.qc-popup-icon:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n.qc-popup-kill {\n color: var(--dsw-alias-state-error-primary, #e5484d);\n}\n.qc-popup-confirm {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 12px;\n padding: 8px 10px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary, #f5a623) 12%, transparent);\n color: var(--dsw-alias-label-primary, #f2f2f2);\n border-bottom: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.25));\n}\n.qc-popup-btn {\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35));\n border-radius: 8px;\n background: transparent;\n color: var(--dsw-alias-label-primary, #f2f2f2);\n font-size: 12px;\n padding: 4px 10px;\n cursor: pointer;\n}\n.qc-popup-btn:hover {\n background: color-mix(in srgb, var(--dsw-alias-label-primary, #f2f2f2) 10%, transparent);\n}\n.qc-popup-btn-plain {\n color: var(--dsw-alias-label-secondary, #cfd3d6);\n}\n.qc-popup-tabs {\n display: flex;\n gap: 2px;\n padding: 6px 10px 0;\n border-bottom: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.25));\n}\n.qc-popup-tab {\n border: 0;\n background: transparent;\n color: var(--dsw-alias-label-secondary, #cfd3d6);\n font-size: 12px;\n padding: 5px 10px;\n cursor: pointer;\n border-bottom: 2px solid transparent;\n}\n.qc-popup-tab-active {\n color: var(--dsw-alias-label-primary, #f2f2f2);\n border-bottom-color: var(--dsw-alias-brand-primary, #4a9eff);\n}\n.qc-popup-output {\n flex: 1;\n overflow: auto;\n padding: 8px 10px;\n font-family: var(--dsw-font-family-mono, monospace);\n font-size: 12px;\n line-height: 1.5;\n}\n.qc-popup-pre {\n margin: 0;\n white-space: pre-wrap;\n word-break: break-word;\n color: var(--dsw-alias-label-primary, #f2f2f2);\n}\n.qc-popup-empty {\n margin: 0;\n color: var(--dsw-alias-label-tertiary, #adb2b8);\n font-size: 12px;\n}\n.qc-popup-error {\n margin: 0 0 6px;\n color: var(--dsw-alias-state-error-primary, #e5484d);\n font-size: 12px;\n}\n.qc-popup-foot {\n display: flex;\n align-items: center;\n padding: 6px 10px;\n border-top: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.25));\n font-size: 11px;\n}\n.qc-popup-state {\n color: var(--dsw-alias-label-secondary, #cfd3d6);\n}\n\n/* Resize handles. The popup pins its RIGHT edge (both anchors); width is\n always dragged from the LEFT border. Height is dragged from the TOP border\n (corner anchor, bottom pinned) or the BOTTOM border (button anchor, top\n pinned below the button). The free corner (top-left / bottom-left) carries\n the diagonal grip. Pointer capture lives in header.tsx; touch-action:none\n lets touch drags resize instead of scrolling. */\n.qc-popup-resize {\n position: absolute;\n z-index: 3;\n touch-action: none;\n user-select: none;\n}\n.qc-popup-resize-left {\n left: 0;\n top: 10px;\n bottom: 10px;\n width: 7px;\n cursor: ew-resize;\n}\n.qc-popup-resize-top {\n top: 0;\n left: 10px;\n right: 10px;\n height: 7px;\n cursor: ns-resize;\n}\n.qc-popup-resize-bottom {\n bottom: 0;\n left: 10px;\n right: 10px;\n height: 7px;\n cursor: ns-resize;\n}\n.qc-popup-resize-left:hover,\n.qc-popup-resize-top:hover,\n.qc-popup-resize-bottom:hover {\n background: color-mix(in srgb, var(--dsw-alias-label-primary, #f2f2f2) 14%, transparent);\n}\n.qc-popup-resize-corner {\n width: 22px;\n height: 22px;\n z-index: 4;\n display: flex;\n padding: 3px;\n color: var(--dsw-alias-label-tertiary, #adb2b8);\n opacity: 0.6;\n transition: opacity .15s ease;\n}\n.qc-popup-resize-corner-tl {\n top: 0;\n left: 0;\n cursor: nwse-resize;\n align-items: flex-start;\n justify-content: flex-start;\n}\n.qc-popup-resize-corner-bl {\n bottom: 0;\n left: 0;\n cursor: nesw-resize;\n align-items: flex-end;\n justify-content: flex-start;\n}\n.qc-popup-resize-corner:hover,\n.qc-popup-resize-corner:active {\n opacity: 1;\n}\n\n/* ── settings card (设置 → 插件配置) — mirrors official PluginCard disclosure ── */\n.qc-settings-card {\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35));\n background: var(--dsw-alias-bg-layer-2, #1f1f21);\n border-radius: 12px;\n list-style: none;\n transition: border-color .16s, background .16s;\n}\n.qc-settings-card:hover {\n border-color: var(--dsw-alias-label-secondary, #cfd3d6);\n}\n.qc-settings-card-open {\n background: var(--dsw-alias-bg-layer-1, #27272a);\n border-color: var(--dsw-alias-label-secondary, #cfd3d6);\n}\n.qc-settings-header {\n appearance: none;\n width: 100%;\n font: inherit;\n color: inherit;\n text-align: left;\n cursor: pointer;\n background: transparent;\n border: 0;\n border-radius: 12px;\n align-items: center;\n gap: 12px;\n padding: 14px 16px;\n display: flex;\n}\n.qc-settings-header:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary, #4a9eff);\n outline-offset: -2px;\n}\n.qc-settings-head-text {\n flex-direction: column;\n flex: 1;\n gap: 4px;\n min-width: 0;\n display: flex;\n}\n.qc-settings-name {\n color: var(--dsw-alias-label-primary, #f2f2f2);\n font-size: 15px;\n font-weight: 600;\n line-height: 1.4;\n}\n.qc-settings-description {\n color: var(--dsw-alias-label-secondary, #cfd3d6);\n font-size: 13px;\n line-height: 1.5;\n}\n.qc-settings-chevron {\n color: var(--dsw-alias-label-secondary, #cfd3d6);\n flex: none;\n transition: transform .16s;\n}\n.qc-settings-chevron-open {\n transform: rotate(180deg);\n}\n.qc-settings-body {\n border-top: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35));\n margin: 0 16px;\n padding-bottom: 12px;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n.qc-settings-intro {\n font-size: 12px;\n color: var(--dsw-alias-label-secondary, #cfd3d6);\n margin: 8px 0 0;\n line-height: 1.5;\n}\n.qc-settings-anchor {\n display: flex;\n align-items: center;\n gap: 12px;\n font-size: 12px;\n}\n.qc-settings-anchor-label {\n color: var(--dsw-alias-label-primary, #f2f2f2);\n font-weight: 500;\n}\n.qc-settings-radio {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n color: var(--dsw-alias-label-secondary, #cfd3d6);\n cursor: pointer;\n}\n.qc-settings-error {\n font-size: 12px;\n color: var(--dsw-alias-state-error-primary, #e5484d);\n}\n.qc-settings-empty {\n font-size: 12px;\n color: var(--dsw-alias-label-secondary, #cfd3d6);\n margin: 0;\n}\n.qc-settings-workspace {\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35));\n border-radius: 10px;\n padding: 10px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.qc-settings-workspace-head {\n display: flex;\n align-items: baseline;\n gap: 8px;\n}\n.qc-settings-workspace-title {\n font-size: 13px;\n font-weight: 600;\n color: var(--dsw-alias-label-primary, #f2f2f2);\n}\n.qc-settings-workspace-path {\n font-size: 11px;\n font-family: var(--dsw-font-family-mono, monospace);\n color: var(--dsw-alias-label-tertiary, #adb2b8);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.qc-settings-no-commands {\n font-size: 12px;\n color: var(--dsw-alias-label-tertiary, #adb2b8);\n margin: 0;\n}\n.qc-settings-command {\n display: flex;\n gap: 6px;\n align-items: center;\n}\n.qc-settings-field {\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35));\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-1, #2a2a2d);\n color: var(--dsw-alias-label-primary, #f2f2f2);\n font-size: 12px;\n padding: 6px 8px;\n outline: none;\n}\n.qc-settings-field::placeholder {\n color: var(--dsw-alias-label-tertiary, #adb2b8);\n}\n.qc-settings-field:focus {\n border-color: var(--dsw-alias-brand-primary, #4a9eff);\n}\n.qc-settings-field-name {\n width: 160px;\n flex: none;\n}\n.qc-settings-field-command {\n flex: 1;\n font-family: var(--dsw-font-family-mono, monospace);\n}\n.qc-settings-icon-btn {\n border: 0;\n background: transparent;\n color: var(--dsw-alias-label-tertiary, #adb2b8);\n cursor: pointer;\n padding: 4px;\n display: inline-flex;\n align-items: center;\n border-radius: 6px;\n}\n.qc-settings-icon-btn:hover {\n background: color-mix(in srgb, var(--dsw-alias-label-primary, #f2f2f2) 10%, transparent);\n color: var(--dsw-alias-state-error-primary, #e5484d);\n}\n.qc-settings-add-btn {\n align-self: flex-start;\n display: inline-flex;\n align-items: center;\n gap: 4px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35));\n border-radius: 8px;\n background: transparent;\n color: var(--dsw-alias-label-primary, #f2f2f2);\n font-size: 12px;\n padding: 4px 10px;\n cursor: pointer;\n}\n.qc-settings-add-btn:hover {\n background: color-mix(in srgb, var(--dsw-alias-label-primary, #f2f2f2) 10%, transparent);\n}\n\n/* ── mobile fallback (dsh-mobile-nav) ──────────────────────────────────────\n The dsh-mobile-nav plugin (≤1023px) hides the ENTIRE header utilities seat\n with `[data-mobile-nav=\"frame\"] [data-phase] header > :first-child >\n :last-child { display: none !important; }`, because the seat used to hold\n only the session-log-export capsule (which it relocates to the drawer\n footer). Our quick-run ▶ button rides that same seat, so it vanished on\n phones.\n\n Re-show ONLY our button: restore the seat's flex layout (out-ranking the\n host rule via `:has(.qc-head-wrap)` specificity (0,5,1) > (0,4,1)) and keep\n every other seat occupant hidden (the session-log capsule stays relocated\n to the drawer footer). Desktop (no [data-mobile-nav]) is untouched. */\n[data-mobile-nav=\"frame\"] [data-phase] header > :first-child > :last-child:has(.qc-head-wrap) {\n display: flex !important;\n margin-left: 4px !important;\n gap: 0 !important;\n}\n[data-mobile-nav=\"frame\"] [data-phase] header > :first-child > :last-child:has(.qc-head-wrap)\n [data-slot=\"conversation.session.header.utilities\"] > :not(.qc-head-wrap) {\n display: none !important;\n}\n";
|
|
998
|
+
const tagId = "@fonlan/dsh-quick-commands/quick-commands.css#98749761";
|
|
999
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
1000
|
+
const tag = document.createElement("style");
|
|
1001
|
+
tag.dataset.plugin = "@fonlan/dsh-quick-commands";
|
|
1002
|
+
tag.dataset.pluginCss = tagId;
|
|
1003
|
+
tag.textContent = css;
|
|
1004
|
+
document.head.appendChild(tag);
|
|
1005
|
+
}
|
|
1006
|
+
//#endregion
|
|
1007
|
+
//#region src/client/index.tsx
|
|
1008
|
+
/** Services required before mounting (provided by the client runtime). */
|
|
1009
|
+
const inject = ["slots", "locale"];
|
|
1010
|
+
/** Client plugin body. */
|
|
1011
|
+
function apply(ctx) {
|
|
1012
|
+
ctx.effect(() => ctx.locale.register(LOCALE_NS, {
|
|
1013
|
+
zh,
|
|
1014
|
+
en
|
|
1015
|
+
}), "quick-commands: dictionaries");
|
|
1016
|
+
ctx.locale.bind(LOCALE_NS);
|
|
1017
|
+
ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
|
|
1018
|
+
name: "settings.plugin.item",
|
|
1019
|
+
key: LOCALE_NS,
|
|
1020
|
+
locale: LOCALE_NS
|
|
1021
|
+
}, QuickCommandsSettingsCard));
|
|
1022
|
+
ctx.slots.inject("conversation.session.header.utilities", () => ctx.slots.register({
|
|
1023
|
+
name: "conversation.session.header.utilities",
|
|
1024
|
+
id: "quick-commands",
|
|
1025
|
+
order: -1,
|
|
1026
|
+
locale: LOCALE_NS
|
|
1027
|
+
}, QuickCommandsHeaderAction));
|
|
1028
|
+
}
|
|
1029
|
+
//#endregion
|
|
1030
|
+
exports.apply = apply;
|
|
1031
|
+
exports.inject = inject;
|
|
1032
|
+
return module.exports;
|
|
1033
|
+
}
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
//# sourceMappingURL=client.js.map
|