Skip to content

Commit 91a09b1

Browse files
committed
feat: show codex live session request info
1 parent 196cd24 commit 91a09b1

6 files changed

Lines changed: 208 additions & 13 deletions

File tree

.agents/skills/gettokens-domain-engineering/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ This skill unifies the technical rules for building, styling, and debugging GetT
133133
- Data ownership starts in the CLIProxyAPI fork. Add an in-memory runtime tracker and a read-only management endpoint first; then expose it through `internal/wailsapp`, root `main.App`, generated `frontend/wailsjs`, and finally the React feature.
134134
- Keep the UI read-only. Do not add request cancel, replay, forced WebSocket recovery, or full payload display unless a later requirement explicitly scopes the action and safety model.
135135
- Default list rows should stay low-noise. Show only the operator-facing identity pair requested for the feed: `sessionID / projectName` and `account / http|ws`. Keep status, model, timing, request ids, execution ids, and redacted diagnostics in detail panes.
136-
- `projectName` is a display label that may be enriched by Wails/frontend from trusted local metadata. Add it as an optional DTO field end-to-end (`internal/wailsapp` -> root `main.App` -> generated `frontend/wailsjs` -> feature model) and fall back to an explicit unknown-project label when absent.
136+
- `projectName` is a display label owned by the CLIProxyAPI live tracker. The sidecar may enrich it from trusted local Codex session metadata (`CODEX_HOME || ~/.codex` session JSONL) before returning the live snapshot; GetTokens should only pass the optional DTO field through Wails/root bindings/frontend model and fall back to an explicit unknown-project label when absent. Do not add Wails/frontend compatibility lookup for old sidecars unless a later requirement explicitly reintroduces compatibility.
137137
- Account resource surfaces inside live-session details should reuse accounts-domain components such as `QuotaBars` and `BillingBalance`. Add a small adapter from live request `quota` / `billing` DTOs into account display shapes instead of copying quota or balance JSX into live sessions.
138138
- Never display raw request/response payloads, credentials, bearer tokens, cookies, or unredacted error bodies. Diagnostic copy must be redacted and bounded.
139139
- When correlating WebSocket and HTTP usage, preserve request ids through context. Usage hooks should update an existing WebSocket request when the request id is known, not create a duplicate HTTP-only session.

