shine-code-submit 1.0.1 → 1.0.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/.claude-plugin/plugin.json +3 -3
- package/README.md +2 -0
- package/dist/install.cjs +1 -1
- package/package.json +4 -4
- package/src/daemon/git.ts +10 -0
- package/src/daemon/server.ts +30 -26
- package/src/daemon/settings.ts +11 -3
- package/src/daemon/ui-assets.ts +2 -2
- package/src/shared/types.ts +1 -13
- package/ui/.build/app.js +42 -42
- package/ui/components/OverviewModule.tsx +2 -2
- package/ui/components/ReportModule.tsx +60 -25
- package/ui/components/SessionDetail.tsx +3 -3
- package/ui/components/SessionTree.tsx +3 -6
- package/ui/components/StatsModule.tsx +3 -3
- package/ui/components/SummaryView.tsx +3 -3
- package/ui/lib/aggregate.ts +2 -1
- package/ui/lib/util.ts +30 -9
- package/ui/style.css +5 -2
|
@@ -3,7 +3,7 @@ import { useApi } from "../hooks/useApi";
|
|
|
3
3
|
import { useEvents } from "../hooks/useEvents";
|
|
4
4
|
import { useAllCommits } from "../hooks/useAllCommits";
|
|
5
5
|
import { useApp } from "../state/AppContext";
|
|
6
|
-
import { fmtDateTime, fmtTokens, shortDir, sumTokenUsage } from "../lib/util";
|
|
6
|
+
import { fmtDateTime, fmtTokens, realInput, shortDir, sumTokenUsage } from "../lib/util";
|
|
7
7
|
|
|
8
8
|
interface TimelineItem {
|
|
9
9
|
ts: number;
|
|
@@ -41,7 +41,7 @@ export function OverviewModule() {
|
|
|
41
41
|
<div className="kpi-grid">
|
|
42
42
|
<div className="kpi-card">
|
|
43
43
|
<span className="kpi-label">Token 输入</span>
|
|
44
|
-
<b className="kpi-value">{fmtTokens(tokenSum.total
|
|
44
|
+
<b className="kpi-value">{fmtTokens(realInput(tokenSum.total))}</b>
|
|
45
45
|
</div>
|
|
46
46
|
<div className="kpi-card">
|
|
47
47
|
<span className="kpi-label">Token 输出</span>
|
|
@@ -7,7 +7,7 @@ import { useApp } from "../state/AppContext";
|
|
|
7
7
|
import { Icon } from "./Icon";
|
|
8
8
|
import { Splitter } from "./Splitter";
|
|
9
9
|
import type { ReportProject, ReportResponse } from "../types";
|
|
10
|
-
import { fmtDateTime, fmtTokens, fmtUsageFull, shortDir } from "../lib/util";
|
|
10
|
+
import { fmtDateTime, fmtTokens, fmtUsageFull, fmtUsageLabeled, realInput, shortDir } from "../lib/util";
|
|
11
11
|
|
|
12
12
|
const PAGE = 20; // 每页 session 数
|
|
13
13
|
|
|
@@ -17,6 +17,8 @@ export function ReportModule() {
|
|
|
17
17
|
const [data, setData] = useState<ReportResponse | null>(null);
|
|
18
18
|
const [err, setErr] = useState<string | null>(null);
|
|
19
19
|
const [selCwd, setSelCwd] = useState<string | null>(null);
|
|
20
|
+
const [uploading, setUploading] = useState(false);
|
|
21
|
+
const [uploadResult, setUploadResult] = useState<{ ok: boolean; text: string } | null>(null);
|
|
20
22
|
|
|
21
23
|
useEffect(() => {
|
|
22
24
|
let cancelled = false;
|
|
@@ -48,24 +50,44 @@ export function ReportModule() {
|
|
|
48
50
|
<b>报表</b>
|
|
49
51
|
<span title="软件版本">v{data.version}</span>
|
|
50
52
|
<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
53
|
<span>
|
|
57
|
-
{data.totals.
|
|
54
|
+
{data.totals.projects} 项目 · {data.totals.sessions} 会话
|
|
58
55
|
</span>
|
|
59
|
-
{/* 占位:后期接「上报到服务器」(POST 本报告到远端)。现在禁用。 */}
|
|
60
56
|
<button
|
|
61
57
|
type="button"
|
|
62
58
|
className="tab"
|
|
63
|
-
disabled
|
|
64
|
-
title="
|
|
59
|
+
disabled={uploading}
|
|
60
|
+
title="手动上报报表到服务器(需先在「设置」配置上报地址)"
|
|
65
61
|
style={{ marginLeft: "auto" }}
|
|
62
|
+
onClick={async () => {
|
|
63
|
+
setUploading(true);
|
|
64
|
+
setUploadResult(null);
|
|
65
|
+
try {
|
|
66
|
+
const r = await fetch(`${location.origin}/api/report/upload`, {
|
|
67
|
+
method: "POST",
|
|
68
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
69
|
+
});
|
|
70
|
+
const j = await r.json().catch(() => ({}));
|
|
71
|
+
if (r.ok && j.status === "ok") {
|
|
72
|
+
setUploadResult({ ok: true, text: "上报成功" });
|
|
73
|
+
} else if (r.ok && j.status === "skipped") {
|
|
74
|
+
setUploadResult({ ok: false, text: `已跳过:${j.reason ?? "无 git 身份"}` });
|
|
75
|
+
} else {
|
|
76
|
+
setUploadResult({ ok: false, text: j.error ?? `HTTP ${r.status}` });
|
|
77
|
+
}
|
|
78
|
+
} catch (e) {
|
|
79
|
+
setUploadResult({ ok: false, text: e instanceof Error ? e.message : String(e) });
|
|
80
|
+
} finally {
|
|
81
|
+
setUploading(false);
|
|
82
|
+
setTimeout(() => setUploadResult(null), 3000);
|
|
83
|
+
}
|
|
84
|
+
}}
|
|
66
85
|
>
|
|
67
|
-
☁ 上报
|
|
86
|
+
☁ {uploading ? "上报中…" : "上报"}
|
|
68
87
|
</button>
|
|
88
|
+
{uploadResult && (
|
|
89
|
+
<span className={uploadResult.ok ? "field-ok" : "field-err"}>{uploadResult.text}</span>
|
|
90
|
+
)}
|
|
69
91
|
</>
|
|
70
92
|
) : (
|
|
71
93
|
<span>{err ? `加载失败:${err}` : "加载中…"}</span>
|
|
@@ -91,8 +113,12 @@ export function ReportModule() {
|
|
|
91
113
|
className={p.cwd === selCwd ? "active" : undefined}
|
|
92
114
|
title={p.cwd}
|
|
93
115
|
onClick={() => setSelCwd(p.cwd)}
|
|
116
|
+
style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: "0.5rem" }}
|
|
94
117
|
>
|
|
95
|
-
{shortDir(p.cwd) || p.cwd}
|
|
118
|
+
<span>{shortDir(p.cwd) || p.cwd}</span>
|
|
119
|
+
<span className="nav-tokens" title={fmtUsageFull(p.totalTokens)}>
|
|
120
|
+
{fmtTokens(realInput(p.totalTokens) + p.totalTokens.output)}
|
|
121
|
+
</span>
|
|
96
122
|
</li>
|
|
97
123
|
))}
|
|
98
124
|
</ul>
|
|
@@ -116,12 +142,12 @@ export function ReportModule() {
|
|
|
116
142
|
);
|
|
117
143
|
}
|
|
118
144
|
|
|
119
|
-
/** 右侧详情:标题(
|
|
145
|
+
/** 右侧详情:标题(仓库地址 + 用户 + token 汇总) + session 表格(分页,隐藏 0 token)。
|
|
120
146
|
* key=sel.cwd:换项目时重挂载,分页回到第 1 页。 */
|
|
121
147
|
function ProjectDetail({ p }: { p: ReportProject }) {
|
|
122
148
|
const [page, setPage] = useState(1);
|
|
123
|
-
// 过滤掉 0 token 的 session(tokenTotal 为 null 或
|
|
124
|
-
const rows = p.sessions.filter((s) => s.tokenTotal && s.tokenTotal
|
|
149
|
+
// 过滤掉 0 token 的 session(tokenTotal 为 null 或 真实输入+输出=0)
|
|
150
|
+
const rows = p.sessions.filter((s) => s.tokenTotal && realInput(s.tokenTotal) + s.tokenTotal.output > 0);
|
|
125
151
|
const pageCount = Math.max(1, Math.ceil(rows.length / PAGE));
|
|
126
152
|
const cur = Math.min(page, pageCount);
|
|
127
153
|
const pageRows = rows.slice((cur - 1) * PAGE, cur * PAGE);
|
|
@@ -129,15 +155,22 @@ function ProjectDetail({ p }: { p: ReportProject }) {
|
|
|
129
155
|
return (
|
|
130
156
|
<>
|
|
131
157
|
<div className="report-title">
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
158
|
+
{p.gitRemote && (
|
|
159
|
+
<span
|
|
160
|
+
className="rt-sum"
|
|
161
|
+
title={p.gitRemote}
|
|
162
|
+
style={{
|
|
163
|
+
flex: "0 0 60ch",
|
|
164
|
+
whiteSpace: "nowrap",
|
|
165
|
+
overflow: "hidden",
|
|
166
|
+
textOverflow: "ellipsis",
|
|
167
|
+
}}
|
|
168
|
+
>
|
|
169
|
+
🔗 {p.gitRemote}
|
|
170
|
+
</span>
|
|
171
|
+
)}
|
|
172
|
+
<span className="rt-sum" title={fmtUsageFull(p.totalTokens)} style={{ marginLeft: "auto", color: "#9dbefe" }}>
|
|
173
|
+
{fmtUsageLabeled(p.totalTokens)}
|
|
141
174
|
</span>
|
|
142
175
|
</div>
|
|
143
176
|
|
|
@@ -150,6 +183,7 @@ function ProjectDetail({ p }: { p: ReportProject }) {
|
|
|
150
183
|
<th>时间</th>
|
|
151
184
|
<th className="rt-num">输入 token</th>
|
|
152
185
|
<th className="rt-num">输出 token</th>
|
|
186
|
+
<th className="rt-num">总数</th>
|
|
153
187
|
</tr>
|
|
154
188
|
</thead>
|
|
155
189
|
<tbody>
|
|
@@ -160,8 +194,9 @@ function ProjectDetail({ p }: { p: ReportProject }) {
|
|
|
160
194
|
{s.sessionId.slice(0, 8)}
|
|
161
195
|
</td>
|
|
162
196
|
<td>{fmtDateTime(s.lastActive)}</td>
|
|
163
|
-
<td className="rt-num">{fmtTokens(s.tokenTotal
|
|
197
|
+
<td className="rt-num">{fmtTokens(realInput(s.tokenTotal!))}</td>
|
|
164
198
|
<td className="rt-num">{fmtTokens(s.tokenTotal!.output)}</td>
|
|
199
|
+
<td className="rt-num">{fmtTokens(realInput(s.tokenTotal!) + s.tokenTotal!.output)}</td>
|
|
165
200
|
</tr>
|
|
166
201
|
))}
|
|
167
202
|
</tbody>
|
|
@@ -2,7 +2,7 @@ import { useState } from "react";
|
|
|
2
2
|
import { useApi } from "../hooks/useApi";
|
|
3
3
|
import { useConversation } from "../hooks/useConversation";
|
|
4
4
|
import { useApp } from "../state/AppContext";
|
|
5
|
-
import {
|
|
5
|
+
import { fmtUsageFull, fmtUsageLabeled, realInput, shortDir } from "../lib/util";
|
|
6
6
|
import { Conversation } from "./Conversation";
|
|
7
7
|
|
|
8
8
|
/** 会话详情(右侧):顶部摘要(sid/cwd/token)+ 该会话对话。
|
|
@@ -27,9 +27,9 @@ export function SessionDetail({ sessionId }: { sessionId: string }) {
|
|
|
27
27
|
{shortDir(session.cwd)}
|
|
28
28
|
</span>
|
|
29
29
|
)}
|
|
30
|
-
{tokenTotal && (tokenTotal
|
|
30
|
+
{tokenTotal && (realInput(tokenTotal) > 0 || tokenTotal.output > 0) && (
|
|
31
31
|
<span className="detail-token" title={fmtUsageFull(tokenTotal)}>
|
|
32
|
-
{
|
|
32
|
+
{fmtUsageLabeled(tokenTotal)}
|
|
33
33
|
</span>
|
|
34
34
|
)}
|
|
35
35
|
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useMemo, useState } from "react";
|
|
2
2
|
import { useApp } from "../state/AppContext";
|
|
3
|
-
import { fmtDateTime,
|
|
3
|
+
import { fmtDateTime, fmtTokens, fmtUsageFull, realInput } from "../lib/util";
|
|
4
4
|
import type { SessionSummary } from "../types";
|
|
5
5
|
|
|
6
6
|
/** 会话树(按 cwd 分组、可折叠、token 角标、时间点)。纯展示 + onSelect 回调。
|
|
@@ -72,15 +72,12 @@ export function SessionTree({
|
|
|
72
72
|
<div className="sess-row">
|
|
73
73
|
<span className="sess-time">{fmtDateTime(s.lastActive)}</span>
|
|
74
74
|
<span className="sess-sid">{s.sessionId.slice(0, 8)}</span>
|
|
75
|
-
{s.tokenTotal && (s.tokenTotal
|
|
75
|
+
{s.tokenTotal && (realInput(s.tokenTotal) > 0 || s.tokenTotal.output > 0) && (
|
|
76
76
|
<span className="sess-tokens" title={fmtUsageFull(s.tokenTotal)}>
|
|
77
|
-
{
|
|
77
|
+
{fmtTokens(realInput(s.tokenTotal) + s.tokenTotal.output)}
|
|
78
78
|
</span>
|
|
79
79
|
)}
|
|
80
80
|
</div>
|
|
81
|
-
<div className="sess-sub">
|
|
82
|
-
{s.lastType ?? "?"} · {s.eventCount}
|
|
83
|
-
</div>
|
|
84
81
|
</li>
|
|
85
82
|
))}
|
|
86
83
|
</ul>
|
|
@@ -3,7 +3,7 @@ import { useApi } from "../hooks/useApi";
|
|
|
3
3
|
import { useAllCommits } from "../hooks/useAllCommits";
|
|
4
4
|
import { useApp } from "../state/AppContext";
|
|
5
5
|
import { aggregateCommitsByProject, aggregateTokenByProject, bucketByDay } from "../lib/aggregate";
|
|
6
|
-
import { fmtUsage, shortDir } from "../lib/util";
|
|
6
|
+
import { fmtUsage, realInput, shortDir } from "../lib/util";
|
|
7
7
|
|
|
8
8
|
type StatTab = "token" | "commits" | "day";
|
|
9
9
|
|
|
@@ -18,7 +18,7 @@ export function StatsModule() {
|
|
|
18
18
|
const projCommits = useMemo(() => aggregateCommitsByProject(commits), [commits]);
|
|
19
19
|
const commitsByDay = useMemo(() => bucketByDay(commits.map((c) => c.time), 7), [commits]);
|
|
20
20
|
|
|
21
|
-
const maxToken = Math.max(1, ...projTokens.map((r) => r.token
|
|
21
|
+
const maxToken = Math.max(1, ...projTokens.map((r) => realInput(r.token) + r.token.output));
|
|
22
22
|
const maxCommitCount = Math.max(1, ...projCommits.map((r) => r.count));
|
|
23
23
|
const maxDayCount = Math.max(1, ...commitsByDay.map((b) => b.count));
|
|
24
24
|
|
|
@@ -60,7 +60,7 @@ export function StatsModule() {
|
|
|
60
60
|
) : (
|
|
61
61
|
<div className="bar-list">
|
|
62
62
|
{projTokens.map((r) => {
|
|
63
|
-
const v = r.token
|
|
63
|
+
const v = realInput(r.token) + r.token.output;
|
|
64
64
|
return (
|
|
65
65
|
<div className="bar-row" key={r.cwd}>
|
|
66
66
|
<span className="bar-label" title={r.cwd}>
|
|
@@ -2,7 +2,7 @@ import { useMemo } from "react";
|
|
|
2
2
|
import { useApi } from "../hooks/useApi";
|
|
3
3
|
import { useAllCommits } from "../hooks/useAllCommits";
|
|
4
4
|
import { useApp } from "../state/AppContext";
|
|
5
|
-
import { fmtDateTime, fmtUsage, fmtUsageFull, shortDir, sumTokenUsage } from "../lib/util";
|
|
5
|
+
import { fmtDateTime, fmtUsage, fmtUsageFull, realInput, shortDir, sumTokenUsage } from "../lib/util";
|
|
6
6
|
|
|
7
7
|
/** 汇总视图(system 模块临时占位,Step 5 替换为 SystemModule):
|
|
8
8
|
* Token 按会话 + 代码提交按时间。提交拉取改用 useAllCommits(与 Overview/Stats 共用)。 */
|
|
@@ -13,7 +13,7 @@ export function SummaryView() {
|
|
|
13
13
|
const tokenRows = useMemo(
|
|
14
14
|
() =>
|
|
15
15
|
sessions
|
|
16
|
-
.filter((s) => s.tokenTotal && (s.tokenTotal
|
|
16
|
+
.filter((s) => s.tokenTotal && (realInput(s.tokenTotal) > 0 || s.tokenTotal.output > 0))
|
|
17
17
|
.slice()
|
|
18
18
|
.sort((a, b) => b.lastActive - a.lastActive),
|
|
19
19
|
[sessions],
|
|
@@ -24,7 +24,7 @@ export function SummaryView() {
|
|
|
24
24
|
const commitCount = commits.length;
|
|
25
25
|
const added = commits.reduce((n, c) => n + (c.added || 0), 0);
|
|
26
26
|
const deleted = commits.reduce((n, c) => n + (c.deleted || 0), 0);
|
|
27
|
-
const hasTokens = tokenSum.total
|
|
27
|
+
const hasTokens = realInput(tokenSum.total) > 0 || tokenSum.total.output > 0;
|
|
28
28
|
|
|
29
29
|
return (
|
|
30
30
|
<div id="summary-view" className="summary-view">
|
package/ui/lib/aggregate.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// 纯聚合函数(统计模块用):按项目/按天分组,无副作用。
|
|
2
2
|
import type { SessionSummary, TokenUsage } from "../types";
|
|
3
|
+
import { realInput } from "./util";
|
|
3
4
|
|
|
4
5
|
export interface ProjTokenRow {
|
|
5
6
|
cwd: string;
|
|
@@ -28,7 +29,7 @@ export function aggregateTokenByProject(sessions: SessionSummary[]): ProjTokenRo
|
|
|
28
29
|
m.set(s.cwd, r);
|
|
29
30
|
}
|
|
30
31
|
return [...m.values()].sort(
|
|
31
|
-
(a, b) => b.token
|
|
32
|
+
(a, b) => realInput(b.token) + b.token.output - (realInput(a.token) + a.token.output),
|
|
32
33
|
);
|
|
33
34
|
}
|
|
34
35
|
|
package/ui/lib/util.ts
CHANGED
|
@@ -41,27 +41,48 @@ export function safeJson(v: unknown): string {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
/** 紧凑数字:1234 → "1.2k",1234567 → "1.2M",<1000 原样。 */
|
|
44
|
+
/** 紧凑数字:1234 → "1.2k",1234567 → "1.2M",1.03e9 → "1B",<1000 原样。 */
|
|
45
45
|
export function fmtTokens(n: number): string {
|
|
46
46
|
if (!Number.isFinite(n) || n < 1000) return String(n);
|
|
47
|
-
if (n < 1_000_000) return trimZero((n /
|
|
48
|
-
return trimZero((n / 1_000_000).toFixed(1)) + "M";
|
|
47
|
+
if (n < 1_000_000) return trimZero((n / 1_000).toFixed(1)) + "k";
|
|
48
|
+
if (n < 1_000_000_000) return trimZero((n / 1_000_000).toFixed(1)) + "M";
|
|
49
|
+
if (n < 1_000_000_000_000) return trimZero((n / 1_000_000_000).toFixed(2)) + "B";
|
|
50
|
+
return trimZero((n / 1_000_000_000_000).toFixed(2)) + "T";
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
/** token
|
|
52
|
-
|
|
53
|
+
/** 真实输入 token = 未缓存输入 + 缓存写 + 缓存读(每次 API 请求的完整 prompt 上下文)。
|
|
54
|
+
* 直接累加 Anthropic API 返回的原始字段,不乘任何系数。 */
|
|
55
|
+
export function realInput(u?: TokenUsage | null): number {
|
|
56
|
+
if (!u) return 0;
|
|
57
|
+
return u.input + u.cacheCreation + u.cacheRead;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** token 用量简写:↑真实输入 ↓输出(真实输入 = 未缓存 + 缓存写 + 缓存读)。无值返回空串。 */
|
|
61
|
+
export function fmtUsage(u?: TokenUsage | null): string {
|
|
62
|
+
if (!u) return "";
|
|
63
|
+
return `↑${fmtTokens(realInput(u))} ↓${fmtTokens(u.output)}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** token 用量三段式:↑真实输入 ↓输出 · 总数(真实输入+输出)。报表/会话导航与详情用。 */
|
|
67
|
+
export function fmtUsageTotal(u?: TokenUsage | null): string {
|
|
53
68
|
if (!u) return "";
|
|
54
|
-
return `↑${fmtTokens(u
|
|
69
|
+
return `↑${fmtTokens(realInput(u))} ↓${fmtTokens(u.output)} · ${fmtTokens(realInput(u) + u.output)}`;
|
|
55
70
|
}
|
|
56
71
|
|
|
57
|
-
/** 完整 token
|
|
72
|
+
/** 完整 token 用量,用于 title 提示:真实输入合计 + 输出 + 四字段明细(均原始值,不加权)。 */
|
|
58
73
|
export function fmtUsageFull(u?: TokenUsage | null): string {
|
|
59
74
|
if (!u) return "";
|
|
60
|
-
return
|
|
75
|
+
return `真实输入 ${realInput(u)} · 输出 ${u.output} · (未缓存 ${u.input} · 缓存写 ${u.cacheCreation} · 缓存读 ${u.cacheRead})`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** token 用量带标签:输入 X · 输出 Y · 总数 Z(真实输入+输出)。会话详情/报表标题用。 */
|
|
79
|
+
export function fmtUsageLabeled(u?: TokenUsage | null): string {
|
|
80
|
+
if (!u) return "";
|
|
81
|
+
return `输入 ${fmtTokens(realInput(u))} · 输出 ${fmtTokens(u.output)} · 总数 ${fmtTokens(realInput(u) + u.output)}`;
|
|
61
82
|
}
|
|
62
83
|
|
|
63
84
|
function trimZero(s: string): string {
|
|
64
|
-
return s.replace(
|
|
85
|
+
return s.replace(/\.?0+$/, "");
|
|
65
86
|
}
|
|
66
87
|
|
|
67
88
|
/** 累加 assistant 消息的 usage(对话视图会话级汇总)。 */
|
package/ui/style.css
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
--c-prompt: #569cd6;
|
|
18
18
|
--c-posttool: #c586c0;
|
|
19
19
|
--c-pretool: #ce9178;
|
|
20
|
-
--nav-w:
|
|
20
|
+
--nav-w: 98px;
|
|
21
21
|
--tree-w: 200px;
|
|
22
22
|
--footer-h: 170px;
|
|
23
23
|
/* 设计 token:间距 / 字号 / 圆角 / 图标尺寸 */
|
|
@@ -170,7 +170,10 @@ h2 {
|
|
|
170
170
|
}
|
|
171
171
|
ul { list-style: none; margin: 0; padding: 0; }
|
|
172
172
|
|
|
173
|
-
|
|
173
|
+
/* 固定宽度而非 flex: 0 0 auto:basis auto 取 content max-content,
|
|
174
|
+
超长会话触发大规模 reflow 时 Chromium 会重算 nav-item padding 导致 content 收缩,
|
|
175
|
+
进而让整个导航栏/会话树左移错位。固定值消除对 reflow 的依赖。 */
|
|
176
|
+
#nav-panel { flex: 0 0 var(--nav-w); background: var(--sidebar); overflow: hidden; border-right: 1px solid var(--border); }
|
|
174
177
|
#sessions-panel ul { flex: 1; overflow: auto; }
|
|
175
178
|
#events-panel { flex: 1 1 0; overflow: hidden; }
|
|
176
179
|
|