Skip to content

Commit 403059b

Browse files
committed
Align request indices and organize metadata evidence
1 parent 0ad61a3 commit 403059b

14 files changed

Lines changed: 665 additions & 16 deletions

docs/architecture.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ Viewer 的 Source 列表已经通过 `SourceRepository` 汇聚 live、SQLite、f
112112
| `src/viewer/session-navigator-renderer.js` | Session Navigator 项目组、会话项和动作菜单的纯 HTML renderer |
113113
| `src/viewer/session-navigator-controller.js` | 长期管理根事件委派、菜单互斥、外部关闭和项目折叠持久化 |
114114
| `src/viewer/raw-view-model.js` | Raw Inspector 上行、下行、Harness、Metadata 的纯 section 数据与方向约束 |
115+
| `src/viewer/metadata-view-model.js` | 请求身份、传输事实、厂商 usage、上行构成和捕获证据的纯 Metadata DTO |
116+
| `src/viewer/metadata-renderer.js` | Metadata 原文/整理切换及分来源统计的纯 HTML renderer |
115117
| `src/viewer/raw-search-model.js` | Raw 搜索条目构建、过滤、摘要命中分段与导航索引的纯模型 |
116118
| `src/viewer/raw-search-controller.js` | Raw 搜索输入法生命周期、延迟重绘、当前命中、高亮和滚动控制器 |
117119
| `src/viewer/raw-inspector-controller.js` | Raw 请求选择、详情懒加载、竞态失效、面板状态与重绘生命周期控制器 |
@@ -323,6 +325,8 @@ Viewer UI 文案由纯 `ui-i18n.js` 集中所有,当前支持 `zh-CN` 与 `en-
323325

324326
Trace Timeline 的搜索分类、命中计数、结果上限、latest-only 和 Turn 窗口由纯 `trace-timeline-model.js` 计算。查询栏、空状态、窗口边界与 Turn 容器编排由 `trace-timeline-renderer.js` 生成;`TraceTimelineController` 长期持有查询栏和 Timeline 根节点,通过一次事件委派处理 IME、筛选、Raw、通用请求证据跳转、折叠与活动态同步。复杂 Turn 在请求卡前增加由 `turn-story-model.js` 生成的“机制流程”:它只消费共享 entry/response/tool result/semantic event/Agent graph DTO,按顺序概括用户请求、工具或 Skill、子 Agent 启动/确认/回流、最终回答和 Harness 压缩事件;简单单请求对话不显示该层。`turn-story-renderer.js` 只负责紧凑呈现,每一步通过统一 `data-request-jump` 回到对应请求,不按 Agent/provider 建第二套分支。请求身份、上行类别/标签/预览、快捷 section、工具事件配对和 Assistant response metadata/折叠由 `request-card-model.js` 生成显式 View DTO,再由 `request-card-renderer.js` 生成请求卡外壳、工具交换和回复 HTML。Thinking 默认折叠,翻译动作只在展开后出现;翻译开始即锁定并显示进度,按 request 保存的展开状态保证异步重绘不会打断阅读。多 Agent 看板由 `agent-graph-model.js` 按 Trace Domain 已确认的分支关系计算筛选、分页和交错事件流,再由 `agent-graph-renderer.js` 生成 HTML;看板不自行推断 parent/child,“子 Agent”Trace 筛选也保留对应看板而不是只留下离散请求卡。上行详情由 `upstream-detail-model.js` 把完整 request 规范成 System、Tools、历史消息、当前新增消息/子 Agent 回流和 provider token DTO,再由 `upstream-detail-renderer.js` 生成 HTML;request-detail cache、展开状态与局部重绘仍由应用层所有。左侧 Session Navigator 由纯 Model/Renderer 与长生命周期 Controller 组成;Controller 持有菜单和折叠偏好,通过动作端口把选择、归档、删除、重命名和导出交回应用层。底部 Agent Composer 同样使用纯 Model/Renderer 与 Controller;Controller 按 source 隔离草稿和发送结果,通过注入的 API/刷新回调执行 detached resume,不读取全局 state。应用层只装配详情/展开状态、翻译动作、API 与局部重绘,把纯 Model DTO 和受信任子块交给 Renderer;renderer 不读取全局 state、不注册动作,也不访问 DOM。应用渲染仍分成 Header、Timeline 和 Composer 三个表面:Timeline 内部动作只重建 Timeline,翻译 Raw 块只刷新 Raw,翻译 Thinking 块只刷新 Timeline;source 装载、完整数据刷新、全局错误状态和 UI/目标翻译语言切换仍可使用组合 `renderAll()`。详细契约见 [Viewer Timeline 模型与局部渲染契约](viewer-timeline-contract.md)、[请求/回复卡片 View 契约](request-card-renderer-contract.md)、[多 Agent 看板 View 契约](agent-graph-view-contract.md)、[上行详情 View 契约](upstream-detail-view-contract.md)、[Session Navigator View 契约](session-navigator-view-contract.md)和[Agent Composer View 契约](agent-composer-view-contract.md)。
325327

328+
用户输入的请求编号进入与其他上行请求共用的固定编号轨道,渐隐连接线表达编号与右对齐自适应气泡的归属,气泡本身不再为编号预留一列。该轨道属于请求卡 renderer 的视觉语义,不改变 request index 或 Turn 归属。
329+
326330
工具事件与子 Agent 事件筛选会保留所属 Turn 的“机制流程”,避免过滤后只剩离散证据而失去链路语义;异常和慢请求筛选继续保持紧凑。筛选计数统一表示匹配的请求/事件数量,顶部统计则保留子 Agent 实例数、工具调用数与工具结果数,两种口径不混用。纯数字或 `#数字` 查询按精确 request index 解释,避免正文、token 或工具结果里的相同数字造成伪命中;其他查询继续搜索可见语义文本。
327331