docs-linhay/memory/2026-05-25.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@
1212
- Settings Release Panel 重设计:`frontend/src/features/settings/components/SettingsReleasePanel.tsx` 改为信息矩阵 + 右侧操作区,并为当前版本、最新 release、GetTokens Git Hash、CLIProxyAPI Git Hash 增加 GitHub 外链入口。URL 构造逻辑收敛到 `settingsRelease.ts`,占位 hash(`DEV```)不显示外链;浏览器预览下外链走 `window.open`,Wails 桌面下走 `BrowserOpenURL`。验证:`npm run test:unit``npm run typecheck``npm run build` 通过;Playwright 预览截图落位 `docs-linhay/screenshots/20260525/settings/20260525-settings-release-panel-after-v02.png`。375px 预览暴露的是既有固定侧边栏桌面壳宽度问题,不作为本次 release panel 阻塞。
1313
- Codex `Feature 配置` 页面完成配置项语义分组与控件收敛:root 按启动默认、模型输出、权限沙箱、工作区文档、工具集成、高级兼容分组;features 按推荐稳定、实验性、高级、兼容旧项分组;notice 按安全提示、迁移提示、结构化 notice 分组;model providers 按 provider id 分组。分组只影响展示层,不改变 draft、preview、save 的字段路径和保留式 TOML patch 语义。固定 enum options 已统一渲染为 `SegmentedControl`,bool / boolean 继续使用 `ToggleSwitch`,并按本地 Codex 源码 `7d47056ea4` 的 `codex-rs/core/config.schema.json` 校准 `approvals_reviewer`、`personality`、`model_auto_compact_token_limit_scope`、`model_providers.*.wire_api` 等选项;布尔型缺省假值不会被误显示成 `false` 枚举项。验证:`go test ./internal/wailsapp -run 'TestGetCodexFeatureConfigReturnsTypedRootDefinitionsAndValues|TestGetCodexFeatureConfigReturnsAllModelProviderSchemaFields'`、`node --test frontend/src/features/status/tests/codexFeatureConfig.test.mjs`、`npm --prefix frontend run typecheck`、`npm --prefix frontend run build` 通过;浏览器确认 `http://127.0.0.1:34115/#frame=codex` 已出现实际非空分组标题,且 DOM 命中 `SegmentedControl: 16`、`ToggleSwitch: 79`、`selectCount: 0`。
1414
- 2026-05-25 的 Account Routing Engine 补充实现已落地 shadow/event ledger:`internal/wailsapp/channel_routing.go` 新增 `shadowEnabled` / `shadowRouteMode`、`ListChannelRouteEvents`、`snapshotVersion`、`policyVersion` 和安全 route event ledger;前端 `ChannelRoutingWorkbench` 已显示 shadow 开关、shadow 选择和 snapshot/policy 摘要;Codex / Claude 浏览器预览截图已落位 `docs-linhay/spaces/20260524-account-routing-engine/screenshots/20260525/codex/20260525-codex-account-routing-shadow-after-v01.png` 与 `docs-linhay/spaces/20260524-account-routing-engine/screenshots/20260525/claude/20260525-claude-account-routing-shadow-after-v01.png`。验证:`go test . ./internal/wailsapp -run 'Test.*ChannelRouting|TestChannelRouteEvent'`、`go test ./...`、`go test ./...`(CLIProxyAPI fork)、`npm --prefix frontend run test:unit -- src/features/channel-routing/tests/channelRouting.test.mjs`、`npm --prefix frontend run typecheck`、`npm --prefix frontend run build` 通过。当前仍未完成真实 upstream 请求冒烟和最后的 legacy path 收敛。
15+
- Codex live sessions 请求信息补齐完成:新建 `docs-linhay/spaces/20260525-codex-live-session-request-info/`;按用户确认不兼容旧 sidecar,`projectName` 主来源改到 CLIProxyAPI fork 的 live tracker。sidecar 在生成 snapshot 时扫描 `CODEX_HOME || ~/.codex` 下 `sessions/` 与 `archived_sessions/` 的 Codex JSONL,从 `session_meta.id`、conversation/window/session key、`turn_context.cwd`、`session_meta.cwd` 或 git repo 反查项目名并返回 `projectName`,lookup 加 10 秒 TTL;GetTokens Wails 侧只透传 sidecar 字段,已移除本地兼容反查层。详情页 `请求时间线` 从事件行改为请求监控信息,展示 request/client/upstream id、账号、provider、transport、耗时、速率、token、错误和关键事件摘要。验证:CLIProxyAPI fork `go test ./internal/gettokenshooks -run LiveSessions`、GetTokens `go test ./internal/wailsapp -run CodexLive`、`node --test frontend/src/features/codex-live-sessions/model.test.mjs`、`npm --prefix frontend run typecheck` 通过;浏览器预览截图已归档到 `docs-linhay/spaces/20260525-codex-live-session-request-info/screenshots/20260525/codex-live-sessions/20260525-codex-live-sessions-request-info-after-v01.png`。

