@dong-victor/dsh-better-sidebar-starter 0.1.1 → 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 +271 -25
- package/lib/client.js.map +1 -1
- package/lib/index.js +299 -6
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -339,6 +339,153 @@ window.__ModuleLoader__.load({
|
|
|
339
339
|
return ANSI_COLORS[seg.bg] ?? null;
|
|
340
340
|
}
|
|
341
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
|
|
342
489
|
//#region src/client/conversationDraft.ts
|
|
343
490
|
/**
|
|
344
491
|
* Append `text` to the session's composer draft (space-separated, like the
|
|
@@ -414,26 +561,54 @@ window.__ModuleLoader__.load({
|
|
|
414
561
|
* renders log lines with auto-scroll.
|
|
415
562
|
* @module dsh-better-sidebar-starter/client/LogView
|
|
416
563
|
*/
|
|
417
|
-
/**
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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];
|
|
421
575
|
const fg = segmentFgColor(seg);
|
|
422
576
|
const bg = segmentBgColor(seg);
|
|
423
|
-
|
|
424
|
-
if (
|
|
425
|
-
if (seg.
|
|
426
|
-
if (seg.
|
|
427
|
-
if (seg.
|
|
428
|
-
if (
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
style
|
|
432
|
-
|
|
433
|
-
|
|
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;
|
|
434
609
|
}
|
|
435
610
|
/** The log view component. */
|
|
436
|
-
function LogView({ instance, ctx, sessionId, onStop, onRestart }) {
|
|
611
|
+
function LogView({ instance, ctx, sessionId, cwd, onStop, onRestart }) {
|
|
437
612
|
const [logLines, setLogLines] = (0, react.useState)([]);
|
|
438
613
|
const [autoScroll, setAutoScroll] = (0, react.useState)(true);
|
|
439
614
|
const [exitInfo, setExitInfo] = (0, react.useState)(null);
|
|
@@ -461,6 +636,51 @@ window.__ModuleLoader__.load({
|
|
|
461
636
|
if (appendToDraft(ctx, sessionId, text)) showToast(`已将选中文本(${text.length} 字符)发送到对话`, true);
|
|
462
637
|
else showToast("发送失败:对话服务不可用", false);
|
|
463
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
|
+
};
|
|
464
684
|
const lineBuffer = (0, react.useRef)({ current: [] });
|
|
465
685
|
/** Flush the buffer into a new log line. */
|
|
466
686
|
const flushLine = (0, react.useCallback)(() => {
|
|
@@ -542,19 +762,19 @@ window.__ModuleLoader__.load({
|
|
|
542
762
|
className: "sts-icon-btn stop",
|
|
543
763
|
title: "停止",
|
|
544
764
|
onClick: onStop
|
|
545
|
-
}, 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", {
|
|
546
766
|
className: "sts-icon-btn",
|
|
547
767
|
title: "重启",
|
|
548
768
|
onClick: onRestart
|
|
549
|
-
}, RestartIcon({ size: 14 }), " 重启"), (0, react.createElement)("button", {
|
|
769
|
+
}, RestartIcon({ size: 14 }), (0, react.createElement)("span", { className: "sts-icon-btn-label" }, "重启")), (0, react.createElement)("button", {
|
|
550
770
|
className: "sts-icon-btn",
|
|
551
771
|
title: "清空",
|
|
552
772
|
onClick: handleClear
|
|
553
|
-
}, TrashIcon({ size: 14 }), " 清空"), (0, react.createElement)("button", {
|
|
773
|
+
}, TrashIcon({ size: 14 }), (0, react.createElement)("span", { className: "sts-icon-btn-label" }, "清空")), (0, react.createElement)("button", {
|
|
554
774
|
className: "sts-icon-btn send",
|
|
555
775
|
title: selLength > 0 ? `发送选中的 ${selLength} 字符到对话` : "发送选中的文本到对话",
|
|
556
776
|
onClick: handleSendSelection
|
|
557
|
-
}, SendToChatIcon({ size: 14 }), selLength > 0 ?
|
|
777
|
+
}, SendToChatIcon({ size: 14 }), (0, react.createElement)("span", { className: "sts-icon-btn-label" }, selLength > 0 ? `发送选中(${selLength})` : "发送选中")), (0, react.createElement)("label", {
|
|
558
778
|
className: "sts-log-toolbar-label",
|
|
559
779
|
onClick: () => setAutoScroll((v) => !v)
|
|
560
780
|
}, autoScroll ? "☑" : "☐", " 自动滚动")), (0, react.createElement)("div", {
|
|
@@ -570,7 +790,9 @@ window.__ModuleLoader__.load({
|
|
|
570
790
|
return (0, react.createElement)("span", {
|
|
571
791
|
key: segKey,
|
|
572
792
|
className: entry.stream === "stderr" ? "sts-log-stderr" : ""
|
|
573
|
-
}, renderAnsiLine(entry.text, segKey)
|
|
793
|
+
}, renderAnsiLine(entry.text, segKey, (link) => {
|
|
794
|
+
handleCodeClick(link);
|
|
795
|
+
}));
|
|
574
796
|
}));
|
|
575
797
|
}), exitInfo !== null ? (0, react.createElement)("div", {
|
|
576
798
|
key: "exit-info",
|
|
@@ -1438,15 +1660,28 @@ window.__ModuleLoader__.load({
|
|
|
1438
1660
|
.sts-icon-btn.trash { color: #eb5757; }
|
|
1439
1661
|
.sts-icon-btn.dup { color: var(--dsw-alias-label-secondary, #b0b0b0); }
|
|
1440
1662
|
.sts-icon-btn.send { color: #4a90d9; }
|
|
1441
|
-
/* Toolbar buttons (log view) are
|
|
1663
|
+
/* Toolbar buttons (log view) are icon-only; the label expands on hover. */
|
|
1442
1664
|
.sts-log-toolbar .sts-icon-btn {
|
|
1443
|
-
width:
|
|
1665
|
+
width: 26px;
|
|
1444
1666
|
height: 26px;
|
|
1445
|
-
padding: 0
|
|
1446
|
-
gap:
|
|
1667
|
+
padding: 0 6px;
|
|
1668
|
+
gap: 0;
|
|
1447
1669
|
font-size: 11px;
|
|
1448
1670
|
opacity: 1;
|
|
1449
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
|
+
}
|
|
1450
1685
|
|
|
1451
1686
|
/* ===== Instance row ===== */
|
|
1452
1687
|
.sts-instance-row {
|
|
@@ -1534,6 +1769,16 @@ window.__ModuleLoader__.load({
|
|
|
1534
1769
|
.sts-log-line {
|
|
1535
1770
|
min-height: 1em;
|
|
1536
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
|
+
}
|
|
1537
1782
|
|
|
1538
1783
|
/* ===== Config detail ===== */
|
|
1539
1784
|
.sts-detail {
|
|
@@ -2420,6 +2665,7 @@ window.__ModuleLoader__.load({
|
|
|
2420
2665
|
instance: selectedInstance,
|
|
2421
2666
|
ctx: props.ctx,
|
|
2422
2667
|
sessionId: scope.sessionId ?? store.getSnapshot().sessionId ?? "",
|
|
2668
|
+
cwd: scope.cwd,
|
|
2423
2669
|
onStop: () => {
|
|
2424
2670
|
handleStop(selectedInstance.id);
|
|
2425
2671
|
},
|