328332
compact 首屏后的完整 request 由 `RequestDetailCache` 按需读取。同一 request 的并发展开共享 Promise,失败可重试,source 切换统一清空;首次加载和缓存命中的应用副作用通过回调注入,缓存层不反向依赖 DOM、全局 state 或翻译模块。
@@ -331,6 +335,8 @@ Raw Inspector 的请求/响应方向由 `raw-view-model.js` 统一。它从完
331335

332336
Raw Inspector 按数据方向组织证据:请求卡和上行视图只展示 System、Tools、Harness、Messages、历史工具调用与回传结果;“完整请求”和“请求 Metadata”会从 capture 中剔除 response、响应状态以及 response 派生统计。请求侧标签保持单层排列,完整请求在首位、Metadata 在末位。完整 Response 与本次响应的协议原生调用条目只从 Assistant 回复进入“模型下行”视图。Assistant 视图保留独立的“上行参考”Tools schema,并明确它不是 response body 返回内容;当该响应包含工具调用时,可按下行的精确工具名一键过滤上行 schema,这只改变显示范围,不混淆证据方向。
333337

338+
请求 Metadata 同时提供“原文”和“整理”两种视图。原文直接展示剔除下行派生字段后的上行 capture metadata;整理视图把请求身份和 HTTP 传输标为捕获事实,把 token usage 标为厂商返回,把 context delta 等证据标为 PMA 规范化,并把字符数和构成占比标为 PMA 计算。整理视图不得把计算结果伪装成协议字段,也不得覆盖原文。
339+
334340
中间时间线与右侧 Inspector 使用不同的信息密度:时间线只消费有界摘要,右侧是完整证据与语义整理入口。右侧 `tool_result` 从本轮原始上行 message 增量读取完整条目,并提供原文/整理切换;例如 `tool_search_output` 会在整理视图中按命名空间展示完整工具描述、参数 schema、参数说明、原始定义与块级翻译操作,不受时间线 800 字预览限制。
335341

336342
Raw Inspector 的结构化翻译视图已经拆为纯 View Model 和 Renderer。View Model 只接收显式材料、查询词、可选工具名范围和译文 lookup 回调,负责工具分组、精确范围过滤、命中排序、缓存统计与展示 DTO;Renderer 只接收 DTO、i18n、Markdown/Pre renderer 和 action id 注册回调。Source/目标语言的缓存探测、lookup 重建、自动刷新去重以及旧异步结果失效由 `TranslationCacheController` 所有;应用层继续拥有活动 request/section、翻译生成、复制/重译动作和局部重绘,但生成副作用必须携带控制器签发的上下文 operation token 并在每个异步边界后复核。单工具动作可聚合说明和全部参数材料,却不改变每项材料既有的翻译 hash 与缓存生命周期。详细边界见 [Viewer 翻译视图契约](translation-view-renderer-contract.md)[Viewer 翻译缓存上下文契约](translation-cache-controller-contract.md)