docs-linhay/references/CLIProxyAPI

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Subproject commit af76c718c6d34bae1945cb26c5fa70a259a4af46
1+
Subproject commit 3930e3a92aab2227a49ce747a92313cc0daeba6b
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Codex Live Session Request Info
2+
3+
## 背景
4+
`#frame=codex&workspace=live-sessions` 当前运行会话列表依赖 sidecar live snapshot。sidecar 运行态能提供 session / request / transport 等信息,但部分场景没有 `projectName`,列表只能显示未知项目;详情页 Timeline 也偏事件流,不能像请求监控页一样快速看到某次请求的关键标识、模型、账号、链路、耗时和 token 信息。
5+
6+
## 目标
7+
1. `SessionRow` 显示项目名称时,若 live snapshot 缺少 `projectName`,由 CLIProxyAPI sidecar 按本地 Codex 会话信息反查并补齐后返回。
8+
2. `SessionDetail` 的 Timeline 区域改为请求级具体信息视图,参考 `http://cpa.host.dxy/user/monitor` 的监控信息密度,优先显示可用于排查的请求字段。
9+
10+
## 范围
11+
- Sidecar:CLIProxyAPI live session tracker 在生成 snapshot 时,对缺失项目名的 live session 做本地 `.codex` 会话反查补全,并通过 `projectName` 返回。
12+
- Wails:`GetCodexLiveSessionsSnapshot` 只透传 sidecar snapshot,不保留兼容旧 sidecar 的本地反查层。
13+
- 前端:`CodexLiveSessionDetail.tsx``Timeline` 从事件列表改为请求列表,展示 request id、client/upstream request id、model、auth、transport、status、timing、usage、error 与关键事件摘要。
14+
- 测试:补充 sidecar 项目名补全回归、Wails 透传回归和前端源码/模型约束测试。
15+
16+
## 非目标
17+
- 不兼容旧 sidecar;`projectName` 主来源在 CLIProxyAPI fork。
18+
- 不展示请求 payload、headers、API key、cookie 或任何未脱敏敏感内容。
19+
- 不新增请求控制、重试、取消等操作。
20+
21+
## 验收标准
22+
1. Given sidecar live tracker 内的 live session 没有 `projectName`,但 `sessionID``executionSessionID``downstreamSessionID``codexWindowID` 或 Codex conversation id 能匹配本地 Codex JSONL;When 打开 live sessions 页面;Then 列表显示 sidecar 反查到的项目名。
23+
2. Given sidecar 已经提供 `projectName`;When GetTokens 请求 live snapshot;Then Wails/root/frontend 保留 sidecar 原值并透传,不执行二次本地扫描覆盖。
24+
3. Given 选中某个 live session;When 查看详情 Timeline;Then 页面展示请求级字段和关键事件摘要,而不是只展示 `lane.kind + label` 的事件行。
25+
4. Given 请求包含 error 或 timing / usage;When 查看 Timeline;Then 能看到状态、错误、耗时、速率和 token 用量,且不展示 payload 或密钥。
26+
5. 自动化验证至少覆盖 CLIProxyAPI fork 的 `go test ./internal/gettokenshooks -run LiveSessions`、GetTokens 的 `go test ./internal/wailsapp -run CodexLive``node --test frontend/src/features/codex-live-sessions/model.test.mjs``npm --prefix frontend run typecheck`
27+
28+
## 验收记录
29+
30+
- 2026-05-25:已完成 sidecar 项目名补全、Wails 透传、详情请求时间线改造和浏览器预览验收。
31+
- 截图:`screenshots/20260525/codex-live-sessions/20260525-codex-live-sessions-request-info-after-v01.png`
32+
- 自动化验证:
33+
- `go test ./internal/wailsapp -run CodexLive`
34+
- `go test ./internal/gettokenshooks -run LiveSessions`(CLIProxyAPI fork)
35+
- `node --test frontend/src/features/codex-live-sessions/model.test.mjs`
36+
- `npm --prefix frontend run typecheck`
37+
38+
## 设计稿入口
39+
40+
- 本期设计稿:`(未产出)`
41+
- 约束:单期只保留一个 HTML 文件;若存在多稿对比,也必须收敛在同一个 HTML 文件内。
42+
43+
## Worktree 映射
44+
45+
- branch:`feat/20260525-codex-live-session-request-info`
46+
- worktree:`../GetTokens-worktrees/20260525-codex-live-session-request-info/`
47+
48+
## 相关链接
49+
- 本地页面:`http://127.0.0.1:34115/#frame=codex&workspace=live-sessions`
50+
- 参考监控页:`http://cpa.host.dxy/user/monitor`
51+
- 目标组件:`frontend/src/features/codex-live-sessions/components/CodexLiveSessionFeed.tsx`
52+
- 目标组件:`frontend/src/features/codex-live-sessions/components/CodexLiveSessionDetail.tsx`
53+
54+
## 当前状态
55+
- 状态:implemented
56+
- 最近更新:2026-05-25

frontend/src/features/codex-live-sessions/components/CodexLiveSessionDetail.tsx

