@dong-victor/dsh-better-sidebar-starter 0.1.0 → 0.1.2
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 +425 -24
- package/lib/client.js.map +1 -1
- package/lib/index.js +299 -6
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -100,6 +100,14 @@ window.__ModuleLoader__.load({
|
|
|
100
100
|
function ClipboardIcon({ size }) {
|
|
101
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
102
|
}
|
|
103
|
+
/** 📄 Copy — duplicate a run config. */
|
|
104
|
+
function CopyIcon({ size }) {
|
|
105
|
+
return svg(size, `<path d="M21 8c0-1.1-.9-2-2-2h-9c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h9c1.1 0 2-.9 2-2V8z" fill="currentColor"/><path d="M3 16V4c0-1.1.9-2 2-2h10v2H5v12H3z" fill="currentColor" opacity="0.6"/>`);
|
|
106
|
+
}
|
|
107
|
+
/** ✈️ Send — send selected text to the conversation composer. */
|
|
108
|
+
function SendToChatIcon({ size }) {
|
|
109
|
+
return svg(size, `<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" fill="currentColor"/>`);
|
|
110
|
+
}
|
|
103
111
|
/** Get the icon for a config type. */
|
|
104
112
|
function typeIcon(type, size) {
|
|
105
113
|
switch (type) {
|
|
@@ -141,7 +149,7 @@ window.__ModuleLoader__.load({
|
|
|
141
149
|
}
|
|
142
150
|
/** The config tree component. */
|
|
143
151
|
function ConfigTree(props) {
|
|
144
|
-
const { configs, instances, selectedInstanceId, selectedConfigId, onSelectConfig, onSelectInstance, onStartConfig, onStopInstance, onEditConfig } = props;
|
|
152
|
+
const { configs, instances, selectedInstanceId, selectedConfigId, onSelectConfig, onSelectInstance, onStartConfig, onStopInstance, onEditConfig, onDuplicateConfig } = props;
|
|
145
153
|
const [collapsed, setCollapsed] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
146
154
|
const toggleGroup = (type) => {
|
|
147
155
|
setCollapsed((prev) => {
|
|
@@ -201,7 +209,14 @@ window.__ModuleLoader__.load({
|
|
|
201
209
|
e.stopPropagation();
|
|
202
210
|
onEditConfig(config.id);
|
|
203
211
|
}
|
|
204
|
-
}, GearIcon({ size: 12 })))
|
|
212
|
+
}, GearIcon({ size: 12 })), (0, react.createElement)("button", {
|
|
213
|
+
className: "sts-icon-btn dup",
|
|
214
|
+
title: "复制(生成副本配置)",
|
|
215
|
+
onClick: (e) => {
|
|
216
|
+
e.stopPropagation();
|
|
217
|
+
onDuplicateConfig(config.id);
|
|
218
|
+
}
|
|
219
|
+
}, CopyIcon({ size: 12 }))))];
|
|
205
220
|
for (const inst of configInstances) {
|
|
206
221
|
const instSelected = selectedInstanceId === inst.id;
|
|
207
222
|
rows.push((0, react.createElement)("div", {
|
|
@@ -324,6 +339,221 @@ window.__ModuleLoader__.load({
|
|
|
324
339
|
return ANSI_COLORS[seg.bg] ?? null;
|
|
325
340
|
}
|
|
326
341
|
//#endregion
|
|
342
|
+
//#region src/client/logHighlight.ts
|
|
343
|
+
const C = {
|
|
344
|
+
ts: "#8f8f98",
|
|
345
|
+
thread: "#b48ead",
|
|
346
|
+
levelInfo: "#6ab04c",
|
|
347
|
+
levelDebug: "#7a9ce8",
|
|
348
|
+
levelWarn: "#e2b93d",
|
|
349
|
+
levelError: "#f0625f",
|
|
350
|
+
clazz: "#61afef",
|
|
351
|
+
exception: "#ff5f56",
|
|
352
|
+
frame: "#c678dd",
|
|
353
|
+
location: "#56b6c2"
|
|
354
|
+
};
|
|
355
|
+
/** Log level → color. */
|
|
356
|
+
function levelColor(level) {
|
|
357
|
+
switch (level) {
|
|
358
|
+
case "DEBUG":
|
|
359
|
+
case "TRACE": return C.levelDebug;
|
|
360
|
+
case "INFO": return C.levelInfo;
|
|
361
|
+
case "WARN": return C.levelWarn;
|
|
362
|
+
case "ERROR":
|
|
363
|
+
case "FATAL":
|
|
364
|
+
case "OFF": return C.levelError;
|
|
365
|
+
default: return C.levelInfo;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
/** One-pass tokenizer: timestamps, levels, threads, exceptions, stacks, classes. */
|
|
369
|
+
const TOKEN_RE = new RegExp([
|
|
370
|
+
"(?<ts>\\d{4}-\\d{2}-\\d{2}[T ]\\d{2}:\\d{2}:\\d{2}[.,]\\d{1,6}(?:Z|[+-]\\d{2}:?\\d{2})?)",
|
|
371
|
+
"|(?<level>\\b(?:DEBUG|INFO|WARN|ERROR|TRACE|FATAL|OFF)\\b)",
|
|
372
|
+
"|(?<thread>\\[[^\\]\\r\\n]+\\])",
|
|
373
|
+
"|(?<exception>\\bCaused by:|[A-Z]\\w*(?:Exception|Error)(?:\\b|:\\s))",
|
|
374
|
+
"|(?<frame>\\s+at\\s+(?<target>[\\w$./<>]+)(?:\\((?<loc>[^)]*)\\))?)",
|
|
375
|
+
"|(?<location>\\((?<lfile>[\\w$]+\\.(?:java|kt|groovy|scala)):(?<lline>\\d+)(?::\\d+)?\\))",
|
|
376
|
+
"|(?<call>(?<callCls>[A-Za-z_$][\\w$]*(?:\\.[A-Za-z_$][\\w$]*)+)\\.(?<callMethod>[A-Za-z_$][\\w$]*)\\s*\\()",
|
|
377
|
+
"|(?<clazz>[A-Za-z_$][\\w$]*(?:\\.[A-Za-z_$][\\w$]*)+\\b)"
|
|
378
|
+
].join(""), "g");
|
|
379
|
+
/** The last segment must look like a class name (starts uppercase); field
|
|
380
|
+
* chains such as System.out are skipped. */
|
|
381
|
+
function looksLikeClassName(fqcn) {
|
|
382
|
+
const last = fqcn.split(".").pop() ?? "";
|
|
383
|
+
return last !== "" && /^[A-Z]/.test(last);
|
|
384
|
+
}
|
|
385
|
+
/** Split a stack-frame target `pkg.Cls.method` into class + method. */
|
|
386
|
+
function parseFrameTarget(target) {
|
|
387
|
+
const dot = target.lastIndexOf(".");
|
|
388
|
+
if (dot > 0 && dot < target.length - 1) return {
|
|
389
|
+
className: target.slice(0, dot),
|
|
390
|
+
method: target.slice(dot + 1)
|
|
391
|
+
};
|
|
392
|
+
return {
|
|
393
|
+
className: target,
|
|
394
|
+
method: ""
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
/** Parse a `File.java:45` (or `File.java:45:12`) location string. */
|
|
398
|
+
function parseFrameLoc(loc) {
|
|
399
|
+
if (loc === void 0 || loc.trim() === "") return {};
|
|
400
|
+
const parts = loc.trim().split(":");
|
|
401
|
+
if (parts.length < 2) return {};
|
|
402
|
+
const line = Number(parts[1]);
|
|
403
|
+
return {
|
|
404
|
+
file: parts[0],
|
|
405
|
+
line: Number.isInteger(line) && line > 0 ? line : void 0
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Tokenize a single log line into highlighted segments.
|
|
410
|
+
* Returns one segment per token; the rest stays plain.
|
|
411
|
+
*/
|
|
412
|
+
function highlightLogText(text) {
|
|
413
|
+
const out = [];
|
|
414
|
+
let last = 0;
|
|
415
|
+
TOKEN_RE.lastIndex = 0;
|
|
416
|
+
let m;
|
|
417
|
+
while ((m = TOKEN_RE.exec(text)) !== null) {
|
|
418
|
+
if (m.index > last) out.push({ text: text.slice(last, m.index) });
|
|
419
|
+
const g = m.groups ?? {};
|
|
420
|
+
if (g.ts !== void 0) out.push({
|
|
421
|
+
text: m[0],
|
|
422
|
+
color: C.ts
|
|
423
|
+
});
|
|
424
|
+
else if (g.level !== void 0) out.push({
|
|
425
|
+
text: m[0],
|
|
426
|
+
color: levelColor(g.level),
|
|
427
|
+
bold: true
|
|
428
|
+
});
|
|
429
|
+
else if (g.thread !== void 0) out.push({
|
|
430
|
+
text: m[0],
|
|
431
|
+
color: C.thread
|
|
432
|
+
});
|
|
433
|
+
else if (g.exception !== void 0) out.push({
|
|
434
|
+
text: m[0],
|
|
435
|
+
color: C.exception,
|
|
436
|
+
bold: true
|
|
437
|
+
});
|
|
438
|
+
else if (g.frame !== void 0) {
|
|
439
|
+
const { className, method } = parseFrameTarget(g.target ?? "");
|
|
440
|
+
const { file, line } = parseFrameLoc(g.loc);
|
|
441
|
+
out.push({
|
|
442
|
+
text: m[0],
|
|
443
|
+
color: C.frame,
|
|
444
|
+
link: {
|
|
445
|
+
kind: "frame",
|
|
446
|
+
className,
|
|
447
|
+
method,
|
|
448
|
+
file,
|
|
449
|
+
line
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
} else if (g.location !== void 0) out.push({
|
|
453
|
+
text: m[0],
|
|
454
|
+
color: C.location,
|
|
455
|
+
link: {
|
|
456
|
+
kind: "location",
|
|
457
|
+
file: g.lfile,
|
|
458
|
+
line: Number(g.lline)
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
else if (g.call !== void 0) {
|
|
462
|
+
const className = g.callCls ?? "";
|
|
463
|
+
out.push({
|
|
464
|
+
text: m[0],
|
|
465
|
+
color: C.clazz,
|
|
466
|
+
link: looksLikeClassName(className) ? {
|
|
467
|
+
kind: "class",
|
|
468
|
+
className,
|
|
469
|
+
method: g.callMethod ?? ""
|
|
470
|
+
} : void 0
|
|
471
|
+
});
|
|
472
|
+
} else if (g.clazz !== void 0) {
|
|
473
|
+
const className = g.clazz;
|
|
474
|
+
out.push({
|
|
475
|
+
text: m[0],
|
|
476
|
+
color: C.clazz,
|
|
477
|
+
link: looksLikeClassName(className) ? {
|
|
478
|
+
kind: "class",
|
|
479
|
+
className
|
|
480
|
+
} : void 0
|
|
481
|
+
});
|
|
482
|
+
} else out.push({ text: m[0] });
|
|
483
|
+
last = m.index + m[0].length;
|
|
484
|
+
}
|
|
485
|
+
if (last < text.length) out.push({ text: text.slice(last) });
|
|
486
|
+
return out;
|
|
487
|
+
}
|
|
488
|
+
//#endregion
|
|
489
|
+
//#region src/client/conversationDraft.ts
|
|
490
|
+
/**
|
|
491
|
+
* Append `text` to the session's composer draft (space-separated, like the
|
|
492
|
+
* @-mentions). Returns false — and warns — when the conversation service
|
|
493
|
+
* or the session scope is unavailable.
|
|
494
|
+
*/
|
|
495
|
+
function appendToDraft(ctx, sessionId, text) {
|
|
496
|
+
try {
|
|
497
|
+
const actx = ctx.sessions?.scope(sessionId);
|
|
498
|
+
if (actx === void 0) return false;
|
|
499
|
+
const conversation = ctx.get.bind(ctx)("conversation");
|
|
500
|
+
if (conversation === void 0 || conversation === null) return false;
|
|
501
|
+
const input = conversation.input.for(actx);
|
|
502
|
+
if (input === void 0) return false;
|
|
503
|
+
const draft = input.state.getSnapshot().draft ?? "";
|
|
504
|
+
input.setDraft(draft.trim() === "" ? text : `${draft} ${text}`);
|
|
505
|
+
return true;
|
|
506
|
+
} catch (error) {
|
|
507
|
+
console.warn("[dsh-better-sidebar-starter] draft insert failed:", error);
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
//#endregion
|
|
512
|
+
//#region src/client/utils.ts
|
|
513
|
+
/**
|
|
514
|
+
* Shared utility functions for the starter plugin.
|
|
515
|
+
* @module dsh-better-sidebar-starter/client/utils
|
|
516
|
+
*/
|
|
517
|
+
let toastEl = null;
|
|
518
|
+
let toastTimer = null;
|
|
519
|
+
/** Show a small floating toast notification. */
|
|
520
|
+
function showToast(message, success) {
|
|
521
|
+
if (toastTimer !== null) {
|
|
522
|
+
clearTimeout(toastTimer);
|
|
523
|
+
toastTimer = null;
|
|
524
|
+
}
|
|
525
|
+
if (toastEl !== null) {
|
|
526
|
+
document.body.removeChild(toastEl);
|
|
527
|
+
toastEl = null;
|
|
528
|
+
}
|
|
529
|
+
const el = document.createElement("div");
|
|
530
|
+
el.textContent = message;
|
|
531
|
+
el.style.cssText = `
|
|
532
|
+
position: fixed;
|
|
533
|
+
top: 16px;
|
|
534
|
+
left: 50%;
|
|
535
|
+
transform: translateX(-50%);
|
|
536
|
+
padding: 8px 16px;
|
|
537
|
+
background: ${success ? "rgba(78,154,6,0.9)" : "rgba(235,87,87,0.9)"};
|
|
538
|
+
color: #fff;
|
|
539
|
+
font-size: 12px;
|
|
540
|
+
border-radius: 6px;
|
|
541
|
+
z-index: 999999;
|
|
542
|
+
box-shadow: 0 2px 12px rgba(0,0,0,0.4);
|
|
543
|
+
pointer-events: none;
|
|
544
|
+
font-family: ui-monospace, SFMono-Regular, "Cascadia Mono", "Cascadia Code", Menlo, Consolas, monospace;
|
|
545
|
+
`;
|
|
546
|
+
document.body.appendChild(el);
|
|
547
|
+
toastEl = el;
|
|
548
|
+
toastTimer = setTimeout(() => {
|
|
549
|
+
if (toastEl !== null) {
|
|
550
|
+
document.body.removeChild(toastEl);
|
|
551
|
+
toastEl = null;
|
|
552
|
+
}
|
|
553
|
+
toastTimer = null;
|
|
554
|
+
}, 2e3);
|
|
555
|
+
}
|
|
556
|
+
//#endregion
|
|
327
557
|
//#region src/client/LogView.tsx
|
|
328
558
|
/**
|
|
329
559
|
* Log view: the right pane showing real-time log output from a running
|
|
@@ -331,32 +561,126 @@ window.__ModuleLoader__.load({
|
|
|
331
561
|
* renders log lines with auto-scroll.
|
|
332
562
|
* @module dsh-better-sidebar-starter/client/LogView
|
|
333
563
|
*/
|
|
334
|
-
/**
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
564
|
+
/** API base. */
|
|
565
|
+
const API_BASE$1 = "/api/dsh-better-sidebar-starter";
|
|
566
|
+
/** Render ANSI-colored text segments, applying Java-log syntax highlighting
|
|
567
|
+
* to text the program did not color itself (ANSI-colored spans keep the
|
|
568
|
+
* program's own colors). Tokens with a source link become clickable. */
|
|
569
|
+
function renderAnsiLine(text, keyBase, onLinkClick) {
|
|
570
|
+
const segments = parseAnsi(text);
|
|
571
|
+
const nodes = [];
|
|
572
|
+
let hlCounter = 0;
|
|
573
|
+
for (let i = 0; i < segments.length; i++) {
|
|
574
|
+
const seg = segments[i];
|
|
338
575
|
const fg = segmentFgColor(seg);
|
|
339
576
|
const bg = segmentBgColor(seg);
|
|
340
|
-
|
|
341
|
-
if (
|
|
342
|
-
if (seg.
|
|
343
|
-
if (seg.
|
|
344
|
-
if (seg.
|
|
345
|
-
if (
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
style
|
|
349
|
-
|
|
350
|
-
|
|
577
|
+
const baseStyle = {};
|
|
578
|
+
if (seg.bold) baseStyle.fontWeight = "bold";
|
|
579
|
+
if (seg.dim) baseStyle.opacity = "0.6";
|
|
580
|
+
if (seg.italic) baseStyle.fontStyle = "italic";
|
|
581
|
+
if (seg.underline) baseStyle.textDecoration = "underline";
|
|
582
|
+
if (fg !== null || bg !== null) {
|
|
583
|
+
const style = { ...baseStyle };
|
|
584
|
+
if (fg !== null) style.color = fg;
|
|
585
|
+
if (bg !== null) style.backgroundColor = bg;
|
|
586
|
+
nodes.push((0, react.createElement)("span", {
|
|
587
|
+
key: `${keyBase}-${i}`,
|
|
588
|
+
style
|
|
589
|
+
}, seg.text));
|
|
590
|
+
} else {
|
|
591
|
+
const hl = highlightLogText(seg.text);
|
|
592
|
+
for (const h of hl) {
|
|
593
|
+
const style = { ...baseStyle };
|
|
594
|
+
if (h.color !== void 0) style.color = h.color;
|
|
595
|
+
if (h.bold === true) style.fontWeight = "bold";
|
|
596
|
+
nodes.push((0, react.createElement)("span", {
|
|
597
|
+
key: `${keyBase}-${i}-hl${hlCounter++}`,
|
|
598
|
+
className: h.link !== void 0 ? "sts-log-link" : void 0,
|
|
599
|
+
style,
|
|
600
|
+
onClick: h.link !== void 0 ? (e) => {
|
|
601
|
+
e.stopPropagation();
|
|
602
|
+
onLinkClick(h.link);
|
|
603
|
+
} : void 0
|
|
604
|
+
}, h.text));
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return nodes;
|
|
351
609
|
}
|
|
352
610
|
/** The log view component. */
|
|
353
|
-
function LogView({ instance, onStop, onRestart }) {
|
|
611
|
+
function LogView({ instance, ctx, sessionId, cwd, onStop, onRestart }) {
|
|
354
612
|
const [logLines, setLogLines] = (0, react.useState)([]);
|
|
355
613
|
const [autoScroll, setAutoScroll] = (0, react.useState)(true);
|
|
356
614
|
const [exitInfo, setExitInfo] = (0, react.useState)(null);
|
|
615
|
+
const [selLength, setSelLength] = (0, react.useState)(0);
|
|
357
616
|
const contentRef = (0, react.useRef)(null);
|
|
358
617
|
const wsRef = (0, react.useRef)(null);
|
|
359
618
|
const lineKeyCounter = (0, react.useRef)(0);
|
|
619
|
+
(0, react.useEffect)(() => {
|
|
620
|
+
const updateSel = () => {
|
|
621
|
+
const sel = window.getSelection();
|
|
622
|
+
const len = sel !== null && !sel.isCollapsed ? sel.toString().trim().length : 0;
|
|
623
|
+
setSelLength(len);
|
|
624
|
+
};
|
|
625
|
+
document.addEventListener("selectionchange", updateSel);
|
|
626
|
+
return () => document.removeEventListener("selectionchange", updateSel);
|
|
627
|
+
}, []);
|
|
628
|
+
/** Send the currently selected log text into the conversation composer. */
|
|
629
|
+
const handleSendSelection = () => {
|
|
630
|
+
const sel = window.getSelection();
|
|
631
|
+
const text = sel !== null && !sel.isCollapsed ? sel.toString().trim() : "";
|
|
632
|
+
if (text === "") {
|
|
633
|
+
showToast("请先在日志中选中文本", false);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
if (appendToDraft(ctx, sessionId, text)) showToast(`已将选中文本(${text.length} 字符)发送到对话`, true);
|
|
637
|
+
else showToast("发送失败:对话服务不可用", false);
|
|
638
|
+
};
|
|
639
|
+
/** Open the source file behind a highlighted class / frame / location token. */
|
|
640
|
+
const handleCodeClick = async (link) => {
|
|
641
|
+
const payload = { root: instance.cwd };
|
|
642
|
+
if (link.kind === "class" || link.kind === "frame") payload.className = link.className;
|
|
643
|
+
if (link.file !== void 0) payload.file = link.file;
|
|
644
|
+
if (link.method !== void 0 && link.method !== "<init>" && link.method !== "<clinit>") payload.method = link.method;
|
|
645
|
+
if (link.line !== void 0) payload.line = link.line;
|
|
646
|
+
try {
|
|
647
|
+
const params = new URLSearchParams({ sessionId });
|
|
648
|
+
if (cwd !== void 0 && cwd !== "") params.set("cwd", cwd);
|
|
649
|
+
const resp = await fetch(`${API_BASE$1}/locate-source?${params.toString()}`, {
|
|
650
|
+
method: "POST",
|
|
651
|
+
headers: { "content-type": "application/json" },
|
|
652
|
+
body: JSON.stringify(payload)
|
|
653
|
+
});
|
|
654
|
+
if (!resp.ok) {
|
|
655
|
+
let reason = "定位源码失败";
|
|
656
|
+
try {
|
|
657
|
+
const err = await resp.json();
|
|
658
|
+
if (typeof err.error === "string" && err.error !== "") reason = err.error;
|
|
659
|
+
} catch {}
|
|
660
|
+
showToast(reason, false);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
const data = await resp.json();
|
|
664
|
+
if (data.ok !== true || typeof data.path !== "string") {
|
|
665
|
+
showToast(data.reason ?? "未找到源文件", false);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
const base = data.path.split(/[\\/]/).pop() ?? data.path;
|
|
669
|
+
const line = typeof data.line === "number" ? data.line : void 0;
|
|
670
|
+
const title = line !== void 0 ? `${base} : ${line}` : base;
|
|
671
|
+
const betterSidebar = ctx.betterSidebar;
|
|
672
|
+
if (betterSidebar !== void 0) betterSidebar.openFile({
|
|
673
|
+
sessionId,
|
|
674
|
+
cwd
|
|
675
|
+
}, data.path, title);
|
|
676
|
+
try {
|
|
677
|
+
navigator.clipboard?.writeText(`${base}${line !== void 0 ? `:${line}` : ""}`);
|
|
678
|
+
} catch {}
|
|
679
|
+
showToast(line !== void 0 ? `已打开 ${base},定位到第 ${line} 行(已复制 ${base}:${line})` : `已打开 ${base}`, true);
|
|
680
|
+
} catch {
|
|
681
|
+
showToast("定位源码失败", false);
|
|
682
|
+
}
|
|
683
|
+
};
|
|
360
684
|
const lineBuffer = (0, react.useRef)({ current: [] });
|
|
361
685
|
/** Flush the buffer into a new log line. */
|
|
362
686
|
const flushLine = (0, react.useCallback)(() => {
|
|
@@ -438,15 +762,19 @@ window.__ModuleLoader__.load({
|
|
|
438
762
|
className: "sts-icon-btn stop",
|
|
439
763
|
title: "停止",
|
|
440
764
|
onClick: onStop
|
|
441
|
-
}, StopIcon({ size: 14 }), " 停止") : null, (0, react.createElement)("button", {
|
|
765
|
+
}, StopIcon({ size: 14 }), (0, react.createElement)("span", { className: "sts-icon-btn-label" }, "停止")) : null, (0, react.createElement)("button", {
|
|
442
766
|
className: "sts-icon-btn",
|
|
443
767
|
title: "重启",
|
|
444
768
|
onClick: onRestart
|
|
445
|
-
}, RestartIcon({ size: 14 }), " 重启"), (0, react.createElement)("button", {
|
|
769
|
+
}, RestartIcon({ size: 14 }), (0, react.createElement)("span", { className: "sts-icon-btn-label" }, "重启")), (0, react.createElement)("button", {
|
|
446
770
|
className: "sts-icon-btn",
|
|
447
771
|
title: "清空",
|
|
448
772
|
onClick: handleClear
|
|
449
|
-
}, TrashIcon({ size: 14 }), " 清空"), (0, react.createElement)("
|
|
773
|
+
}, TrashIcon({ size: 14 }), (0, react.createElement)("span", { className: "sts-icon-btn-label" }, "清空")), (0, react.createElement)("button", {
|
|
774
|
+
className: "sts-icon-btn send",
|
|
775
|
+
title: selLength > 0 ? `发送选中的 ${selLength} 字符到对话` : "发送选中的文本到对话",
|
|
776
|
+
onClick: handleSendSelection
|
|
777
|
+
}, SendToChatIcon({ size: 14 }), (0, react.createElement)("span", { className: "sts-icon-btn-label" }, selLength > 0 ? `发送选中(${selLength})` : "发送选中")), (0, react.createElement)("label", {
|
|
450
778
|
className: "sts-log-toolbar-label",
|
|
451
779
|
onClick: () => setAutoScroll((v) => !v)
|
|
452
780
|
}, autoScroll ? "☑" : "☐", " 自动滚动")), (0, react.createElement)("div", {
|
|
@@ -462,7 +790,9 @@ window.__ModuleLoader__.load({
|
|
|
462
790
|
return (0, react.createElement)("span", {
|
|
463
791
|
key: segKey,
|
|
464
792
|
className: entry.stream === "stderr" ? "sts-log-stderr" : ""
|
|
465
|
-
}, renderAnsiLine(entry.text, segKey)
|
|
793
|
+
}, renderAnsiLine(entry.text, segKey, (link) => {
|
|
794
|
+
handleCodeClick(link);
|
|
795
|
+
}));
|
|
466
796
|
}));
|
|
467
797
|
}), exitInfo !== null ? (0, react.createElement)("div", {
|
|
468
798
|
key: "exit-info",
|
|
@@ -487,7 +817,7 @@ window.__ModuleLoader__.load({
|
|
|
487
817
|
custom: "Custom"
|
|
488
818
|
};
|
|
489
819
|
/** The detail card component. */
|
|
490
|
-
function ConfigDetail({ config, onStart, onEdit, onDelete }) {
|
|
820
|
+
function ConfigDetail({ config, onStart, onEdit, onDuplicate, onDelete }) {
|
|
491
821
|
const envEntries = Object.entries(config.env);
|
|
492
822
|
const dateFmt = (ts) => {
|
|
493
823
|
if (ts === null) return "—";
|
|
@@ -500,6 +830,10 @@ window.__ModuleLoader__.load({
|
|
|
500
830
|
className: "sts-detail-btn danger",
|
|
501
831
|
onClick: onDelete
|
|
502
832
|
}, (0, react.createElement)(TrashIcon, { size: 14 }), " 删除"), (0, react.createElement)("button", {
|
|
833
|
+
className: "sts-detail-btn",
|
|
834
|
+
onClick: onDuplicate,
|
|
835
|
+
title: "复制为副本(保留全部信息)"
|
|
836
|
+
}, (0, react.createElement)(CopyIcon, { size: 14 }), " 复制"), (0, react.createElement)("button", {
|
|
503
837
|
className: "sts-detail-btn",
|
|
504
838
|
onClick: onEdit
|
|
505
839
|
}, (0, react.createElement)(GearIcon, { size: 14 }), " 编辑"), (0, react.createElement)("button", {
|
|
@@ -1324,6 +1658,30 @@ window.__ModuleLoader__.load({
|
|
|
1324
1658
|
.sts-icon-btn.stop { color: #eb5757; }
|
|
1325
1659
|
.sts-icon-btn.gear { color: var(--dsw-alias-label-secondary, #b0b0b0); }
|
|
1326
1660
|
.sts-icon-btn.trash { color: #eb5757; }
|
|
1661
|
+
.sts-icon-btn.dup { color: var(--dsw-alias-label-secondary, #b0b0b0); }
|
|
1662
|
+
.sts-icon-btn.send { color: #4a90d9; }
|
|
1663
|
+
/* Toolbar buttons (log view) are icon-only; the label expands on hover. */
|
|
1664
|
+
.sts-log-toolbar .sts-icon-btn {
|
|
1665
|
+
width: 26px;
|
|
1666
|
+
height: 26px;
|
|
1667
|
+
padding: 0 6px;
|
|
1668
|
+
gap: 0;
|
|
1669
|
+
font-size: 11px;
|
|
1670
|
+
opacity: 1;
|
|
1671
|
+
}
|
|
1672
|
+
.sts-log-toolbar .sts-icon-btn .sts-icon-btn-label {
|
|
1673
|
+
display: inline-block;
|
|
1674
|
+
max-width: 0;
|
|
1675
|
+
overflow: hidden;
|
|
1676
|
+
white-space: nowrap;
|
|
1677
|
+
opacity: 0;
|
|
1678
|
+
transition: max-width 0.18s ease, opacity 0.18s ease, margin-left 0.18s ease;
|
|
1679
|
+
}
|
|
1680
|
+
.sts-log-toolbar .sts-icon-btn:hover .sts-icon-btn-label {
|
|
1681
|
+
max-width: 120px;
|
|
1682
|
+
margin-left: 4px;
|
|
1683
|
+
opacity: 1;
|
|
1684
|
+
}
|
|
1327
1685
|
|
|
1328
1686
|
/* ===== Instance row ===== */
|
|
1329
1687
|
.sts-instance-row {
|
|
@@ -1411,6 +1769,16 @@ window.__ModuleLoader__.load({
|
|
|
1411
1769
|
.sts-log-line {
|
|
1412
1770
|
min-height: 1em;
|
|
1413
1771
|
}
|
|
1772
|
+
/* Clickable source tokens (classes / stack frames) inside the log. */
|
|
1773
|
+
.sts-log-link {
|
|
1774
|
+
cursor: pointer;
|
|
1775
|
+
border-bottom: 1px dashed rgba(97, 175, 239, 0.4);
|
|
1776
|
+
transition: background 0.1s ease;
|
|
1777
|
+
}
|
|
1778
|
+
.sts-log-link:hover {
|
|
1779
|
+
background: rgba(97, 175, 239, 0.14);
|
|
1780
|
+
border-bottom-color: #61afef;
|
|
1781
|
+
}
|
|
1414
1782
|
|
|
1415
1783
|
/* ===== Config detail ===== */
|
|
1416
1784
|
.sts-detail {
|
|
@@ -2168,6 +2536,34 @@ window.__ModuleLoader__.load({
|
|
|
2168
2536
|
});
|
|
2169
2537
|
setShowModal(true);
|
|
2170
2538
|
};
|
|
2539
|
+
/**
|
|
2540
|
+
* Duplicate a config: open the editor prefilled with ALL of the original
|
|
2541
|
+
* config's info under an auto-generated "xxx-副本" name (deduped against
|
|
2542
|
+
* existing names). Saving it (id omitted) creates a brand-new config.
|
|
2543
|
+
*/
|
|
2544
|
+
const handleDuplicateConfig = (configId) => {
|
|
2545
|
+
const config = configs.find((c) => c.id === configId);
|
|
2546
|
+
if (config === void 0) return;
|
|
2547
|
+
const stem = config.name.endsWith("-副本") ? config.name.slice(0, -3) : config.name;
|
|
2548
|
+
const taken = new Set(configs.map((c) => c.name));
|
|
2549
|
+
let dupName = `${stem}-副本`;
|
|
2550
|
+
let suffix = 2;
|
|
2551
|
+
while (taken.has(dupName)) {
|
|
2552
|
+
dupName = `${stem}-副本${suffix}`;
|
|
2553
|
+
suffix++;
|
|
2554
|
+
}
|
|
2555
|
+
setEditingConfig({
|
|
2556
|
+
name: dupName,
|
|
2557
|
+
type: config.type,
|
|
2558
|
+
command: config.command,
|
|
2559
|
+
cwd: config.cwd,
|
|
2560
|
+
env: config.env,
|
|
2561
|
+
jvmArgs: config.jvmArgs,
|
|
2562
|
+
args: config.args,
|
|
2563
|
+
runtime: config.runtime
|
|
2564
|
+
});
|
|
2565
|
+
setShowModal(true);
|
|
2566
|
+
};
|
|
2171
2567
|
/** Save a config (create or update). */
|
|
2172
2568
|
const handleSaveConfig = (0, react.useCallback)(async (config) => {
|
|
2173
2569
|
try {
|
|
@@ -2260,12 +2656,16 @@ window.__ModuleLoader__.load({
|
|
|
2260
2656
|
onStopInstance: (id) => {
|
|
2261
2657
|
handleStop(id);
|
|
2262
2658
|
},
|
|
2263
|
-
onEditConfig: handleEditConfig
|
|
2659
|
+
onEditConfig: handleEditConfig,
|
|
2660
|
+
onDuplicateConfig: handleDuplicateConfig
|
|
2264
2661
|
})), (0, react.createElement)("div", {
|
|
2265
2662
|
className: `sts-split-gutter${dragging ? " dragging" : ""}${isHorizontal ? "" : " sts-split-gutter-v"}`,
|
|
2266
2663
|
onMouseDown: handleMouseDown
|
|
2267
2664
|
}), (0, react.createElement)("div", { className: `sts-split-second${isHorizontal ? "" : " sts-split-second-v"}` }, selectedInstance !== null ? (0, react.createElement)(LogView, {
|
|
2268
2665
|
instance: selectedInstance,
|
|
2666
|
+
ctx: props.ctx,
|
|
2667
|
+
sessionId: scope.sessionId ?? store.getSnapshot().sessionId ?? "",
|
|
2668
|
+
cwd: scope.cwd,
|
|
2269
2669
|
onStop: () => {
|
|
2270
2670
|
handleStop(selectedInstance.id);
|
|
2271
2671
|
},
|
|
@@ -2278,6 +2678,7 @@ window.__ModuleLoader__.load({
|
|
|
2278
2678
|
handleStart(selectedConfig.id);
|
|
2279
2679
|
},
|
|
2280
2680
|
onEdit: () => handleEditConfig(selectedConfig.id),
|
|
2681
|
+
onDuplicate: () => handleDuplicateConfig(selectedConfig.id),
|
|
2281
2682
|
onDelete: () => {
|
|
2282
2683
|
handleDeleteConfig(selectedConfig.id);
|
|
2283
2684
|
}
|