@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 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
- /** Render ANSI-colored text segments. */
418
- function renderAnsiLine(text, keyBase) {
419
- return parseAnsi(text).map((seg, i) => {
420
- const style = {};
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
- if (fg !== null) style.color = fg;
424
- if (bg !== null) style.backgroundColor = bg;
425
- if (seg.bold) style.fontWeight = "bold";
426
- if (seg.dim) style.opacity = "0.6";
427
- if (seg.italic) style.fontStyle = "italic";
428
- if (seg.underline) style.textDecoration = "underline";
429
- return (0, react.createElement)("span", {
430
- key: `${keyBase}-${i}`,
431
- style
432
- }, seg.text);
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 ? ` 发送选中(${selLength})` : " 发送选中"), (0, react.createElement)("label", {
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 always visible and fit icon + text. */
1663
+ /* Toolbar buttons (log view) are icon-only; the label expands on hover. */
1442
1664
  .sts-log-toolbar .sts-icon-btn {
1443
- width: auto;
1665
+ width: 26px;
1444
1666
  height: 26px;
1445
- padding: 0 8px;
1446
- gap: 3px;
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
  },