@dong-victor/dsh-better-sidebar-starter 0.1.2 → 0.1.5

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"/>`);
@@ -131,7 +143,7 @@ window.__ModuleLoader__.load({
131
143
  "python",
132
144
  "custom"
133
145
  ];
134
- const TYPE_LABELS$2 = {
146
+ const TYPE_LABELS$1 = {
135
147
  npm: "npm",
136
148
  springboot: "Spring Boot",
137
149
  python: "Python",
@@ -175,7 +187,7 @@ window.__ModuleLoader__.load({
175
187
  if (configs.length === 0) return (0, react.createElement)("div", { className: "sts-empty" }, "暂无运行配置", (0, react.createElement)("br"), "点击右上角 + 新建配置");
176
188
  return (0, react.createElement)("div", { className: "sts-tree" }, ...groups.map((group) => {
177
189
  const isCollapsed = collapsed.has(group.type);
178
- const label = TYPE_LABELS$2[group.type] ?? group.type;
190
+ const label = TYPE_LABELS$1[group.type] ?? group.type;
179
191
  const runningCount = instances.filter((inst) => inst.configType === group.type && inst.status === "running").length;
180
192
  return (0, react.createElement)("div", { key: group.type }, (0, react.createElement)("div", {
181
193
  className: "sts-group-header",
@@ -562,10 +574,85 @@ 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. */
631
+ /** Split plain text into plain/url parts. Trailing punctuation that is
632
+ * almost certainly sentence text (.,;:)])。,) stays outside the link. */
633
+ const URL_RE = /https?:\/\/[^\s"'<>()\[\]{}]+/g;
634
+ function splitUrlParts(text) {
635
+ const out = [];
636
+ let last = 0;
637
+ for (const m of text.matchAll(URL_RE)) {
638
+ const idx = m.index ?? 0;
639
+ if (idx > last) out.push({ text: text.slice(last, idx) });
640
+ let url = m[0];
641
+ let trail = "";
642
+ while (url.length > 0 && /[.,;:)\]}》」),。;:]$/.test(url)) {
643
+ trail = url.slice(-1) + trail;
644
+ url = url.slice(0, -1);
645
+ }
646
+ if (url !== "") out.push({
647
+ text: url,
648
+ url
649
+ });
650
+ if (trail !== "") out.push({ text: trail });
651
+ last = idx + m[0].length;
652
+ }
653
+ if (last < text.length) out.push({ text: text.slice(last) });
654
+ return out;
655
+ }
569
656
  function renderAnsiLine(text, keyBase, onLinkClick) {
570
657
  const segments = parseAnsi(text);
571
658
  const nodes = [];
@@ -587,8 +674,28 @@ window.__ModuleLoader__.load({
587
674
  key: `${keyBase}-${i}`,
588
675
  style
589
676
  }, seg.text));
590
- } else {
591
- const hl = highlightLogText(seg.text);
677
+ } else for (const part of splitUrlParts(seg.text)) {
678
+ if (part.url !== void 0) {
679
+ const url = part.url;
680
+ nodes.push((0, react.createElement)("span", {
681
+ key: `${keyBase}-${i}-u${hlCounter++}`,
682
+ className: "sts-log-url",
683
+ style: {
684
+ ...baseStyle,
685
+ textDecoration: "underline",
686
+ cursor: "pointer"
687
+ },
688
+ title: `在浏览器打开 ${url}`,
689
+ onClick: (e) => {
690
+ e.stopPropagation();
691
+ try {
692
+ window.open(url, "_blank", "noopener");
693
+ } catch {}
694
+ }
695
+ }, part.url));
696
+ continue;
697
+ }
698
+ const hl = highlightLogText(part.text);
592
699
  for (const h of hl) {
593
700
  const style = { ...baseStyle };
594
701
  if (h.color !== void 0) style.color = h.color;
@@ -636,51 +743,6 @@ window.__ModuleLoader__.load({
636
743
  if (appendToDraft(ctx, sessionId, text)) showToast(`已将选中文本(${text.length} 字符)发送到对话`, true);
637
744
  else showToast("发送失败:对话服务不可用", false);
638
745
  };
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
746
  const lineBuffer = (0, react.useRef)({ current: [] });
685
747
  /** Flush the buffer into a new log line. */
686
748
  const flushLine = (0, react.useCallback)(() => {
@@ -791,7 +853,7 @@ window.__ModuleLoader__.load({
791
853
  key: segKey,
792
854
  className: entry.stream === "stderr" ? "sts-log-stderr" : ""
793
855
  }, renderAnsiLine(entry.text, segKey, (link) => {
794
- handleCodeClick(link);
856
+ handleLogCodeClick(ctx, sessionId, cwd, instance.cwd, link);
795
857
  }));
796
858
  }));
797
859
  }), exitInfo !== null ? (0, react.createElement)("div", {
@@ -804,51 +866,200 @@ window.__ModuleLoader__.load({
804
866
  }, `\n进程${exitInfo.status === "exited" ? "退出" : exitInfo.status === "killed" ? "被终止" : "出错"}`, exitInfo.exitCode !== null ? `,退出码: ${exitInfo.exitCode}` : "") : null));
805
867
  }
806
868
  //#endregion
807
- //#region src/client/ConfigDetail.tsx
869
+ //#region src/client/LogHistoryView.tsx
808
870
  /**
809
- * Config detail card: shown in the right pane when an idle config is selected.
810
- * Displays the config's full info and offers start / edit / delete buttons.
811
- * @module dsh-better-sidebar-starter/client/ConfigDetail
871
+ * Log history view: read-only viewer for persisted run logs
872
+ * (`GET /logs-history`). Two states: the file list under the workspace's
873
+ * `.dsh/logs` directory, and the rendered content of one log file. Reuses
874
+ * the LogView line renderer (ANSI + Java-log highlighting + code links),
875
+ * so previous runs — including failed spawns — stay inspectable forever.
876
+ * @module dsh-better-sidebar-starter/client/LogHistoryView
812
877
  */
813
- const TYPE_LABELS$1 = {
814
- npm: "npm",
815
- springboot: "Spring Boot",
816
- python: "Python",
817
- custom: "Custom"
818
- };
819
- /** The detail card component. */
820
- function ConfigDetail({ config, onStart, onEdit, onDuplicate, onDelete }) {
821
- const envEntries = Object.entries(config.env);
822
- const dateFmt = (ts) => {
823
- if (ts === null) return "—";
824
- return new Date(ts).toLocaleString("zh-CN", { hour12: false });
878
+ /** API base. */
879
+ const API_BASE$1 = "/api/dsh-better-sidebar-starter";
880
+ /** Render one history log line (stderr red, meta dim). */
881
+ function renderLine(line, onLinkClick) {
882
+ return (0, react.createElement)("div", {
883
+ key: `line-${line.key}`,
884
+ className: "sts-log-line"
885
+ }, ...line.entries.map((entry, i) => {
886
+ const segKey = `line-${line.key}-e${i}`;
887
+ const cls = entry.stream === "stderr" ? "sts-log-stderr" : entry.stream === "meta" ? "sts-log-meta" : "";
888
+ return (0, react.createElement)("span", {
889
+ key: segKey,
890
+ className: cls
891
+ }, renderAnsiLine(entry.text, segKey, onLinkClick));
892
+ }));
893
+ }
894
+ /** Split a raw log entry into RenderLine-ready single-line entries. */
895
+ function splitEntry(entry, keyBase, out) {
896
+ const parts = entry.text.split("\n");
897
+ let current = [];
898
+ const pushLine = () => {
899
+ if (current.length > 0) {
900
+ out.push({
901
+ entries: current,
902
+ key: keyBase++
903
+ });
904
+ current = [];
905
+ }
825
906
  };
826
- return (0, react.createElement)("div", { className: "sts-detail" }, (0, react.createElement)("div", { className: "sts-detail-card" }, (0, react.createElement)("div", { className: "sts-detail-title" }, typeIcon(config.type, 20), config.name), (0, react.createElement)("div", { className: "sts-detail-row" }, (0, react.createElement)("div", { className: "sts-detail-label" }, "类型"), (0, react.createElement)("div", { className: "sts-detail-value" }, TYPE_LABELS$1[config.type] ?? config.type)), (0, react.createElement)("div", { className: "sts-detail-row" }, (0, react.createElement)("div", { className: "sts-detail-label" }, "命令"), (0, react.createElement)("div", { className: "sts-detail-value" }, config.command)), (0, react.createElement)("div", { className: "sts-detail-row" }, (0, react.createElement)("div", { className: "sts-detail-label" }, "工作目录"), (0, react.createElement)("div", { className: "sts-detail-value" }, config.cwd)), config.type === "springboot" && config.jvmArgs ? (0, react.createElement)("div", { className: "sts-detail-row" }, (0, react.createElement)("div", { className: "sts-detail-label" }, "JVM参数"), (0, react.createElement)("div", { className: "sts-detail-value" }, config.jvmArgs)) : null, config.args ? (0, react.createElement)("div", { className: "sts-detail-row" }, (0, react.createElement)("div", { className: "sts-detail-label" }, "程序实参"), (0, react.createElement)("div", { className: "sts-detail-value" }, config.args)) : null, envEntries.length > 0 ? (0, react.createElement)("div", { className: "sts-detail-row" }, (0, react.createElement)("div", { className: "sts-detail-label" }, "环境变量"), (0, react.createElement)("div", { style: { flex: 1 } }, ...envEntries.map(([key, value]) => (0, react.createElement)("div", {
827
- key,
828
- className: "sts-detail-value"
829
- }, `${key} = ${value}`)))) : null, (0, react.createElement)("div", { className: "sts-detail-row" }, (0, react.createElement)("div", { className: "sts-detail-label" }, "创建时间"), (0, react.createElement)("div", { className: "sts-detail-value" }, dateFmt(config.createdAt))), config.lastRunAt !== null ? (0, react.createElement)("div", { className: "sts-detail-row" }, (0, react.createElement)("div", { className: "sts-detail-label" }, "最近运行"), (0, react.createElement)("div", { className: "sts-detail-value" }, dateFmt(config.lastRunAt))) : null, (0, react.createElement)("div", { className: "sts-detail-actions" }, (0, react.createElement)("button", {
830
- className: "sts-detail-btn danger",
831
- onClick: onDelete
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", {
837
- className: "sts-detail-btn",
838
- onClick: onEdit
839
- }, (0, react.createElement)(GearIcon, { size: 14 }), " 编辑"), (0, react.createElement)("button", {
840
- className: "sts-detail-btn primary",
841
- onClick: onStart,
907
+ for (let i = 0; i < parts.length; i++) {
908
+ if (i > 0) pushLine();
909
+ if (parts[i] !== "") current.push({
910
+ stream: entry.stream,
911
+ text: parts[i],
912
+ ts: entry.ts
913
+ });
914
+ }
915
+ if (parts.length > 1 && parts[parts.length - 1] === "") pushLine();
916
+ }
917
+ /** The log history viewer. */
918
+ function LogHistoryView({ ctx, sessionId, cwd, onClose, filter }) {
919
+ const [files, setFiles] = (0, react.useState)(null);
920
+ const [loading, setLoading] = (0, react.useState)(false);
921
+ const [activeFile, setActiveFile] = (0, react.useState)(null);
922
+ const [lines, setLines] = (0, react.useState)([]);
923
+ const [loadedName, setLoadedName] = (0, react.useState)("");
924
+ const lineKeyCounter = (0, react.useRef)(0);
925
+ const loadList = (0, react.useCallback)(async () => {
926
+ setLoading(true);
927
+ try {
928
+ const params = new URLSearchParams({ sessionId });
929
+ if (cwd !== void 0 && cwd !== "") params.set("cwd", cwd);
930
+ const resp = await fetch(`${API_BASE$1}/logs-history?${params.toString()}`);
931
+ if (!resp.ok) {
932
+ let reason = filter !== void 0 ? "加载历史日志失败" : "加载历史日志失败";
933
+ try {
934
+ const err = await resp.json();
935
+ if (typeof err.error === "string" && err.error !== "") reason = err.error;
936
+ } catch {}
937
+ showToast(reason, false);
938
+ setFiles([]);
939
+ return;
940
+ }
941
+ const all = (await resp.json()).logs ?? [];
942
+ const shown = filter !== void 0 ? all.filter((f) => f.name.startsWith(filter)) : all;
943
+ setFiles(shown);
944
+ } catch {
945
+ showToast("加载历史日志失败", false);
946
+ setFiles([]);
947
+ } finally {
948
+ setLoading(false);
949
+ }
950
+ }, [
951
+ sessionId,
952
+ cwd,
953
+ filter
954
+ ]);
955
+ const [initialized, setInitialized] = (0, react.useState)(false);
956
+ if (!initialized) {
957
+ setInitialized(true);
958
+ loadList();
959
+ }
960
+ /** Open one log file and render its content. */
961
+ const openFile = (0, react.useCallback)(async (info) => {
962
+ setLoading(true);
963
+ try {
964
+ const params = new URLSearchParams({
965
+ sessionId,
966
+ file: info.file
967
+ });
968
+ if (cwd !== void 0 && cwd !== "") params.set("cwd", cwd);
969
+ const resp = await fetch(`${API_BASE$1}/logs-history?${params.toString()}`);
970
+ if (!resp.ok) {
971
+ let reason = "读取日志失败";
972
+ try {
973
+ const err = await resp.json();
974
+ if (typeof err.error === "string" && err.error !== "") reason = err.error;
975
+ } catch {}
976
+ showToast(reason, false);
977
+ return;
978
+ }
979
+ const data = await resp.json();
980
+ const out = [];
981
+ for (const entry of data.logs ?? []) {
982
+ splitEntry(entry, lineKeyCounter.current, out);
983
+ lineKeyCounter.current += 2e5;
984
+ }
985
+ setActiveFile(info);
986
+ setLoadedName(info.name);
987
+ setLines(out);
988
+ } catch {
989
+ showToast("读取日志失败", false);
990
+ } finally {
991
+ setLoading(false);
992
+ }
993
+ }, [sessionId, cwd]);
994
+ if (activeFile === null) {
995
+ const title = filter !== void 0 ? `${filter} · 历史日志` : "历史日志";
996
+ return (0, react.createElement)("div", { style: {
997
+ display: "flex",
998
+ flexDirection: "column",
999
+ height: "100%",
1000
+ minHeight: 0
1001
+ } }, (0, react.createElement)("div", { className: "sts-log-toolbar" }, title, (0, react.createElement)("div", { className: "sts-log-toolbar-spacer" }), (0, react.createElement)("button", {
1002
+ className: "sts-icon-btn",
1003
+ title: "刷新",
1004
+ onClick: () => {
1005
+ loadList();
1006
+ }
1007
+ }, RefreshIcon({ size: 14 }), (0, react.createElement)("span", { className: "sts-icon-btn-label" }, "刷新")), (0, react.createElement)("button", {
1008
+ className: "sts-icon-btn",
1009
+ title: filter !== void 0 ? "返回服务视图" : "关闭历史日志",
1010
+ onClick: onClose
1011
+ }, CloseIcon({ size: 14 }), (0, react.createElement)("span", { className: "sts-icon-btn-label" }, filter !== void 0 ? "返回" : "关闭"))), (0, react.createElement)("div", { style: {
1012
+ overflow: "auto",
1013
+ flex: 1,
1014
+ minHeight: 0,
1015
+ padding: "4px 0"
1016
+ } }, loading && files === null ? (0, react.createElement)("div", { className: "sts-empty" }, "加载中…") : files !== null && files.length === 0 ? (0, react.createElement)("div", { className: "sts-empty" }, filter !== void 0 ? `「${filter}」暂无历史日志。每次启动都会在 <工作空间>/logs/ 下永久保留一份日志,即使启动失败或 dsh 重启也能查看。` : "暂无历史日志(工作空间 logs/ 为空)。每次启动服务都会在 <工作空间>/logs/ 下永久保留一份日志,即使启动失败或 dsh 重启也能查看。") : (0, react.createElement)("div", {}, (files ?? []).map((f) => (0, react.createElement)("button", {
1017
+ key: f.file,
1018
+ className: "sts-history-item",
1019
+ title: f.file,
1020
+ onClick: () => {
1021
+ openFile(f);
1022
+ }
1023
+ }, (0, react.createElement)(FileIcon, { size: 13 }), (0, react.createElement)("span", { style: {
1024
+ flex: 1,
1025
+ textAlign: "left",
1026
+ overflow: "hidden",
1027
+ textOverflow: "ellipsis",
1028
+ whiteSpace: "nowrap"
1029
+ } }, f.name), (0, react.createElement)("span", { className: "sts-history-item-meta" }, `${new Date(f.mtime).toLocaleString()} ${(f.size / 1024).toFixed(1)} KB`))))));
1030
+ }
1031
+ return (0, react.createElement)("div", { style: {
1032
+ display: "flex",
1033
+ flexDirection: "column",
1034
+ height: "100%",
1035
+ minHeight: 0
1036
+ } }, (0, react.createElement)("div", { className: "sts-log-toolbar" }, (0, react.createElement)("button", {
1037
+ className: "sts-icon-btn",
1038
+ title: "返回文件列表",
1039
+ onClick: () => {
1040
+ setActiveFile(null);
1041
+ setLines([]);
1042
+ }
1043
+ }, BackIcon({ size: 14 }), (0, react.createElement)("span", { className: "sts-icon-btn-label" }, "返回")), loadedName, (0, react.createElement)("span", { style: {
1044
+ opacity: .5,
1045
+ marginLeft: "4px"
1046
+ } }, "(历史日志)"), (0, react.createElement)("div", { className: "sts-log-toolbar-spacer" }), (0, react.createElement)("button", {
1047
+ className: "sts-icon-btn",
1048
+ title: "清空视图",
1049
+ onClick: () => setLines([])
1050
+ }, TrashIcon({ size: 14 }), (0, react.createElement)("span", { className: "sts-icon-btn-label" }, "清空")), (0, react.createElement)("button", {
1051
+ className: "sts-icon-btn",
1052
+ title: "关闭历史日志",
1053
+ onClick: onClose
1054
+ }, 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) => {
1055
+ handleLogCodeClick(ctx, sessionId, cwd, cwd, link);
1056
+ })), lines.length === 0 ? null : (0, react.createElement)("div", {
1057
+ key: "history-end",
842
1058
  style: {
843
- background: "#1a1a1e",
844
- border: "1px solid #3a3a3e",
845
- color: "#fff",
846
- borderRadius: "6px",
847
- padding: "6px 14px",
848
- cursor: "pointer",
849
- fontSize: "12px"
1059
+ opacity: .5,
1060
+ marginTop: "6px"
850
1061
  }
851
- }, (0, react.createElement)(PlayIcon, { size: 14 }), " 启动"))));
1062
+ }, `—— 历史日志已加载完毕(${lines.length} 行),此为只读视图 ——`)));
852
1063
  }
853
1064
  //#endregion
854
1065
  //#region src/client/DirChooser.tsx
@@ -1495,6 +1706,43 @@ window.__ModuleLoader__.load({
1495
1706
  border-color: var(--dsw-alias-border-l3, #3a3a3e);
1496
1707
  color: var(--dsw-alias-label-primary, #e6e6e6);
1497
1708
  }
1709
+ .sts-toolbar-btn.active {
1710
+ background: var(--dsw-alias-interactive-bg-active, rgba(0,122,204,0.18));
1711
+ border-color: var(--dsw-alias-brand, #007acc);
1712
+ color: var(--dsw-alias-label-primary, #e6e6e6);
1713
+ }
1714
+
1715
+ /* ===== Log history viewer ===== */
1716
+ .sts-history-item {
1717
+ display: flex;
1718
+ align-items: center;
1719
+ gap: 8px;
1720
+ width: calc(100% - 8px);
1721
+ margin: 2px 4px;
1722
+ padding: 6px 8px;
1723
+ border: 1px solid transparent;
1724
+ border-radius: 6px;
1725
+ background: transparent;
1726
+ color: var(--dsw-alias-label-secondary, #b0b0b0);
1727
+ cursor: pointer;
1728
+ font-size: 12px;
1729
+ text-align: left;
1730
+ transition: background 0.12s, border-color 0.12s;
1731
+ }
1732
+ .sts-history-item:hover {
1733
+ background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.06));
1734
+ border-color: var(--dsw-alias-border-l2, #2a2a2e);
1735
+ color: var(--dsw-alias-label-primary, #e6e6e6);
1736
+ }
1737
+ .sts-history-item-meta {
1738
+ flex-shrink: 0;
1739
+ font-size: 11px;
1740
+ opacity: 0.55;
1741
+ }
1742
+ .sts-log-meta {
1743
+ opacity: 0.72;
1744
+ font-style: italic;
1745
+ }
1498
1746
 
1499
1747
  /* ===== Split pane (horizontal: default for right panel) ===== */
1500
1748
  .sts-split {
@@ -1779,6 +2027,13 @@ window.__ModuleLoader__.load({
1779
2027
  background: rgba(97, 175, 239, 0.14);
1780
2028
  border-bottom-color: #61afef;
1781
2029
  }
2030
+ /* Clickable http(s) URLs inside the log (open in a new browser tab). */
2031
+ .sts-log-url {
2032
+ color: #61afef;
2033
+ }
2034
+ .sts-log-url:hover {
2035
+ background: rgba(97, 175, 239, 0.18);
2036
+ }
1782
2037
 
1783
2038
  /* ===== Config detail ===== */
1784
2039
  .sts-detail {
@@ -2578,8 +2833,7 @@ window.__ModuleLoader__.load({
2578
2833
  }
2579
2834
  } catch {}
2580
2835
  }, [sessionQuery, fetchConfigs]);
2581
- /** Delete a config. */
2582
- const handleDeleteConfig = (0, react.useCallback)(async (configId) => {
2836
+ (0, react.useCallback)(async (configId) => {
2583
2837
  for (const inst of instances) if (inst.configId === configId && inst.status === "running") await handleStop(inst.id);
2584
2838
  try {
2585
2839
  if ((await fetch(`${API_BASE}/configs?${sessionQuery()}&id=${encodeURIComponent(configId)}`, { method: "DELETE" })).ok) {
@@ -2672,17 +2926,15 @@ window.__ModuleLoader__.load({
2672
2926
  onRestart: () => {
2673
2927
  handleRestart(selectedInstance.id);
2674
2928
  }
2675
- }) : selectedConfig !== null ? (0, react.createElement)(ConfigDetail, {
2676
- config: selectedConfig,
2677
- onStart: () => {
2678
- handleStart(selectedConfig.id);
2679
- },
2680
- onEdit: () => handleEditConfig(selectedConfig.id),
2681
- onDuplicate: () => handleDuplicateConfig(selectedConfig.id),
2682
- onDelete: () => {
2683
- handleDeleteConfig(selectedConfig.id);
2929
+ }) : selectedConfig !== null ? (0, react.createElement)(LogHistoryView, {
2930
+ ctx: props.ctx,
2931
+ sessionId: scope.sessionId ?? store.getSnapshot().sessionId ?? "",
2932
+ cwd: scope.cwd,
2933
+ filter: selectedConfig.name,
2934
+ onClose: () => {
2935
+ setSelectedConfigId(null);
2684
2936
  }
2685
- }) : (0, react.createElement)("div", { className: "sts-empty" }, "选择左侧配置查看详情或日志"))), showModal ? (0, react.createElement)(ConfigEditorModal, {
2937
+ }) : (0, react.createElement)("div", { className: "sts-empty" }, "选择左侧配置查看其历史日志,或选择运行中的服务查看实时日志"))), showModal ? (0, react.createElement)(ConfigEditorModal, {
2686
2938
  config: editingConfig,
2687
2939
  scope: {
2688
2940
  sessionId: scope.sessionId ?? store.getSnapshot().sessionId ?? "",