shine-code-submit 0.2.10 → 1.0.0
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/.claude-plugin/plugin.json +1 -1
- package/dist/install.cjs +1 -1
- package/package.json +1 -1
- package/src/daemon/git.ts +11 -0
- package/src/daemon/server.ts +179 -2
- package/src/daemon/settings.ts +31 -0
- package/src/daemon/ui-assets.ts +2 -2
- package/src/shared/types.ts +50 -0
- package/ui/.build/app.js +49 -49
- package/ui/components/App.tsx +8 -2
- package/ui/components/Icon.tsx +8 -1
- package/ui/components/ReportModule.tsx +186 -0
- package/ui/components/SettingsModule.tsx +127 -0
- package/ui/components/SideNav.tsx +3 -1
- package/ui/style.css +28 -0
- package/ui/types.ts +7 -1
package/ui/components/App.tsx
CHANGED
|
@@ -6,6 +6,8 @@ import { EventsModule } from "./EventsModule";
|
|
|
6
6
|
import { CommitsModule } from "./CommitsModule";
|
|
7
7
|
import { OverviewModule } from "./OverviewModule";
|
|
8
8
|
import { StatsModule } from "./StatsModule";
|
|
9
|
+
import { ReportModule } from "./ReportModule";
|
|
10
|
+
import { SettingsModule } from "./SettingsModule";
|
|
9
11
|
import { SystemModule } from "./SystemModule";
|
|
10
12
|
|
|
11
13
|
/** 模块路由:按 activeModule 渲染对应模块。 */
|
|
@@ -20,8 +22,12 @@ function ModuleRouter() {
|
|
|
20
22
|
// return <EventsModule />; // 暂时屏蔽
|
|
21
23
|
// case "commits":
|
|
22
24
|
// return <CommitsModule />; // 暂时屏蔽
|
|
23
|
-
case "stats":
|
|
24
|
-
|
|
25
|
+
// case "stats":
|
|
26
|
+
// return <StatsModule />; // 暂时屏蔽
|
|
27
|
+
case "report":
|
|
28
|
+
return <ReportModule />;
|
|
29
|
+
case "settings":
|
|
30
|
+
return <SettingsModule />;
|
|
25
31
|
case "system":
|
|
26
32
|
return <SystemModule />;
|
|
27
33
|
default:
|
package/ui/components/Icon.tsx
CHANGED
|
@@ -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({
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// 「数据上报」模块:顶部汇总标题 + 左侧项目导航(仅项目名) + 右侧 session 表格(分页,隐藏 0 token)。
|
|
2
|
+
// 表格列:Session / 时间 / 输入 token / 输出 token;标题显示该项目的输入/输出 token 汇总。
|
|
3
|
+
// 数据来自 GET /api/report?since=0(全部)。后期真·上报服务器时,把顶部占位按钮接上即可。
|
|
4
|
+
import { useEffect, useState } from "react";
|
|
5
|
+
import { useApi } from "../hooks/useApi";
|
|
6
|
+
import { useApp } from "../state/AppContext";
|
|
7
|
+
import { Icon } from "./Icon";
|
|
8
|
+
import { Splitter } from "./Splitter";
|
|
9
|
+
import type { ReportProject, ReportResponse } from "../types";
|
|
10
|
+
import { fmtDateTime, fmtTokens, fmtUsageFull, shortDir } from "../lib/util";
|
|
11
|
+
|
|
12
|
+
const PAGE = 20; // 每页 session 数
|
|
13
|
+
|
|
14
|
+
export function ReportModule() {
|
|
15
|
+
const { token } = useApp();
|
|
16
|
+
const api = useApi(token);
|
|
17
|
+
const [data, setData] = useState<ReportResponse | null>(null);
|
|
18
|
+
const [err, setErr] = useState<string | null>(null);
|
|
19
|
+
const [selCwd, setSelCwd] = useState<string | null>(null);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
let cancelled = false;
|
|
23
|
+
setErr(null);
|
|
24
|
+
api<ReportResponse>(`/api/report?since=0`)
|
|
25
|
+
.then((d) => !cancelled && setData(d))
|
|
26
|
+
.catch((e) => !cancelled && setErr(String(e)));
|
|
27
|
+
return () => {
|
|
28
|
+
cancelled = true;
|
|
29
|
+
};
|
|
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;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="stats-view">
|
|
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 本报告到远端)。现在禁用。 */}
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
className="tab"
|
|
63
|
+
disabled
|
|
64
|
+
title="后期接入:把本报告上报到服务器(占位)"
|
|
65
|
+
style={{ marginLeft: "auto" }}
|
|
66
|
+
>
|
|
67
|
+
☁ 上报(敬请期待)
|
|
68
|
+
</button>
|
|
69
|
+
</>
|
|
70
|
+
) : (
|
|
71
|
+
<span>{err ? `加载失败:${err}` : "加载中…"}</span>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
|
|
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>
|
|
101
|
+
|
|
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>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
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
|
+
|
|
129
|
+
return (
|
|
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)}` : ""}
|
|
141
|
+
</span>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
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>
|
|
171
|
+
|
|
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
|
+
</>
|
|
185
|
+
);
|
|
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,7 +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" },
|
|
11
|
+
// { id: "stats", label: "统计", icon: "chart" }, // 暂时屏蔽,恢复取消注释即可
|
|
12
|
+
{ id: "report", label: "报表", icon: "log" },
|
|
13
|
+
{ id: "settings", label: "设置", icon: "sliders" },
|
|
12
14
|
{ id: "system", label: "系统", icon: "server" },
|
|
13
15
|
];
|
|
14
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
|
@@ -5,6 +5,9 @@ import type {
|
|
|
5
5
|
CommitsResponse,
|
|
6
6
|
EventsResponse,
|
|
7
7
|
HookEvent,
|
|
8
|
+
ReportProject,
|
|
9
|
+
ReportResponse,
|
|
10
|
+
ReportSession,
|
|
8
11
|
SessionSummary,
|
|
9
12
|
StatsResponse,
|
|
10
13
|
TokenUsage,
|
|
@@ -18,6 +21,9 @@ export type {
|
|
|
18
21
|
CommitsResponse,
|
|
19
22
|
EventsResponse,
|
|
20
23
|
HookEvent,
|
|
24
|
+
ReportProject,
|
|
25
|
+
ReportResponse,
|
|
26
|
+
ReportSession,
|
|
21
27
|
SessionSummary,
|
|
22
28
|
StatsResponse,
|
|
23
29
|
TokenUsage,
|
|
@@ -43,4 +49,4 @@ export type Payload = Record<string, unknown>;
|
|
|
43
49
|
export type ViewMode = "events" | "conversation" | "commits" | "summary";
|
|
44
50
|
|
|
45
51
|
/** 左侧导航模块(渐进重构:Step 1 起与 viewMode 并存,selectModule 经映射驱动 viewMode)。 */
|
|
46
|
-
export type ModuleId = "overview" | "sessions" | "events" | "commits" | "stats" | "system";
|
|
52
|
+
export type ModuleId = "overview" | "sessions" | "events" | "commits" | "stats" | "report" | "settings" | "system";
|