@dong-victor/dsh-better-sidebar-starter 0.1.1 → 0.1.3
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 +528 -27
- package/lib/client.js.map +1 -1
- package/lib/index.js +1946 -15
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -56,6 +56,18 @@ window.__ModuleLoader__.load({
|
|
|
56
56
|
function CloseIcon({ size }) {
|
|
57
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
58
|
}
|
|
59
|
+
/** ◀ Back — history file list. */
|
|
60
|
+
function BackIcon({ size }) {
|
|
61
|
+
return svg(size, `<path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z" fill="currentColor"/>`);
|
|
62
|
+
}
|
|
63
|
+
/** 📄 File — history log entry. */
|
|
64
|
+
function FileIcon({ size }) {
|
|
65
|
+
return svg(size, `<path d="M6 2c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6H6zm7 7V3.5L18.5 9H13z" fill="currentColor" opacity="0.9"/>`);
|
|
66
|
+
}
|
|
67
|
+
/** ⟳ Refresh — reload file list. */
|
|
68
|
+
function RefreshIcon({ size }) {
|
|
69
|
+
return svg(size, `<path d="M17.65 6.35A7.95 7.95 0 0 0 12 4a8 8 0 1 0 7.73 10h-2.08A6 6 0 1 1 12 6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z" fill="currentColor"/>`);
|
|
70
|
+
}
|
|
59
71
|
/** ▾ Chevron down — tree expand. */
|
|
60
72
|
function ChevronDownIcon({ size }) {
|
|
61
73
|
return svg(size, `<path d="M7 10l5 5 5-5z" fill="currentColor"/>`);
|
|
@@ -339,6 +351,153 @@ window.__ModuleLoader__.load({
|
|
|
339
351
|
return ANSI_COLORS[seg.bg] ?? null;
|
|
340
352
|
}
|
|
341
353
|
//#endregion
|
|
354
|
+
//#region src/client/logHighlight.ts
|
|
355
|
+
const C = {
|
|
356
|
+
ts: "#8f8f98",
|
|
357
|
+
thread: "#b48ead",
|
|
358
|
+
levelInfo: "#6ab04c",
|
|
359
|
+
levelDebug: "#7a9ce8",
|
|
360
|
+
levelWarn: "#e2b93d",
|
|
361
|
+
levelError: "#f0625f",
|
|
362
|
+
clazz: "#61afef",
|
|
363
|
+
exception: "#ff5f56",
|
|
364
|
+
frame: "#c678dd",
|
|
365
|
+
location: "#56b6c2"
|
|
366
|
+
};
|
|
367
|
+
/** Log level → color. */
|
|
368
|
+
function levelColor(level) {
|
|
369
|
+
switch (level) {
|
|
370
|
+
case "DEBUG":
|
|
371
|
+
case "TRACE": return C.levelDebug;
|
|
372
|
+
case "INFO": return C.levelInfo;
|
|
373
|
+
case "WARN": return C.levelWarn;
|
|
374
|
+
case "ERROR":
|
|
375
|
+
case "FATAL":
|
|
376
|
+
case "OFF": return C.levelError;
|
|
377
|
+
default: return C.levelInfo;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
/** One-pass tokenizer: timestamps, levels, threads, exceptions, stacks, classes. */
|
|
381
|
+
const TOKEN_RE = new RegExp([
|
|
382
|
+
"(?<ts>\\d{4}-\\d{2}-\\d{2}[T ]\\d{2}:\\d{2}:\\d{2}[.,]\\d{1,6}(?:Z|[+-]\\d{2}:?\\d{2})?)",
|
|
383
|
+
"|(?<level>\\b(?:DEBUG|INFO|WARN|ERROR|TRACE|FATAL|OFF)\\b)",
|
|
384
|
+
"|(?<thread>\\[[^\\]\\r\\n]+\\])",
|
|
385
|
+
"|(?<exception>\\bCaused by:|[A-Z]\\w*(?:Exception|Error)(?:\\b|:\\s))",
|
|
386
|
+
"|(?<frame>\\s+at\\s+(?<target>[\\w$./<>]+)(?:\\((?<loc>[^)]*)\\))?)",
|
|
387
|
+
"|(?<location>\\((?<lfile>[\\w$]+\\.(?:java|kt|groovy|scala)):(?<lline>\\d+)(?::\\d+)?\\))",
|
|
388
|
+
"|(?<call>(?<callCls>[A-Za-z_$][\\w$]*(?:\\.[A-Za-z_$][\\w$]*)+)\\.(?<callMethod>[A-Za-z_$][\\w$]*)\\s*\\()",
|
|
389
|
+
"|(?<clazz>[A-Za-z_$][\\w$]*(?:\\.[A-Za-z_$][\\w$]*)+\\b)"
|
|
390
|
+
].join(""), "g");
|
|
391
|
+
/** The last segment must look like a class name (starts uppercase); field
|
|
392
|
+
* chains such as System.out are skipped. */
|
|
393
|
+
function looksLikeClassName(fqcn) {
|
|
394
|
+
const last = fqcn.split(".").pop() ?? "";
|
|
395
|
+
return last !== "" && /^[A-Z]/.test(last);
|
|
396
|
+
}
|
|
397
|
+
/** Split a stack-frame target `pkg.Cls.method` into class + method. */
|
|
398
|
+
function parseFrameTarget(target) {
|
|
399
|
+
const dot = target.lastIndexOf(".");
|
|
400
|
+
if (dot > 0 && dot < target.length - 1) return {
|
|
401
|
+
className: target.slice(0, dot),
|
|
402
|
+
method: target.slice(dot + 1)
|
|
403
|
+
};
|
|
404
|
+
return {
|
|
405
|
+
className: target,
|
|
406
|
+
method: ""
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
/** Parse a `File.java:45` (or `File.java:45:12`) location string. */
|
|
410
|
+
function parseFrameLoc(loc) {
|
|
411
|
+
if (loc === void 0 || loc.trim() === "") return {};
|
|
412
|
+
const parts = loc.trim().split(":");
|
|
413
|
+
if (parts.length < 2) return {};
|
|
414
|
+
const line = Number(parts[1]);
|
|
415
|
+
return {
|
|
416
|
+
file: parts[0],
|
|
417
|
+
line: Number.isInteger(line) && line > 0 ? line : void 0
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Tokenize a single log line into highlighted segments.
|
|
422
|
+
* Returns one segment per token; the rest stays plain.
|
|
423
|
+
*/
|
|
424
|
+
function highlightLogText(text) {
|
|
425
|
+
const out = [];
|
|
426
|
+
let last = 0;
|
|
427
|
+
TOKEN_RE.lastIndex = 0;
|
|
428
|
+
let m;
|
|
429
|
+
while ((m = TOKEN_RE.exec(text)) !== null) {
|
|
430
|
+
if (m.index > last) out.push({ text: text.slice(last, m.index) });
|
|
431
|
+
const g = m.groups ?? {};
|
|
432
|
+
if (g.ts !== void 0) out.push({
|
|
433
|
+
text: m[0],
|
|
434
|
+
color: C.ts
|
|
435
|
+
});
|
|
436
|
+
else if (g.level !== void 0) out.push({
|
|
437
|
+
text: m[0],
|
|
438
|
+
color: levelColor(g.level),
|
|
439
|
+
bold: true
|
|
440
|
+
});
|
|
441
|
+
else if (g.thread !== void 0) out.push({
|
|
442
|
+
text: m[0],
|
|
443
|
+
color: C.thread
|
|
444
|
+
});
|
|
445
|
+
else if (g.exception !== void 0) out.push({
|
|
446
|
+
text: m[0],
|
|
447
|
+
color: C.exception,
|
|
448
|
+
bold: true
|
|
449
|
+
});
|
|
450
|
+
else if (g.frame !== void 0) {
|
|
451
|
+
const { className, method } = parseFrameTarget(g.target ?? "");
|
|
452
|
+
const { file, line } = parseFrameLoc(g.loc);
|
|
453
|
+
out.push({
|
|
454
|
+
text: m[0],
|
|
455
|
+
color: C.frame,
|
|
456
|
+
link: {
|
|
457
|
+
kind: "frame",
|
|
458
|
+
className,
|
|
459
|
+
method,
|
|
460
|
+
file,
|
|
461
|
+
line
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
} else if (g.location !== void 0) out.push({
|
|
465
|
+
text: m[0],
|
|
466
|
+
color: C.location,
|
|
467
|
+
link: {
|
|
468
|
+
kind: "location",
|
|
469
|
+
file: g.lfile,
|
|
470
|
+
line: Number(g.lline)
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
else if (g.call !== void 0) {
|
|
474
|
+
const className = g.callCls ?? "";
|
|
475
|
+
out.push({
|
|
476
|
+
text: m[0],
|
|
477
|
+
color: C.clazz,
|
|
478
|
+
link: looksLikeClassName(className) ? {
|
|
479
|
+
kind: "class",
|
|
480
|
+
className,
|
|
481
|
+
method: g.callMethod ?? ""
|
|
482
|
+
} : void 0
|
|
483
|
+
});
|
|
484
|
+
} else if (g.clazz !== void 0) {
|
|
485
|
+
const className = g.clazz;
|
|
486
|
+
out.push({
|
|
487
|
+
text: m[0],
|
|
488
|
+
color: C.clazz,
|
|
489
|
+
link: looksLikeClassName(className) ? {
|
|
490
|
+
kind: "class",
|
|
491
|
+
className
|
|
492
|
+
} : void 0
|
|
493
|
+
});
|
|
494
|
+
} else out.push({ text: m[0] });
|
|
495
|
+
last = m.index + m[0].length;
|
|
496
|
+
}
|
|
497
|
+
if (last < text.length) out.push({ text: text.slice(last) });
|
|
498
|
+
return out;
|
|
499
|
+
}
|
|
500
|
+
//#endregion
|
|
342
501
|
//#region src/client/conversationDraft.ts
|
|
343
502
|
/**
|
|
344
503
|
* Append `text` to the session's composer draft (space-separated, like the
|
|
@@ -414,26 +573,104 @@ window.__ModuleLoader__.load({
|
|
|
414
573
|
* renders log lines with auto-scroll.
|
|
415
574
|
* @module dsh-better-sidebar-starter/client/LogView
|
|
416
575
|
*/
|
|
417
|
-
/**
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
576
|
+
/** API base. */
|
|
577
|
+
const API_BASE$2 = "/api/dsh-better-sidebar-starter";
|
|
578
|
+
/** Open the source behind a log token via the locate-source API. Shared by the
|
|
579
|
+
* live LogView and the persisted LogHistoryView. `root` is the locating root
|
|
580
|
+
* (live instance cwd, or the session workspace for history logs). */
|
|
581
|
+
async function handleLogCodeClick(ctx, sessionId, cwd, root, link) {
|
|
582
|
+
const payload = {};
|
|
583
|
+
if (root !== void 0 && root !== "") payload.root = root;
|
|
584
|
+
if (link.kind === "class" || link.kind === "frame") payload.className = link.className;
|
|
585
|
+
if (link.file !== void 0) payload.file = link.file;
|
|
586
|
+
if (link.method !== void 0 && link.method !== "<init>" && link.method !== "<clinit>") payload.method = link.method;
|
|
587
|
+
if (link.line !== void 0) payload.line = link.line;
|
|
588
|
+
try {
|
|
589
|
+
const params = new URLSearchParams({ sessionId });
|
|
590
|
+
if (cwd !== void 0 && cwd !== "") params.set("cwd", cwd);
|
|
591
|
+
const resp = await fetch(`${API_BASE$2}/locate-source?${params.toString()}`, {
|
|
592
|
+
method: "POST",
|
|
593
|
+
headers: { "content-type": "application/json" },
|
|
594
|
+
body: JSON.stringify(payload)
|
|
595
|
+
});
|
|
596
|
+
if (!resp.ok) {
|
|
597
|
+
let reason = "定位源码失败";
|
|
598
|
+
try {
|
|
599
|
+
const err = await resp.json();
|
|
600
|
+
if (typeof err.error === "string" && err.error !== "") reason = err.error;
|
|
601
|
+
} catch {}
|
|
602
|
+
showToast(reason, false);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const data = await resp.json();
|
|
606
|
+
if (data.ok !== true || typeof data.path !== "string") {
|
|
607
|
+
showToast(data.reason ?? "未找到源文件", false);
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
const base = data.path.split(/[\\/]/).pop() ?? data.path;
|
|
611
|
+
const line = typeof data.line === "number" ? data.line : void 0;
|
|
612
|
+
const decompiled = data.decompiled === true;
|
|
613
|
+
const badge = decompiled ? " [反编译]" : "";
|
|
614
|
+
const title = line !== void 0 ? `${base} : ${line}${badge}` : `${base}${badge}`;
|
|
615
|
+
const betterSidebar = ctx.betterSidebar;
|
|
616
|
+
if (betterSidebar !== void 0) betterSidebar.openFile({
|
|
617
|
+
sessionId,
|
|
618
|
+
cwd
|
|
619
|
+
}, data.path, title);
|
|
620
|
+
try {
|
|
621
|
+
navigator.clipboard?.writeText(`${base}${line !== void 0 ? `:${line}` : ""}`);
|
|
622
|
+
} catch {}
|
|
623
|
+
showToast(line !== void 0 ? `已打开 ${base},定位到第 ${line} 行(已复制 ${base}:${line})${decompiled ? "(反编译产物)" : ""}` : decompiled ? `已打开反编译产物 ${base}` : `已打开 ${base}`, true);
|
|
624
|
+
} catch {
|
|
625
|
+
showToast("定位源码失败", false);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
/** Render ANSI-colored text segments, applying Java-log syntax highlighting
|
|
629
|
+
* to text the program did not color itself (ANSI-colored spans keep the
|
|
630
|
+
* program's own colors). Tokens with a source link become clickable. */
|
|
631
|
+
function renderAnsiLine(text, keyBase, onLinkClick) {
|
|
632
|
+
const segments = parseAnsi(text);
|
|
633
|
+
const nodes = [];
|
|
634
|
+
let hlCounter = 0;
|
|
635
|
+
for (let i = 0; i < segments.length; i++) {
|
|
636
|
+
const seg = segments[i];
|
|
421
637
|
const fg = segmentFgColor(seg);
|
|
422
638
|
const bg = segmentBgColor(seg);
|
|
423
|
-
|
|
424
|
-
if (
|
|
425
|
-
if (seg.
|
|
426
|
-
if (seg.
|
|
427
|
-
if (seg.
|
|
428
|
-
if (
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
style
|
|
432
|
-
|
|
433
|
-
|
|
639
|
+
const baseStyle = {};
|
|
640
|
+
if (seg.bold) baseStyle.fontWeight = "bold";
|
|
641
|
+
if (seg.dim) baseStyle.opacity = "0.6";
|
|
642
|
+
if (seg.italic) baseStyle.fontStyle = "italic";
|
|
643
|
+
if (seg.underline) baseStyle.textDecoration = "underline";
|
|
644
|
+
if (fg !== null || bg !== null) {
|
|
645
|
+
const style = { ...baseStyle };
|
|
646
|
+
if (fg !== null) style.color = fg;
|
|
647
|
+
if (bg !== null) style.backgroundColor = bg;
|
|
648
|
+
nodes.push((0, react.createElement)("span", {
|
|
649
|
+
key: `${keyBase}-${i}`,
|
|
650
|
+
style
|
|
651
|
+
}, seg.text));
|
|
652
|
+
} else {
|
|
653
|
+
const hl = highlightLogText(seg.text);
|
|
654
|
+
for (const h of hl) {
|
|
655
|
+
const style = { ...baseStyle };
|
|
656
|
+
if (h.color !== void 0) style.color = h.color;
|
|
657
|
+
if (h.bold === true) style.fontWeight = "bold";
|
|
658
|
+
nodes.push((0, react.createElement)("span", {
|
|
659
|
+
key: `${keyBase}-${i}-hl${hlCounter++}`,
|
|
660
|
+
className: h.link !== void 0 ? "sts-log-link" : void 0,
|
|
661
|
+
style,
|
|
662
|
+
onClick: h.link !== void 0 ? (e) => {
|
|
663
|
+
e.stopPropagation();
|
|
664
|
+
onLinkClick(h.link);
|
|
665
|
+
} : void 0
|
|
666
|
+
}, h.text));
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return nodes;
|
|
434
671
|
}
|
|
435
672
|
/** The log view component. */
|
|
436
|
-
function LogView({ instance, ctx, sessionId, onStop, onRestart }) {
|
|
673
|
+
function LogView({ instance, ctx, sessionId, cwd, onStop, onRestart }) {
|
|
437
674
|
const [logLines, setLogLines] = (0, react.useState)([]);
|
|
438
675
|
const [autoScroll, setAutoScroll] = (0, react.useState)(true);
|
|
439
676
|
const [exitInfo, setExitInfo] = (0, react.useState)(null);
|
|
@@ -542,19 +779,19 @@ window.__ModuleLoader__.load({
|
|
|
542
779
|
className: "sts-icon-btn stop",
|
|
543
780
|
title: "停止",
|
|
544
781
|
onClick: onStop
|
|
545
|
-
}, StopIcon({ size: 14 }), " 停止") : null, (0, react.createElement)("button", {
|
|
782
|
+
}, StopIcon({ size: 14 }), (0, react.createElement)("span", { className: "sts-icon-btn-label" }, "停止")) : null, (0, react.createElement)("button", {
|
|
546
783
|
className: "sts-icon-btn",
|
|
547
784
|
title: "重启",
|
|
548
785
|
onClick: onRestart
|
|
549
|
-
}, RestartIcon({ size: 14 }), " 重启"), (0, react.createElement)("button", {
|
|
786
|
+
}, RestartIcon({ size: 14 }), (0, react.createElement)("span", { className: "sts-icon-btn-label" }, "重启")), (0, react.createElement)("button", {
|
|
550
787
|
className: "sts-icon-btn",
|
|
551
788
|
title: "清空",
|
|
552
789
|
onClick: handleClear
|
|
553
|
-
}, TrashIcon({ size: 14 }), " 清空"), (0, react.createElement)("button", {
|
|
790
|
+
}, TrashIcon({ size: 14 }), (0, react.createElement)("span", { className: "sts-icon-btn-label" }, "清空")), (0, react.createElement)("button", {
|
|
554
791
|
className: "sts-icon-btn send",
|
|
555
792
|
title: selLength > 0 ? `发送选中的 ${selLength} 字符到对话` : "发送选中的文本到对话",
|
|
556
793
|
onClick: handleSendSelection
|
|
557
|
-
}, SendToChatIcon({ size: 14 }), selLength > 0 ?
|
|
794
|
+
}, SendToChatIcon({ size: 14 }), (0, react.createElement)("span", { className: "sts-icon-btn-label" }, selLength > 0 ? `发送选中(${selLength})` : "发送选中")), (0, react.createElement)("label", {
|
|
558
795
|
className: "sts-log-toolbar-label",
|
|
559
796
|
onClick: () => setAutoScroll((v) => !v)
|
|
560
797
|
}, autoScroll ? "☑" : "☐", " 自动滚动")), (0, react.createElement)("div", {
|
|
@@ -570,7 +807,9 @@ window.__ModuleLoader__.load({
|
|
|
570
807
|
return (0, react.createElement)("span", {
|
|
571
808
|
key: segKey,
|
|
572
809
|
className: entry.stream === "stderr" ? "sts-log-stderr" : ""
|
|
573
|
-
}, renderAnsiLine(entry.text, segKey)
|
|
810
|
+
}, renderAnsiLine(entry.text, segKey, (link) => {
|
|
811
|
+
handleLogCodeClick(ctx, sessionId, cwd, instance.cwd, link);
|
|
812
|
+
}));
|
|
574
813
|
}));
|
|
575
814
|
}), exitInfo !== null ? (0, react.createElement)("div", {
|
|
576
815
|
key: "exit-info",
|
|
@@ -582,6 +821,194 @@ window.__ModuleLoader__.load({
|
|
|
582
821
|
}, `\n进程${exitInfo.status === "exited" ? "退出" : exitInfo.status === "killed" ? "被终止" : "出错"}`, exitInfo.exitCode !== null ? `,退出码: ${exitInfo.exitCode}` : "") : null));
|
|
583
822
|
}
|
|
584
823
|
//#endregion
|
|
824
|
+
//#region src/client/LogHistoryView.tsx
|
|
825
|
+
/**
|
|
826
|
+
* Log history view: read-only viewer for persisted run logs
|
|
827
|
+
* (`GET /logs-history`). Two states: the file list under the workspace's
|
|
828
|
+
* `.dsh/logs` directory, and the rendered content of one log file. Reuses
|
|
829
|
+
* the LogView line renderer (ANSI + Java-log highlighting + code links),
|
|
830
|
+
* so previous runs — including failed spawns — stay inspectable forever.
|
|
831
|
+
* @module dsh-better-sidebar-starter/client/LogHistoryView
|
|
832
|
+
*/
|
|
833
|
+
/** API base. */
|
|
834
|
+
const API_BASE$1 = "/api/dsh-better-sidebar-starter";
|
|
835
|
+
/** Render one history log line (stderr red, meta dim). */
|
|
836
|
+
function renderLine(line, onLinkClick) {
|
|
837
|
+
return (0, react.createElement)("div", {
|
|
838
|
+
key: `line-${line.key}`,
|
|
839
|
+
className: "sts-log-line"
|
|
840
|
+
}, ...line.entries.map((entry, i) => {
|
|
841
|
+
const segKey = `line-${line.key}-e${i}`;
|
|
842
|
+
const cls = entry.stream === "stderr" ? "sts-log-stderr" : entry.stream === "meta" ? "sts-log-meta" : "";
|
|
843
|
+
return (0, react.createElement)("span", {
|
|
844
|
+
key: segKey,
|
|
845
|
+
className: cls
|
|
846
|
+
}, renderAnsiLine(entry.text, segKey, onLinkClick));
|
|
847
|
+
}));
|
|
848
|
+
}
|
|
849
|
+
/** Split a raw log entry into RenderLine-ready single-line entries. */
|
|
850
|
+
function splitEntry(entry, keyBase, out) {
|
|
851
|
+
const parts = entry.text.split("\n");
|
|
852
|
+
let current = [];
|
|
853
|
+
const pushLine = () => {
|
|
854
|
+
if (current.length > 0) {
|
|
855
|
+
out.push({
|
|
856
|
+
entries: current,
|
|
857
|
+
key: keyBase++
|
|
858
|
+
});
|
|
859
|
+
current = [];
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
for (let i = 0; i < parts.length; i++) {
|
|
863
|
+
if (i > 0) pushLine();
|
|
864
|
+
if (parts[i] !== "") current.push({
|
|
865
|
+
stream: entry.stream,
|
|
866
|
+
text: parts[i],
|
|
867
|
+
ts: entry.ts
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
if (parts.length > 1 && parts[parts.length - 1] === "") pushLine();
|
|
871
|
+
}
|
|
872
|
+
/** The log history viewer. */
|
|
873
|
+
function LogHistoryView({ ctx, sessionId, cwd, onClose }) {
|
|
874
|
+
const [files, setFiles] = (0, react.useState)(null);
|
|
875
|
+
const [loading, setLoading] = (0, react.useState)(false);
|
|
876
|
+
const [activeFile, setActiveFile] = (0, react.useState)(null);
|
|
877
|
+
const [lines, setLines] = (0, react.useState)([]);
|
|
878
|
+
const [loadedName, setLoadedName] = (0, react.useState)("");
|
|
879
|
+
const lineKeyCounter = (0, react.useRef)(0);
|
|
880
|
+
const loadList = (0, react.useCallback)(async () => {
|
|
881
|
+
setLoading(true);
|
|
882
|
+
try {
|
|
883
|
+
const params = new URLSearchParams({ sessionId });
|
|
884
|
+
if (cwd !== void 0 && cwd !== "") params.set("cwd", cwd);
|
|
885
|
+
const resp = await fetch(`${API_BASE$1}/logs-history?${params.toString()}`);
|
|
886
|
+
if (!resp.ok) {
|
|
887
|
+
let reason = "加载历史日志失败";
|
|
888
|
+
try {
|
|
889
|
+
const err = await resp.json();
|
|
890
|
+
if (typeof err.error === "string" && err.error !== "") reason = err.error;
|
|
891
|
+
} catch {}
|
|
892
|
+
showToast(reason, false);
|
|
893
|
+
setFiles([]);
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
const data = await resp.json();
|
|
897
|
+
setFiles(data.logs ?? []);
|
|
898
|
+
} catch {
|
|
899
|
+
showToast("加载历史日志失败", false);
|
|
900
|
+
setFiles([]);
|
|
901
|
+
} finally {
|
|
902
|
+
setLoading(false);
|
|
903
|
+
}
|
|
904
|
+
}, [sessionId, cwd]);
|
|
905
|
+
const [initialized, setInitialized] = (0, react.useState)(false);
|
|
906
|
+
if (!initialized) {
|
|
907
|
+
setInitialized(true);
|
|
908
|
+
loadList();
|
|
909
|
+
}
|
|
910
|
+
/** Open one log file and render its content. */
|
|
911
|
+
const openFile = (0, react.useCallback)(async (info) => {
|
|
912
|
+
setLoading(true);
|
|
913
|
+
try {
|
|
914
|
+
const params = new URLSearchParams({
|
|
915
|
+
sessionId,
|
|
916
|
+
file: info.file
|
|
917
|
+
});
|
|
918
|
+
if (cwd !== void 0 && cwd !== "") params.set("cwd", cwd);
|
|
919
|
+
const resp = await fetch(`${API_BASE$1}/logs-history?${params.toString()}`);
|
|
920
|
+
if (!resp.ok) {
|
|
921
|
+
let reason = "读取日志失败";
|
|
922
|
+
try {
|
|
923
|
+
const err = await resp.json();
|
|
924
|
+
if (typeof err.error === "string" && err.error !== "") reason = err.error;
|
|
925
|
+
} catch {}
|
|
926
|
+
showToast(reason, false);
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
const data = await resp.json();
|
|
930
|
+
const out = [];
|
|
931
|
+
for (const entry of data.logs ?? []) {
|
|
932
|
+
splitEntry(entry, lineKeyCounter.current, out);
|
|
933
|
+
lineKeyCounter.current += 2e5;
|
|
934
|
+
}
|
|
935
|
+
setActiveFile(info);
|
|
936
|
+
setLoadedName(info.name);
|
|
937
|
+
setLines(out);
|
|
938
|
+
} catch {
|
|
939
|
+
showToast("读取日志失败", false);
|
|
940
|
+
} finally {
|
|
941
|
+
setLoading(false);
|
|
942
|
+
}
|
|
943
|
+
}, [sessionId, cwd]);
|
|
944
|
+
if (activeFile === null) return (0, react.createElement)("div", { style: {
|
|
945
|
+
display: "flex",
|
|
946
|
+
flexDirection: "column",
|
|
947
|
+
height: "100%",
|
|
948
|
+
minHeight: 0
|
|
949
|
+
} }, (0, react.createElement)("div", { className: "sts-log-toolbar" }, "历史日志", (0, react.createElement)("div", { className: "sts-log-toolbar-spacer" }), (0, react.createElement)("button", {
|
|
950
|
+
className: "sts-icon-btn",
|
|
951
|
+
title: "刷新",
|
|
952
|
+
onClick: () => {
|
|
953
|
+
loadList();
|
|
954
|
+
}
|
|
955
|
+
}, RefreshIcon({ size: 14 }), (0, react.createElement)("span", { className: "sts-icon-btn-label" }, "刷新")), (0, react.createElement)("button", {
|
|
956
|
+
className: "sts-icon-btn",
|
|
957
|
+
title: "关闭历史日志",
|
|
958
|
+
onClick: onClose
|
|
959
|
+
}, CloseIcon({ size: 14 }), (0, react.createElement)("span", { className: "sts-icon-btn-label" }, "关闭"))), (0, react.createElement)("div", { style: {
|
|
960
|
+
overflow: "auto",
|
|
961
|
+
flex: 1,
|
|
962
|
+
minHeight: 0,
|
|
963
|
+
padding: "4px 0"
|
|
964
|
+
} }, loading && files === null ? (0, react.createElement)("div", { className: "sts-empty" }, "加载中…") : files !== null && files.length === 0 ? (0, react.createElement)("div", { className: "sts-empty" }, "暂无历史日志(工作空间 .dsh/logs 为空)。每次启动服务都会在 <工作空间>/.dsh/logs 下永久保留一份日志,即使启动失败或实例被清理也能查看。") : (0, react.createElement)("div", {}, (files ?? []).map((f) => (0, react.createElement)("button", {
|
|
965
|
+
key: f.file,
|
|
966
|
+
className: "sts-history-item",
|
|
967
|
+
title: f.file,
|
|
968
|
+
onClick: () => {
|
|
969
|
+
openFile(f);
|
|
970
|
+
}
|
|
971
|
+
}, (0, react.createElement)(FileIcon, { size: 13 }), (0, react.createElement)("span", { style: {
|
|
972
|
+
flex: 1,
|
|
973
|
+
textAlign: "left",
|
|
974
|
+
overflow: "hidden",
|
|
975
|
+
textOverflow: "ellipsis",
|
|
976
|
+
whiteSpace: "nowrap"
|
|
977
|
+
} }, f.name), (0, react.createElement)("span", { className: "sts-history-item-meta" }, `${new Date(f.mtime).toLocaleString()} ${(f.size / 1024).toFixed(1)} KB`))))));
|
|
978
|
+
return (0, react.createElement)("div", { style: {
|
|
979
|
+
display: "flex",
|
|
980
|
+
flexDirection: "column",
|
|
981
|
+
height: "100%",
|
|
982
|
+
minHeight: 0
|
|
983
|
+
} }, (0, react.createElement)("div", { className: "sts-log-toolbar" }, (0, react.createElement)("button", {
|
|
984
|
+
className: "sts-icon-btn",
|
|
985
|
+
title: "返回文件列表",
|
|
986
|
+
onClick: () => {
|
|
987
|
+
setActiveFile(null);
|
|
988
|
+
setLines([]);
|
|
989
|
+
}
|
|
990
|
+
}, BackIcon({ size: 14 }), (0, react.createElement)("span", { className: "sts-icon-btn-label" }, "返回")), loadedName, (0, react.createElement)("span", { style: {
|
|
991
|
+
opacity: .5,
|
|
992
|
+
marginLeft: "4px"
|
|
993
|
+
} }, "(历史日志)"), (0, react.createElement)("div", { className: "sts-log-toolbar-spacer" }), (0, react.createElement)("button", {
|
|
994
|
+
className: "sts-icon-btn",
|
|
995
|
+
title: "清空视图",
|
|
996
|
+
onClick: () => setLines([])
|
|
997
|
+
}, TrashIcon({ size: 14 }), (0, react.createElement)("span", { className: "sts-icon-btn-label" }, "清空")), (0, react.createElement)("button", {
|
|
998
|
+
className: "sts-icon-btn",
|
|
999
|
+
title: "关闭历史日志",
|
|
1000
|
+
onClick: onClose
|
|
1001
|
+
}, CloseIcon({ size: 14 }), (0, react.createElement)("span", { className: "sts-icon-btn-label" }, "关闭"))), (0, react.createElement)("div", { className: "sts-log-content" }, loading && lines.length === 0 ? (0, react.createElement)("div", { className: "sts-empty" }, "读取中…") : lines.length === 0 ? (0, react.createElement)("div", { className: "sts-empty" }, "日志文件为空。") : lines.map((line) => renderLine(line, (link) => {
|
|
1002
|
+
handleLogCodeClick(ctx, sessionId, cwd, cwd, link);
|
|
1003
|
+
})), lines.length === 0 ? null : (0, react.createElement)("div", {
|
|
1004
|
+
key: "history-end",
|
|
1005
|
+
style: {
|
|
1006
|
+
opacity: .5,
|
|
1007
|
+
marginTop: "6px"
|
|
1008
|
+
}
|
|
1009
|
+
}, `—— 历史日志已加载完毕(${lines.length} 行),此为只读视图 ——`)));
|
|
1010
|
+
}
|
|
1011
|
+
//#endregion
|
|
585
1012
|
//#region src/client/ConfigDetail.tsx
|
|
586
1013
|
/**
|
|
587
1014
|
* Config detail card: shown in the right pane when an idle config is selected.
|
|
@@ -1273,6 +1700,43 @@ window.__ModuleLoader__.load({
|
|
|
1273
1700
|
border-color: var(--dsw-alias-border-l3, #3a3a3e);
|
|
1274
1701
|
color: var(--dsw-alias-label-primary, #e6e6e6);
|
|
1275
1702
|
}
|
|
1703
|
+
.sts-toolbar-btn.active {
|
|
1704
|
+
background: var(--dsw-alias-interactive-bg-active, rgba(0,122,204,0.18));
|
|
1705
|
+
border-color: var(--dsw-alias-brand, #007acc);
|
|
1706
|
+
color: var(--dsw-alias-label-primary, #e6e6e6);
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
/* ===== Log history viewer ===== */
|
|
1710
|
+
.sts-history-item {
|
|
1711
|
+
display: flex;
|
|
1712
|
+
align-items: center;
|
|
1713
|
+
gap: 8px;
|
|
1714
|
+
width: calc(100% - 8px);
|
|
1715
|
+
margin: 2px 4px;
|
|
1716
|
+
padding: 6px 8px;
|
|
1717
|
+
border: 1px solid transparent;
|
|
1718
|
+
border-radius: 6px;
|
|
1719
|
+
background: transparent;
|
|
1720
|
+
color: var(--dsw-alias-label-secondary, #b0b0b0);
|
|
1721
|
+
cursor: pointer;
|
|
1722
|
+
font-size: 12px;
|
|
1723
|
+
text-align: left;
|
|
1724
|
+
transition: background 0.12s, border-color 0.12s;
|
|
1725
|
+
}
|
|
1726
|
+
.sts-history-item:hover {
|
|
1727
|
+
background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.06));
|
|
1728
|
+
border-color: var(--dsw-alias-border-l2, #2a2a2e);
|
|
1729
|
+
color: var(--dsw-alias-label-primary, #e6e6e6);
|
|
1730
|
+
}
|
|
1731
|
+
.sts-history-item-meta {
|
|
1732
|
+
flex-shrink: 0;
|
|
1733
|
+
font-size: 11px;
|
|
1734
|
+
opacity: 0.55;
|
|
1735
|
+
}
|
|
1736
|
+
.sts-log-meta {
|
|
1737
|
+
opacity: 0.72;
|
|
1738
|
+
font-style: italic;
|
|
1739
|
+
}
|
|
1276
1740
|
|
|
1277
1741
|
/* ===== Split pane (horizontal: default for right panel) ===== */
|
|
1278
1742
|
.sts-split {
|
|
@@ -1438,15 +1902,28 @@ window.__ModuleLoader__.load({
|
|
|
1438
1902
|
.sts-icon-btn.trash { color: #eb5757; }
|
|
1439
1903
|
.sts-icon-btn.dup { color: var(--dsw-alias-label-secondary, #b0b0b0); }
|
|
1440
1904
|
.sts-icon-btn.send { color: #4a90d9; }
|
|
1441
|
-
/* Toolbar buttons (log view) are
|
|
1905
|
+
/* Toolbar buttons (log view) are icon-only; the label expands on hover. */
|
|
1442
1906
|
.sts-log-toolbar .sts-icon-btn {
|
|
1443
|
-
width:
|
|
1907
|
+
width: 26px;
|
|
1444
1908
|
height: 26px;
|
|
1445
|
-
padding: 0
|
|
1446
|
-
gap:
|
|
1909
|
+
padding: 0 6px;
|
|
1910
|
+
gap: 0;
|
|
1447
1911
|
font-size: 11px;
|
|
1448
1912
|
opacity: 1;
|
|
1449
1913
|
}
|
|
1914
|
+
.sts-log-toolbar .sts-icon-btn .sts-icon-btn-label {
|
|
1915
|
+
display: inline-block;
|
|
1916
|
+
max-width: 0;
|
|
1917
|
+
overflow: hidden;
|
|
1918
|
+
white-space: nowrap;
|
|
1919
|
+
opacity: 0;
|
|
1920
|
+
transition: max-width 0.18s ease, opacity 0.18s ease, margin-left 0.18s ease;
|
|
1921
|
+
}
|
|
1922
|
+
.sts-log-toolbar .sts-icon-btn:hover .sts-icon-btn-label {
|
|
1923
|
+
max-width: 120px;
|
|
1924
|
+
margin-left: 4px;
|
|
1925
|
+
opacity: 1;
|
|
1926
|
+
}
|
|
1450
1927
|
|
|
1451
1928
|
/* ===== Instance row ===== */
|
|
1452
1929
|
.sts-instance-row {
|
|
@@ -1534,6 +2011,16 @@ window.__ModuleLoader__.load({
|
|
|
1534
2011
|
.sts-log-line {
|
|
1535
2012
|
min-height: 1em;
|
|
1536
2013
|
}
|
|
2014
|
+
/* Clickable source tokens (classes / stack frames) inside the log. */
|
|
2015
|
+
.sts-log-link {
|
|
2016
|
+
cursor: pointer;
|
|
2017
|
+
border-bottom: 1px dashed rgba(97, 175, 239, 0.4);
|
|
2018
|
+
transition: background 0.1s ease;
|
|
2019
|
+
}
|
|
2020
|
+
.sts-log-link:hover {
|
|
2021
|
+
background: rgba(97, 175, 239, 0.14);
|
|
2022
|
+
border-bottom-color: #61afef;
|
|
2023
|
+
}
|
|
1537
2024
|
|
|
1538
2025
|
/* ===== Config detail ===== */
|
|
1539
2026
|
.sts-detail {
|
|
@@ -2159,6 +2646,7 @@ window.__ModuleLoader__.load({
|
|
|
2159
2646
|
const [splitRatio, setSplitRatio] = (0, react.useState)(.33);
|
|
2160
2647
|
const [dragging, setDragging] = (0, react.useState)(false);
|
|
2161
2648
|
const [isHorizontal, setIsHorizontal] = (0, react.useState)(true);
|
|
2649
|
+
const [historyOpen, setHistoryOpen] = (0, react.useState)(false);
|
|
2162
2650
|
const containerRef = (0, react.useRef)(null);
|
|
2163
2651
|
(0, react.useEffect)(() => {
|
|
2164
2652
|
const el = containerRef.current;
|
|
@@ -2395,7 +2883,14 @@ window.__ModuleLoader__.load({
|
|
|
2395
2883
|
}, (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", {
|
|
2396
2884
|
className: "sts-toolbar-btn",
|
|
2397
2885
|
onClick: handleNewConfig
|
|
2398
|
-
}, (0, react.createElement)(PlusIcon, { size: 13 }), "新建配置")
|
|
2886
|
+
}, (0, react.createElement)(PlusIcon, { size: 13 }), "新建配置"), (0, react.createElement)("button", {
|
|
2887
|
+
className: `sts-toolbar-btn${historyOpen ? " active" : ""}`,
|
|
2888
|
+
title: "查看持久化历史日志(.dsh/logs,含启动失败记录)",
|
|
2889
|
+
onClick: () => {
|
|
2890
|
+
setHistoryOpen((v) => !v);
|
|
2891
|
+
if (historyOpen) {}
|
|
2892
|
+
}
|
|
2893
|
+
}, (0, react.createElement)(FileIcon, { size: 13 }), "历史日志")), (0, react.createElement)("div", { className: `sts-split${isHorizontal ? "" : " sts-split-vertical"}` }, (0, react.createElement)("div", {
|
|
2399
2894
|
className: `sts-split-first${isHorizontal ? "" : " sts-split-first-v"}`,
|
|
2400
2895
|
style: isHorizontal ? { width: `${splitRatio * 100}%` } : { height: `${splitRatio * 100}%` }
|
|
2401
2896
|
}, configs.length === 0 ? (0, react.createElement)("div", { className: "sts-empty" }, "暂无运行配置,点击右上角 + 新建") : (0, react.createElement)(ConfigTree, {
|
|
@@ -2416,10 +2911,16 @@ window.__ModuleLoader__.load({
|
|
|
2416
2911
|
})), (0, react.createElement)("div", {
|
|
2417
2912
|
className: `sts-split-gutter${dragging ? " dragging" : ""}${isHorizontal ? "" : " sts-split-gutter-v"}`,
|
|
2418
2913
|
onMouseDown: handleMouseDown
|
|
2419
|
-
}), (0, react.createElement)("div", { className: `sts-split-second${isHorizontal ? "" : " sts-split-second-v"}` },
|
|
2914
|
+
}), (0, react.createElement)("div", { className: `sts-split-second${isHorizontal ? "" : " sts-split-second-v"}` }, historyOpen ? (0, react.createElement)(LogHistoryView, {
|
|
2915
|
+
ctx: props.ctx,
|
|
2916
|
+
sessionId: scope.sessionId ?? store.getSnapshot().sessionId ?? "",
|
|
2917
|
+
cwd: scope.cwd,
|
|
2918
|
+
onClose: () => setHistoryOpen(false)
|
|
2919
|
+
}) : selectedInstance !== null ? (0, react.createElement)(LogView, {
|
|
2420
2920
|
instance: selectedInstance,
|
|
2421
2921
|
ctx: props.ctx,
|
|
2422
2922
|
sessionId: scope.sessionId ?? store.getSnapshot().sessionId ?? "",
|
|
2923
|
+
cwd: scope.cwd,
|
|
2423
2924
|
onStop: () => {
|
|
2424
2925
|
handleStop(selectedInstance.id);
|
|
2425
2926
|
},
|