@dong-victor/dsh-better-sidebar-starter 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/lib/client.js ADDED
@@ -0,0 +1,2465 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@dong-victor/dsh-better-sidebar-starter",
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
+ //#region src/client/icons.tsx
9
+ /**
10
+ * SVG icons for the starter plugin. Each icon is a React component that
11
+ * takes a size prop and renders an inline SVG. Monochrome, currentColor fill.
12
+ * @module dsh-better-sidebar-starter/client/icons
13
+ */
14
+ function svg(size, pathChildren) {
15
+ const svgHtml = `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">${pathChildren}</svg>`;
16
+ return (0, react.createElement)("span", {
17
+ style: {
18
+ display: "inline-flex",
19
+ alignItems: "center",
20
+ justifyContent: "center",
21
+ width: size,
22
+ height: size
23
+ },
24
+ dangerouslySetInnerHTML: { __html: svgHtml }
25
+ });
26
+ }
27
+ /** 🚀 Rocket — the tab icon. */
28
+ function RocketIcon({ size }) {
29
+ return svg(size, `<path d="M9.19 8.35c.34-.05.67-.1 1-.13 1.2-1.49 2.85-2.49 4.85-3.02 2-.53 4.16-.42 6.5.33.75 2.34.86 4.5.33 6.5-.53 2-1.53 3.65-3.02 4.85-.04.33-.08.66-.13 1-.13.87-.32 1.71-.57 2.5-.42 1.3-1.02 2.5-1.78 3.6-.42.6-1.1.96-1.86.96-.72 0-1.4-.34-1.84-.93l-2.06-2.76c-.2-.27-.5-.43-.82-.43H7.68c-.32 0-.62-.16-.82-.43l-2.06-2.76C4.36 19.34 4 18.66 4 17.94c0-.76.36-1.44.96-1.86 1.1-.76 2.3-1.36 3.6-1.78.79-.25 1.63-.44 2.5-.57l.13-1c.05-.34.1-.67.13-1z" fill="currentColor" opacity="0.85"/><circle cx="15.5" cy="8.5" r="1.5" fill="currentColor" opacity="0.6"/><path d="M5 14c-1 0-2 .5-2 2 0 1 .5 2 .5 2s1-.5 1.5-.5 1.5.5 1.5.5 1-.5 1-2-.5-2-2-2z" fill="currentColor" opacity="0.5"/>`);
30
+ }
31
+ /** ▶ Play — start button. */
32
+ function PlayIcon({ size }) {
33
+ return svg(size, `<path d="M8 5v14l11-7z" fill="currentColor"/>`);
34
+ }
35
+ /** ⏹ Stop — stop button. */
36
+ function StopIcon({ size }) {
37
+ return svg(size, `<rect x="6" y="6" width="12" height="12" rx="1" fill="currentColor"/>`);
38
+ }
39
+ /** 🔄 Restart — restart button. */
40
+ function RestartIcon({ size }) {
41
+ return svg(size, `<path d="M12 5V2L7 7l5 5V8c2.76 0 5 2.24 5 5s-2.24 5-5 5-5-2.24-5-5H5c0 3.87 3.13 7 7 7s7-3.13 7-7-3.13-7-7-7z" fill="currentColor"/>`);
42
+ }
43
+ /** ⚙ Gear — edit button. */
44
+ function GearIcon({ size }) {
45
+ return svg(size, `<path d="M12 8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm8.94 3l-1.97-.39c-.11-.37-.26-.72-.44-1.06l1.14-1.64c.26-.37.22-.88-.09-1.2l-1.28-1.28c-.32-.31-.83-.35-1.2-.09l-1.64 1.14c-.34-.18-.69-.33-1.06-.44L14.94 4c-.1-.42-.47-.72-.9-.72h-1.8c-.43 0-.8.3-.9.72l-.39 1.97c-.37.11-.72.26-1.06.44L7.25 5.27c-.37-.26-.88-.22-1.2.09L4.77 6.64c-.31.32-.35.83-.09 1.2l1.14 1.64c-.18.34-.33.69-.44 1.06L3.4 11c-.42.1-.72.47-.72.9v1.8c0 .43.3.8.72.9l1.97.39c.11.37.26.72.44 1.06l-1.14 1.64c-.26.37-.22.88.09 1.2l1.28 1.28c.32.31.83.35 1.2.09l1.64-1.14c.34.18.69.33 1.06.44l.39 1.97c.1.42.47.72.9.72h1.8c.43 0 .8-.3.9-.72l.39-1.97c.37-.11.72-.26 1.06-.44l1.64 1.14c.37.26.88.22 1.2-.09l1.28-1.28c.31-.32.35-.83.09-1.2l-1.14-1.64c.18-.34.33-.69.44-1.06L20.6 14c.42-.1.72-.47.72-.9v-1.8c0-.43-.3-.8-.72-.9z" fill="currentColor"/>`);
46
+ }
47
+ /** 🗑 Trash — delete button. */
48
+ function TrashIcon({ size }) {
49
+ return svg(size, `<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" fill="currentColor"/>`);
50
+ }
51
+ /** ➕ Plus — add new config. */
52
+ function PlusIcon({ size }) {
53
+ return svg(size, `<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" fill="currentColor"/>`);
54
+ }
55
+ /** ✕ Close — modal cancel. */
56
+ function CloseIcon({ size }) {
57
+ return svg(size, `<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" fill="currentColor"/>`);
58
+ }
59
+ /** ▾ Chevron down — tree expand. */
60
+ function ChevronDownIcon({ size }) {
61
+ return svg(size, `<path d="M7 10l5 5 5-5z" fill="currentColor"/>`);
62
+ }
63
+ /** ▸ Chevron right — tree collapse. */
64
+ function ChevronRightIcon({ size }) {
65
+ return svg(size, `<path d="M10 7l5 5-5 5z" fill="currentColor"/>`);
66
+ }
67
+ /** 🟢 Dot — running status. */
68
+ function RunningDotIcon({ size }) {
69
+ return svg(size, `<circle cx="12" cy="12" r="5" fill="#4e9a06"/>`);
70
+ }
71
+ /** ✅ Check — completed status. */
72
+ function CheckIcon({ size }) {
73
+ return svg(size, `<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" fill="#4e9a06"/>`);
74
+ }
75
+ /** 🔴 Error dot — failed status. */
76
+ function ErrorDotIcon({ size }) {
77
+ return svg(size, `<circle cx="12" cy="12" r="5" fill="#cc0000"/>`);
78
+ }
79
+ /** 📦 npm package icon. */
80
+ function NpmIcon({ size }) {
81
+ return svg(size, `<rect x="3" y="3" width="18" height="18" rx="2" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M7 17V8h4v6h2V8h4v9h-4v-3h-2v3H7z" fill="currentColor"/>`);
82
+ }
83
+ /** ☕ springboot icon. */
84
+ function SpringIcon({ size }) {
85
+ return svg(size, `<path d="M12 2C7.5 2 4 5.5 4 10c0 3 2 5.5 4.5 7 .5-2 1.5-3.5 3-4.5 1.5 1 2.5 2.5 3 4.5 2.5-1.5 4.5-4 4.5-7 0-4.5-3.5-8-7-8z" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M8 10c1-1 2.5-1.5 4-1.5s3 .5 4 1.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>`);
86
+ }
87
+ /** 🐍 python icon. */
88
+ function PythonIcon({ size }) {
89
+ return svg(size, `<path d="M12 4c-2 0-4 1-4 3v2h4v1H6c-1 0-2 1-2 3s1 3 2 3h2v-2c0-2 2-3 4-3h2c1 0 2-1 2-2V7c0-2-2-3-4-3z" fill="none" stroke="currentColor" stroke-width="1.5"/><circle cx="10.5" cy="6.5" r="0.8" fill="currentColor"/><path d="M12 20c2 0 4-1 4-3v-2h-4v-1h4c1 0 2-1 2-3s-1-3-2-3h-2v2c0 2-2 3-4 3h-2c-1 0-2 1-2 2v3c0 2 2 3 4 3z" fill="none" stroke="currentColor" stroke-width="1.5"/><circle cx="13.5" cy="17.5" r="0.8" fill="currentColor"/>`);
90
+ }
91
+ /** ⚡ custom icon. */
92
+ function CustomIcon({ size }) {
93
+ return svg(size, `<path d="M11 21h-1l1-7H7.5c-.6 0-.8-.4-.5-.9 1.5-2.6 3.7-6.2 6.5-10.1h1l-1 7h3.5c.6 0 .8.4.5.9C14.5 14.7 12.3 18.1 11 21z" fill="currentColor"/>`);
94
+ }
95
+ /** 📁 Folder — directory tree icon. */
96
+ function FolderIcon({ size }) {
97
+ return svg(size, `<path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z" fill="currentColor" opacity="0.7"/>`);
98
+ }
99
+ /** 📋 Clipboard — bulk paste env. */
100
+ function ClipboardIcon({ size }) {
101
+ return svg(size, `<path d="M19 3h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-7 0c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm2 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z" fill="currentColor"/>`);
102
+ }
103
+ /** Get the icon for a config type. */
104
+ function typeIcon(type, size) {
105
+ switch (type) {
106
+ case "npm": return NpmIcon({ size });
107
+ case "springboot": return SpringIcon({ size });
108
+ case "python": return PythonIcon({ size });
109
+ default: return CustomIcon({ size });
110
+ }
111
+ }
112
+ //#endregion
113
+ //#region src/client/ConfigTree.tsx
114
+ /**
115
+ * Config tree: the left pane showing all run configurations grouped by type,
116
+ * with running instances as child nodes.
117
+ * @module dsh-better-sidebar-starter/client/ConfigTree
118
+ */
119
+ /** Type group order. */
120
+ const TYPE_ORDER = [
121
+ "npm",
122
+ "springboot",
123
+ "python",
124
+ "custom"
125
+ ];
126
+ const TYPE_LABELS$2 = {
127
+ npm: "npm",
128
+ springboot: "Spring Boot",
129
+ python: "Python",
130
+ custom: "Custom"
131
+ };
132
+ /** Status icon for an instance. */
133
+ function statusIcon(status, size) {
134
+ switch (status) {
135
+ case "running": return RunningDotIcon({ size });
136
+ case "exited": return CheckIcon({ size });
137
+ case "killed":
138
+ case "error": return ErrorDotIcon({ size });
139
+ default: return null;
140
+ }
141
+ }
142
+ /** The config tree component. */
143
+ function ConfigTree(props) {
144
+ const { configs, instances, selectedInstanceId, selectedConfigId, onSelectConfig, onSelectInstance, onStartConfig, onStopInstance, onEditConfig } = props;
145
+ const [collapsed, setCollapsed] = (0, react.useState)(/* @__PURE__ */ new Set());
146
+ const toggleGroup = (type) => {
147
+ setCollapsed((prev) => {
148
+ const next = new Set(prev);
149
+ if (next.has(type)) next.delete(type);
150
+ else next.add(type);
151
+ return next;
152
+ });
153
+ };
154
+ const groups = (0, react.useMemo)(() => {
155
+ const map = /* @__PURE__ */ new Map();
156
+ for (const config of configs) {
157
+ const arr = map.get(config.type) ?? [];
158
+ arr.push(config);
159
+ map.set(config.type, arr);
160
+ }
161
+ for (const arr of map.values()) arr.sort((a, b) => (b.lastRunAt ?? 0) - (a.lastRunAt ?? 0));
162
+ return TYPE_ORDER.filter((type) => map.has(type) && map.get(type).length > 0).map((type) => ({
163
+ type,
164
+ items: map.get(type)
165
+ }));
166
+ }, [configs]);
167
+ if (configs.length === 0) return (0, react.createElement)("div", { className: "sts-empty" }, "暂无运行配置", (0, react.createElement)("br"), "点击右上角 + 新建配置");
168
+ return (0, react.createElement)("div", { className: "sts-tree" }, ...groups.map((group) => {
169
+ const isCollapsed = collapsed.has(group.type);
170
+ const label = TYPE_LABELS$2[group.type] ?? group.type;
171
+ const runningCount = instances.filter((inst) => inst.configType === group.type && inst.status === "running").length;
172
+ return (0, react.createElement)("div", { key: group.type }, (0, react.createElement)("div", {
173
+ className: "sts-group-header",
174
+ onClick: () => toggleGroup(group.type)
175
+ }, isCollapsed ? (0, react.createElement)(ChevronRightIcon, { size: 12 }) : (0, react.createElement)(ChevronDownIcon, { size: 12 }), typeIcon(group.type, 14), label, (0, react.createElement)("span", { className: "sts-group-count" }, ` (${group.items.length}${runningCount > 0 ? `, ${runningCount} 运行中` : ""})`)), (0, react.createElement)("div", { className: `sts-group-children${isCollapsed ? " collapsed" : ""}` }, ...group.items.flatMap((config) => {
176
+ const configInstances = instances.filter((inst) => inst.configId === config.id);
177
+ const isRunning = configInstances.some((inst) => inst.status === "running");
178
+ const isSelected = selectedConfigId === config.id && selectedInstanceId === null;
179
+ const rows = [(0, react.createElement)("div", {
180
+ key: config.id,
181
+ className: `sts-config-row${isSelected ? " selected" : ""}${isRunning ? " running" : ""}`,
182
+ onClick: () => onSelectConfig(config.id)
183
+ }, isRunning ? RunningDotIcon({ size: 12 }) : typeIcon(config.type, 12), (0, react.createElement)("div", { className: "sts-config-name" }, config.name), (0, react.createElement)("div", { className: "sts-config-cmd" }, config.command), (0, react.createElement)("div", { className: "sts-config-actions" }, isRunning ? (0, react.createElement)("button", {
184
+ className: "sts-icon-btn stop",
185
+ title: "停止全部",
186
+ onClick: (e) => {
187
+ e.stopPropagation();
188
+ for (const inst of configInstances) if (inst.status === "running") onStopInstance(inst.id);
189
+ }
190
+ }, StopIcon({ size: 14 })) : (0, react.createElement)("button", {
191
+ className: "sts-icon-btn play",
192
+ title: "启动",
193
+ onClick: (e) => {
194
+ e.stopPropagation();
195
+ onStartConfig(config.id);
196
+ }
197
+ }, PlayIcon({ size: 14 })), (0, react.createElement)("button", {
198
+ className: "sts-icon-btn gear",
199
+ title: "编辑",
200
+ onClick: (e) => {
201
+ e.stopPropagation();
202
+ onEditConfig(config.id);
203
+ }
204
+ }, GearIcon({ size: 12 }))))];
205
+ for (const inst of configInstances) {
206
+ const instSelected = selectedInstanceId === inst.id;
207
+ rows.push((0, react.createElement)("div", {
208
+ key: inst.id,
209
+ className: `sts-instance-row${instSelected ? " selected" : ""}`,
210
+ onClick: () => onSelectInstance(inst.id)
211
+ }, statusIcon(inst.status, 10), `#${inst.id.slice(0, 8)}`, (0, react.createElement)("span", { className: "sts-instance-pid" }, inst.status === "running" ? ` pid:${inst.pid}` : ` ${inst.status}`), inst.status === "running" ? (0, react.createElement)("button", {
212
+ className: "sts-icon-btn stop",
213
+ title: "停止",
214
+ onClick: (e) => {
215
+ e.stopPropagation();
216
+ onStopInstance(inst.id);
217
+ }
218
+ }, StopIcon({ size: 12 })) : null));
219
+ }
220
+ return rows;
221
+ })));
222
+ }));
223
+ }
224
+ //#endregion
225
+ //#region src/client/ansi.ts
226
+ function cloneState(s) {
227
+ return {
228
+ text: "",
229
+ bold: s.bold,
230
+ dim: s.dim,
231
+ italic: s.italic,
232
+ underline: s.underline,
233
+ fg: s.fg,
234
+ bg: s.bg
235
+ };
236
+ }
237
+ /** Apply one SGR parameter to the parse state. */
238
+ function applySgr(state, code) {
239
+ if (code === 0) {
240
+ state.bold = false;
241
+ state.dim = false;
242
+ state.italic = false;
243
+ state.underline = false;
244
+ state.fg = null;
245
+ state.bg = null;
246
+ } else if (code === 1) state.bold = true;
247
+ else if (code === 2) state.dim = true;
248
+ else if (code === 3) state.italic = true;
249
+ else if (code === 4) state.underline = true;
250
+ else if (code === 22) {
251
+ state.bold = false;
252
+ state.dim = false;
253
+ } else if (code === 23) state.italic = false;
254
+ else if (code === 24) state.underline = false;
255
+ else if (code >= 30 && code <= 37) state.fg = code - 30;
256
+ else if (code === 39) state.fg = null;
257
+ else if (code >= 40 && code <= 47) state.bg = code - 40;
258
+ else if (code === 49) state.bg = null;
259
+ else if (code >= 90 && code <= 97) state.fg = code - 90 + 8;
260
+ else if (code >= 100 && code <= 107) state.bg = code - 100 + 8;
261
+ }
262
+ /**
263
+ * Parse a string with ANSI escape codes into styled segments.
264
+ * Returns an array of AnsiSegment objects ready for React rendering.
265
+ */
266
+ function parseAnsi(input) {
267
+ const state = {
268
+ bold: false,
269
+ dim: false,
270
+ italic: false,
271
+ underline: false,
272
+ fg: null,
273
+ bg: null
274
+ };
275
+ const segments = [];
276
+ let current = cloneState(state);
277
+ const regex = /\x1b\[([0-9;]*)m|\x1b\][^\x07]*\x07|\x1b\[[0-9;]*[A-Za-z]/g;
278
+ let lastIndex = 0;
279
+ let match;
280
+ while ((match = regex.exec(input)) !== null) {
281
+ if (match.index > lastIndex) current.text += input.slice(lastIndex, match.index);
282
+ if (match[1] !== void 0) {
283
+ if (current.text !== "") segments.push(current);
284
+ const params = match[1] === "" ? "0" : match[1];
285
+ for (const codeStr of params.split(";")) {
286
+ const code = codeStr === "" ? 0 : Number(codeStr);
287
+ if (!Number.isNaN(code)) applySgr(state, code);
288
+ }
289
+ current = cloneState(state);
290
+ }
291
+ lastIndex = regex.lastIndex;
292
+ }
293
+ if (lastIndex < input.length) current.text += input.slice(lastIndex);
294
+ if (current.text !== "") segments.push(current);
295
+ return segments;
296
+ }
297
+ /** Map an ANSI color index (0-15) to a CSS color string. */
298
+ const ANSI_COLORS = [
299
+ "#000000",
300
+ "#cc0000",
301
+ "#4e9a06",
302
+ "#c4a000",
303
+ "#3465a4",
304
+ "#75507b",
305
+ "#06989a",
306
+ "#d3d7cf",
307
+ "#555753",
308
+ "#ef2929",
309
+ "#8ae234",
310
+ "#fce94f",
311
+ "#729fcf",
312
+ "#ad7fa8",
313
+ "#34e2e2",
314
+ "#eeeeec"
315
+ ];
316
+ /** Resolve a segment's foreground color to a CSS string, or null for default. */
317
+ function segmentFgColor(seg) {
318
+ if (seg.fg === null) return null;
319
+ return ANSI_COLORS[seg.fg] ?? null;
320
+ }
321
+ /** Resolve a segment's background color to a CSS string, or null for default. */
322
+ function segmentBgColor(seg) {
323
+ if (seg.bg === null) return null;
324
+ return ANSI_COLORS[seg.bg] ?? null;
325
+ }
326
+ //#endregion
327
+ //#region src/client/LogView.tsx
328
+ /**
329
+ * Log view: the right pane showing real-time log output from a running
330
+ * instance. Connects to the WebSocket endpoint, parses ANSI colors, and
331
+ * renders log lines with auto-scroll.
332
+ * @module dsh-better-sidebar-starter/client/LogView
333
+ */
334
+ /** Render ANSI-colored text segments. */
335
+ function renderAnsiLine(text, keyBase) {
336
+ return parseAnsi(text).map((seg, i) => {
337
+ const style = {};
338
+ const fg = segmentFgColor(seg);
339
+ const bg = segmentBgColor(seg);
340
+ if (fg !== null) style.color = fg;
341
+ if (bg !== null) style.backgroundColor = bg;
342
+ if (seg.bold) style.fontWeight = "bold";
343
+ if (seg.dim) style.opacity = "0.6";
344
+ if (seg.italic) style.fontStyle = "italic";
345
+ if (seg.underline) style.textDecoration = "underline";
346
+ return (0, react.createElement)("span", {
347
+ key: `${keyBase}-${i}`,
348
+ style
349
+ }, seg.text);
350
+ });
351
+ }
352
+ /** The log view component. */
353
+ function LogView({ instance, onStop, onRestart }) {
354
+ const [logLines, setLogLines] = (0, react.useState)([]);
355
+ const [autoScroll, setAutoScroll] = (0, react.useState)(true);
356
+ const [exitInfo, setExitInfo] = (0, react.useState)(null);
357
+ const contentRef = (0, react.useRef)(null);
358
+ const wsRef = (0, react.useRef)(null);
359
+ const lineKeyCounter = (0, react.useRef)(0);
360
+ const lineBuffer = (0, react.useRef)({ current: [] });
361
+ /** Flush the buffer into a new log line. */
362
+ const flushLine = (0, react.useCallback)(() => {
363
+ const entries = lineBuffer.current.current;
364
+ if (entries.length === 0) return;
365
+ lineBuffer.current.current = [];
366
+ setLogLines((prev) => {
367
+ const key = lineKeyCounter.current++;
368
+ const next = [...prev, {
369
+ entries,
370
+ key
371
+ }];
372
+ if (next.length > 5e3) next.splice(0, next.length - 5e3);
373
+ return next;
374
+ });
375
+ }, []);
376
+ /** Add an entry to the current line buffer, splitting on newlines. */
377
+ const addEntry = (0, react.useCallback)((entry) => {
378
+ const parts = entry.text.split("\n");
379
+ for (let i = 0; i < parts.length; i++) {
380
+ if (i > 0) flushLine();
381
+ if (parts[i] !== "") lineBuffer.current.current.push({
382
+ stream: entry.stream,
383
+ text: parts[i],
384
+ ts: entry.ts
385
+ });
386
+ }
387
+ if (parts.length > 1 && parts[parts.length - 1] === "") flushLine();
388
+ }, [flushLine]);
389
+ (0, react.useEffect)(() => {
390
+ const url = `/api/dsh-better-sidebar-starter/logs?instanceId=${encodeURIComponent(instance.id)}`;
391
+ const ws = new WebSocket(url);
392
+ wsRef.current = ws;
393
+ ws.onmessage = (event) => {
394
+ try {
395
+ const msg = JSON.parse(event.data);
396
+ if (msg.type === "history") {
397
+ setLogLines([]);
398
+ lineBuffer.current.current = [];
399
+ for (const entry of msg.entries) addEntry(entry);
400
+ flushLine();
401
+ } else if (msg.type === "log") addEntry(msg.entry);
402
+ else if (msg.type === "status") {
403
+ if (msg.status !== "running") {
404
+ flushLine();
405
+ setExitInfo({
406
+ status: msg.status,
407
+ exitCode: msg.exitCode
408
+ });
409
+ }
410
+ }
411
+ } catch {}
412
+ };
413
+ return () => {
414
+ ws.close();
415
+ wsRef.current = null;
416
+ };
417
+ }, [
418
+ instance.id,
419
+ addEntry,
420
+ flushLine
421
+ ]);
422
+ (0, react.useEffect)(() => {
423
+ if (autoScroll && contentRef.current !== null) contentRef.current.scrollTop = contentRef.current.scrollHeight;
424
+ }, [logLines, autoScroll]);
425
+ const handleClear = () => {
426
+ setLogLines([]);
427
+ lineBuffer.current.current = [];
428
+ };
429
+ return (0, react.createElement)("div", { style: {
430
+ display: "flex",
431
+ flexDirection: "column",
432
+ height: "100%",
433
+ minHeight: 0
434
+ } }, (0, react.createElement)("div", { className: "sts-log-toolbar" }, instance.configName, (0, react.createElement)("span", { style: {
435
+ opacity: .5,
436
+ marginLeft: "4px"
437
+ } }, `#${instance.id.slice(0, 8)}`), (0, react.createElement)("div", { className: "sts-log-toolbar-spacer" }), instance.status === "running" ? (0, react.createElement)("button", {
438
+ className: "sts-icon-btn stop",
439
+ title: "停止",
440
+ onClick: onStop
441
+ }, StopIcon({ size: 14 }), " 停止") : null, (0, react.createElement)("button", {
442
+ className: "sts-icon-btn",
443
+ title: "重启",
444
+ onClick: onRestart
445
+ }, RestartIcon({ size: 14 }), " 重启"), (0, react.createElement)("button", {
446
+ className: "sts-icon-btn",
447
+ title: "清空",
448
+ onClick: handleClear
449
+ }, TrashIcon({ size: 14 }), " 清空"), (0, react.createElement)("label", {
450
+ className: "sts-log-toolbar-label",
451
+ onClick: () => setAutoScroll((v) => !v)
452
+ }, autoScroll ? "☑" : "☐", " 自动滚动")), (0, react.createElement)("div", {
453
+ ref: contentRef,
454
+ className: "sts-log-content"
455
+ }, ...logLines.map((line) => {
456
+ const keyStr = `line-${line.key}`;
457
+ return (0, react.createElement)("div", {
458
+ key: keyStr,
459
+ className: "sts-log-line"
460
+ }, ...line.entries.map((entry, i) => {
461
+ const segKey = `${keyStr}-e${i}`;
462
+ return (0, react.createElement)("span", {
463
+ key: segKey,
464
+ className: entry.stream === "stderr" ? "sts-log-stderr" : ""
465
+ }, renderAnsiLine(entry.text, segKey));
466
+ }));
467
+ }), exitInfo !== null ? (0, react.createElement)("div", {
468
+ key: "exit-info",
469
+ style: {
470
+ color: exitInfo.status === "exited" && exitInfo.exitCode === 0 ? "#4e9a06" : "#cc0000",
471
+ fontWeight: "bold",
472
+ marginTop: "4px"
473
+ }
474
+ }, `\n进程${exitInfo.status === "exited" ? "退出" : exitInfo.status === "killed" ? "被终止" : "出错"}`, exitInfo.exitCode !== null ? `,退出码: ${exitInfo.exitCode}` : "") : null));
475
+ }
476
+ //#endregion
477
+ //#region src/client/ConfigDetail.tsx
478
+ /**
479
+ * Config detail card: shown in the right pane when an idle config is selected.
480
+ * Displays the config's full info and offers start / edit / delete buttons.
481
+ * @module dsh-better-sidebar-starter/client/ConfigDetail
482
+ */
483
+ const TYPE_LABELS$1 = {
484
+ npm: "npm",
485
+ springboot: "Spring Boot",
486
+ python: "Python",
487
+ custom: "Custom"
488
+ };
489
+ /** The detail card component. */
490
+ function ConfigDetail({ config, onStart, onEdit, onDelete }) {
491
+ const envEntries = Object.entries(config.env);
492
+ const dateFmt = (ts) => {
493
+ if (ts === null) return "—";
494
+ return new Date(ts).toLocaleString("zh-CN", { hour12: false });
495
+ };
496
+ return (0, react.createElement)("div", { className: "sts-detail" }, (0, react.createElement)("div", { className: "sts-detail-card" }, (0, react.createElement)("div", { className: "sts-detail-title" }, typeIcon(config.type, 20), config.name), (0, react.createElement)("div", { className: "sts-detail-row" }, (0, react.createElement)("div", { className: "sts-detail-label" }, "类型"), (0, react.createElement)("div", { className: "sts-detail-value" }, TYPE_LABELS$1[config.type] ?? config.type)), (0, react.createElement)("div", { className: "sts-detail-row" }, (0, react.createElement)("div", { className: "sts-detail-label" }, "命令"), (0, react.createElement)("div", { className: "sts-detail-value" }, config.command)), (0, react.createElement)("div", { className: "sts-detail-row" }, (0, react.createElement)("div", { className: "sts-detail-label" }, "工作目录"), (0, react.createElement)("div", { className: "sts-detail-value" }, config.cwd)), config.type === "springboot" && config.jvmArgs ? (0, react.createElement)("div", { className: "sts-detail-row" }, (0, react.createElement)("div", { className: "sts-detail-label" }, "JVM参数"), (0, react.createElement)("div", { className: "sts-detail-value" }, config.jvmArgs)) : null, config.args ? (0, react.createElement)("div", { className: "sts-detail-row" }, (0, react.createElement)("div", { className: "sts-detail-label" }, "程序实参"), (0, react.createElement)("div", { className: "sts-detail-value" }, config.args)) : null, envEntries.length > 0 ? (0, react.createElement)("div", { className: "sts-detail-row" }, (0, react.createElement)("div", { className: "sts-detail-label" }, "环境变量"), (0, react.createElement)("div", { style: { flex: 1 } }, ...envEntries.map(([key, value]) => (0, react.createElement)("div", {
497
+ key,
498
+ className: "sts-detail-value"
499
+ }, `${key} = ${value}`)))) : null, (0, react.createElement)("div", { className: "sts-detail-row" }, (0, react.createElement)("div", { className: "sts-detail-label" }, "创建时间"), (0, react.createElement)("div", { className: "sts-detail-value" }, dateFmt(config.createdAt))), config.lastRunAt !== null ? (0, react.createElement)("div", { className: "sts-detail-row" }, (0, react.createElement)("div", { className: "sts-detail-label" }, "最近运行"), (0, react.createElement)("div", { className: "sts-detail-value" }, dateFmt(config.lastRunAt))) : null, (0, react.createElement)("div", { className: "sts-detail-actions" }, (0, react.createElement)("button", {
500
+ className: "sts-detail-btn danger",
501
+ onClick: onDelete
502
+ }, (0, react.createElement)(TrashIcon, { size: 14 }), " 删除"), (0, react.createElement)("button", {
503
+ className: "sts-detail-btn",
504
+ onClick: onEdit
505
+ }, (0, react.createElement)(GearIcon, { size: 14 }), " 编辑"), (0, react.createElement)("button", {
506
+ className: "sts-detail-btn primary",
507
+ onClick: onStart,
508
+ style: {
509
+ background: "#1a1a1e",
510
+ border: "1px solid #3a3a3e",
511
+ color: "#fff",
512
+ borderRadius: "6px",
513
+ padding: "6px 14px",
514
+ cursor: "pointer",
515
+ fontSize: "12px"
516
+ }
517
+ }, (0, react.createElement)(PlayIcon, { size: 14 }), " 启动"))));
518
+ }
519
+ //#endregion
520
+ //#region src/client/DirChooser.tsx
521
+ /**
522
+ * Shared directory chooser: a filesystem folder tree (backed by the sidebar
523
+ * fs.tree API, which can list any absolute path) plus a browse button that
524
+ * opens a modal popup. Browsing starts at the user's home directory and can
525
+ * navigate up to any drive/root, so runtime tool paths (JDK/Maven/...) that
526
+ * live outside the workspace can be selected.
527
+ * @module dsh-better-sidebar-starter/client/DirChooser
528
+ */
529
+ /** Call the sidebar fs.tree API directly via fetch. Returns directories only. */
530
+ async function fsTree$1(scope, path) {
531
+ const payload = {
532
+ sessionId: scope.sessionId,
533
+ path
534
+ };
535
+ if (scope.cwd !== void 0 && scope.cwd !== "") payload.cwd = scope.cwd;
536
+ const resp = await fetch("/sidebar/api/fs.tree", {
537
+ method: "POST",
538
+ headers: { "content-type": "application/json" },
539
+ body: JSON.stringify(payload)
540
+ });
541
+ if (!resp.ok) throw new Error(`fs.tree ${path}: ${resp.status}`);
542
+ const parsed = await resp.json();
543
+ if (!parsed.ok || parsed.value === void 0) throw new Error(`fs.tree ${path}: ${parsed.error?.message ?? "unknown error"}`);
544
+ return parsed.value.entries.filter((e) => e.isDir && !e.hidden);
545
+ }
546
+ /** Fetch the user's home directory from the host. Falls back to cwd. */
547
+ async function fetchHomeDefault(scope) {
548
+ try {
549
+ const r = await fetch("/api/dsh-better-sidebar-starter/detect-env", { method: "GET" });
550
+ if (r.ok) {
551
+ const data = await r.json();
552
+ if (data.home && data.home !== "") return data.home;
553
+ }
554
+ } catch {}
555
+ return scope.cwd ?? ".";
556
+ }
557
+ /** Normalize a path, returning its parent or null if at the root. */
558
+ function parentOf(p) {
559
+ const trimmed = p.replace(/[\\/]+$/, "");
560
+ const lastSep = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\"));
561
+ if (lastSep <= 0) return null;
562
+ return trimmed.substring(0, lastSep);
563
+ }
564
+ /** The folder tree panel: shows the current dir's subdirectories with up-nav. */
565
+ function DirTree(props) {
566
+ const { scope, selectedPath, onPick } = props;
567
+ const [cur, setCur] = (0, react.useState)(".");
568
+ const [entries, setEntries] = (0, react.useState)(null);
569
+ const [loading, setLoading] = (0, react.useState)(false);
570
+ const [error, setError] = (0, react.useState)(null);
571
+ const [expanded, setExpanded] = (0, react.useState)(/* @__PURE__ */ new Set());
572
+ const scopeRef = (0, react.useRef)(scope);
573
+ const initDoneRef = (0, react.useRef)(false);
574
+ const initialValueRef = (0, react.useRef)(selectedPath);
575
+ (0, react.useEffect)(() => {
576
+ if (initDoneRef.current) return;
577
+ initDoneRef.current = true;
578
+ let cancelled = false;
579
+ const s = scopeRef.current;
580
+ const startPath = initialValueRef.current?.trim() && initialValueRef.current !== "." && initialValueRef.current !== "./" ? initialValueRef.current.trim() : null;
581
+ if (startPath !== null) {
582
+ setCur(startPath);
583
+ loadDir(s, startPath);
584
+ } else fetchHomeDefault(s).then((home) => {
585
+ if (cancelled) return;
586
+ setCur(home);
587
+ loadDir(s, home);
588
+ });
589
+ return () => {
590
+ cancelled = true;
591
+ };
592
+ }, []);
593
+ const loadDir = (s, path) => {
594
+ setLoading(true);
595
+ setError(null);
596
+ fsTree$1(s, path).then((dirs) => {
597
+ setEntries(dirs);
598
+ setLoading(false);
599
+ }).catch((err) => {
600
+ setError(err instanceof Error ? err.message : String(err));
601
+ setLoading(false);
602
+ });
603
+ };
604
+ const goUp = () => {
605
+ const parent = parentOf(cur);
606
+ if (parent !== null) {
607
+ setCur(parent);
608
+ loadDir(scopeRef.current, parent);
609
+ }
610
+ };
611
+ const navigateInto = (path) => {
612
+ setCur(path);
613
+ loadDir(scopeRef.current, path);
614
+ };
615
+ const toggleExpand = (path) => {
616
+ setExpanded((prev) => {
617
+ const next = new Set(prev);
618
+ if (next.has(path)) next.delete(path);
619
+ else next.add(path);
620
+ return next;
621
+ });
622
+ };
623
+ const isSelected = (p) => selectedPath !== "" && selectedPath !== "." && selectedPath === p;
624
+ return (0, react.createElement)("div", { className: "sts-dirtree" }, (0, react.createElement)("div", { className: "sts-dirtree-nav" }, (0, react.createElement)("button", {
625
+ className: "sts-dirtree-up",
626
+ onClick: goUp,
627
+ disabled: parentOf(cur) === null,
628
+ title: "上级目录"
629
+ }, "↑"), (0, react.createElement)("button", {
630
+ className: "sts-dirtree-home",
631
+ onClick: () => loadDir(scopeRef.current, cur),
632
+ title: "刷新"
633
+ }, "↻"), (0, react.createElement)("span", {
634
+ className: "sts-dirtree-cur",
635
+ title: cur
636
+ }, cur || "(.)")), (0, react.createElement)("div", {
637
+ className: `sts-tree-row${isSelected(cur) ? " selected" : ""}`,
638
+ style: { paddingLeft: "8px" },
639
+ onClick: () => onPick(cur)
640
+ }, (0, react.createElement)("span", { className: "sts-tree-icon" }, (0, react.createElement)(FolderIcon, { size: 14 })), (0, react.createElement)("span", { className: "sts-tree-label" }, cur || "(.)"), (0, react.createElement)("span", { className: "sts-tree-select-hint" }, "● 选择此目录")), loading ? (0, react.createElement)("div", {
641
+ className: "sts-tree-loading",
642
+ style: { paddingLeft: "16px" }
643
+ }, "加载中...") : error !== null ? (0, react.createElement)("div", {
644
+ className: "sts-tree-error",
645
+ style: { paddingLeft: "16px" }
646
+ }, error) : entries !== null && entries.length > 0 ? (0, react.createElement)("div", { className: "sts-dirtree-sub" }, ...entries.map((entry) => (0, react.createElement)(DirRow, {
647
+ key: entry.path,
648
+ scope,
649
+ entry,
650
+ depth: 1,
651
+ selectedPath,
652
+ expanded,
653
+ onToggleExpand: toggleExpand,
654
+ onPick,
655
+ onNavigate: navigateInto
656
+ }))) : (0, react.createElement)("div", {
657
+ className: "sts-tree-empty",
658
+ style: { paddingLeft: "16px" }
659
+ }, "无子目录"));
660
+ }
661
+ /** One directory row with recursive expansion + click into. */
662
+ function DirRow(props) {
663
+ const { scope, entry, depth, selectedPath, expanded, onToggleExpand, onPick, onNavigate } = props;
664
+ const [children, setChildren] = (0, react.useState)(null);
665
+ const [loading, setLoading] = (0, react.useState)(false);
666
+ const isExpanded = expanded.has(entry.path);
667
+ const isSelected = selectedPath === entry.path;
668
+ const handleClick = () => {
669
+ onPick(entry.path);
670
+ if (isExpanded) {
671
+ onToggleExpand(entry.path);
672
+ return;
673
+ }
674
+ onToggleExpand(entry.path);
675
+ if (children === null) {
676
+ setLoading(true);
677
+ fsTree$1(scope, entry.path).then((dirs) => {
678
+ setChildren(dirs);
679
+ setLoading(false);
680
+ }).catch(() => {
681
+ setChildren([]);
682
+ setLoading(false);
683
+ });
684
+ }
685
+ };
686
+ return (0, react.createElement)("div", null, (0, react.createElement)("div", {
687
+ className: `sts-tree-row${isSelected ? " selected" : ""}`,
688
+ style: { paddingLeft: `${depth * 16 + 8}px` },
689
+ onClick: handleClick
690
+ }, (0, react.createElement)("span", { className: `sts-tree-chevron${isExpanded ? " expanded" : ""}` }, (0, react.createElement)(isExpanded ? ChevronDownIcon : ChevronRightIcon, { size: 12 })), (0, react.createElement)("span", { className: "sts-tree-icon" }, (0, react.createElement)(FolderIcon, { size: 14 })), (0, react.createElement)("span", { className: "sts-tree-label" }, entry.name)), isExpanded ? loading ? (0, react.createElement)("div", {
691
+ className: "sts-tree-loading",
692
+ style: { paddingLeft: `${(depth + 1) * 16 + 8}px` }
693
+ }, "加载中...") : children !== null && children.length > 0 ? (0, react.createElement)("div", null, ...children.map((child) => (0, react.createElement)(DirRow, {
694
+ key: child.path,
695
+ scope,
696
+ entry: child,
697
+ depth: depth + 1,
698
+ selectedPath,
699
+ expanded,
700
+ onToggleExpand,
701
+ onPick,
702
+ onNavigate: props.onNavigate
703
+ }))) : null : null);
704
+ }
705
+ /** A read-only path input + browse button that opens a modal folder tree. */
706
+ function DirChooser(props) {
707
+ const { scope, value, onChange, compact, placeholder } = props;
708
+ const [open, setOpen] = (0, react.useState)(false);
709
+ return (0, react.createElement)("div", { className: "sts-settings-path-row" }, (0, react.createElement)("span", { className: "sts-settings-path-icon" }, (0, react.createElement)(FolderIcon, { size: 14 })), (0, react.createElement)("input", {
710
+ className: compact ? "sts-modal-input mono" : "sts-settings-input",
711
+ value,
712
+ readOnly: true,
713
+ placeholder: placeholder ?? "",
714
+ title: value,
715
+ onClick: () => setOpen(true)
716
+ }), (0, react.createElement)("button", {
717
+ className: "sts-settings-browse",
718
+ onClick: () => setOpen(true),
719
+ title: "选择文件夹"
720
+ }, "浏览"), open ? (0, react.createElement)(DirChooserModal, {
721
+ scope,
722
+ value,
723
+ onConfirm: (p) => {
724
+ onChange(p);
725
+ setOpen(false);
726
+ },
727
+ onCancel: () => setOpen(false)
728
+ }) : null);
729
+ }
730
+ /** Modal popup hosting the folder tree with confirm/cancel. */
731
+ function DirChooserModal(props) {
732
+ const { scope, value, onConfirm, onCancel } = props;
733
+ const [sel, setSel] = (0, react.useState)(value);
734
+ return (0, react.createElement)("div", {
735
+ className: "sts-modal-overlay",
736
+ onClick: (e) => {
737
+ if (e.target === e.currentTarget) onCancel();
738
+ }
739
+ }, (0, react.createElement)("div", { className: "sts-dir-modal" }, (0, react.createElement)("div", { className: "sts-modal-header" }, "选择文件夹", (0, react.createElement)("button", {
740
+ className: "sts-icon-btn",
741
+ onClick: onCancel
742
+ }, (0, react.createElement)(CloseIcon, { size: 16 }))), (0, react.createElement)("div", { className: "sts-dir-modal-tree" }, (0, react.createElement)(DirTree, {
743
+ scope,
744
+ selectedPath: sel,
745
+ onPick: setSel
746
+ })), (0, react.createElement)("div", { className: "sts-modal-footer" }, (0, react.createElement)("button", {
747
+ className: "sts-modal-btn",
748
+ onClick: onCancel
749
+ }, "取消"), (0, react.createElement)("button", {
750
+ className: "sts-modal-btn primary",
751
+ onClick: () => onConfirm(sel),
752
+ style: {
753
+ background: "#1a1a1e",
754
+ border: "1px solid #3a3a3e",
755
+ color: "#fff",
756
+ borderRadius: "6px",
757
+ padding: "8px 18px",
758
+ cursor: "pointer",
759
+ fontSize: "13px"
760
+ }
761
+ }, (0, react.createElement)(CheckIcon, { size: 14 }), " 确定"))));
762
+ }
763
+ //#endregion
764
+ //#region src/client/PathPicker.tsx
765
+ /**
766
+ * Path picker: a read-only input + browse button that opens a folder tree
767
+ * modal (DirChooser). Shared by the settings panel and the config editor.
768
+ * @module dsh-better-sidebar-starter/client/PathPicker
769
+ */
770
+ /** A folder-browse input with a hosted modal tree chooser. */
771
+ function PathPicker(props) {
772
+ const { scope, value, onChange, compact, desc } = props;
773
+ return (0, react.createElement)(DirChooser, {
774
+ scope,
775
+ value,
776
+ onChange,
777
+ compact,
778
+ placeholder: desc
779
+ });
780
+ }
781
+ //#endregion
782
+ //#region src/client/ConfigEditorModal.tsx
783
+ /**
784
+ * Config editor modal dialog: create or edit a run configuration.
785
+ * Lets the user set name, type, command, cwd (tree picker), jvmArgs, args,
786
+ * and environment variables.
787
+ * @module dsh-better-sidebar-starter/client/ConfigEditorModal
788
+ */
789
+ /** Call the sidebar fs.tree API directly via fetch. Returns directories only.
790
+ * If path is omitted, the server uses the session cwd (root listing).
791
+ * The sidebar API wraps responses in { ok: true, value: ... } envelope. */
792
+ async function fsTree(scope, path) {
793
+ const payload = { sessionId: scope.sessionId };
794
+ if (scope.cwd !== void 0 && scope.cwd !== "") payload.cwd = scope.cwd;
795
+ if (path !== void 0) payload.path = path;
796
+ const resp = await fetch("/sidebar/api/fs.tree", {
797
+ method: "POST",
798
+ headers: { "content-type": "application/json" },
799
+ body: JSON.stringify(payload)
800
+ });
801
+ if (!resp.ok) throw new Error(`fs.tree ${path ?? "(root)"}: ${resp.status}`);
802
+ const parsed = await resp.json();
803
+ if (!parsed.ok || parsed.value === void 0) throw new Error(`fs.tree ${path ?? "(root)"}: ${parsed.error?.message ?? "unknown error"}`);
804
+ return parsed.value.entries.filter((e) => e.isDir && !e.hidden);
805
+ }
806
+ /** Recursive tree node: loads its own children on expand. */
807
+ function TreeNode(props) {
808
+ const { scope, entry, depth, selectedPath, onSelect } = props;
809
+ const [expanded, setExpanded] = (0, react.useState)(false);
810
+ const [children, setChildren] = (0, react.useState)(null);
811
+ const [loading, setLoading] = (0, react.useState)(false);
812
+ const handleToggle = () => {
813
+ if (!expanded && children === null) {
814
+ setLoading(true);
815
+ fsTree(scope, entry.path).then((dirs) => {
816
+ setChildren(dirs);
817
+ setLoading(false);
818
+ }).catch(() => {
819
+ setChildren([]);
820
+ setLoading(false);
821
+ });
822
+ }
823
+ setExpanded(!expanded);
824
+ };
825
+ const isSelected = selectedPath === entry.path;
826
+ return (0, react.createElement)("div", null, (0, react.createElement)("div", {
827
+ className: `sts-tree-row${isSelected ? " selected" : ""}`,
828
+ style: { paddingLeft: `${depth * 16 + 8}px` },
829
+ onClick: () => {
830
+ onSelect(entry.path);
831
+ handleToggle();
832
+ }
833
+ }, (0, react.createElement)("span", { className: `sts-tree-chevron${expanded ? " expanded" : ""}` }, (0, react.createElement)(ChevronRightIcon, { size: 12 })), (0, react.createElement)("span", { className: "sts-tree-icon" }, (0, react.createElement)(FolderIcon, { size: 14 })), (0, react.createElement)("span", { className: "sts-tree-label" }, entry.name)), expanded ? loading ? (0, react.createElement)("div", {
834
+ className: "sts-tree-loading",
835
+ style: { paddingLeft: `${(depth + 1) * 16 + 8}px` }
836
+ }, "加载中...") : children !== null && children.length > 0 ? (0, react.createElement)("div", null, ...children.map((child) => (0, react.createElement)(TreeNode, {
837
+ key: child.path,
838
+ scope,
839
+ entry: child,
840
+ depth: depth + 1,
841
+ selectedPath,
842
+ onSelect
843
+ }))) : (0, react.createElement)("div", {
844
+ className: "sts-tree-empty",
845
+ style: { paddingLeft: `${(depth + 1) * 16 + 8}px` }
846
+ }, "无子目录") : null);
847
+ }
848
+ /** Directory tree picker: shows a collapsible tree rooted at session cwd. */
849
+ function DirTreePicker(props) {
850
+ const { scope, value, onSelect } = props;
851
+ const [expanded, setExpanded] = (0, react.useState)(false);
852
+ const [children, setChildren] = (0, react.useState)(null);
853
+ const [loading, setLoading] = (0, react.useState)(false);
854
+ const [error, setError] = (0, react.useState)(null);
855
+ const handleToggle = () => {
856
+ if (!expanded && children === null) {
857
+ setLoading(true);
858
+ setError(null);
859
+ fsTree(scope).then((dirs) => {
860
+ setChildren(dirs);
861
+ setLoading(false);
862
+ }).catch((err) => {
863
+ setError(err instanceof Error ? err.message : String(err));
864
+ setLoading(false);
865
+ });
866
+ }
867
+ setExpanded(!expanded);
868
+ };
869
+ return (0, react.createElement)("div", { className: "sts-tree-picker" }, (0, react.createElement)("div", {
870
+ className: `sts-tree-row${value === "." || value === "" || value === "./" ? " selected" : ""}`,
871
+ style: { paddingLeft: "8px" },
872
+ onClick: () => {
873
+ onSelect(".");
874
+ handleToggle();
875
+ }
876
+ }, (0, react.createElement)("span", { className: `sts-tree-chevron${expanded ? " expanded" : ""}` }, (0, react.createElement)(ChevronRightIcon, { size: 12 })), (0, react.createElement)("span", { className: "sts-tree-icon" }, (0, react.createElement)(FolderIcon, { size: 14 })), (0, react.createElement)("span", { className: "sts-tree-label" }, "(工作空间根目录)")), expanded ? loading ? (0, react.createElement)("div", {
877
+ className: "sts-tree-loading",
878
+ style: { paddingLeft: "24px" }
879
+ }, "加载中...") : error !== null ? (0, react.createElement)("div", {
880
+ className: "sts-tree-error",
881
+ style: { paddingLeft: "24px" }
882
+ }, error) : children !== null && children.length > 0 ? (0, react.createElement)("div", null, ...children.map((child) => (0, react.createElement)(TreeNode, {
883
+ key: child.path,
884
+ scope,
885
+ entry: child,
886
+ depth: 1,
887
+ selectedPath: value,
888
+ onSelect
889
+ }))) : (0, react.createElement)("div", {
890
+ className: "sts-tree-empty",
891
+ style: { paddingLeft: "24px" }
892
+ }, "无子目录") : null);
893
+ }
894
+ /** Type \u2192 default command mapping. */
895
+ const TYPE_DEFAULTS = {
896
+ npm: "npm run dev",
897
+ springboot: "mvn spring-boot:run",
898
+ python: "python main.py",
899
+ custom: ""
900
+ };
901
+ const TYPE_LABELS = {
902
+ npm: "npm",
903
+ springboot: "Spring Boot",
904
+ python: "Python",
905
+ custom: "Custom"
906
+ };
907
+ /** The modal dialog component. */
908
+ function ConfigEditorModal({ config, scope, onSave, onCancel }) {
909
+ const [name, setName] = (0, react.useState)(config?.name ?? "");
910
+ const [type, setType] = (0, react.useState)(config?.type ?? "custom");
911
+ const [command, setCommand] = (0, react.useState)(config?.command ?? "");
912
+ const [cwd, setCwd] = (0, react.useState)(config?.cwd ?? ".");
913
+ const [jvmArgs, setJvmArgs] = (0, react.useState)(config?.jvmArgs ?? "");
914
+ const [args, setArgs] = (0, react.useState)(config?.args ?? "");
915
+ const [envRows, setEnvRows] = (0, react.useState)(config ? Object.entries(config.env).map(([key, value]) => ({
916
+ key,
917
+ value
918
+ })) : []);
919
+ const [showTree, setShowTree] = (0, react.useState)(false);
920
+ const [showRuntime, setShowRuntime] = (0, react.useState)(false);
921
+ const [rtJava, setRtJava] = (0, react.useState)(config?.runtime?.java ?? "");
922
+ const [rtNode, setRtNode] = (0, react.useState)(config?.runtime?.node ?? "");
923
+ const [rtPython, setRtPython] = (0, react.useState)(config?.runtime?.python ?? "");
924
+ const [rtMvn, setRtMvn] = (0, react.useState)(config?.runtime?.mvn ?? "");
925
+ (0, react.useEffect)(() => {
926
+ if (command === "" && TYPE_DEFAULTS[type] !== "") setCommand(TYPE_DEFAULTS[type]);
927
+ }, [type]);
928
+ const handleSave = () => {
929
+ const env = {};
930
+ for (const row of envRows) if (row.key.trim() !== "") env[row.key.trim()] = row.value;
931
+ const runtime = {};
932
+ if (rtJava.trim() !== "") runtime.java = rtJava.trim();
933
+ if (rtNode.trim() !== "") runtime.node = rtNode.trim();
934
+ if (rtPython.trim() !== "") runtime.python = rtPython.trim();
935
+ if (rtMvn.trim() !== "") runtime.mvn = rtMvn.trim();
936
+ onSave({
937
+ id: config?.id,
938
+ name: name.trim(),
939
+ type,
940
+ command: command.trim(),
941
+ cwd: cwd.trim() === "" ? "." : cwd.trim(),
942
+ env,
943
+ jvmArgs: jvmArgs.trim(),
944
+ args: args.trim(),
945
+ runtime
946
+ });
947
+ };
948
+ const updateEnvRow = (idx, field, val) => {
949
+ setEnvRows((prev) => prev.map((row, i) => i === idx ? {
950
+ ...row,
951
+ [field]: val
952
+ } : row));
953
+ };
954
+ const addEnvRow = () => setEnvRows((prev) => [...prev, {
955
+ key: "",
956
+ value: ""
957
+ }]);
958
+ const removeEnvRow = (idx) => setEnvRows((prev) => prev.filter((_, i) => i !== idx));
959
+ const [showEnvPaste, setShowEnvPaste] = (0, react.useState)(false);
960
+ const [envPasteText, setEnvPasteText] = (0, react.useState)("");
961
+ /** Parse "K=V;K=V,..." (or newline/comma separated) into env rows, merging
962
+ * into the existing rows (later values override earlier for duplicate keys). */
963
+ const applyEnvPaste = () => {
964
+ const existing = new Map(envRows.filter((r) => r.key.trim() !== "").map((r) => [r.key.trim(), r.value]));
965
+ const text = envPasteText.trim();
966
+ if (text === "") return;
967
+ const pairs = text.split(/[;,\n]+/);
968
+ for (const pair of pairs) {
969
+ const p = pair.trim();
970
+ if (p === "") continue;
971
+ const eq = p.indexOf("=");
972
+ if (eq <= 0) continue;
973
+ const key = p.substring(0, eq).trim();
974
+ const value = p.substring(eq + 1).trim();
975
+ if (key !== "") existing.set(key, value);
976
+ }
977
+ setEnvRows(Array.from(existing, ([key, value]) => ({
978
+ key,
979
+ value
980
+ })));
981
+ setEnvPasteText("");
982
+ setShowEnvPaste(false);
983
+ };
984
+ const isSpringboot = type === "springboot";
985
+ return (0, react.createElement)("div", {
986
+ className: "sts-modal-overlay",
987
+ onClick: (e) => {
988
+ if (e.target === e.currentTarget) onCancel();
989
+ }
990
+ }, (0, react.createElement)("div", { className: "sts-modal" }, (0, react.createElement)("div", { className: "sts-modal-header" }, config?.id ? "编辑运行配置" : "新建运行配置", (0, react.createElement)("button", {
991
+ className: "sts-icon-btn",
992
+ onClick: onCancel
993
+ }, (0, react.createElement)(CloseIcon, { size: 16 }))), (0, react.createElement)("div", { className: "sts-modal-body" }, (0, react.createElement)("div", { className: "sts-modal-field" }, (0, react.createElement)("label", { className: "sts-modal-label" }, "名称"), (0, react.createElement)("input", {
994
+ className: "sts-modal-input",
995
+ value: name,
996
+ onChange: (e) => setName(e.target.value),
997
+ placeholder: "配置名称"
998
+ })), (0, react.createElement)("div", { className: "sts-modal-field" }, (0, react.createElement)("label", { className: "sts-modal-label" }, "类型"), (0, react.createElement)("select", {
999
+ className: "sts-modal-select",
1000
+ value: type,
1001
+ onChange: (e) => setType(e.target.value)
1002
+ }, ...Object.entries(TYPE_LABELS).map(([val, label]) => (0, react.createElement)("option", {
1003
+ key: val,
1004
+ value: val
1005
+ }, label)))), (0, react.createElement)("div", { className: "sts-modal-field" }, (0, react.createElement)("label", { className: "sts-modal-label" }, "命令"), (0, react.createElement)("input", {
1006
+ className: "sts-modal-input mono",
1007
+ value: command,
1008
+ onChange: (e) => setCommand(e.target.value),
1009
+ placeholder: "要执行的命令"
1010
+ })), (0, react.createElement)("div", { className: "sts-modal-field" }, (0, react.createElement)("label", { className: "sts-modal-label" }, "工作目录"), (0, react.createElement)("div", {
1011
+ className: "sts-cwd-display",
1012
+ onClick: () => setShowTree(!showTree)
1013
+ }, (0, react.createElement)("span", { className: "sts-cwd-icon" }, (0, react.createElement)(FolderIcon, { size: 14 })), (0, react.createElement)("span", { className: "sts-cwd-value" }, cwd)), showTree ? (0, react.createElement)("div", { className: "sts-tree-picker-container" }, (0, react.createElement)(DirTreePicker, {
1014
+ scope,
1015
+ value: cwd,
1016
+ onSelect: (p) => setCwd(p)
1017
+ })) : null), isSpringboot ? (0, react.createElement)("div", { className: "sts-modal-field" }, (0, react.createElement)("label", { className: "sts-modal-label" }, "虚拟机参数 (JVM Args)"), (0, react.createElement)("input", {
1018
+ className: "sts-modal-input mono",
1019
+ value: jvmArgs,
1020
+ onChange: (e) => setJvmArgs(e.target.value),
1021
+ placeholder: "例: -Xmx512m -Dspring.profiles.active=dev"
1022
+ })) : null, (0, react.createElement)("div", { className: "sts-modal-field" }, (0, react.createElement)("label", { className: "sts-modal-label" }, "程序实参"), (0, react.createElement)("input", {
1023
+ className: "sts-modal-input mono",
1024
+ value: args,
1025
+ onChange: (e) => setArgs(e.target.value),
1026
+ placeholder: "例: --server.port=8080 --debug"
1027
+ })), (0, react.createElement)("div", { className: "sts-modal-field" }, (0, react.createElement)("div", {
1028
+ className: `sts-collapse-header${showRuntime ? " open" : ""}`,
1029
+ onClick: () => setShowRuntime(!showRuntime)
1030
+ }, (0, react.createElement)("span", { className: `sts-collapse-chevron${showRuntime ? " expanded" : ""}` }, (0, react.createElement)(ChevronRightIcon, { size: 12 })), (0, react.createElement)("span", null, "运行时环境 (高级)"), (0, react.createElement)("span", { className: "sts-collapse-hint" }, "默认用全局配置")), showRuntime ? (0, react.createElement)("div", { className: "sts-collapse-body" }, (0, react.createElement)("div", { className: "sts-modal-label" }, "留空则用全局/系统配置"), (0, react.createElement)("div", { className: "sts-modal-field" }, (0, react.createElement)("label", { className: "sts-modal-label" }, "Java 路径"), (0, react.createElement)(PathPicker, {
1031
+ scope,
1032
+ kind: "java",
1033
+ value: rtJava,
1034
+ onChange: setRtJava,
1035
+ compact: true,
1036
+ desc: "选择 JDK 目录"
1037
+ })), (0, react.createElement)("div", { className: "sts-modal-field" }, (0, react.createElement)("label", { className: "sts-modal-label" }, "Node.js 路径"), (0, react.createElement)(PathPicker, {
1038
+ scope,
1039
+ kind: "node",
1040
+ value: rtNode,
1041
+ onChange: setRtNode,
1042
+ compact: true,
1043
+ desc: "选择 Node 目录"
1044
+ })), (0, react.createElement)("div", { className: "sts-modal-field" }, (0, react.createElement)("label", { className: "sts-modal-label" }, "Python 路径"), (0, react.createElement)(PathPicker, {
1045
+ scope,
1046
+ kind: "python",
1047
+ value: rtPython,
1048
+ onChange: setRtPython,
1049
+ compact: true,
1050
+ desc: "选择 Python 目录"
1051
+ })), (0, react.createElement)("div", { className: "sts-modal-field" }, (0, react.createElement)("label", { className: "sts-modal-label" }, "Maven 路径 (MVN)"), (0, react.createElement)(PathPicker, {
1052
+ scope,
1053
+ kind: "mvn",
1054
+ value: rtMvn,
1055
+ onChange: setRtMvn,
1056
+ compact: true,
1057
+ desc: "选择 Maven 目录"
1058
+ }))) : null), (0, react.createElement)("div", { className: "sts-modal-field" }, (0, react.createElement)("label", { className: "sts-modal-label" }, "环境变量"), ...envRows.map((row, idx) => (0, react.createElement)("div", {
1059
+ key: idx,
1060
+ className: "sts-env-row"
1061
+ }, (0, react.createElement)("input", {
1062
+ className: "sts-modal-input sts-env-key",
1063
+ value: row.key,
1064
+ onChange: (e) => updateEnvRow(idx, "key", e.target.value),
1065
+ placeholder: "变量名 (如 JAVA_HOME)"
1066
+ }), (0, react.createElement)("span", { className: "sts-env-sep" }, "="), (0, react.createElement)("input", {
1067
+ className: "sts-modal-input sts-env-val",
1068
+ value: row.value,
1069
+ onChange: (e) => updateEnvRow(idx, "value", e.target.value),
1070
+ placeholder: "变量值"
1071
+ }), (0, react.createElement)("button", {
1072
+ className: "sts-icon-btn trash",
1073
+ onClick: () => removeEnvRow(idx)
1074
+ }, (0, react.createElement)(TrashIcon, { size: 14 })))), (0, react.createElement)("div", { className: "sts-env-actions" }, (0, react.createElement)("button", {
1075
+ className: "sts-env-add",
1076
+ onClick: addEnvRow
1077
+ }, (0, react.createElement)(PlusIcon, { size: 12 }), " 添加环境变量"), (0, react.createElement)("button", {
1078
+ className: "sts-env-add",
1079
+ onClick: () => setShowEnvPaste(!showEnvPaste)
1080
+ }, (0, react.createElement)(ClipboardIcon, { size: 12 }), showEnvPaste ? " 取消" : " 批量粘贴")), showEnvPaste ? (0, react.createElement)("div", { className: "sts-env-paste" }, (0, react.createElement)("textarea", {
1081
+ className: "sts-env-paste-input",
1082
+ value: envPasteText,
1083
+ onChange: (e) => setEnvPasteText(e.target.value),
1084
+ placeholder: "粘贴 KEY=VALUE;KEY=VALUE;或以换行/逗号分隔,如:\nDB_DATABASE=cassiopeia_ap;DB_HOST=10.18.18.18;DB_PORT=3306;MQ_HOST=10.18.18.18;MQ_PASSWORD=admin123;MQ_PORT=5672;MQ_USERNAME=admin;MQ_VIRTUAL_HOST=/;REDIS_HOST=10.18.18.17;REDIS_PASSWORD=Qaz!wsx2;REDIS_PORT=6379",
1085
+ rows: 4
1086
+ }), (0, react.createElement)("div", { className: "sts-env-paste-footer" }, (0, react.createElement)("button", {
1087
+ className: "sts-env-paste-apply",
1088
+ onClick: applyEnvPaste
1089
+ }, "确定并生成环境变量"), (0, react.createElement)("span", { className: "sts-env-paste-hint" }, "重复将覆盖"))) : null)), (0, react.createElement)("div", { className: "sts-modal-footer" }, (0, react.createElement)("button", {
1090
+ className: "sts-modal-btn",
1091
+ onClick: onCancel
1092
+ }, "取消"), (0, react.createElement)("button", {
1093
+ className: "sts-modal-btn primary",
1094
+ onClick: handleSave,
1095
+ disabled: name.trim() === "",
1096
+ style: {
1097
+ background: "#1a1a1e",
1098
+ border: "1px solid #3a3a3e",
1099
+ color: "#fff",
1100
+ borderRadius: "6px",
1101
+ padding: "8px 18px",
1102
+ cursor: name.trim() === "" ? "not-allowed" : "pointer",
1103
+ fontSize: "13px",
1104
+ opacity: name.trim() === "" ? .5 : 1
1105
+ }
1106
+ }, "保存"))));
1107
+ }
1108
+ //#endregion
1109
+ //#region src/client/styles.ts
1110
+ /**
1111
+ * CSS styles for the starter plugin panel. Uses .sts- prefixed class names
1112
+ * (scoped enough to avoid collisions with the host shell).
1113
+ * Uses --dsw-alias-* CSS variables from the DSH design system to follow the
1114
+ * active theme automatically (dark/light).
1115
+ * @module dsh-better-sidebar-starter/client/styles
1116
+ */
1117
+ const STYLES_CSS = `
1118
+ /* ===== Root container ===== */
1119
+ .sts-root {
1120
+ display: flex;
1121
+ flex-direction: column;
1122
+ height: 100%;
1123
+ min-height: 0;
1124
+ font-size: 13px;
1125
+ color: var(--dsw-alias-label-primary, #e6e6e6);
1126
+ background: var(--dsw-alias-bg-base, #1a1a1e);
1127
+ }
1128
+
1129
+ /* ===== Toolbar ===== */
1130
+ .sts-toolbar {
1131
+ display: flex;
1132
+ align-items: center;
1133
+ justify-content: space-between;
1134
+ padding: 0 12px;
1135
+ height: 40px;
1136
+ border-bottom: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
1137
+ flex-shrink: 0;
1138
+ }
1139
+ .sts-toolbar-title {
1140
+ display: flex;
1141
+ align-items: center;
1142
+ gap: 8px;
1143
+ font: var(--dsw-font-m-14, 500 14px/20px sans-serif);
1144
+ color: var(--dsw-alias-label-primary, #e6e6e6);
1145
+ }
1146
+ .sts-toolbar-btn {
1147
+ display: flex;
1148
+ align-items: center;
1149
+ gap: 4px;
1150
+ padding: 4px 10px;
1151
+ border: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
1152
+ border-radius: 6px;
1153
+ background: transparent;
1154
+ color: var(--dsw-alias-label-secondary, #b0b0b0);
1155
+ cursor: pointer;
1156
+ font-size: 12px;
1157
+ transition: background 0.12s, border-color 0.12s;
1158
+ }
1159
+ .sts-toolbar-btn:hover {
1160
+ background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.06));
1161
+ border-color: var(--dsw-alias-border-l3, #3a3a3e);
1162
+ color: var(--dsw-alias-label-primary, #e6e6e6);
1163
+ }
1164
+
1165
+ /* ===== Split pane (horizontal: default for right panel) ===== */
1166
+ .sts-split {
1167
+ display: flex;
1168
+ flex: 1;
1169
+ min-height: 0;
1170
+ }
1171
+ .sts-split-first {
1172
+ overflow-y: auto;
1173
+ overflow-x: hidden;
1174
+ border-right: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
1175
+ flex-shrink: 0;
1176
+ }
1177
+ .sts-split-gutter {
1178
+ width: 1px;
1179
+ background: var(--dsw-alias-border-l2, #2a2a2e);
1180
+ cursor: col-resize;
1181
+ flex-shrink: 0;
1182
+ transition: width 0.12s, background 0.12s;
1183
+ }
1184
+ .sts-split-gutter:hover,
1185
+ .sts-split-gutter.dragging {
1186
+ width: 3px;
1187
+ background: var(--dsw-alias-interactive-bg-hover-accent, #007acc);
1188
+ }
1189
+ .sts-split-second {
1190
+ flex: 1;
1191
+ display: flex;
1192
+ flex-direction: column;
1193
+ min-width: 0;
1194
+ min-height: 0;
1195
+ }
1196
+
1197
+ /* ===== Split pane (vertical: for bottom panel) ===== */
1198
+ .sts-split-vertical {
1199
+ flex-direction: column;
1200
+ }
1201
+ .sts-split-first-v {
1202
+ border-right: none;
1203
+ border-bottom: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
1204
+ width: 100% !important;
1205
+ overflow-y: auto;
1206
+ overflow-x: hidden;
1207
+ flex-shrink: 0;
1208
+ }
1209
+ .sts-split-gutter-v {
1210
+ width: auto;
1211
+ height: 1px;
1212
+ cursor: row-resize;
1213
+ transition: height 0.12s, background 0.12s;
1214
+ }
1215
+ .sts-split-gutter-v:hover,
1216
+ .sts-split-gutter-v.dragging {
1217
+ width: auto;
1218
+ height: 3px;
1219
+ background: var(--dsw-alias-interactive-bg-hover-accent, #007acc);
1220
+ }
1221
+ .sts-split-second-v {
1222
+ flex: 1;
1223
+ display: flex;
1224
+ flex-direction: column;
1225
+ min-width: 0;
1226
+ min-height: 0;
1227
+ }
1228
+
1229
+ /* ===== Config tree ===== */
1230
+ .sts-tree {
1231
+ padding: 4px 4px 8px;
1232
+ user-select: none;
1233
+ }
1234
+ .sts-group-header {
1235
+ display: flex;
1236
+ align-items: center;
1237
+ gap: 6px;
1238
+ padding: 6px 8px;
1239
+ font: var(--dsw-font-xxxs-11, 500 11px/16px sans-serif);
1240
+ color: var(--dsw-alias-label-tertiary, #888);
1241
+ text-transform: uppercase;
1242
+ letter-spacing: 0.5px;
1243
+ cursor: pointer;
1244
+ border-radius: 6px;
1245
+ }
1246
+ .sts-group-header:hover {
1247
+ background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.04));
1248
+ }
1249
+ .sts-group-count {
1250
+ color: rgba(255,255,255,0.4);
1251
+ font-weight: 400;
1252
+ font-variant-numeric: tabular-nums;
1253
+ }
1254
+ .sts-group-children {
1255
+ overflow: hidden;
1256
+ }
1257
+ .sts-group-children.collapsed {
1258
+ display: none;
1259
+ }
1260
+
1261
+ /* ===== Config row ===== */
1262
+ .sts-config-row {
1263
+ display: flex;
1264
+ align-items: center;
1265
+ gap: 8px;
1266
+ padding: 5px 8px 5px 24px;
1267
+ cursor: pointer;
1268
+ font-size: 13px;
1269
+ border-radius: 6px;
1270
+ margin: 1px 2px;
1271
+ transition: background 0.1s;
1272
+ color: var(--dsw-alias-label-primary, #e6e6e6);
1273
+ }
1274
+ .sts-config-row:hover {
1275
+ background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.04));
1276
+ }
1277
+ .sts-config-row.selected {
1278
+ background: var(--dsw-alias-interactive-bg-active, rgba(0,122,204,0.12));
1279
+ }
1280
+ .sts-config-name {
1281
+ flex: 1;
1282
+ overflow: hidden;
1283
+ text-overflow: ellipsis;
1284
+ white-space: nowrap;
1285
+ font-weight: 400;
1286
+ }
1287
+ .sts-config-cmd {
1288
+ font-size: 10px;
1289
+ color: rgba(255,255,255,0.4);
1290
+ font-family: ui-monospace, SFMono-Regular, "Cascadia Mono", "Cascadia Code", Menlo, Consolas, "DejaVu Sans Mono", "Liberation Mono", monospace;
1291
+ overflow: hidden;
1292
+ text-overflow: ellipsis;
1293
+ white-space: nowrap;
1294
+ max-width: 100px;
1295
+ }
1296
+ .sts-config-actions {
1297
+ display: flex;
1298
+ align-items: center;
1299
+ gap: 1px;
1300
+ flex-shrink: 0;
1301
+ }
1302
+ .sts-icon-btn {
1303
+ display: flex;
1304
+ align-items: center;
1305
+ justify-content: center;
1306
+ width: 22px;
1307
+ height: 22px;
1308
+ border: none;
1309
+ border-radius: 4px;
1310
+ background: transparent;
1311
+ color: var(--dsw-alias-label-secondary, #b0b0b0);
1312
+ cursor: pointer;
1313
+ opacity: 0;
1314
+ transition: opacity 0.1s, background 0.1s;
1315
+ }
1316
+ .sts-config-row:hover .sts-icon-btn {
1317
+ opacity: 0.7;
1318
+ }
1319
+ .sts-icon-btn:hover {
1320
+ opacity: 1 !important;
1321
+ background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.08));
1322
+ }
1323
+ .sts-icon-btn.play { color: #6ab04c; }
1324
+ .sts-icon-btn.stop { color: #eb5757; }
1325
+ .sts-icon-btn.gear { color: var(--dsw-alias-label-secondary, #b0b0b0); }
1326
+ .sts-icon-btn.trash { color: #eb5757; }
1327
+
1328
+ /* ===== Instance row ===== */
1329
+ .sts-instance-row {
1330
+ display: flex;
1331
+ align-items: center;
1332
+ gap: 8px;
1333
+ padding: 3px 8px 3px 40px;
1334
+ font-size: 11px;
1335
+ cursor: pointer;
1336
+ border-radius: 6px;
1337
+ margin: 1px 2px;
1338
+ color: var(--dsw-alias-label-tertiary, #888);
1339
+ transition: background 0.1s;
1340
+ }
1341
+ .sts-instance-row:hover {
1342
+ background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.04));
1343
+ }
1344
+ .sts-instance-row.selected {
1345
+ background: var(--dsw-alias-interactive-bg-active, rgba(0,122,204,0.12));
1346
+ color: var(--dsw-alias-label-primary, #e6e6e6);
1347
+ }
1348
+ .sts-instance-pid {
1349
+ color: rgba(255,255,255,0.4);
1350
+ font-family: ui-monospace, SFMono-Regular, "Cascadia Mono", "Cascadia Code", Menlo, Consolas, "DejaVu Sans Mono", "Liberation Mono", monospace;
1351
+ font-variant-numeric: tabular-nums;
1352
+ }
1353
+
1354
+ /* ===== Empty states ===== */
1355
+ .sts-empty {
1356
+ display: flex;
1357
+ flex-direction: column;
1358
+ align-items: center;
1359
+ justify-content: center;
1360
+ height: 100%;
1361
+ color: rgba(255,255,255,0.4);
1362
+ font-size: 12px;
1363
+ padding: 24px;
1364
+ text-align: center;
1365
+ gap: 8px;
1366
+ line-height: 20px;
1367
+ }
1368
+
1369
+ /* ===== Log view ===== */
1370
+ .sts-log-toolbar {
1371
+ display: flex;
1372
+ align-items: center;
1373
+ gap: 2px;
1374
+ padding: 0 8px;
1375
+ height: 36px;
1376
+ border-bottom: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
1377
+ flex-shrink: 0;
1378
+ color: var(--dsw-alias-label-secondary, #b0b0b0);
1379
+ font-size: 12px;
1380
+ }
1381
+ .sts-log-toolbar-spacer {
1382
+ flex: 1;
1383
+ }
1384
+ .sts-log-toolbar-label {
1385
+ display: flex;
1386
+ align-items: center;
1387
+ gap: 4px;
1388
+ font-size: 11px;
1389
+ cursor: pointer;
1390
+ color: var(--dsw-alias-label-tertiary, #888);
1391
+ padding: 2px 6px;
1392
+ border-radius: 4px;
1393
+ }
1394
+ .sts-log-toolbar-label:hover {
1395
+ color: var(--dsw-alias-label-primary, #e6e6e6);
1396
+ background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.04));
1397
+ }
1398
+ .sts-log-content {
1399
+ flex: 1;
1400
+ overflow-y: auto;
1401
+ overflow-x: hidden;
1402
+ padding: 8px 12px;
1403
+ font-family: ui-monospace, SFMono-Regular, "Cascadia Mono", "Cascadia Code", Menlo, Consolas, "DejaVu Sans Mono", "Liberation Mono", monospace;
1404
+ font-size: 12px;
1405
+ line-height: 1.6;
1406
+ white-space: pre-wrap;
1407
+ word-break: break-all;
1408
+ color: var(--dsw-alias-label-primary, #e6e6e6);
1409
+ background: var(--dsw-alias-bg-base, #1a1a1e);
1410
+ }
1411
+ .sts-log-line {
1412
+ min-height: 1em;
1413
+ }
1414
+
1415
+ /* ===== Config detail ===== */
1416
+ .sts-detail {
1417
+ padding: 20px;
1418
+ overflow-y: auto;
1419
+ flex: 1;
1420
+ }
1421
+ .sts-detail-card {
1422
+ border: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
1423
+ background: var(--dsw-alias-bg-layer-2, #202024);
1424
+ border-radius: 12px;
1425
+ padding: 20px;
1426
+ display: flex;
1427
+ flex-direction: column;
1428
+ gap: 16px;
1429
+ }
1430
+ .sts-detail-title {
1431
+ display: flex;
1432
+ align-items: center;
1433
+ gap: 10px;
1434
+ font-size: 16px;
1435
+ font-weight: 500;
1436
+ color: var(--dsw-alias-label-primary, #e6e6e6);
1437
+ padding-bottom: 12px;
1438
+ border-bottom: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
1439
+ }
1440
+ .sts-detail-row {
1441
+ display: flex;
1442
+ gap: 12px;
1443
+ font-size: 13px;
1444
+ align-items: baseline;
1445
+ }
1446
+ .sts-detail-label {
1447
+ width: 72px;
1448
+ flex-shrink: 0;
1449
+ color: var(--dsw-alias-label-tertiary, #888);
1450
+ font-size: 11px;
1451
+ text-transform: uppercase;
1452
+ letter-spacing: 0.3px;
1453
+ }
1454
+ .sts-detail-value {
1455
+ font-family: ui-monospace, SFMono-Regular, "Cascadia Mono", "Cascadia Code", Menlo, Consolas, "DejaVu Sans Mono", "Liberation Mono", monospace;
1456
+ word-break: break-all;
1457
+ color: var(--dsw-alias-label-primary, #e6e6e6);
1458
+ }
1459
+ .sts-detail-actions {
1460
+ display: flex;
1461
+ gap: 8px;
1462
+ justify-content: flex-end;
1463
+ margin-top: 12px;
1464
+ padding-top: 16px;
1465
+ border-top: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
1466
+ }
1467
+ .sts-detail-btn {
1468
+ display: flex;
1469
+ align-items: center;
1470
+ gap: 6px;
1471
+ padding: 6px 14px;
1472
+ border: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
1473
+ border-radius: 6px;
1474
+ background: transparent;
1475
+ color: var(--dsw-alias-label-secondary, #b0b0b0);
1476
+ cursor: pointer;
1477
+ font-size: 12px;
1478
+ transition: background 0.12s, color 0.12s;
1479
+ }
1480
+ .sts-detail-btn:hover {
1481
+ background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.06));
1482
+ color: var(--dsw-alias-label-primary, #e6e6e6);
1483
+ }
1484
+ .sts-detail-btn.danger {
1485
+ color: #eb5757;
1486
+ border-color: rgba(235,87,87,0.4);
1487
+ }
1488
+ .sts-detail-btn.danger:hover {
1489
+ background: rgba(235,87,87,0.1);
1490
+ }
1491
+
1492
+ /* ===== Modal ===== */
1493
+ .sts-modal-overlay {
1494
+ position: fixed;
1495
+ top: 0; left: 0; right: 0; bottom: 0;
1496
+ background: rgba(0,0,0,0.45);
1497
+ backdrop-filter: blur(2px);
1498
+ display: flex;
1499
+ align-items: center;
1500
+ justify-content: center;
1501
+ z-index: 10000;
1502
+ }
1503
+ .sts-modal {
1504
+ background: var(--dsw-alias-bg-layer-3, #242428);
1505
+ border: 1px solid var(--dsw-alias-border-l3, #3a3a3e);
1506
+ border-radius: 12px;
1507
+ width: 480px;
1508
+ max-width: 90vw;
1509
+ max-height: 80vh;
1510
+ overflow-y: auto;
1511
+ display: flex;
1512
+ flex-direction: column;
1513
+ box-shadow: 0 8px 32px rgba(0,0,0,0.3);
1514
+ }
1515
+ .sts-modal-header {
1516
+ display: flex;
1517
+ align-items: center;
1518
+ justify-content: space-between;
1519
+ padding: 14px 20px;
1520
+ font-size: 15px;
1521
+ font-weight: 500;
1522
+ color: var(--dsw-alias-label-primary, #e6e6e6);
1523
+ border-bottom: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
1524
+ }
1525
+ .sts-modal-body {
1526
+ padding: 20px;
1527
+ display: flex;
1528
+ flex-direction: column;
1529
+ gap: 16px;
1530
+ }
1531
+ .sts-modal-field {
1532
+ display: flex;
1533
+ flex-direction: column;
1534
+ gap: 6px;
1535
+ }
1536
+ .sts-modal-label {
1537
+ font-size: 11px;
1538
+ color: var(--dsw-alias-label-tertiary, #888);
1539
+ text-transform: uppercase;
1540
+ letter-spacing: 0.3px;
1541
+ }
1542
+ .sts-modal-input,
1543
+ .sts-modal-select {
1544
+ padding: 8px 10px;
1545
+ border: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
1546
+ border-radius: 6px;
1547
+ background: var(--dsw-alias-bg-layer-1, #1a1a1e);
1548
+ color: var(--dsw-alias-label-primary, #e6e6e6);
1549
+ font-size: 13px;
1550
+ font-family: inherit;
1551
+ outline: none;
1552
+ transition: border-color 0.12s, background 0.12s;
1553
+ }
1554
+ .sts-modal-input:focus,
1555
+ .sts-modal-select:focus {
1556
+ border-color: var(--dsw-alias-button-primary-fill, #007acc);
1557
+ background: var(--dsw-alias-bg-base, #1a1a1e);
1558
+ }
1559
+ .sts-modal-input.mono {
1560
+ font-family: ui-monospace, SFMono-Regular, "Cascadia Mono", "Cascadia Code", Menlo, Consolas, "DejaVu Sans Mono", "Liberation Mono", monospace;
1561
+ }
1562
+ .sts-env-row {
1563
+ display: flex;
1564
+ gap: 6px;
1565
+ align-items: center;
1566
+ }
1567
+ .sts-env-key {
1568
+ flex: 1;
1569
+ }
1570
+ .sts-env-val {
1571
+ flex: 2;
1572
+ }
1573
+ .sts-env-sep {
1574
+ color: var(--dsw-alias-label-dimmed, #555);
1575
+ font-family: ui-monospace, SFMono-Regular, "Cascadia Mono", "Cascadia Code", Menlo, Consolas, "DejaVu Sans Mono", "Liberation Mono", monospace;
1576
+ }
1577
+ .sts-env-add {
1578
+ font-size: 12px;
1579
+ padding: 6px 10px;
1580
+ border: 1px dashed var(--dsw-alias-border-l3, #3a3a3e);
1581
+ border-radius: 6px;
1582
+ background: transparent;
1583
+ color: var(--dsw-alias-label-tertiary, #888);
1584
+ cursor: pointer;
1585
+ align-self: flex-start;
1586
+ transition: color 0.12s, border-color 0.12s;
1587
+ }
1588
+ .sts-env-add:hover {
1589
+ color: var(--dsw-alias-label-primary, #e6e6e6);
1590
+ border-color: var(--dsw-alias-button-primary-fill, #007acc);
1591
+ }
1592
+ .sts-modal-footer {
1593
+ display: flex;
1594
+ justify-content: flex-end;
1595
+ gap: 8px;
1596
+ padding: 14px 20px;
1597
+ border-top: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
1598
+ }
1599
+ .sts-modal-btn {
1600
+ padding: 8px 18px;
1601
+ border: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
1602
+ border-radius: 6px;
1603
+ background: transparent;
1604
+ color: var(--dsw-alias-label-secondary, #b0b0b0);
1605
+ cursor: pointer;
1606
+ font-size: 13px;
1607
+ transition: background 0.12s, color 0.12s;
1608
+ }
1609
+ .sts-modal-btn:hover {
1610
+ background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.06));
1611
+ color: var(--dsw-alias-label-primary, #e6e6e6);
1612
+ }
1613
+
1614
+ /* ===== Directory tree picker ===== */
1615
+ .sts-cwd-display {
1616
+ display: flex;
1617
+ align-items: center;
1618
+ gap: 6px;
1619
+ padding: 8px 10px;
1620
+ border: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
1621
+ border-radius: 6px;
1622
+ background: var(--dsw-alias-bg-layer-1, #1a1a1e);
1623
+ cursor: pointer;
1624
+ transition: border-color 0.12s;
1625
+ font-size: 13px;
1626
+ }
1627
+ .sts-cwd-display:hover {
1628
+ border-color: var(--dsw-alias-border-l3, #3a3a3e);
1629
+ }
1630
+ .sts-cwd-icon {
1631
+ display: flex;
1632
+ align-items: center;
1633
+ color: var(--dsw-alias-label-secondary, #b0b0b0);
1634
+ }
1635
+ .sts-cwd-value {
1636
+ font-family: ui-monospace, SFMono-Regular, "Cascadia Mono", "Cascadia Code", Menlo, Consolas, monospace;
1637
+ color: var(--dsw-alias-label-primary, #e6e6e6);
1638
+ overflow: hidden;
1639
+ text-overflow: ellipsis;
1640
+ white-space: nowrap;
1641
+ }
1642
+ .sts-tree-picker-container {
1643
+ margin-top: 4px;
1644
+ border: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
1645
+ border-radius: 6px;
1646
+ background: var(--dsw-alias-bg-layer-1, #1a1a1e);
1647
+ max-height: 240px;
1648
+ overflow-y: auto;
1649
+ padding: 4px;
1650
+ }
1651
+ .sts-tree-row {
1652
+ display: flex;
1653
+ align-items: center;
1654
+ gap: 4px;
1655
+ padding: 4px 8px;
1656
+ cursor: pointer;
1657
+ border-radius: 4px;
1658
+ font-size: 13px;
1659
+ color: var(--dsw-alias-label-secondary, #b0b0b0);
1660
+ transition: background 0.08s;
1661
+ white-space: nowrap;
1662
+ }
1663
+ .sts-tree-row:hover {
1664
+ background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.04));
1665
+ color: var(--dsw-alias-label-primary, #e6e6e6);
1666
+ }
1667
+ .sts-tree-row.selected {
1668
+ background: var(--dsw-alias-interactive-bg-active, rgba(0,122,204,0.12));
1669
+ color: var(--dsw-alias-label-primary, #e6e6e6);
1670
+ }
1671
+ .sts-tree-chevron {
1672
+ display: flex;
1673
+ align-items: center;
1674
+ transition: transform 0.1s;
1675
+ flex-shrink: 0;
1676
+ }
1677
+ .sts-tree-chevron.expanded {
1678
+ transform: rotate(90deg);
1679
+ }
1680
+ .sts-tree-icon {
1681
+ display: flex;
1682
+ align-items: center;
1683
+ flex-shrink: 0;
1684
+ color: var(--dsw-alias-label-tertiary, #888);
1685
+ }
1686
+ .sts-tree-label {
1687
+ overflow: hidden;
1688
+ text-overflow: ellipsis;
1689
+ }
1690
+ .sts-tree-loading,
1691
+ .sts-tree-error,
1692
+ .sts-tree-empty {
1693
+ padding: 8px 12px;
1694
+ font-size: 12px;
1695
+ color: rgba(255,255,255,0.4);
1696
+ }
1697
+ .sts-tree-error {
1698
+ color: #eb5757;
1699
+ }
1700
+
1701
+ /* ===== Settings panel ===== */
1702
+ .sts-settings-panel {
1703
+ display: flex;
1704
+ flex-direction: column;
1705
+ gap: 4px;
1706
+ padding: 8px;
1707
+ min-width: 320px;
1708
+ }
1709
+ .sts-settings-row {
1710
+ padding: 4px 0;
1711
+ }
1712
+ .sts-settings-switch-row {
1713
+ display: flex;
1714
+ align-items: center;
1715
+ gap: 8px;
1716
+ cursor: pointer;
1717
+ font-size: 13px;
1718
+ color: var(--dsw-alias-label-primary, #e6e6e6);
1719
+ }
1720
+ .sts-settings-switch {
1721
+ width: 16px;
1722
+ height: 16px;
1723
+ cursor: pointer;
1724
+ }
1725
+ .sts-settings-sep {
1726
+ margin: 8px 0 4px;
1727
+ padding: 0 4px;
1728
+ font-size: 10px;
1729
+ text-transform: uppercase;
1730
+ letter-spacing: 0.5px;
1731
+ color: rgba(255,255,255,0.4);
1732
+ }
1733
+ .sts-settings-path {
1734
+ display: flex;
1735
+ flex-direction: column;
1736
+ gap: 4px;
1737
+ padding: 4px 0;
1738
+ }
1739
+ .sts-settings-path-header {
1740
+ display: flex;
1741
+ align-items: center;
1742
+ justify-content: space-between;
1743
+ }
1744
+ .sts-settings-label {
1745
+ font-size: 12px;
1746
+ color: var(--dsw-alias-label-secondary, #b0b0b0);
1747
+ }
1748
+ .sts-settings-badge {
1749
+ display: flex;
1750
+ align-items: center;
1751
+ gap: 2px;
1752
+ font-size: 10px;
1753
+ padding: 1px 6px;
1754
+ border-radius: 10px;
1755
+ }
1756
+ .sts-settings-badge.ok {
1757
+ color: #4e9a06;
1758
+ background: rgba(78,154,6,0.12);
1759
+ }
1760
+ .sts-settings-badge.err {
1761
+ color: #eb5757;
1762
+ background: rgba(235,87,87,0.12);
1763
+ }
1764
+ .sts-settings-path-row {
1765
+ display: flex;
1766
+ align-items: center;
1767
+ gap: 4px;
1768
+ }
1769
+ .sts-settings-path-icon {
1770
+ display: flex;
1771
+ align-items: center;
1772
+ color: var(--dsw-alias-label-tertiary, #888);
1773
+ flex-shrink: 0;
1774
+ }
1775
+ .sts-settings-input {
1776
+ flex: 1;
1777
+ padding: 6px 8px;
1778
+ border: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
1779
+ border-radius: 4px;
1780
+ background: var(--dsw-alias-bg-layer-1, #1a1a1e);
1781
+ color: var(--dsw-alias-label-primary, #e6e6e6);
1782
+ font-size: 12px;
1783
+ font-family: ui-monospace, SFMono-Regular, "Cascadia Mono", "Cascadia Code", Menlo, Consolas, monospace;
1784
+ outline: none;
1785
+ min-width: 0;
1786
+ }
1787
+ .sts-settings-input:focus {
1788
+ border-color: var(--dsw-alias-border-l3, #3a3a3e);
1789
+ }
1790
+ .sts-settings-input::placeholder {
1791
+ color: rgba(255,255,255,0.3);
1792
+ }
1793
+ .sts-settings-browse {
1794
+ padding: 6px 10px;
1795
+ border: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
1796
+ border-radius: 4px;
1797
+ background: transparent;
1798
+ color: var(--dsw-alias-label-secondary, #b0b0b0);
1799
+ cursor: pointer;
1800
+ font-size: 12px;
1801
+ flex-shrink: 0;
1802
+ transition: background 0.12s, color 0.12s;
1803
+ }
1804
+ .sts-settings-browse:hover {
1805
+ background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.06));
1806
+ color: var(--dsw-alias-label-primary, #e6e6e6);
1807
+ }
1808
+ .sts-settings-msg {
1809
+ font-size: 10px;
1810
+ padding: 0 4px;
1811
+ color: rgba(255,255,255,0.4);
1812
+ }
1813
+ .sts-settings-msg.valid {
1814
+ color: #4e9a06;
1815
+ }
1816
+ .sts-settings-msg.invalid {
1817
+ color: #eb5757;
1818
+ }
1819
+
1820
+ /* ===== Collapsible advanced section ===== */
1821
+ .sts-collapse-header {
1822
+ display: flex;
1823
+ align-items: center;
1824
+ gap: 6px;
1825
+ padding: 8px 10px;
1826
+ border: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
1827
+ border-radius: 6px;
1828
+ cursor: pointer;
1829
+ font-size: 13px;
1830
+ color: var(--dsw-alias-label-primary, #e6e6e6);
1831
+ background: var(--dsw-alias-bg-layer-1, #1a1a1e);
1832
+ transition: border-color 0.12s;
1833
+ user-select: none;
1834
+ }
1835
+ .sts-collapse-header:hover {
1836
+ border-color: var(--dsw-alias-border-l3, #3a3a3e);
1837
+ }
1838
+ .sts-collapse-chevron {
1839
+ display: flex;
1840
+ align-items: center;
1841
+ transition: transform 0.12s;
1842
+ color: var(--dsw-alias-label-secondary, #b0b0b0);
1843
+ }
1844
+ .sts-collapse-chevron.expanded {
1845
+ transform: rotate(90deg);
1846
+ }
1847
+ .sts-collapse-hint {
1848
+ margin-left: auto;
1849
+ font-size: 10px;
1850
+ color: rgba(255,255,255,0.4);
1851
+ }
1852
+ .sts-collapse-body {
1853
+ margin-top: 8px;
1854
+ padding: 12px;
1855
+ border: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
1856
+ border-radius: 6px;
1857
+ background: var(--dsw-alias-bg-layer-1, #1a1a1e);
1858
+ display: flex;
1859
+ flex-direction: column;
1860
+ gap: 10px;
1861
+ }
1862
+
1863
+ /* ===== Settings picker rows & dir chooser modal ===== */
1864
+ .sts-settings-picker-row {
1865
+ margin: 6px 0;
1866
+ }
1867
+ .sts-settings-picker-row .sts-settings-label {
1868
+ display: block;
1869
+ margin-bottom: 4px;
1870
+ font-size: 11px;
1871
+ }
1872
+ .sts-dir-modal {
1873
+ background: var(--dsw-alias-bg-layer-3, #242428);
1874
+ border: 1px solid var(--dsw-alias-border-l3, #3a3a3e);
1875
+ border-radius: 12px;
1876
+ width: 420px;
1877
+ max-width: 90vw;
1878
+ max-height: 70vh;
1879
+ display: flex;
1880
+ flex-direction: column;
1881
+ box-shadow: 0 8px 32px rgba(0,0,0,0.3);
1882
+ }
1883
+ .sts-dir-modal-tree {
1884
+ flex: 1;
1885
+ overflow-y: auto;
1886
+ padding: 8px;
1887
+ min-height: 160px;
1888
+ }
1889
+ .sts-dirtree {
1890
+ display: flex;
1891
+ flex-direction: column;
1892
+ gap: 2px;
1893
+ }
1894
+ .sts-dirtree-nav {
1895
+ display: flex;
1896
+ align-items: center;
1897
+ gap: 4px;
1898
+ padding: 4px 4px 8px;
1899
+ border-bottom: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
1900
+ margin-bottom: 4px;
1901
+ }
1902
+ .sts-dirtree-up,
1903
+ .sts-dirtree-home {
1904
+ width: 24px;
1905
+ height: 24px;
1906
+ border: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
1907
+ border-radius: 4px;
1908
+ background: transparent;
1909
+ color: var(--dsw-alias-label-secondary, #b0b0b0);
1910
+ cursor: pointer;
1911
+ font-size: 13px;
1912
+ flex-shrink: 0;
1913
+ transition: background 0.12s, color 0.12s;
1914
+ }
1915
+ .sts-dirtree-up:hover,
1916
+ .sts-dirtree-home:hover {
1917
+ background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.06));
1918
+ color: var(--dsw-alias-label-primary, #e6e6e6);
1919
+ }
1920
+ .sts-dirtree-up:disabled {
1921
+ opacity: 0.4;
1922
+ cursor: default;
1923
+ }
1924
+ .sts-dirtree-cur {
1925
+ flex: 1;
1926
+ font-size: 11px;
1927
+ color: var(--dsw-alias-label-tertiary, #888);
1928
+ overflow: hidden;
1929
+ text-overflow: ellipsis;
1930
+ white-space: nowrap;
1931
+ direction: rtl;
1932
+ text-align: left;
1933
+ font-family: ui-monospace, SFMono-Regular, "Cascadia Mono", "Cascadia Code", Menlo, Consolas, monospace;
1934
+ }
1935
+ .sts-tree-select-hint {
1936
+ margin-left: auto;
1937
+ font-size: 10px;
1938
+ color: rgba(255,255,255,0.4);
1939
+ }
1940
+ .sts-dirtree-sub {
1941
+ border-top: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
1942
+ margin-top: 2px;
1943
+ padding-top: 2px;
1944
+ }
1945
+
1946
+ /* ===== Env bulk paste ===== */
1947
+ .sts-env-actions {
1948
+ display: flex;
1949
+ gap: 6px;
1950
+ align-items: center;
1951
+ margin-top: 2px;
1952
+ }
1953
+ .sts-env-paste {
1954
+ margin-top: 8px;
1955
+ display: flex;
1956
+ flex-direction: column;
1957
+ gap: 6px;
1958
+ }
1959
+ .sts-env-paste-input {
1960
+ width: 100%;
1961
+ padding: 8px 10px;
1962
+ border: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
1963
+ border-radius: 6px;
1964
+ background: var(--dsw-alias-bg-layer-1, #1a1a1e);
1965
+ color: var(--dsw-alias-label-primary, #e6e6e6);
1966
+ font-size: 12px;
1967
+ font-family: ui-monospace, SFMono-Regular, "Cascadia Mono", "Cascadia Code", Menlo, Consolas, monospace;
1968
+ outline: none;
1969
+ resize: vertical;
1970
+ min-height: 80px;
1971
+ box-sizing: border-box;
1972
+ }
1973
+ .sts-env-paste-input:focus {
1974
+ border-color: var(--dsw-alias-border-l3, #3a3a3e);
1975
+ }
1976
+ .sts-env-paste-input::placeholder {
1977
+ color: rgba(255,255,255,0.3);
1978
+ font-family: ui-monospace, SFMono-Regular, "Cascadia Mono", "Cascadia Code", Menlo, Consolas, monospace;
1979
+ }
1980
+ .sts-env-paste-footer {
1981
+ display: flex;
1982
+ align-items: center;
1983
+ gap: 10px;
1984
+ }
1985
+ .sts-env-paste-apply {
1986
+ padding: 6px 14px;
1987
+ border: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
1988
+ border-radius: 6px;
1989
+ background: transparent;
1990
+ color: var(--dsw-alias-label-primary, #e6e6e6);
1991
+ cursor: pointer;
1992
+ font-size: 12px;
1993
+ transition: background 0.12s;
1994
+ }
1995
+ .sts-env-paste-apply:hover {
1996
+ background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.06));
1997
+ }
1998
+ .sts-env-paste-hint {
1999
+ font-size: 10px;
2000
+ color: rgba(255,255,255,0.4);
2001
+ }
2002
+ `;
2003
+ //#endregion
2004
+ //#region src/client/ServicesTab.tsx
2005
+ /**
2006
+ * Services tab: the main sidebar panel component. Left 1/3 config tree,
2007
+ * right 2/3 log view or config detail. Manages config CRUD via the host
2008
+ * REST API and tracks running instances.
2009
+ * @module dsh-better-sidebar-starter/client/ServicesTab
2010
+ */
2011
+ /** API base. */
2012
+ const API_BASE = "/api/dsh-better-sidebar-starter";
2013
+ /** Instance poll interval (ms). */
2014
+ const POLL_INTERVAL = 3e3;
2015
+ /** Inject styles once. */
2016
+ let STYLES_INJECTED = false;
2017
+ function injectStyles() {
2018
+ if (STYLES_INJECTED) return;
2019
+ STYLES_INJECTED = true;
2020
+ const tag = document.createElement("style");
2021
+ tag.id = "dsh-better-sidebar-starter-styles";
2022
+ tag.setAttribute("data-plugin", "@dong-victor/dsh-better-sidebar-starter");
2023
+ tag.textContent = STYLES_CSS;
2024
+ document.head.appendChild(tag);
2025
+ }
2026
+ /** The main services tab component. */
2027
+ function ServicesTab(props) {
2028
+ injectStyles();
2029
+ const { scope, store } = props;
2030
+ const [configs, setConfigs] = (0, react.useState)([]);
2031
+ const [instances, setInstances] = (0, react.useState)([]);
2032
+ const [selectedConfigId, setSelectedConfigId] = (0, react.useState)(null);
2033
+ const [selectedInstanceId, setSelectedInstanceId] = (0, react.useState)(null);
2034
+ const [editingConfig, setEditingConfig] = (0, react.useState)(null);
2035
+ const [showModal, setShowModal] = (0, react.useState)(false);
2036
+ const [splitRatio, setSplitRatio] = (0, react.useState)(.33);
2037
+ const [dragging, setDragging] = (0, react.useState)(false);
2038
+ const [isHorizontal, setIsHorizontal] = (0, react.useState)(true);
2039
+ const containerRef = (0, react.useRef)(null);
2040
+ (0, react.useEffect)(() => {
2041
+ const el = containerRef.current;
2042
+ if (el === null) return;
2043
+ const check = () => {
2044
+ const rect = el.getBoundingClientRect();
2045
+ setIsHorizontal(rect.width > rect.height * 1.2);
2046
+ };
2047
+ check();
2048
+ const ro = new ResizeObserver(check);
2049
+ ro.observe(el);
2050
+ return () => ro.disconnect();
2051
+ }, []);
2052
+ (0, react.useEffect)(() => {
2053
+ const blob = store.getPrefs()?.pluginSettings?.["dsh-better-sidebar-starter:services"] ?? {};
2054
+ if (typeof blob.splitRatio === "number") setSplitRatio(Math.max(.2, Math.min(.5, blob.splitRatio)));
2055
+ }, [store]);
2056
+ /** Persist split ratio to plugin settings. */
2057
+ const persistSplitRatio = (0, react.useCallback)((ratio) => {
2058
+ const service = store.service;
2059
+ if (service !== void 0 && typeof service.updateTab === "function") {}
2060
+ }, [store]);
2061
+ /** Build a query string for the session scope. */
2062
+ const sessionQuery = (0, react.useCallback)(() => {
2063
+ const params = new URLSearchParams({ sessionId: scope.sessionId });
2064
+ if (scope.cwd !== void 0) params.set("cwd", scope.cwd);
2065
+ return params.toString();
2066
+ }, [scope]);
2067
+ /** Fetch configs from the host. */
2068
+ const fetchConfigs = (0, react.useCallback)(async () => {
2069
+ try {
2070
+ const resp = await fetch(`${API_BASE}/configs?${sessionQuery()}`);
2071
+ if (resp.ok) {
2072
+ const data = await resp.json();
2073
+ setConfigs(data.configs);
2074
+ }
2075
+ } catch {}
2076
+ }, [sessionQuery]);
2077
+ /** Fetch instances from the host. */
2078
+ const fetchInstances = (0, react.useCallback)(async () => {
2079
+ try {
2080
+ const resp = await fetch(`${API_BASE}/instances?${sessionQuery()}`);
2081
+ if (resp.ok) {
2082
+ const data = await resp.json();
2083
+ setInstances(data.instances);
2084
+ }
2085
+ } catch {}
2086
+ }, [sessionQuery]);
2087
+ (0, react.useEffect)(() => {
2088
+ fetchConfigs();
2089
+ fetchInstances();
2090
+ }, [fetchConfigs, fetchInstances]);
2091
+ (0, react.useEffect)(() => {
2092
+ const timer = setInterval(() => void fetchInstances(), POLL_INTERVAL);
2093
+ return () => clearInterval(timer);
2094
+ }, [fetchInstances]);
2095
+ /** Start a config. */
2096
+ const handleStart = (0, react.useCallback)(async (configId) => {
2097
+ try {
2098
+ const blob = store.getPrefs()?.pluginSettings?.["dsh-better-sidebar-starter:services"] ?? {};
2099
+ const globalEnv = {};
2100
+ if (typeof blob.javaHome === "string" && blob.javaHome.trim() !== "") globalEnv.JAVA_HOME = blob.javaHome.trim();
2101
+ if (typeof blob.nodePath === "string" && blob.nodePath.trim() !== "") globalEnv.NODE_PATH = blob.nodePath.trim();
2102
+ if (typeof blob.pythonPath === "string" && blob.pythonPath.trim() !== "") globalEnv.PYTHON_PATH = blob.pythonPath.trim();
2103
+ if (typeof blob.mvnPath === "string" && blob.mvnPath.trim() !== "") globalEnv.MVN_PATH = blob.mvnPath.trim();
2104
+ const resp = await fetch(`${API_BASE}/run?${sessionQuery()}`, {
2105
+ method: "POST",
2106
+ headers: { "content-type": "application/json" },
2107
+ body: JSON.stringify({
2108
+ configId,
2109
+ globalEnv
2110
+ })
2111
+ });
2112
+ if (resp.ok) {
2113
+ const data = await resp.json();
2114
+ setSelectedInstanceId(data.instance.id);
2115
+ setSelectedConfigId(null);
2116
+ fetchInstances();
2117
+ fetchConfigs();
2118
+ }
2119
+ } catch {}
2120
+ }, [
2121
+ sessionQuery,
2122
+ fetchInstances,
2123
+ fetchConfigs,
2124
+ store
2125
+ ]);
2126
+ /** Stop an instance. */
2127
+ const handleStop = (0, react.useCallback)(async (instanceId) => {
2128
+ try {
2129
+ await fetch(`${API_BASE}/stop`, {
2130
+ method: "POST",
2131
+ headers: { "content-type": "application/json" },
2132
+ body: JSON.stringify({ instanceId })
2133
+ });
2134
+ fetchInstances();
2135
+ } catch {}
2136
+ }, [fetchInstances]);
2137
+ /** Restart = stop + start the same config. */
2138
+ const handleRestart = (0, react.useCallback)(async (instanceId) => {
2139
+ const inst = instances.find((i) => i.id === instanceId);
2140
+ if (inst === void 0) return;
2141
+ await handleStop(instanceId);
2142
+ await new Promise((r) => setTimeout(r, 200));
2143
+ await handleStart(inst.configId);
2144
+ }, [
2145
+ instances,
2146
+ handleStop,
2147
+ handleStart
2148
+ ]);
2149
+ /** Open the new config modal. */
2150
+ const handleNewConfig = () => {
2151
+ setEditingConfig(null);
2152
+ setShowModal(true);
2153
+ };
2154
+ /** Open the edit config modal. */
2155
+ const handleEditConfig = (configId) => {
2156
+ const config = configs.find((c) => c.id === configId);
2157
+ if (config === void 0) return;
2158
+ setEditingConfig({
2159
+ id: config.id,
2160
+ name: config.name,
2161
+ type: config.type,
2162
+ command: config.command,
2163
+ cwd: config.cwd,
2164
+ env: config.env,
2165
+ jvmArgs: config.jvmArgs,
2166
+ args: config.args,
2167
+ runtime: config.runtime
2168
+ });
2169
+ setShowModal(true);
2170
+ };
2171
+ /** Save a config (create or update). */
2172
+ const handleSaveConfig = (0, react.useCallback)(async (config) => {
2173
+ try {
2174
+ if ((await fetch(`${API_BASE}/configs?${sessionQuery()}`, {
2175
+ method: "POST",
2176
+ headers: { "content-type": "application/json" },
2177
+ body: JSON.stringify({ config })
2178
+ })).ok) {
2179
+ setShowModal(false);
2180
+ setEditingConfig(null);
2181
+ fetchConfigs();
2182
+ }
2183
+ } catch {}
2184
+ }, [sessionQuery, fetchConfigs]);
2185
+ /** Delete a config. */
2186
+ const handleDeleteConfig = (0, react.useCallback)(async (configId) => {
2187
+ for (const inst of instances) if (inst.configId === configId && inst.status === "running") await handleStop(inst.id);
2188
+ try {
2189
+ if ((await fetch(`${API_BASE}/configs?${sessionQuery()}&id=${encodeURIComponent(configId)}`, { method: "DELETE" })).ok) {
2190
+ if (selectedConfigId === configId) setSelectedConfigId(null);
2191
+ fetchConfigs();
2192
+ fetchInstances();
2193
+ }
2194
+ } catch {}
2195
+ }, [
2196
+ instances,
2197
+ handleStop,
2198
+ sessionQuery,
2199
+ fetchConfigs,
2200
+ fetchInstances,
2201
+ selectedConfigId
2202
+ ]);
2203
+ /** Select a config (deselect any instance). */
2204
+ const handleSelectConfig = (configId) => {
2205
+ setSelectedConfigId(configId);
2206
+ setSelectedInstanceId(null);
2207
+ };
2208
+ /** Select an instance. */
2209
+ const handleSelectInstance = (instanceId) => {
2210
+ setSelectedInstanceId(instanceId);
2211
+ setSelectedConfigId(null);
2212
+ };
2213
+ const handleMouseDown = (e) => {
2214
+ e.preventDefault();
2215
+ setDragging(true);
2216
+ };
2217
+ (0, react.useEffect)(() => {
2218
+ if (!dragging) return;
2219
+ const handleMove = (e) => {
2220
+ if (containerRef.current === null) return;
2221
+ const rect = containerRef.current.getBoundingClientRect();
2222
+ const ratio = isHorizontal ? (e.clientX - rect.left) / rect.width : (e.clientY - rect.top) / rect.height;
2223
+ const clamped = Math.max(.2, Math.min(.5, ratio));
2224
+ setSplitRatio(clamped);
2225
+ persistSplitRatio(clamped);
2226
+ };
2227
+ const handleUp = () => setDragging(false);
2228
+ document.addEventListener("mousemove", handleMove);
2229
+ document.addEventListener("mouseup", handleUp);
2230
+ return () => {
2231
+ document.removeEventListener("mousemove", handleMove);
2232
+ document.removeEventListener("mouseup", handleUp);
2233
+ };
2234
+ }, [
2235
+ dragging,
2236
+ isHorizontal,
2237
+ persistSplitRatio
2238
+ ]);
2239
+ const selectedInstance = selectedInstanceId !== null ? instances.find((i) => i.id === selectedInstanceId) ?? null : null;
2240
+ const selectedConfig = selectedConfigId !== null ? configs.find((c) => c.id === selectedConfigId) ?? null : null;
2241
+ return (0, react.createElement)("div", {
2242
+ ref: containerRef,
2243
+ className: "sts-root"
2244
+ }, (0, react.createElement)("div", { className: "sts-toolbar" }, (0, react.createElement)("div", { className: "sts-toolbar-title" }, (0, react.createElement)(RocketIcon, { size: 16 }), "服务"), (0, react.createElement)("button", {
2245
+ className: "sts-toolbar-btn",
2246
+ onClick: handleNewConfig
2247
+ }, (0, react.createElement)(PlusIcon, { size: 13 }), "新建配置")), (0, react.createElement)("div", { className: `sts-split${isHorizontal ? "" : " sts-split-vertical"}` }, (0, react.createElement)("div", {
2248
+ className: `sts-split-first${isHorizontal ? "" : " sts-split-first-v"}`,
2249
+ style: isHorizontal ? { width: `${splitRatio * 100}%` } : { height: `${splitRatio * 100}%` }
2250
+ }, configs.length === 0 ? (0, react.createElement)("div", { className: "sts-empty" }, "暂无运行配置,点击右上角 + 新建") : (0, react.createElement)(ConfigTree, {
2251
+ configs,
2252
+ instances,
2253
+ selectedInstanceId,
2254
+ selectedConfigId,
2255
+ onSelectConfig: handleSelectConfig,
2256
+ onSelectInstance: handleSelectInstance,
2257
+ onStartConfig: (id) => {
2258
+ handleStart(id);
2259
+ },
2260
+ onStopInstance: (id) => {
2261
+ handleStop(id);
2262
+ },
2263
+ onEditConfig: handleEditConfig
2264
+ })), (0, react.createElement)("div", {
2265
+ className: `sts-split-gutter${dragging ? " dragging" : ""}${isHorizontal ? "" : " sts-split-gutter-v"}`,
2266
+ onMouseDown: handleMouseDown
2267
+ }), (0, react.createElement)("div", { className: `sts-split-second${isHorizontal ? "" : " sts-split-second-v"}` }, selectedInstance !== null ? (0, react.createElement)(LogView, {
2268
+ instance: selectedInstance,
2269
+ onStop: () => {
2270
+ handleStop(selectedInstance.id);
2271
+ },
2272
+ onRestart: () => {
2273
+ handleRestart(selectedInstance.id);
2274
+ }
2275
+ }) : selectedConfig !== null ? (0, react.createElement)(ConfigDetail, {
2276
+ config: selectedConfig,
2277
+ onStart: () => {
2278
+ handleStart(selectedConfig.id);
2279
+ },
2280
+ onEdit: () => handleEditConfig(selectedConfig.id),
2281
+ onDelete: () => {
2282
+ handleDeleteConfig(selectedConfig.id);
2283
+ }
2284
+ }) : (0, react.createElement)("div", { className: "sts-empty" }, "选择左侧配置查看详情或日志"))), showModal ? (0, react.createElement)(ConfigEditorModal, {
2285
+ config: editingConfig,
2286
+ scope: {
2287
+ sessionId: scope.sessionId ?? store.getSnapshot().sessionId ?? "",
2288
+ cwd: scope.cwd
2289
+ },
2290
+ onSave: (c) => {
2291
+ handleSaveConfig(c);
2292
+ },
2293
+ onCancel: () => {
2294
+ setShowModal(false);
2295
+ setEditingConfig(null);
2296
+ }
2297
+ }) : null);
2298
+ }
2299
+ //#endregion
2300
+ //#region src/client/SettingsPanel.tsx
2301
+ /**
2302
+ * Custom settings panel for the "服务" tab: renders switch toggles plus
2303
+ * validated directory pickers for Java/Node/Python/Maven paths.
2304
+ * @module dsh-better-sidebar-starter/client/SettingsPanel
2305
+ */
2306
+ /** The custom settings panel rendered in the gear popup. */
2307
+ function SettingsPanel(props) {
2308
+ const { store, pluginSettings, updatePluginSetting } = props;
2309
+ const scope = { sessionId: store.getSnapshot().sessionId ?? "" };
2310
+ const get = (key) => typeof pluginSettings[key] === "string" ? pluginSettings[key] : "";
2311
+ const getBool = (key) => pluginSettings[key] === true;
2312
+ const [autoSelect, setAutoSelect] = (0, react.useState)(getBool("autoSelectOnStart"));
2313
+ const [autoScroll, setAutoScroll] = (0, react.useState)(getBool("autoScroll"));
2314
+ const [javaHome, setJavaHome] = (0, react.useState)(get("javaHome"));
2315
+ const [nodePath, setNodePath] = (0, react.useState)(get("nodePath"));
2316
+ const [pythonPath, setPythonPath] = (0, react.useState)(get("pythonPath"));
2317
+ const [mvnPath, setMvnPath] = (0, react.useState)(get("mvnPath"));
2318
+ (0, react.useEffect)(() => {
2319
+ if (!(get("javaHome") === "" && get("nodePath") === "" && get("pythonPath") === "" && get("mvnPath") === "")) return;
2320
+ let cancelled = false;
2321
+ fetch("/api/dsh-better-sidebar-starter/detect-env").then((r) => r.ok ? r.json() : null).then((detected) => {
2322
+ if (cancelled || detected === null) return;
2323
+ if (detected.javaHome !== void 0 && detected.javaHome !== "") {
2324
+ setJavaHome(detected.javaHome);
2325
+ updatePluginSetting("javaHome", detected.javaHome);
2326
+ }
2327
+ if (detected.nodePath !== void 0 && detected.nodePath !== "") {
2328
+ setNodePath(detected.nodePath);
2329
+ updatePluginSetting("nodePath", detected.nodePath);
2330
+ }
2331
+ if (detected.pythonPath !== void 0 && detected.pythonPath !== "") {
2332
+ setPythonPath(detected.pythonPath);
2333
+ updatePluginSetting("pythonPath", detected.pythonPath);
2334
+ }
2335
+ if (detected.mvnPath !== void 0 && detected.mvnPath !== "") {
2336
+ setMvnPath(detected.mvnPath);
2337
+ updatePluginSetting("mvnPath", detected.mvnPath);
2338
+ }
2339
+ }).catch(() => {});
2340
+ return () => {
2341
+ cancelled = true;
2342
+ };
2343
+ }, []);
2344
+ const handleSwitch = (key, setter) => (v) => {
2345
+ setter(v);
2346
+ updatePluginSetting(key, v);
2347
+ };
2348
+ const handlePath = (key, setter) => (v) => {
2349
+ setter(v);
2350
+ updatePluginSetting(key, v);
2351
+ };
2352
+ return (0, react.createElement)("div", { className: "sts-settings-panel" }, (0, react.createElement)("div", { className: "sts-settings-row" }, (0, react.createElement)("label", { className: "sts-settings-switch-row" }, (0, react.createElement)("input", {
2353
+ type: "checkbox",
2354
+ className: "sts-settings-switch",
2355
+ checked: autoSelect,
2356
+ onChange: (e) => handleSwitch("autoSelectOnStart", setAutoSelect)(e.target.checked)
2357
+ }), (0, react.createElement)("span", null, "启动后自动选中日志"))), (0, react.createElement)("div", { className: "sts-settings-row" }, (0, react.createElement)("label", { className: "sts-settings-switch-row" }, (0, react.createElement)("input", {
2358
+ type: "checkbox",
2359
+ className: "sts-settings-switch",
2360
+ checked: autoScroll,
2361
+ onChange: (e) => handleSwitch("autoScroll", setAutoScroll)(e.target.checked)
2362
+ }), (0, react.createElement)("span", null, "自动滚动到最新日志"))), (0, react.createElement)("div", { className: "sts-settings-sep" }, "运行环境路径"), (0, react.createElement)(PathPickerRow, {
2363
+ label: "Java 主目录 (JAVA_HOME)",
2364
+ scope,
2365
+ kind: "java",
2366
+ value: javaHome,
2367
+ onChange: handlePath("javaHome", setJavaHome),
2368
+ desc: "Spring Boot 用的 JDK,留空则自动查找"
2369
+ }), (0, react.createElement)(PathPickerRow, {
2370
+ label: "Node.js 路径",
2371
+ scope,
2372
+ kind: "node",
2373
+ value: nodePath,
2374
+ onChange: handlePath("nodePath", setNodePath),
2375
+ desc: "npm 用的 Node,留空则用系统默认"
2376
+ }), (0, react.createElement)(PathPickerRow, {
2377
+ label: "Python 路径",
2378
+ scope,
2379
+ kind: "python",
2380
+ value: pythonPath,
2381
+ onChange: handlePath("pythonPath", setPythonPath),
2382
+ desc: "Python 解释器,留空则用系统默认"
2383
+ }), (0, react.createElement)(PathPickerRow, {
2384
+ label: "Maven 路径 (MVN)",
2385
+ scope,
2386
+ kind: "mvn",
2387
+ value: mvnPath,
2388
+ onChange: handlePath("mvnPath", setMvnPath),
2389
+ desc: "springboot 用的 Maven,留空则自动查找"
2390
+ }));
2391
+ }
2392
+ /** A labeled path picker row for the settings panel. */
2393
+ function PathPickerRow(props) {
2394
+ return (0, react.createElement)("div", { className: "sts-settings-picker-row" }, (0, react.createElement)("label", { className: "sts-settings-label" }, props.label), (0, react.createElement)(PathPicker, {
2395
+ scope: props.scope,
2396
+ kind: props.kind,
2397
+ value: props.value,
2398
+ onChange: props.onChange,
2399
+ desc: props.desc
2400
+ }));
2401
+ }
2402
+ //#endregion
2403
+ //#region src/client/index.tsx
2404
+ /**
2405
+ * Client half of the dsh-better-sidebar-starter plugin: registers the
2406
+ * "服务" (Services) sidebar tab through the `ctx.betterSidebar` service.
2407
+ * The tab renders a split-pane panel: left config tree + right log/detail.
2408
+ * The host half (same package) owns the session-scoped config/process routes.
2409
+ * @module dsh-better-sidebar-starter/client
2410
+ */
2411
+ /** Services required before mounting: the betterSidebar registry (provided
2412
+ * by dsh-better-sidebar's own client half) and the session store. */
2413
+ const inject = ["betterSidebar", "sessions"];
2414
+ /** Plugin body. */
2415
+ function apply(ctx) {
2416
+ ctx.effect(() => ctx.betterSidebar.registerTab({
2417
+ id: "dsh-better-sidebar-starter:services",
2418
+ title: () => "服务",
2419
+ icon: (size) => (0, react.createElement)(RocketIcon, { size }),
2420
+ order: 50,
2421
+ single: true,
2422
+ settings: {
2423
+ pluginToggles: [
2424
+ {
2425
+ key: "autoSelectOnStart",
2426
+ type: "switch",
2427
+ title: "启动后自动选中日志"
2428
+ },
2429
+ {
2430
+ key: "autoScroll",
2431
+ type: "switch",
2432
+ title: "自动滚动到最新日志"
2433
+ },
2434
+ {
2435
+ key: "javaHome",
2436
+ type: "text",
2437
+ title: "Java 主目录 (JAVA_HOME)",
2438
+ desc: "Spring Boot 用的 JDK 路径,留空则自动查找"
2439
+ },
2440
+ {
2441
+ key: "nodePath",
2442
+ type: "text",
2443
+ title: "Node.js 路径",
2444
+ desc: "npm 命令用的 Node 路径,留空则用系统默认"
2445
+ },
2446
+ {
2447
+ key: "pythonPath",
2448
+ type: "text",
2449
+ title: "Python 路径",
2450
+ desc: "Python 命令用的解释器路径,留空则用系统默认"
2451
+ }
2452
+ ],
2453
+ render: (props) => (0, react.createElement)(SettingsPanel, props)
2454
+ },
2455
+ component: (props) => (0, react.createElement)(ServicesTab, props)
2456
+ }), "dsh-better-sidebar-starter: services tab");
2457
+ }
2458
+ //#endregion
2459
+ exports.apply = apply;
2460
+ exports.inject = inject;
2461
+ return module.exports;
2462
+ }
2463
+ });
2464
+
2465
+ //# sourceMappingURL=client.js.map