Lines changed: 135 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export function SessionDetail({
7171
<SessionCard session={session} request={request} t={t} />
7272
<TransportLane events={timeline} t={t} />
7373
<TimingMetrics request={request} t={t} />
74-
<Timeline events={timeline} t={t} />
74+
<Timeline requests={session.requests} fallbackEvents={timeline} t={t} />
7575
<DiagnosticSummary diagnostic={diagnostic} t={t} />
7676
</div>
7777
</div>
@@ -272,27 +272,151 @@ function SessionCard({ session, request, t }: { session: CodexLiveSession; reque
272272
);
273273
}
274274

275-
function Timeline({ events, t }: { events: readonly CodexLiveTimelineEvent[]; t: Translate }) {
275+
function Timeline({
276+
requests,
277+
fallbackEvents,
278+
t,
279+
}: {
280+
requests: readonly CodexLiveRequest[];
281+
fallbackEvents: readonly CodexLiveTimelineEvent[];
282+
t: Translate;
283+
}) {
276284
return (
277285
<div className="grid gap-3">
278286
<div className="font-mono text-[length:var(--font-size-ui-sm)] font-black uppercase tracking-[0.16em] text-[var(--text-muted)]">
279287
{t('codex_live_sessions.request_timeline')}
280288
</div>
281-
<div className="divide-y-2 divide-[var(--border-color)] border-2 border-[var(--border-color)]">
282-
{events.map((event) => (
283-
<div key={event.id} className="grid gap-3 p-3 md:grid-cols-[5.2rem_8rem_1fr]">
284-
<span className="font-mono text-[length:var(--font-size-ui-sm)] font-black text-[var(--text-muted)]">{event.at}</span>
285-
<span className="font-mono text-[length:var(--font-size-ui-sm)] font-black uppercase text-[var(--text-primary)]">
286-
{event.lane}.{event.kind}
287-
</span>
288-
<span className="min-w-0 text-[length:var(--font-size-ui-lg)] font-bold text-[var(--text-muted)]">{event.label}</span>
289+
<div className="grid gap-3">
290+
{requests.length === 0 ? (
291+
<div className="border-2 border-dashed border-[var(--border-color)] bg-[var(--bg-surface)] p-3">
292+
<div className="font-mono text-[length:var(--font-size-ui-sm)] font-black uppercase text-[var(--text-muted)]">
293+
{t('codex_live_sessions.unknown_request')}
294+
</div>
295+
<div className="mt-2 grid gap-1">
296+
{fallbackEvents.slice(0, 4).map((event) => (
297+
<EventLine key={event.id} event={event} />
298+
))}
299+
</div>
289300
</div>
290-
))}
301+
) : (
302+
requests.map((request) => {
303+
const timing = request.timing;
304+
const usage = request.usage;
305+
const requestRows: Array<[string, string]> = [
306+
[t('codex_live_sessions.meta_client_request'), request.clientRequestID || t('codex_live_sessions.unknown')],
307+
[t('codex_live_sessions.meta_upstream_request'), request.upstreamRequestID || t('codex_live_sessions.unknown')],
308+
[t('codex_live_sessions.account_label'), request.authLabel || request.authID || t('codex_live_sessions.unknown_auth')],
309+
[t('codex_live_sessions.account_provider'), request.provider || t('codex_live_sessions.unknown')],
310+
[t('codex_live_sessions.account_transport'), `${request.downstreamTransport}${request.upstreamTransport}`],
311+
];
312+
if (request.proxyRoute) {
313+
requestRows.push([t('codex_live_sessions.account_proxy'), request.proxyRoute]);
314+
}
315+
316+
const metricRows: Array<[string, string]> = [
317+
[t('codex_live_sessions.timing_total'), formatOptionalDuration(timing?.totalDurationMs)],
318+
[t('codex_live_sessions.timing_ttft'), formatOptionalDuration(timing?.firstEventMs)],
319+
[t('codex_live_sessions.timing_first_token'), formatOptionalDuration(timing?.firstTokenMs)],
320+
[t('codex_live_sessions.timing_output_rate'), formatOptionalRate(timing?.outputTokensPerSecond)],
321+
[t('codex_live_sessions.tokens_total'), usage ? usage.totalTokens.toLocaleString() : 'n/a'],
322+
[t('codex_live_sessions.tokens_output'), usage ? usage.outputTokens.toLocaleString() : 'n/a'],
323+
];
324+
325+
return (
326+
<div key={request.requestID} className="border-2 border-[var(--border-color)] bg-[var(--bg-surface)]">
327+
<div className="grid gap-2 border-b-2 border-[var(--border-color)] p-3 lg:grid-cols-[1fr_auto] lg:items-start">
328+
<div className="min-w-0">
329+
<div className="flex flex-wrap items-center gap-2">
330+
<span className="border border-[var(--border-color)] bg-[var(--bg-main)] px-2 py-0.5 font-mono text-[length:var(--font-size-ui-xs)] font-black uppercase text-[var(--text-primary)]">
331+
{t(statusLabelKeys[request.status])}
332+
</span>
333+
<span className="font-mono text-[length:var(--font-size-ui-xs)] font-black uppercase text-[var(--text-muted)]">
334+
#{request.sequence} · {request.model}
335+
</span>
336+
</div>
337+
<div className="mt-2 truncate font-mono text-[length:var(--font-size-ui-lg)] font-black text-[var(--text-primary)]">
338+
{request.requestID}
339+
</div>
340+
</div>
341+
<div className="text-left font-mono text-[length:var(--font-size-ui-xs)] font-black uppercase text-[var(--text-muted)] lg:text-right">
342+
<div>{request.startedAt}</div>
343+
<div>{request.completedAt || t('codex_live_sessions.status_streaming')}</div>
344+
</div>
345+
</div>
346+
347+
<div className="grid gap-3 p-3 xl:grid-cols-[1fr_1fr]">
348+
<div className="grid gap-1">
349+
{requestRows.map(([label, value]) => (
350+
<RequestInfoRow key={label} label={label} value={value} />
351+
))}
352+
</div>
353+
<div className="grid grid-cols-2 gap-2 md:grid-cols-3">
354+
{metricRows.map(([label, value]) => (
355+
<MetricCell key={label} label={label} value={value} />
356+
))}
357+
</div>
358+
</div>
359+
360+
{request.error ? (
361+
<div className="border-t border-[var(--border-color)] px-3 py-2 text-[length:var(--font-size-ui-sm)] font-bold text-[var(--color-danger)]">
362+
{request.error.statusCode ? `${request.error.statusCode} ` : ''}
363+
{request.error.code ? `${request.error.code}: ` : ''}
364+
{request.error.message}
365+
</div>
366+
) : null}
367+
368+
<div className="border-t border-[color:color-mix(in_srgb,var(--border-color)_45%,transparent)] p-3">
369+
<div className="grid gap-1">
370+
{request.timeline.slice(0, 5).map((event) => (
371+
<EventLine key={event.id} event={event} />
372+
))}
373+
{request.timeline.length === 0 ? (
374+
<span className="text-[length:var(--font-size-ui-sm)] font-bold text-[var(--text-muted)]">
375+
{t('codex_live_sessions.no_event')}
376+
</span>
377+
) : null}
378+
</div>
379+
</div>
380+
</div>
381+
);
382+
})
383+
)}
291384
</div>
292385
</div>
293386
);
294387
}
295388

389+
function RequestInfoRow({ label, value }: { label: string; value: string }) {
390+
return (
391+
<div className="grid min-w-0 gap-2 border-b border-[color:color-mix(in_srgb,var(--border-color)_28%,transparent)] py-1 font-mono text-[length:var(--font-size-ui-sm)] md:grid-cols-[9.5rem_1fr]">
392+
<span className="font-black uppercase text-[var(--text-muted)]">{label}</span>
393+
<span className="min-w-0 truncate font-bold text-[var(--text-primary)]">{value}</span>
394+
</div>
395+
);
396+
}
397+
398+
function MetricCell({ label, value }: { label: string; value: string }) {
399+
return (
400+
<div className="min-w-0 border border-[color:color-mix(in_srgb,var(--border-color)_45%,transparent)] bg-[var(--bg-main)] p-2">
401+
<div className="truncate font-mono text-[length:var(--font-size-ui-lg)] font-black text-[var(--text-primary)]">{value}</div>
402+
<div className="mt-1 truncate font-mono text-[length:var(--font-size-ui-xs)] font-black uppercase text-[var(--text-muted)]">{label}</div>
403+
</div>
404+
);
405+
}
406+
407+
function EventLine({ event }: { event: CodexLiveTimelineEvent }) {
408+
return (
409+
<div className="grid min-w-0 grid-cols-[5.8rem_8px_7.5rem_1fr] gap-2 text-[length:var(--font-size-ui-sm)]">
410+
<span className="font-mono font-black text-[var(--text-muted)]">{event.at}</span>
411+
<span className={`mt-1.5 h-2 w-2 ${severityDotClass(event.severity)}`} />
412+
<span className="truncate font-mono font-black uppercase text-[var(--text-primary)]">
413+
{event.lane}.{event.kind}
414+
</span>
415+
<span className="min-w-0 truncate font-bold text-[var(--text-muted)]">{event.label}</span>
416+
</div>
417+
);
418+
}
419+
296420
function DiagnosticSummary({ diagnostic, t }: { diagnostic: string; t: Translate }) {
297421
return (
298422
<div className="grid gap-3">

0 commit comments

Comments
 (0)