shine-code-submit 0.2.11 → 1.0.1

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.
@@ -7,6 +7,7 @@ import { CommitsModule } from "./CommitsModule";
7
7
  import { OverviewModule } from "./OverviewModule";
8
8
  import { StatsModule } from "./StatsModule";
9
9
  import { ReportModule } from "./ReportModule";
10
+ import { SettingsModule } from "./SettingsModule";
10
11
  import { SystemModule } from "./SystemModule";
11
12
 
12
13
  /** 模块路由:按 activeModule 渲染对应模块。 */
@@ -21,10 +22,12 @@ function ModuleRouter() {
21
22
  // return <EventsModule />; // 暂时屏蔽
22
23
  // case "commits":
23
24
  // return <CommitsModule />; // 暂时屏蔽
24
- case "stats":
25
- return <StatsModule />;
25
+ // case "stats":
26
+ // return <StatsModule />; // 暂时屏蔽
26
27
  case "report":
27
28
  return <ReportModule />;
29
+ case "settings":
30
+ return <SettingsModule />;
28
31
  case "system":
29
32
  return <SystemModule />;
30
33
  default:
@@ -17,7 +17,8 @@ export type IconName =
17
17
  | "home"
18
18
  | "git"
19
19
  | "chart"
20
- | "server";
20
+ | "server"
21
+ | "sliders";
21
22
 
22
23
  const PATHS: Record<IconName, ReactNode> = {
23
24
  sessions: (
@@ -84,6 +85,12 @@ const PATHS: Record<IconName, ReactNode> = {
84
85
  <path d="M8 7h.01M8 17h.01" />
85
86
  </>
86
87
  ),
88
+ sliders: (
89
+ <>
90
+ <path d="M4 21v-7M4 10V3M12 21v-9M12 8V3M20 21v-5M20 12V3" />
91
+ <path d="M1 14h6M9 8h6M17 16h6" />
92
+ </>
93
+ ),
87
94
  };
88
95
 
89
96
  export function Icon({
@@ -1,201 +1,186 @@
1
- // 「数据上报」模块:跨项目聚合(版本/git 用户/会话数+每会话 token/提交次数+行数+时间)。
2
- // 数据来自 GET /api/report?since=<ms>(0=全部)。后期要真·上报服务器时,把底部占位按钮接上即可。
1
+ // 「数据上报」模块:顶部汇总标题 + 左侧项目导航(仅项目名) + 右侧 session 表格(分页,隐藏 0 token)。
2
+ // 表格列:Session / 时间 / 输入 token / 输出 token;标题显示该项目的输入/输出 token 汇总。
3
+ // 数据来自 GET /api/report?since=0(全部)。后期真·上报服务器时,把顶部占位按钮接上即可。
3
4
  import { useEffect, useState } from "react";
4
5
  import { useApi } from "../hooks/useApi";
5
6
  import { useApp } from "../state/AppContext";
7
+ import { Icon } from "./Icon";
8
+ import { Splitter } from "./Splitter";
6
9
  import type { ReportProject, ReportResponse } from "../types";
7
- import { fmtDateTime, fmtTokens, fmtUsage, fmtUsageFull, shortDir } from "../lib/util";
10
+ import { fmtDateTime, fmtTokens, fmtUsageFull, shortDir } from "../lib/util";
8
11
 
9
- type Range = "all" | "7d" | "30d";
10
-
11
- const RANGES: Array<{ id: Range; label: string; days: number }> = [
12
- { id: "all", label: "全部", days: 0 },
13
- { id: "7d", label: "近 7 天", days: 7 },
14
- { id: "30d", label: "近 30 天", days: 30 },
15
- ];
16
-
17
- function rangeSince(r: Range): number {
18
- const days = RANGES.find((x) => x.id === r)!.days;
19
- return days > 0 ? Date.now() - days * 86400000 : 0;
20
- }
12
+ const PAGE = 20; // 每页 session
21
13
 
22
14
  export function ReportModule() {
23
15
  const { token } = useApp();
24
16
  const api = useApi(token);
25
- const [range, setRange] = useState<Range>("all");
26
17
  const [data, setData] = useState<ReportResponse | null>(null);
27
18
  const [err, setErr] = useState<string | null>(null);
28
- const [openCwd, setOpenCwd] = useState<string | null>(null);
19
+ const [selCwd, setSelCwd] = useState<string | null>(null);
29
20
 
30
21
  useEffect(() => {
31
22
  let cancelled = false;
32
23
  setErr(null);
33
- setData(null);
34
- api<ReportResponse>(`/api/report?since=${rangeSince(range)}`)
24
+ api<ReportResponse>(`/api/report?since=0`)
35
25
  .then((d) => !cancelled && setData(d))
36
26
  .catch((e) => !cancelled && setErr(String(e)));
37
27
  return () => {
38
28
  cancelled = true;
39
29
  };
40
- }, [api, range]);
30
+ }, [api]);
31
+
32
+ // 默认选中第一个项目
33
+ useEffect(() => {
34
+ if (data && !selCwd && data.projects[0]) setSelCwd(data.projects[0].cwd);
35
+ }, [data, selCwd]);
36
+
37
+ const sel = data?.projects.find((p) => p.cwd === selCwd) ?? null;
41
38
 
42
39
  return (
43
40
  <div className="stats-view">
44
- <div className="toolbar">
45
- <div className="tabs">
46
- {RANGES.map((r) => (
41
+ {/* 顶部汇总标题 */}
42
+ <div
43
+ className="panel-header"
44
+ style={{ display: "flex", gap: "1.1rem", alignItems: "baseline", flexWrap: "wrap" }}
45
+ >
46
+ {data ? (
47
+ <>
48
+ <b>报表</b>
49
+ <span title="软件版本">v{data.version}</span>
50
+ <span title="git 用户">👤 {data.gitUser ?? "—"}</span>
51
+ <span>{data.totals.projects} 项目</span>
52
+ <span>{data.totals.sessions} 会话</span>
53
+ <span title={fmtUsageFull(data.totals.tokens)}>
54
+ token {fmtTokens(data.totals.tokens.input + data.totals.tokens.output)}
55
+ </span>
56
+ <span>
57
+ {data.totals.commitCount} 提交 · +{fmtTokens(data.totals.added)}/-{fmtTokens(data.totals.deleted)}
58
+ </span>
59
+ {/* 占位:后期接「上报到服务器」(POST 本报告到远端)。现在禁用。 */}
47
60
  <button
48
- key={r.id}
49
61
  type="button"
50
- className={`tab${range === r.id ? " active" : ""}`}
51
- onClick={() => setRange(r.id)}
62
+ className="tab"
63
+ disabled
64
+ title="后期接入:把本报告上报到服务器(占位)"
65
+ style={{ marginLeft: "auto" }}
52
66
  >
53
- {r.label}
67
+ ☁ 上报(敬请期待)
54
68
  </button>
55
- ))}
56
- </div>
57
- {/* 占位:后期接「上报到服务器」(POST 本报告到远端)。现在禁用,提示敬请期待。 */}
58
- <button type="button" className="tab" disabled title="后期接入:把本报告上报到服务器(占位)">
59
- ☁ 上报到服务器(敬请期待)
60
- </button>
69
+ </>
70
+ ) : (
71
+ <span>{err ? `加载失败:${err}` : "加载中…"}</span>
72
+ )}
61
73
  </div>
62
74
 
63
- <div className="stats-body">
64
- {err && <div className="sum-empty">加载失败:{err}</div>}
65
- {!err && !data && <div className="sum-empty">加载中…</div>}
66
- {data && (
67
- <>
68
- <section className="sum-section">
69
- <div className="sum-head">
70
- <h3>汇总</h3>
71
- </div>
72
- <div className="bar-list">
73
- <div className="bar-row">
74
- <span className="bar-label">软件版本</span>
75
- <span className="bar-val">{data.version}</span>
76
- </div>
77
- <div className="bar-row">
78
- <span className="bar-label">git 用户</span>
79
- <span className="bar-val">{data.gitUser ?? ""}</span>
80
- </div>
81
- <div className="bar-row">
82
- <span className="bar-label">项目数</span>
83
- <span className="bar-val">{data.totals.projects}</span>
84
- </div>
85
- <div className="bar-row">
86
- <span className="bar-label">会话总数</span>
87
- <span className="bar-val">{data.totals.sessions}</span>
88
- </div>
89
- <div className="bar-row">
90
- <span className="bar-label">token 总量</span>
91
- <span className="bar-val" title={fmtUsageFull(data.totals.tokens)}>
92
- {fmtUsage(data.totals.tokens) || "—"}
93
- </span>
94
- </div>
95
- <div className="bar-row">
96
- <span className="bar-label">提交总数</span>
97
- <span className="bar-val">
98
- {data.totals.commitCount} · +{fmtTokens(data.totals.added)}/-{fmtTokens(data.totals.deleted)}
99
- </span>
100
- </div>
101
- </div>
102
- </section>
75
+ {/* 项目导航 | 详情表格 */}
76
+ <div className="sessions-with-tree">
77
+ <aside className="sessions-tree-panel panel">
78
+ <div className="panel-header">
79
+ <h2>项目 · {data?.projects.length ?? 0}</h2>
80
+ </div>
81
+ {!data || data.projects.length === 0 ? (
82
+ <div className="empty-state" style={{ padding: "2rem 1rem" }}>
83
+ <span className="es-hint">暂无项目</span>
84
+ <span className="es-sub">启动 Claude Code 后会出现</span>
85
+ </div>
86
+ ) : (
87
+ <ul className="report-nav">
88
+ {data.projects.map((p) => (
89
+ <li
90
+ key={p.cwd}
91
+ className={p.cwd === selCwd ? "active" : undefined}
92
+ title={p.cwd}
93
+ onClick={() => setSelCwd(p.cwd)}
94
+ >
95
+ {shortDir(p.cwd) || p.cwd}
96
+ </li>
97
+ ))}
98
+ </ul>
99
+ )}
100
+ </aside>
103
101
 
104
- {data.projects.length === 0 && <div className="sum-empty">窗口内无项目数据</div>}
105
- {data.projects.map((p) => (
106
- <ReportCard
107
- key={p.cwd}
108
- p={p}
109
- open={openCwd === p.cwd}
110
- onToggle={() => setOpenCwd(openCwd === p.cwd ? null : p.cwd)}
111
- />
112
- ))}
113
- </>
114
- )}
102
+ <Splitter orient="v" varName="--tree-w" />
103
+
104
+ <div className="sessions-main">
105
+ {sel ? (
106
+ <ProjectDetail key={sel.cwd} p={sel} />
107
+ ) : (
108
+ <div className="empty-state">
109
+ <Icon name="log" size={30} />
110
+ <span className="es-hint">选左侧项目查看详情</span>
111
+ </div>
112
+ )}
113
+ </div>
115
114
  </div>
116
115
  </div>
117
116
  );
118
117
  }
119
118
 
120
- function ReportCard({ p, open, onToggle }: { p: ReportProject; open: boolean; onToggle: () => void }) {
119
+ /** 右侧详情:标题(输入/输出 token 汇总 + 提交汇总) + session 表格(分页,隐藏 0 token)
120
+ * key=sel.cwd:换项目时重挂载,分页回到第 1 页。 */
121
+ function ProjectDetail({ p }: { p: ReportProject }) {
122
+ const [page, setPage] = useState(1);
123
+ // 过滤掉 0 token 的 session(tokenTotal 为 null 或 input+output=0)
124
+ const rows = p.sessions.filter((s) => s.tokenTotal && s.tokenTotal.input + s.tokenTotal.output > 0);
125
+ const pageCount = Math.max(1, Math.ceil(rows.length / PAGE));
126
+ const cur = Math.min(page, pageCount);
127
+ const pageRows = rows.slice((cur - 1) * PAGE, cur * PAGE);
128
+
121
129
  return (
122
- <section className="sum-section">
123
- <div className="sum-head">
124
- <h3 title={p.cwd}>{shortDir(p.cwd) || p.cwd}</h3>
125
- <span style={{ marginLeft: "auto", opacity: 0.65, fontSize: "var(--fs-xs)" }}>
126
- {p.gitUser ? `@${p.gitUser}` : "@—"}
127
- {p.gitError ? " · git 不可用" : ""}
130
+ <>
131
+ <div className="report-title">
132
+ <span className="rt-sum" title={fmtUsageFull(p.totalTokens)}>
133
+ 输入 token <b>{fmtTokens(p.totalTokens.input)}</b>
134
+ </span>
135
+ <span className="rt-sum" title={fmtUsageFull(p.totalTokens)}>
136
+ 输出 token <b>{fmtTokens(p.totalTokens.output)}</b>
137
+ </span>
138
+ <span className="rt-sum" style={{ marginLeft: "auto" }}>
139
+ {p.commits.count} 提交 · +{fmtTokens(p.commits.added)}/-{fmtTokens(p.commits.deleted)}
140
+ {p.commits.lastTime ? ` · 最近 ${fmtDateTime(p.commits.lastTime)}` : ""}
128
141
  </span>
129
- <button type="button" className="tab" onClick={onToggle} style={{ marginLeft: "0.5rem" }}>
130
- {open ? "收起" : "展开"}
131
- </button>
132
- </div>
133
- <div className="bar-list">
134
- <div className="bar-row">
135
- <span className="bar-label">会话</span>
136
- <span className="bar-val">{p.sessionCount}</span>
137
- </div>
138
- <div className="bar-row">
139
- <span className="bar-label">token</span>
140
- <span className="bar-val" title={fmtUsageFull(p.totalTokens)}>
141
- {fmtUsage(p.totalTokens) || "—"}
142
- </span>
143
- </div>
144
- <div className="bar-row">
145
- <span className="bar-label">提交</span>
146
- <span className="bar-val">
147
- {p.commits.count} · +{fmtTokens(p.commits.added)}/-{fmtTokens(p.commits.deleted)}
148
- </span>
149
- </div>
150
- <div className="bar-row">
151
- <span className="bar-label">最近提交</span>
152
- <span className="bar-val">{p.commits.lastTime ? fmtDateTime(p.commits.lastTime) : "—"}</span>
153
- </div>
154
142
  </div>
155
143
 
156
- {open && (
157
- <>
158
- <div className="sum-head" style={{ marginTop: "0.8rem" }}>
159
- <h3>会话 token 明细 · {p.sessions.length}</h3>
160
- </div>
161
- {p.sessions.length === 0 ? (
162
- <div className="sum-empty">无会话</div>
163
- ) : (
164
- <div className="bar-list">
165
- {p.sessions.map((s) => (
166
- <div className="bar-row" key={s.sessionId}>
167
- <span className="bar-label" title={s.sessionId}>
168
- {s.sessionId.slice(0, 8)} · {fmtDateTime(s.lastActive)}
169
- </span>
170
- <span className="bar-val" title={fmtUsageFull(s.tokenTotal)}>
171
- {fmtUsage(s.tokenTotal) || "—"}
172
- </span>
173
- </div>
174
- ))}
175
- </div>
176
- )}
144
+ <div style={{ overflow: "auto", flex: "1 1 0", minHeight: 0 }}>
145
+ <table className="report-table">
146
+ <thead>
147
+ <tr>
148
+ <th className="rt-idx">#</th>
149
+ <th>Session</th>
150
+ <th>时间</th>
151
+ <th className="rt-num">输入 token</th>
152
+ <th className="rt-num">输出 token</th>
153
+ </tr>
154
+ </thead>
155
+ <tbody>
156
+ {pageRows.map((s, idx) => (
157
+ <tr key={s.sessionId}>
158
+ <td className="rt-idx">{(cur - 1) * PAGE + idx + 1}</td>
159
+ <td className="rt-sid" title={s.sessionId}>
160
+ {s.sessionId.slice(0, 8)}
161
+ </td>
162
+ <td>{fmtDateTime(s.lastActive)}</td>
163
+ <td className="rt-num">{fmtTokens(s.tokenTotal!.input)}</td>
164
+ <td className="rt-num">{fmtTokens(s.tokenTotal!.output)}</td>
165
+ </tr>
166
+ ))}
167
+ </tbody>
168
+ </table>
169
+ {rows.length === 0 && <div className="sum-empty">无有效会话(均已隐藏 0 token)</div>}
170
+ </div>
177
171
 
178
- <div className="sum-head" style={{ marginTop: "0.8rem" }}>
179
- <h3>最近提交 · {p.recentCommits.length}</h3>
180
- </div>
181
- {p.recentCommits.length === 0 ? (
182
- <div className="sum-empty">无提交</div>
183
- ) : (
184
- <div className="bar-list">
185
- {p.recentCommits.map((c) => (
186
- <div className="bar-row" key={c.hash}>
187
- <span className="bar-label" title={c.subject}>
188
- {(c.subject || c.hash).slice(0, 40)}
189
- </span>
190
- <span className="bar-val">
191
- {fmtDateTime(c.time)} · +{c.added}/-{c.deleted}
192
- </span>
193
- </div>
194
- ))}
195
- </div>
196
- )}
197
- </>
198
- )}
199
- </section>
172
+ <div className="report-pager">
173
+ <button type="button" disabled={cur <= 1} onClick={() => setPage(cur - 1)}>
174
+ ‹ 上一页
175
+ </button>
176
+ <span>
177
+ {cur} / {pageCount} 页
178
+ </span>
179
+ <button type="button" disabled={cur >= pageCount} onClick={() => setPage(cur + 1)}>
180
+ 下一页
181
+ </button>
182
+ <span style={{ marginLeft: "auto" }}>共 {rows.length} 个会话(已隐藏 0 token)</span>
183
+ </div>
184
+ </>
200
185
  );
201
186
  }
@@ -0,0 +1,127 @@
1
+ // 「设置」模块:上报地址 + 自动上报间隔(分钟)。
2
+ // GET /api/settings 读、PUT /api/settings 写。daemon 侧按间隔定时 POST 报表到上报地址。
3
+ import { useEffect, useState } from "react";
4
+ import { useApi } from "../hooks/useApi";
5
+ import { useApp } from "../state/AppContext";
6
+
7
+ interface Settings {
8
+ reportUrl?: string | null;
9
+ reportIntervalMin?: number | null;
10
+ }
11
+
12
+ export function SettingsModule() {
13
+ const { token } = useApp();
14
+ const api = useApi(token);
15
+ const base = location.origin;
16
+ const [url, setUrl] = useState("");
17
+ const [intervalStr, setIntervalStr] = useState("");
18
+ const [loading, setLoading] = useState(true);
19
+ const [saving, setSaving] = useState(false);
20
+ const [msg, setMsg] = useState<{ kind: "ok" | "err"; text: string } | null>(null);
21
+
22
+ useEffect(() => {
23
+ api<Settings>("/api/settings")
24
+ .then((s) => {
25
+ setUrl(s.reportUrl ?? "");
26
+ setIntervalStr(s.reportIntervalMin != null ? String(s.reportIntervalMin) : "");
27
+ setLoading(false);
28
+ })
29
+ .catch(() => {
30
+ setMsg({ kind: "err", text: "读取设置失败" });
31
+ setLoading(false);
32
+ });
33
+ }, [api]);
34
+
35
+ const save = async () => {
36
+ setSaving(true);
37
+ setMsg(null);
38
+ const iv = parseInt(intervalStr, 10);
39
+ const body = {
40
+ reportUrl: url.trim() || null,
41
+ reportIntervalMin: Number.isFinite(iv) && iv > 0 ? iv : null,
42
+ };
43
+ try {
44
+ const res = await fetch(base + "/api/settings", {
45
+ method: "PUT",
46
+ headers: { Authorization: "Bearer " + token, "content-type": "application/json" },
47
+ body: JSON.stringify(body),
48
+ });
49
+ if (!res.ok) throw new Error(String(res.status));
50
+ const s = (await res.json()) as Settings;
51
+ setUrl(s.reportUrl ?? "");
52
+ setIntervalStr(s.reportIntervalMin != null ? String(s.reportIntervalMin) : "");
53
+ setMsg({ kind: "ok", text: "已保存" });
54
+ setTimeout(() => setMsg(null), 2000);
55
+ } catch {
56
+ setMsg({ kind: "err", text: "保存失败,请重试" });
57
+ } finally {
58
+ setSaving(false);
59
+ }
60
+ };
61
+
62
+ return (
63
+ <div className="stats-view">
64
+ <div className="panel-header">
65
+ <h2>设置</h2>
66
+ </div>
67
+ <div className="stats-body">
68
+ {loading ? (
69
+ <div className="sum-empty">加载中…</div>
70
+ ) : (
71
+ <>
72
+ <section className="sum-section">
73
+ <div className="sum-head">
74
+ <h3>上报</h3>
75
+ </div>
76
+ <div className="field-row">
77
+ <label>上报地址</label>
78
+ <input
79
+ className="field-input"
80
+ type="url"
81
+ placeholder="https://your-server/api/report"
82
+ value={url}
83
+ onChange={(e) => setUrl(e.target.value)}
84
+ spellCheck={false}
85
+ />
86
+ </div>
87
+ <div className="field-row">
88
+ <label>上报间隔</label>
89
+ <input
90
+ className="field-input"
91
+ type="number"
92
+ min={1}
93
+ placeholder="0 = 不自动上报"
94
+ value={intervalStr}
95
+ onChange={(e) => setIntervalStr(e.target.value)}
96
+ spellCheck={false}
97
+ style={{ flex: "0 0 120px" }}
98
+ />
99
+ <span className="field-hint" style={{ padding: 0 }}>
100
+ 分钟(填了地址且间隔大于 0 才会自动上报)
101
+ </span>
102
+ </div>
103
+ <div className="field-row">
104
+ <label /> {/* 占位对齐 */}
105
+ <button
106
+ type="button"
107
+ className="tab"
108
+ onClick={save}
109
+ disabled={saving}
110
+ title="保存设置"
111
+ >
112
+ {saving ? "保存中…" : "保存"}
113
+ </button>
114
+ {msg && (
115
+ <span className={msg.kind === "ok" ? "field-ok" : "field-err"}>{msg.text}</span>
116
+ )}
117
+ </div>
118
+ <div className="field-hint">
119
+ daemon 每分钟检查一次:地址 + 间隔都配了,就把「报表」数据 POST 到该地址;留空 / 间隔 0 = 不上报。改完无需重启。
120
+ </div>
121
+ </section>
122
+ </>
123
+ )}
124
+ </div>
125
+ </div>
126
+ );
127
+ }
@@ -8,8 +8,9 @@ const ITEMS: Array<{ id: ModuleId; label: string; icon: IconName }> = [
8
8
  { id: "sessions", label: "会话", icon: "sessions" },
9
9
  // { id: "events", label: "事件", icon: "activity" }, // 暂时屏蔽,恢复取消注释即可
10
10
  // { id: "commits", label: "提交", icon: "git" }, // 暂时屏蔽,恢复取消注释即可
11
- { id: "stats", label: "统计", icon: "chart" },
12
- { id: "report", label: "数据上报", icon: "log" },
11
+ // { id: "stats", label: "统计", icon: "chart" }, // 暂时屏蔽,恢复取消注释即可
12
+ { id: "report", label: "报表", icon: "log" },
13
+ { id: "settings", label: "设置", icon: "sliders" },
13
14
  { id: "system", label: "系统", icon: "server" },
14
15
  ];
15
16
 
package/ui/style.css CHANGED
@@ -847,3 +847,31 @@ ul { list-style: none; margin: 0; padding: 0; }
847
847
  .cf-add { color: #9bcd97; text-align: right; }
848
848
  .cf-del { color: #fc533a; text-align: right; }
849
849
  .cf-path { color: var(--text); overflow: hidden; text-overflow: ellipsis; }
850
+
851
+ /* 数据上报模块:表格 + 标题汇总 */
852
+ .report-title { display: flex; gap: 1.2rem; align-items: baseline; flex-wrap: wrap; padding: 0.55rem 0.8rem; border-bottom: 1px solid var(--border); flex: 0 0 auto; }
853
+ .report-title h2 { margin: 0; font-size: var(--fs-md); }
854
+ .report-title .rt-sum { color: var(--muted); font-size: var(--fs-xs); white-space: nowrap; }
855
+ .report-title .rt-sum b { color: var(--text); font-family: Consolas, "Courier New", monospace; font-variant-numeric: tabular-nums; }
856
+ .report-table { width: 100%; border-collapse: collapse; font-size: var(--fs-xs); }
857
+ .report-table th { position: sticky; top: 0; background: var(--bg); text-align: left; color: var(--muted); font-weight: 600; padding: 0.45rem 0.6rem; border-bottom: 1px solid var(--border-light); white-space: nowrap; }
858
+ .report-table td { padding: 0.32rem 0.6rem; border-bottom: 1px solid var(--border); color: var(--text); white-space: nowrap; }
859
+ .report-table th.rt-num, .report-table td.rt-num { text-align: right; font-family: Consolas, "Courier New", monospace; font-variant-numeric: tabular-nums; color: var(--muted); }
860
+ .report-table td.rt-sid { font-family: Consolas, "Courier New", monospace; color: var(--c-session); }
861
+ .report-table th.rt-idx, .report-table td.rt-idx { text-align: center; width: 3rem; color: var(--muted); }
862
+ .report-table tr:hover td { background: var(--hover); }
863
+ .report-nav { list-style: none; margin: 0; padding: 0.3rem; }
864
+ .report-nav li { padding: 0.45rem 0.7rem; border-radius: 4px; cursor: pointer; color: var(--text); font-size: var(--fs-sm); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
865
+ .report-nav li:hover { background: var(--hover); }
866
+ .report-nav li.active { background: var(--accent); color: #fff; }
867
+ .report-pager { display: flex; align-items: center; gap: 0.6rem; padding: 0.4rem 0.8rem; border-top: 1px solid var(--border); color: var(--muted); font-size: var(--fs-xs); flex: 0 0 auto; }
868
+ .report-pager button { background: none; border: 1px solid var(--border-light); color: var(--text); border-radius: 4px; padding: 0.15rem 0.55rem; cursor: pointer; font-size: var(--fs-xs); }
869
+ .report-pager button:disabled { opacity: 0.4; cursor: default; }
870
+ /* 设置模块:表单输入 */
871
+ .field-row { display: flex; align-items: center; gap: 0.6rem; padding: 0.55rem 0.8rem; }
872
+ .field-row label { flex: 0 0 88px; color: var(--muted); font-size: var(--fs-xs); }
873
+ .field-input { background: var(--bg); border: 1px solid var(--border-light); color: var(--text); border-radius: 4px; padding: 0.4rem 0.6rem; font-size: var(--fs-sm); font-family: Consolas, "Courier New", monospace; min-width: 0; flex: 1 1 auto; }
874
+ .field-input:focus { outline: none; border-color: var(--accent-line); }
875
+ .field-hint { color: var(--muted); font-size: var(--fs-xs); padding: 0.3rem 0.8rem; }
876
+ .field-ok { color: #9bcd97; }
877
+ .field-err { color: var(--c-err, #fc533a); }
package/ui/types.ts CHANGED
@@ -49,4 +49,4 @@ export type Payload = Record<string, unknown>;
49
49
  export type ViewMode = "events" | "conversation" | "commits" | "summary";
50
50
 
51
51
  /** 左侧导航模块(渐进重构:Step 1 起与 viewMode 并存,selectModule 经映射驱动 viewMode)。 */
52
- export type ModuleId = "overview" | "sessions" | "events" | "commits" | "stats" | "report" | "system";
52
+ export type ModuleId = "overview" | "sessions" | "events" | "commits" | "stats" | "report" | "settings" | "system";