@dong-victor/dsh-better-sidebar-starter 0.1.2 → 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 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"/>`);
@@ -562,7 +574,57 @@ window.__ModuleLoader__.load({
562
574
  * @module dsh-better-sidebar-starter/client/LogView
563
575
  */
564
576
  /** API base. */
565
- const API_BASE$1 = "/api/dsh-better-sidebar-starter";
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
+ }
566
628
  /** Render ANSI-colored text segments, applying Java-log syntax highlighting
567
629
  * to text the program did not color itself (ANSI-colored spans keep the
568
630
  * program's own colors). Tokens with a source link become clickable. */
@@ -636,51 +698,6 @@ window.__ModuleLoader__.load({
636
698
  if (appendToDraft(ctx, sessionId, text)) showToast(`已将选中文本(${text.length} 字符)发送到对话`, true);
637
699
  else showToast("发送失败:对话服务不可用", false);
638
700
  };
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
- };
684
701
  const lineBuffer = (0, react.useRef)({ current: [] });
685
702
  /** Flush the buffer into a new log line. */
686
703
  const flushLine = (0, react.useCallback)(() => {
@@ -791,7 +808,7 @@ window.__ModuleLoader__.load({
791
808
  key: segKey,
792
809
  className: entry.stream === "stderr" ? "sts-log-stderr" : ""
793
810
  }, renderAnsiLine(entry.text, segKey, (link) => {
794
- handleCodeClick(link);
811
+ handleLogCodeClick(ctx, sessionId, cwd, instance.cwd, link);
795
812
  }));
796
813
  }));
797
814
  }), exitInfo !== null ? (0, react.createElement)("div", {
@@ -804,6 +821,194 @@ window.__ModuleLoader__.load({
804
821
  }, `\n进程${exitInfo.status === "exited" ? "退出" : exitInfo.status === "killed" ? "被终止" : "出错"}`, exitInfo.exitCode !== null ? `,退出码: ${exitInfo.exitCode}` : "") : null));
805
822
  }
806
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
807
1012
  //#region src/client/ConfigDetail.tsx
808
1013
  /**
809
1014
  * Config detail card: shown in the right pane when an idle config is selected.
@@ -1495,6 +1700,43 @@ window.__ModuleLoader__.load({
1495
1700
  border-color: var(--dsw-alias-border-l3, #3a3a3e);
1496
1701
  color: var(--dsw-alias-label-primary, #e6e6e6);
1497
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
+ }
1498
1740
 
1499
1741
  /* ===== Split pane (horizontal: default for right panel) ===== */
1500
1742
  .sts-split {
@@ -2404,6 +2646,7 @@ window.__ModuleLoader__.load({
2404
2646
  const [splitRatio, setSplitRatio] = (0, react.useState)(.33);
2405
2647
  const [dragging, setDragging] = (0, react.useState)(false);
2406
2648
  const [isHorizontal, setIsHorizontal] = (0, react.useState)(true);
2649
+ const [historyOpen, setHistoryOpen] = (0, react.useState)(false);
2407
2650
  const containerRef = (0, react.useRef)(null);
2408
2651
  (0, react.useEffect)(() => {
2409
2652
  const el = containerRef.current;
@@ -2640,7 +2883,14 @@ window.__ModuleLoader__.load({
2640
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", {
2641
2884
  className: "sts-toolbar-btn",
2642
2885
  onClick: handleNewConfig
2643
- }, (0, react.createElement)(PlusIcon, { size: 13 }), "新建配置")), (0, react.createElement)("div", { className: `sts-split${isHorizontal ? "" : " sts-split-vertical"}` }, (0, react.createElement)("div", {
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", {
2644
2894
  className: `sts-split-first${isHorizontal ? "" : " sts-split-first-v"}`,
2645
2895
  style: isHorizontal ? { width: `${splitRatio * 100}%` } : { height: `${splitRatio * 100}%` }
2646
2896
  }, configs.length === 0 ? (0, react.createElement)("div", { className: "sts-empty" }, "暂无运行配置,点击右上角 + 新建") : (0, react.createElement)(ConfigTree, {
@@ -2661,7 +2911,12 @@ window.__ModuleLoader__.load({
2661
2911
  })), (0, react.createElement)("div", {
2662
2912
  className: `sts-split-gutter${dragging ? " dragging" : ""}${isHorizontal ? "" : " sts-split-gutter-v"}`,
2663
2913
  onMouseDown: handleMouseDown
2664
- }), (0, react.createElement)("div", { className: `sts-split-second${isHorizontal ? "" : " sts-split-second-v"}` }, selectedInstance !== null ? (0, react.createElement)(LogView, {
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, {
2665
2920
  instance: selectedInstance,
2666
2921
  ctx: props.ctx,
2667
2922
  sessionId: scope.sessionId ?? store.getSnapshot().sessionId ?? "",