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.
@@ -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.input)}</b>
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.commitCount} 提交 · +{fmtTokens(data.totals.added)}/-{fmtTokens(data.totals.deleted)}
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
- /** 右侧详情:标题(输入/输出 token 汇总 + 提交汇总) + session 表格(分页,隐藏 0 token)。
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 或 input+output=0)
124
- const rows = p.sessions.filter((s) => s.tokenTotal && s.tokenTotal.input + s.tokenTotal.output > 0);
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
- <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)}` : ""}
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!.input)}</td>
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 { fmtUsage, fmtUsageFull, shortDir } from "../lib/util";
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.input > 0 || tokenTotal.output > 0) && (
30
+ {tokenTotal && (realInput(tokenTotal) > 0 || tokenTotal.output > 0) && (
31
31
  <span className="detail-token" title={fmtUsageFull(tokenTotal)}>
32
- {fmtUsage(tokenTotal)}
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, fmtUsage, fmtUsageFull } from "../lib/util";
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.input > 0 || s.tokenTotal.output > 0) && (
75
+ {s.tokenTotal && (realInput(s.tokenTotal) > 0 || s.tokenTotal.output > 0) && (
76
76
  <span className="sess-tokens" title={fmtUsageFull(s.tokenTotal)}>
77
- {fmtUsage(s.tokenTotal)}
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.input + r.token.output));
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.input + r.token.output;
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.input > 0 || s.tokenTotal.output > 0))
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.input > 0 || tokenSum.total.output > 0;
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">
@@ -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.input + b.token.output - (a.token.input + a.token.output),
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 / 1000).toFixed(1)) + "k";
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 用量简写:↑输入 ↓输出(cache 明细放 title)。无值返回空串。 */
52
- export function fmtUsage(u?: Pick<TokenUsage, "input" | "output"> | null): string {
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.input)} ↓${fmtTokens(u.output)}`;
69
+ return `↑${fmtTokens(realInput(u))} ↓${fmtTokens(u.output)} · ${fmtTokens(realInput(u) + u.output)}`;
55
70
  }
56
71
 
57
- /** 完整 token 用量(input/output/cacheCreation/cacheRead),用于 title 提示。 */
72
+ /** 完整 token 用量,用于 title 提示:真实输入合计 + 输出 + 四字段明细(均原始值,不加权)。 */
58
73
  export function fmtUsageFull(u?: TokenUsage | null): string {
59
74
  if (!u) return "";
60
- return `输入 ${u.input} · 输出 ${u.output} · 缓存写 ${u.cacheCreation} · 缓存读 ${u.cacheRead}`;
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(/\.0$/, "");
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: 200px;
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
- #nav-panel { flex: 0 0 auto; background: var(--sidebar); overflow: hidden; border-right: 1px solid var(--border); }
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