feat(web-shell): polish stats table layout and todo panel UI (#6559)

* feat(web-shell): polish stats table layout and todo panel UI

- Use CSS grid for model usage table with fixed column widths
- Add loading spinner for in_progress todo items
- Add strikethrough for completed todo items
- Introduce nested variant for PivotRow to show thoughts as output sub-item
- Clarify i18n labels: stats.prompt -> Input Tokens, stats.output -> Output Tokens

* fix(web-shell): address review suggestions for stats table and todo panel

- Add prefers-reduced-motion media query for todo spinner (accessibility)
- Right-align numeric columns in model usage table for magnitude comparison
- Consolidate duplicate i18n keys (stats.prompt/output → stats.inputTokens/outputTokens)

* fix(web-shell): address stats review feedback

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>
Co-authored-by: 易良 <1204183885@qq.com>
This commit is contained in:
ytahdn 2026-07-09 14:06:51 +08:00 committed by GitHub
parent 8c896f6b09
commit 48e5d5d0d7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 69 additions and 20 deletions

View file

@ -118,6 +118,14 @@
min-height: 20px;
}
.modelUsageRow {
display: grid;
grid-template-columns:
minmax(14ch, 1.4fr)
repeat(3, minmax(9ch, 1fr));
column-gap: 16px;
}
.tableNameCol {
color: var(--foreground);
font-weight: 700;
@ -142,6 +150,13 @@
white-space: nowrap;
}
.tableValueCol {
min-width: 9ch;
color: var(--foreground);
text-align: right;
white-space: nowrap;
}
/* ---------- Semantic colors ---------- */
.success {

View file

@ -247,11 +247,13 @@ function StatsOverview({ status }: { status: DaemonSessionStatsStatus }) {
<div className={styles.spacer} />
{/* Header */}
<div className={styles.tableRow}>
<div className={`${styles.tableRow} ${styles.modelUsageRow}`}>
<span className={styles.tableNameCol}>{t('stats.modelUsage')}</span>
<span className={styles.tableNumCol}>{t('stats.reqs')}</span>
<span className={styles.tableNumCol}>{t('stats.inputTokens')}</span>
<span className={styles.tableNumCol}>
<span className={styles.tableValueCol}>{t('stats.reqs')}</span>
<span className={styles.tableValueCol}>
{t('stats.inputTokens')}
</span>
<span className={styles.tableValueCol}>
{t('stats.outputTokens')}
</span>
</div>
@ -259,15 +261,18 @@ function StatsOverview({ status }: { status: DaemonSessionStatsStatus }) {
{/* Rows */}
{entries.map((e) => (
<div key={e.key} className={styles.tableRow}>
<div
key={e.key}
className={`${styles.tableRow} ${styles.modelUsageRow}`}
>
<span className={styles.tableNameCol}>{e.label}</span>
<span className={styles.tableNumCol}>
<span className={styles.tableValueCol}>
{e.metrics.api.totalRequests}
</span>
<span className={`${styles.tableNumCol} ${styles.warning}`}>
<span className={`${styles.tableValueCol} ${styles.warning}`}>
{e.metrics.tokens.prompt.toLocaleString()}
</span>
<span className={`${styles.tableNumCol} ${styles.warning}`}>
<span className={`${styles.tableValueCol} ${styles.warning}`}>
{e.metrics.tokens.candidates.toLocaleString()}
</span>
</div>
@ -366,7 +371,7 @@ function ModelStatsCard({ status }: { status: DaemonSessionStatsStatus }) {
))}
/>
<PivotRow
metric={t('stats.prompt')}
metric={t('stats.inputTokens')}
values={vals((m) => m.tokens.prompt.toLocaleString())}
variant="sub"
/>
@ -382,6 +387,11 @@ function ModelStatsCard({ status }: { status: DaemonSessionStatsStatus }) {
variant="sub"
/>
)}
<PivotRow
metric={t('stats.outputTokens')}
values={vals((m) => m.tokens.candidates.toLocaleString())}
variant="sub"
/>
{hasThoughts && (
<PivotRow
metric={t('stats.thoughts')}
@ -389,11 +399,6 @@ function ModelStatsCard({ status }: { status: DaemonSessionStatsStatus }) {
variant="sub"
/>
)}
<PivotRow
metric={t('stats.output')}
values={vals((m) => m.tokens.candidates.toLocaleString())}
variant="sub"
/>
</div>
);
}

View file

@ -118,6 +118,15 @@
font-size: 13px;
}
.loadingIcon {
width: 12px;
height: 12px;
border: 2px solid color-mix(in srgb, var(--agent-blue-500) 24%, transparent);
border-top-color: var(--agent-blue-500);
border-radius: 50%;
animation: todoPanelSpin 0.8s linear infinite;
}
.content {
min-width: 0;
overflow: hidden;
@ -131,13 +140,33 @@
color: var(--muted-foreground);
}
.completed .content {
text-decoration: line-through;
text-decoration-color: color-mix(
in srgb,
var(--muted-foreground) 72%,
transparent
);
}
.inProgress .icon,
.inProgress .content {
color: var(--foreground);
font-weight: 600;
}
.pending .icon,
.pending .content {
color: var(--muted-foreground);
}
@keyframes todoPanelSpin {
to {
transform: rotate(1turn);
}
}
@media (prefers-reduced-motion: reduce) {
.loadingIcon {
animation: none;
}
}

View file

@ -72,7 +72,11 @@ export const TodoPanel = memo(function TodoPanel({
className={`${styles.item} ${getStatusClass(todo.status)}`}
>
<span className={styles.icon} aria-hidden="true">
{getTodoStatusIcon(todo.status)}
{todo.status === 'in_progress' ? (
<span className={styles.loadingIcon} />
) : (
getTodoStatusIcon(todo.status)
)}
</span>
<span className={styles.content} title={todo.content}>
{todo.content}

View file

@ -1431,11 +1431,9 @@ const EN: Messages = {
'stats.modified': 'Modified:',
'stats.noApiCalls': 'No API calls have been made in this session.',
'stats.noToolCalls': 'No tool calls have been made in this session.',
'stats.output': 'Output',
'stats.outputTokens': 'Output Tokens',
'stats.overview': 'Session Overview',
'stats.performance': 'Performance',
'stats.prompt': 'Prompt',
'stats.prompts': 'Prompts',
'stats.rejected': 'Rejected:',
'stats.reqs': 'Reqs',
@ -3073,11 +3071,9 @@ const ZH: Messages = {
'stats.modified': '已修改:',
'stats.noApiCalls': '本次会话暂无 API 调用。',
'stats.noToolCalls': '本次会话暂无工具调用。',
'stats.output': '输出',
'stats.outputTokens': '输出 Token',
'stats.overview': '会话概览',
'stats.performance': '性能',
'stats.prompt': '提示',
'stats.prompts': '对话轮次',
'stats.rejected': '已拒绝:',
'stats.reqs': '请求',