docs/codebase-map.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ pma CLI / adapter
5656
| Capture 到 Viewer request/Turn/Agent/stats DTO | `viewer-trace-projector.mjs``src/trace/*`[投影器契约](viewer-trace-projector-contract.md) | 在 HTTP route、Source provider、详情或 cursor 路径分别拼 DTO |
5757
| Viewer HTTP 安全和 API | `src/contracts/viewer-api.mjs``http.mjs``viewer-router.mjs`[DTO 契约](viewer-api-dto-contract.md)[Router 契约](viewer-router-contract.md)`src/viewer/api-client.js` |`server.mjs` 重新写 URL/method/intent 分支、分别解释 Source/request detail/Timeline envelope,或让 feature renderer 发 `fetch` |
5858
| 中栏 Timeline、Turn 机制流程、请求卡、多 Agent 看板、上行详情 | `trace-timeline-*``turn-story-*``request-card-model.js``request-card-renderer.js``agent-graph-*``upstream-detail-*`[请求卡 View 契约](request-card-renderer-contract.md);中栏只呈现可扫读摘要,完整参数、结果和统计进入右侧证据栏 |`client.js` 重写请求分类/摘要/工具配对,在机制流程中按 provider 分支,让 renderer 读取全局 `state`/DOM,或把 Raw/统计重新塞入中栏 |
59-
| Raw、Messages、翻译展示 | `raw-inspector-controller.js``raw-view-model.js``raw-*``message-*``translation-*`[Raw Inspector Controller 契约](raw-inspector-controller-contract.md)[翻译视图契约](translation-view-renderer-contract.md)[Raw 搜索浏览器契约](raw-search-browser-contract.md);Tools schema 以一个工具一个整体展示,Assistant 可按当前下行工具名过滤上行参考;Raw Response 保留厂商协议原始字段,统一摘要只服务整理视图 | `client.js` 新增长段领域 HTML,或绕过 Controller 直接提交异步 Raw 结果;Raw 搜索只检查截断预览而不检查完整值;把 Tools schema 误称为 Response 内容;把统一 `text/thinking/tool_calls` 再拼回 Raw Response |
59+
| Raw、Metadata、Messages、翻译展示 | `raw-inspector-controller.js``raw-view-model.js``raw-*``metadata-view-model.js``metadata-renderer.js``message-*``translation-*`[Raw Inspector Controller 契约](raw-inspector-controller-contract.md)[翻译视图契约](translation-view-renderer-contract.md)[Raw 搜索浏览器契约](raw-search-browser-contract.md)Metadata 原文保留上行捕获字段,整理视图明确区分厂商返回、捕获事实、PMA 规范化和 PMA 计算;Tools schema 以一个工具一个整体展示,Assistant 可按当前下行工具名过滤上行参考;Raw Response 保留厂商协议原始字段,统一摘要只服务整理视图 | `client.js` 新增长段领域 HTML,或绕过 Controller 直接提交异步 Raw 结果;Raw 搜索只检查截断预览而不检查完整值;把 Tools schema 误称为 Response 内容;把 PMA 计算伪装成协议字段;把统一 `text/thinking/tool_calls` 再拼回 Raw Response |
6060
| System 提示词变化与大文本退化 | `system-diff-model.js``system-diff-renderer.js`[System Diff 契约](system-diff-view-contract.md) |`client.js` 重建无上限 LCS 矩阵,或把块摘要称作精确行 diff |
6161
| 三栏折叠、宽度与拖动 | `pane-layout-model.js``pane-layout-controller.js`;状态在 `client-store.js` |`client.js` 直接读写 CSS 变量或重复绑定 resizer |
6262
| UI 状态与交互动作 | `client-store.js`、feature controller、`session-navigator-*``agent-composer-*``client.js` 装配层 | model/renderer 写全局状态 |

scripts/package-smoke.mjs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ for (const required of [
5959
"src/viewer/markdown.js",
6060
"src/viewer/message-view-model.js",
6161
"src/viewer/messages-renderer.js",
62+
"src/viewer/metadata-renderer.js",
63+
"src/viewer/metadata-view-model.js",
6264
"src/viewer/pane-layout-controller.js",
6365
"src/viewer/pane-layout-model.js",
6466
"src/viewer/raw-inspector-controller.js",
@@ -193,14 +195,14 @@ const allowedPatterns = [
193195
const unexpectedFiles = packageFiles.filter((file) => !allowedPatterns.some((pattern) => pattern.test(file)));
194196
assert.deepEqual(unexpectedFiles, [], `npm package includes files outside the release allowlist: ${unexpectedFiles.join(", ")}`);
195197

196-
const MAX_PACKAGE_ENTRIES = 142;
198+
const MAX_PACKAGE_ENTRIES = 144;
197199
// The managed Agent runtimes, protocol-native response reconstruction, semantic
198200
// trace views, and provider-neutral subagent correlation are shipped product
199201
// code. Keep a narrow post-feature budget while the unchanged unpacked-size cap
200202
// and path allowlist prevent fixtures, design docs, captures, and other
201203
// release-unsafe files from leaking into the package.
202-
const MAX_PACKED_BYTES = 345_000;
203-
const MAX_UNPACKED_BYTES = 1_490_000;
204+
const MAX_PACKED_BYTES = 348_000;
205+
const MAX_UNPACKED_BYTES = 1_510_000;
204206
assert.ok(packs[0].entryCount <= MAX_PACKAGE_ENTRIES, `npm package contains too many files: ${packs[0].entryCount}/${MAX_PACKAGE_ENTRIES}`);
205207
assert.ok(packs[0].size <= MAX_PACKED_BYTES, `npm package is too large when packed: ${packs[0].size}/${MAX_PACKED_BYTES} bytes`);
206208
assert.ok(

scripts/raw-inspector-renderer-contract-smoke.mjs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import {
1212
renderRequestRawNavigation,
1313
renderResponseRawNavigation,
1414
} from "../src/viewer/raw-inspector-renderer.js";
15+
import {
16+
renderMetadataControls,
17+
renderOrganizedMetadata,
18+
} from "../src/viewer/metadata-renderer.js";
1519

1620
const translate = (key, values = {}) => `${key}${values.section ? `:${values.section}` : ""}${values.count != null ? `:${values.count}` : ""}`;
1721
const escapeHtml = (value) => String(value).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll('"', "&quot;");
@@ -159,4 +163,49 @@ assert.match(sectionEvidence, /PMA &lt;view>/);
159163
assert.doesNotMatch(sectionEvidence, /<script>/);
160164
assert.equal(renderRawSectionEvidence({ evidence: null, escapeHtml }), "");
161165

166+
const metadataControls = renderMetadataControls({ mode: "organized", translate, escapeHtml });
167+
assert.match(metadataControls, /data-metadata-mode="source"/);
168+
assert.match(metadataControls, /data-metadata-mode="organized"/);
169+
assert.match(metadataControls, /class="active" data-metadata-mode="organized"/);
170+
171+
const metadataSummary = renderOrganizedMetadata({
172+
view: {
173+
identity: [{ key: "request_index", value: 18 }],
174+
transport: [{ key: "path", value: "/v1/messages?<unsafe>" }],
175+
providerUsage: {
176+
input: 120,
177+
cache: 80,
178+
actualInput: 40,
179+
output: 12,
180+
totalInput: 120,
181+
cacheRatio: 2 / 3,
182+
actualRatio: 1 / 3,
183+
},
184+
composition: {
185+
unit: "chars",
186+
total: 1000,
187+
sections: [
188+
{ key: "system", chars: 300, ratio: 0.3 },
189+
{ key: "tools", chars: 500, ratio: 0.5 },
190+
],
191+
},
192+
evidence: {
193+
transport: "capture_proxy",
194+
request: { exact: true, available: true },
195+
headerRedactions: ["authorization"],
196+
contextDelta: { new_messages: 1 },
197+
},
198+
},
199+
translate,
200+
escapeHtml,
201+
formatNumber: (value) => String(value),
202+
});
203+
assert.match(metadataSummary, /metadata-summary/);
204+
assert.match(metadataSummary, /metadataCapturedFact/);
205+
assert.match(metadataSummary, /metadataProviderFact/);
206+
assert.match(metadataSummary, /metadataCalculated/);
207+
assert.match(metadataSummary, /66\.7%/);
208+
assert.match(metadataSummary, /30\.0%/);
209+
assert.doesNotMatch(metadataSummary, /<unsafe>/);
210+
162211
console.log("raw inspector renderer contract smoke passed");

scripts/raw-view-model-contract-smoke.mjs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
responseToolCallSectionLabel,
2222
responseUsesReconstructedDownstream,
2323
} from "../src/viewer/raw-view-model.js";
24+
import { buildMetadataView } from "../src/viewer/metadata-view-model.js";
2425

2526
const request = {
2627
id: "request-1",
@@ -95,6 +96,47 @@ assert.deepEqual(metadata.context_delta, { status: "changed" });
9596
assert.equal("response" in metadata, false);
9697
assert.equal("status" in metadata, false);
9798

99+
const metadataView = buildMetadataView({
100+
...request,
101+
raw: {
102+
...request.raw,
103+
conversation_id: "conversation-1",
104+
received_at: "2026-07-25T10:00:00.000Z",
105+
raw_body_length: 1000,
106+
body_source: "original",
107+
},
108+
summary: {
109+
...request.summary,
110+
composition: {
111+
unit: "chars",
112+
total_payload_chars: 1000,
113+
input_chars: 1000,
114+
sections: {
115+
system: { chars: 300, ratio: 0.3 },
116+
tools: { chars: 500, ratio: 0.5 },
117+
history_context: { chars: 150, ratio: 0.15 },
118+
current_user: { chars: 50, ratio: 0.05 },
119+
},
120+
},
121+
response: {
122+
...request.summary.response,
123+
usage: {
124+
input_tokens: 100,
125+
cache_read_input_tokens: 80,
126+
output_tokens: 12,
127+
},
128+
},
129+
},
130+
});
131+
assert.equal(metadataView.identity.find((fact) => fact.key === "capture_id").value, "capture-1");
132+
assert.equal(metadataView.providerUsage.cache, 80);
133+
assert.equal(metadataView.providerUsage.actualInput, 100);
134+
assert.equal(metadataView.composition.total, 1000);
135+
assert.deepEqual(
136+
metadataView.composition.sections.map((section) => section.key),
137+
["system", "tools", "history_context", "current_user"],
138+
);
139+
98140
assert.deepEqual(rawSectionData(request, "system").value, {
99141
body_system: "system prompt",
100142
message_system: [{ role: "system", content: "message system" }],

scripts/request-card-renderer-contract-smoke.mjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,12 @@ const upstream = renderTimelineUpstreamEntry({
8282
escapeHtml,
8383
});
8484
assert.match(upstream, /upstream-entry tool-result user-turn/);
85+
assert.match(upstream, /class="user-turn-index-anchor"/);
86+
assert.match(upstream, /class="user-turn-connector"/);
87+
assert.match(upstream, /class="user-turn-bubble"/);
8588
assert.match(upstream, /class="user-turn-content"/);
8689
assert.match(upstream, /<span class="request-index">#7<\/span>/);
87-
assert.match(upstream, /request-index[\s\S]*upstream-entry-preview[\s\S]*upstream-entry-actions/);
90+
assert.match(upstream, /request-index[\s\S]*user-turn-connector[\s\S]*user-turn-bubble[\s\S]*upstream-entry-preview[\s\S]*upstream-entry-actions/);
8891
assert.doesNotMatch(upstream, /class="upstream-entry-row"/);
8992
assert.doesNotMatch(upstream, /&lt;User &amp; result&gt;/);
9093
assert.doesNotMatch(upstream, /<script>/);

src/server/viewer-static-assets.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ const VIEWER_STATIC_ASSETS = new Map([
2020
["/language-preferences-controller.js", javascriptAsset("language-preferences-controller.js")],
2121
["/markdown.js", javascriptAsset("markdown.js")],
2222
["/message-view-model.js", javascriptAsset("message-view-model.js")],
23+
["/metadata-view-model.js", javascriptAsset("metadata-view-model.js")],
24+
["/metadata-renderer.js", javascriptAsset("metadata-renderer.js")],
2325
["/messages-renderer.js", javascriptAsset("messages-renderer.js")],
2426
["/pane-layout-controller.js", javascriptAsset("pane-layout-controller.js")],
2527
["/pane-layout-model.js", javascriptAsset("pane-layout-model.js")],

src/viewer/client-store.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export const VIEWER_CLIENT_STATE_DEFAULTS = Object.freeze({
55
activeRawSection: "full",
66
activeRawMode: "request",
77
rawMessagesMode: "organized",
8+
rawMetadataMode: "organized",
89
uiLanguage: "zh-CN",
910
targetTranslationLanguage: "zh-CN",
1011
translationMode: "source",
@@ -17,7 +18,7 @@ export const VIEWER_CLIENT_STATE_DEFAULTS = Object.freeze({
1718

1819
const DOMAIN_KEYS = Object.freeze({
1920
selection: new Set(["activeSourceId", "activeId", "activeRequestId"]),
20-
rawView: new Set(["activeRawSection", "activeRawMode", "rawMessagesMode"]),
21+
rawView: new Set(["activeRawSection", "activeRawMode", "rawMessagesMode", "rawMetadataMode"]),
2122
language: new Set(["uiLanguage", "targetTranslationLanguage", "translationMode"]),
2223
layout: new Set(["rawOpen", "rawWidth", "sidebarOpen", "sidebarWidth"]),
2324
timeline: new Set(["latestOnly"]),

0 commit comments

Comments
 (0)