@wacry/codeks-bridge 0.0.0-stage → 0.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.
Files changed (104) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +13 -0
  3. package/README.md +40 -2
  4. package/catalog/README.md +63 -0
  5. package/catalog/claude-opus-5-5.json +194 -0
  6. package/catalog/claude-sonnet-5-5.json +194 -0
  7. package/catalog/deepseek-flash.json +203 -0
  8. package/catalog/gpt-5.6-sol.json +152 -0
  9. package/catalog/gpt-6-astra.json +184 -0
  10. package/catalog/gpt-6-luna.json +178 -0
  11. package/catalog/gpt-6-sol.json +182 -0
  12. package/catalog/gpt-6.1-sol.json +187 -0
  13. package/dist/adapter-options.d.ts +5 -0
  14. package/dist/adapter-options.js +41 -0
  15. package/dist/auth.d.ts +2 -0
  16. package/dist/auth.js +42 -0
  17. package/dist/backends/anthropic-tools.d.ts +13 -0
  18. package/dist/backends/anthropic-tools.js +41 -0
  19. package/dist/backends/anthropic.d.ts +9 -0
  20. package/dist/backends/anthropic.js +356 -0
  21. package/dist/backends/chat.d.ts +16 -0
  22. package/dist/backends/chat.js +284 -0
  23. package/dist/backends/deepseek.d.ts +6 -0
  24. package/dist/backends/deepseek.js +37 -0
  25. package/dist/backends/responses.d.ts +5 -0
  26. package/dist/backends/responses.js +227 -0
  27. package/dist/carrier.d.ts +22 -0
  28. package/dist/carrier.js +61 -0
  29. package/dist/catalog.d.ts +8 -0
  30. package/dist/catalog.js +40 -0
  31. package/dist/cli.d.ts +2 -0
  32. package/dist/cli.js +64 -0
  33. package/dist/compaction-handoff.d.ts +13 -0
  34. package/dist/compaction-handoff.js +126 -0
  35. package/dist/compaction-history.d.ts +7 -0
  36. package/dist/compaction-history.js +20 -0
  37. package/dist/compaction.d.ts +20 -0
  38. package/dist/compaction.js +134 -0
  39. package/dist/config.d.ts +6 -0
  40. package/dist/config.js +170 -0
  41. package/dist/errors.d.ts +43 -0
  42. package/dist/errors.js +141 -0
  43. package/dist/headers.d.ts +6 -0
  44. package/dist/headers.js +39 -0
  45. package/dist/history.d.ts +7 -0
  46. package/dist/history.js +85 -0
  47. package/dist/ids.d.ts +1 -0
  48. package/dist/ids.js +4 -0
  49. package/dist/index.d.ts +5 -0
  50. package/dist/index.js +4 -0
  51. package/dist/inspect/capture.d.ts +119 -0
  52. package/dist/inspect/capture.js +370 -0
  53. package/dist/inspect/http.d.ts +7 -0
  54. package/dist/inspect/http.js +149 -0
  55. package/dist/inspect/view.d.ts +239 -0
  56. package/dist/inspect/view.js +388 -0
  57. package/dist/model-catalog.d.ts +18 -0
  58. package/dist/model-catalog.js +16 -0
  59. package/dist/profiles.d.ts +6 -0
  60. package/dist/profiles.js +63 -0
  61. package/dist/prompts.d.ts +5 -0
  62. package/dist/prompts.js +118 -0
  63. package/dist/server.d.ts +4 -0
  64. package/dist/server.js +232 -0
  65. package/dist/sse.d.ts +12 -0
  66. package/dist/sse.js +93 -0
  67. package/dist/tool-history.d.ts +4 -0
  68. package/dist/tool-history.js +73 -0
  69. package/dist/tools.d.ts +35 -0
  70. package/dist/tools.js +193 -0
  71. package/dist/types.d.ts +125 -0
  72. package/dist/types.js +1 -0
  73. package/dist/upstream.d.ts +3 -0
  74. package/dist/upstream.js +89 -0
  75. package/dist/writer.d.ts +95 -0
  76. package/dist/writer.js +430 -0
  77. package/docs/backends-anthropic.md +88 -0
  78. package/docs/backends-deepseek-chat.md +75 -0
  79. package/docs/backends-responses.md +33 -0
  80. package/docs/compaction.md +180 -0
  81. package/docs/config.md +271 -0
  82. package/docs/design.md +97 -0
  83. package/docs/inspect.md +148 -0
  84. package/docs/reasoning.md +72 -0
  85. package/docs/releasing.md +50 -0
  86. package/docs/responses-stream.md +90 -0
  87. package/docs/testing.md +177 -0
  88. package/docs/tools.md +68 -0
  89. package/examples/config.json +35 -0
  90. package/examples/gpt-claude.json +27 -0
  91. package/package.json +46 -4
  92. package/ui/api.js +64 -0
  93. package/ui/app.css +379 -0
  94. package/ui/app.js +489 -0
  95. package/ui/detail.js +713 -0
  96. package/ui/diff.js +281 -0
  97. package/ui/dom.js +258 -0
  98. package/ui/events.js +376 -0
  99. package/ui/favicon.svg +1 -0
  100. package/ui/index.html +58 -0
  101. package/ui/labels.js +106 -0
  102. package/ui/list.js +180 -0
  103. package/ui/structure.js +285 -0
  104. package/ui/tree.js +158 -0
package/docs/tools.md ADDED
@@ -0,0 +1,68 @@
1
+ # 工具转换
2
+
3
+ 客户端负责执行工具,bridge 只转换定义、调用和结果。Responses 后端保留原生定义;Messages 与 Chat 后端只向模型提供普通函数。
4
+
5
+ ## Lite 工具声明
6
+
7
+ 客户端默认把完整工具 schema 放在 `input[].additional_tools`。GPT 保留原始声明项。DeepSeek 及 Claude 的 `top_level` 模式在转换消息前提取声明,与顶层 `tools` 按历史顺序合并;Claude 默认 `inline` 模式保留增量定义生效的位置。声明不会变成普通 reminder。
8
+
9
+ 增量声明只包含新增或变更的成员,按 namespace 与工具名覆盖旧定义,保留未变的其他成员和已分配的上游名字。工具移除的 developer 通知保留原位置,不从提示词文字推断删工具。codeks 将默认 `functions` namespace、空 namespace 和省略 namespace 视为同一身份,因此历史调用和显式工具选择均按此规则查找;返回调用保留声明中的原 namespace。
10
+
11
+ Claude 的 `adapters.claude-messages.tool_updates` 有两种明确模式:
12
+
13
+ | 模式 | 初始定义 | 中途新增与同名更新 |
14
+ | --- | --- | --- |
15
+ | `inline`(默认) | 顶层 `tools` 及会话开始前的声明组成原生顶部工具表 | 在原生中途 system 消息中发送 `tool_addition`,携带完整 `tool_definition`;此前定义不变 |
16
+ | `top_level` | 各批定义累积成同一顶部工具表 | 用最新定义覆盖同身份工具,适用于尚未支持完整 inline 的端点 |
17
+
18
+ 完整 inline 定义能同时表达新工具和同名工具更新,避免为了更新定义重写早先的工具前缀;这是选择它作为默认方式的理由。bridge 在 user 或整批工具结果之后、下一条 assistant 之前发送中途 system 工具声明;工具交换内的声明先等待结果完整,无法形成合法边界则报错。普通 system/developer 提醒仍转为原位 user reminder,不因工具使用原生中途 system 而改变角色。
19
+
20
+ 端点需要支持 `inline-tools-2026-09-15` beta;bridge 在请求包含中途更新时自动追加,保留已有 beta。不兼容时直接交付错误,不自动切换模式。`top_level` 可能改变缓存前缀和旧 thinking 的绑定;失配处理由独立的 [thinking 策略](backends-anthropic.md#失配策略与故障续接)决定。Code Mode Only 下多数工具位于 `exec` 内的目录,因此外层更新较少,但仍按协议完整处理。
21
+
22
+ ## 工具发现与网页搜索
23
+
24
+ `supports_search_tool` 表示客户端的工具发现能力,与网页搜索不同。当前源目录的 GPT 条目显式设为 `true`,bridge 保留它。是否出现独立 `tool_search` 还取决于客户端工具模式和功能开关,不能只从目录字段推断。
25
+
26
+ 当前目录采用 Code Mode Only:大多数工具通过 `exec` 中的函数调用,模型从 `ALL_TOOLS` 读取目录;外层不会因此增加一个独立搜索工具。网页搜索对应 `web_search`,由客户端配置单独关闭;图片生成也有独立开关,均不通过 `supports_search_tool` 控制。
27
+
28
+ ## 名字
29
+
30
+ 普通函数保留原名。namespace 中的工具使用 `namespace__name`;namespace 已以 `_` 结尾时直接拼接名字。名字规范化为字母、数字、`_` 和 `-`。只有两个不同工具的规范化结果实际冲突时,才为后来的名字加 `_2`、`_3` 等后缀。返回调用通过该请求的映射恢复原 name/namespace。
31
+
32
+ 共享转换层将 Messages/Chat 工具名统一限制为 64 字符,沿用 Chat 端点的较小限制,不为不同后端维护两套命名规则;这不表示所有端点的原生上限都是 64。超长名称保留 55 字符可读前缀,加 `_` 与 8 位摘要,以区别同前缀工具。正常长度不加 hash。历史名称根据当前工具集映射,因此同一次工具调用的名称不依赖模型临时起名。
33
+
34
+ ## 自由文本与语法
35
+
36
+ Responses custom tool 转成只有一个必填 `input: string` 字段的函数。模型返回 JSON 后,bridge 还原为 `custom_tool_call`,客户端拿到原始字符串。
37
+
38
+ - Lark 定义原样写进 description 的代码块。
39
+ - Regex 定义原样写进 description,要求 `input` 匹配。
40
+ - 不给特定编辑或执行工具追加私有提示词,不修改其原描述。
41
+
42
+ Messages/Chat 没有 Responses 的服务端语法约束能力,因此这是模型遵循的说明,不是同等的 constrained decoding。函数参数的 JSON 外壳仍会检查;错误、不完整的工具参数不会交给客户端执行。
43
+
44
+ ## 明文与多代理
45
+
46
+ Messages 对不符合端点格式的 call ID 采用确定性映射,并添加短摘要以避免两个不同 ID 规范化后碰撞;调用和结果使用同一映射。正常 ID 保持不变。
47
+
48
+ 普通 JSON Schema 清理 Responses 专用的布尔 `encrypted` 关键字,不删除名为 `encrypted` 的普通属性或数据。客户端使用明文多代理工具;返回的函数项显式携带 `encrypted_function_args: []`。
49
+
50
+ 根 schema 的 `$ref`、`$defs`、`allOf`、`anyOf`、`oneOf` 及其他约束均保留;缺少根 `type` 时补为 `object`,已有类型不覆盖,由上游判断是否支持。codeks 可以发送没有根 `type` 的引用或组合 schema,不能因此把参数清空。未提供参数 schema 时使用空对象定义;显式提供非对象 schema 值则报错,不在 bridge 内解析引用或删减约束。
51
+
52
+ codeks V2 多代理工具的 task/message 是明文,历史中的 `agent_message` 对 Messages/Chat 转成 user 消息,保留 author、recipient 和内容顺序。它不会成为模型 reasoning。不能解密的工具或 agent message 会明确报错。
53
+
54
+ ## 结果与范围
55
+
56
+ 文本结果按对应 API 的 tool result 形式传递。Messages 可以把图片放进 `tool_result`;Chat 的工具结果只放文本,图片在同组所有工具结果之后组成 user 消息,并标注来源 call ID,避免打断并行调用的结果序列。
57
+
58
+ Responses 允许主动工具通知和一次调用的多次输出,而 Messages/Chat 要求明确配对的调用与结果。转换只作用于当前非 GPT 请求副本:
59
+
60
+ - 没有 `call_id` 的命名 `function_call_output` 转成 user 通知,标出原 `namespace`、`name`,保留文字和图片。
61
+ - 同一 `call_id` 的第一条输出作为工具结果;后续输出转成带调用身份的 user 通知。这样 Code Mode 的 `notify()` 与最终输出都能到达模型,后续通知不会覆盖或拼接进先前已发送的结果。
62
+ - 完整工具批次中夹入的 user/developer 提醒与 agent 消息,在该批全部结果之后按原次序发送。这包括图片缩放提醒。Messages 的同一 user 消息里所有 `tool_result` 必须在文字之前,Chat 的工具结果也必须保持相邻。
63
+
64
+ 归一化只整理完整的配对批次,不补造缺失调用或结果,不跨过后续 assistant 步骤修复不完整交换。GPT 仍收到原生输出项。规则集中在 `src/tool-history.ts`,会话文件中的历史保持不变。
65
+
66
+ 从第一个输出工具调用开始,bridge 按原顺序缓存该调用及之后的输出,只有上游完整成功后才交给客户端。这样保留 thinking、文字与调用的交错顺序,后续回放不会改变签名前缀。截断、容量错误和断流不会释放部分调用;代价是工具之后的进度文字也要等响应成功后才显示。
67
+
68
+ Messages/Chat 不提供 hosted web search、image generation 等工具。默认定义中出现这些项时不放入上游工具表;显式 `tool_choice` 选择不支持的工具会报错。生成的客户端配置关闭 web search 和 image generation。当前不转换历史搜索/图片生成结果,遇到这类不支持的历史项会提示错误。
@@ -0,0 +1,35 @@
1
+ {
2
+ "host": "127.0.0.1",
3
+ "port": 8787,
4
+ "upstreams": {
5
+ "openai": {
6
+ "base_url": "https://api.openai.com/v1",
7
+ "api_key_env": "OPENAI_API_KEY"
8
+ },
9
+ "anthropic": {
10
+ "base_url": "https://api.anthropic.com/v1",
11
+ "api_key_env": "ANTHROPIC_API_KEY"
12
+ },
13
+ "deepseek": {
14
+ "base_url": "https://api.deepseek.com",
15
+ "api_key_env": "DEEPSEEK_API_KEY"
16
+ }
17
+ },
18
+ "models": {
19
+ "gpt": {
20
+ "upstream": "openai",
21
+ "model": "gpt-6.1-sol",
22
+ "catalog": "gpt-6.1-sol"
23
+ },
24
+ "claude": {
25
+ "upstream": "anthropic",
26
+ "model": "claude-sonnet-5-5",
27
+ "catalog": "claude-sonnet-5-5"
28
+ },
29
+ "deepseek": {
30
+ "upstream": "deepseek",
31
+ "model": "deepseek-flash",
32
+ "catalog": "deepseek-flash"
33
+ }
34
+ }
35
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "host": "127.0.0.1",
3
+ "port": 8787,
4
+ "upstreams": {
5
+ "openai": {
6
+ "base_url": "https://api.openai.com/v1",
7
+ "api_key_env": "OPENAI_API_KEY"
8
+ },
9
+ "anthropic": {
10
+ "base_url": "https://api.anthropic.com/v1",
11
+ "api_key_env": "ANTHROPIC_API_KEY"
12
+ }
13
+ },
14
+ "models": {
15
+ "gpt": {
16
+ "catalog": "gpt-6.1-sol",
17
+ "upstream": "openai",
18
+ "model": "gpt-6.1-sol"
19
+ },
20
+ "claude": {
21
+ "catalog": "claude-opus-5-5",
22
+ "upstream": "anthropic",
23
+ "model": "claude-opus-5-5",
24
+ "compaction": { "mode": "native", "model": "gpt" }
25
+ }
26
+ }
27
+ }
package/package.json CHANGED
@@ -1,6 +1,48 @@
1
1
  {
2
2
  "name": "@wacry/codeks-bridge",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.0.3",
4
+ "description": "Configurable local Responses API bridge for codeks with GPT, Claude Messages, and DeepSeek Chat adapters.",
5
+ "license": "Apache-2.0",
6
+ "author": "David Zhang",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/WAcry/codeks-bridge.git"
10
+ },
11
+ "type": "module",
12
+ "bin": {
13
+ "codeks-bridge": "dist/cli.js"
14
+ },
15
+ "exports": {
16
+ ".": "./dist/index.js"
17
+ },
18
+ "types": "./dist/index.d.ts",
19
+ "publishConfig": {
20
+ "access": "public",
21
+ "registry": "https://registry.npmjs.org"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "catalog",
26
+ "docs",
27
+ "examples",
28
+ "ui",
29
+ "README.md",
30
+ "LICENSE",
31
+ "NOTICE"
32
+ ],
33
+ "engines": {
34
+ "node": ">=22.18"
35
+ },
36
+ "scripts": {
37
+ "prepack": "npm run build",
38
+ "build": "tsc -p tsconfig.build.json",
39
+ "typecheck": "tsc --noEmit -p tsconfig.json",
40
+ "test": "node --test \"test/**/*.test.ts\"",
41
+ "check": "node scripts/check.ts",
42
+ "dev": "node src/cli.ts serve"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^22.18.0",
46
+ "typescript": "^5.9.0"
47
+ }
48
+ }
package/ui/api.js ADDED
@@ -0,0 +1,64 @@
1
+ // Access to the inspector API (/inspect/api/*) and to imported export files.
2
+ // URLs are relative, so the page works under /inspect/ on any host.
3
+
4
+ const TOKEN_KEY = 'codeks-inspector-token';
5
+ export const EXPORT_FORMAT = 'codeks-bridge-capture';
6
+
7
+ export class ApiError extends Error {
8
+ constructor(status, message, code) {
9
+ super(message);
10
+ this.status = status;
11
+ this.code = code;
12
+ }
13
+ }
14
+
15
+ export const getToken = () => sessionStorage.getItem(TOKEN_KEY);
16
+ export const setToken = (t) => (t ? sessionStorage.setItem(TOKEN_KEY, t) : sessionStorage.removeItem(TOKEN_KEY));
17
+
18
+ /** Calls the API. Throws ApiError (status 0 = network failure). */
19
+ export async function api(path, { method = 'GET', json, raw = false } = {}) {
20
+ const headers = {};
21
+ const token = getToken();
22
+ if (token) headers.authorization = `Bearer ${token}`;
23
+ let body;
24
+ if (json !== undefined) {
25
+ headers['content-type'] = 'application/json';
26
+ body = JSON.stringify(json);
27
+ }
28
+ let res;
29
+ try {
30
+ res = await fetch(`api/${path}`, { method, headers, body, cache: 'no-store', credentials: 'same-origin' });
31
+ } catch (e) {
32
+ throw new ApiError(0, `无法连接 bridge:${e.message || e}`);
33
+ }
34
+ if (!res.ok) {
35
+ let code = null;
36
+ let message = `${res.status} ${res.statusText}`;
37
+ try {
38
+ const j = await res.json();
39
+ code = j?.error?.code ?? null;
40
+ if (j?.error?.message) message = `${res.status}:${j.error.message}`;
41
+ } catch { /* not JSON */ }
42
+ throw new ApiError(res.status, message, code);
43
+ }
44
+ return raw ? res : res.json();
45
+ }
46
+
47
+ /** Validates an export file and returns its exchanges (Detail objects). */
48
+ export function parseExport(text) {
49
+ let data;
50
+ try { data = JSON.parse(text); } catch (e) { throw new Error(`不是 JSON 文件:${e.message}`); }
51
+ if (data?.format !== EXPORT_FORMAT) throw new Error(`不是 ${EXPORT_FORMAT} 导出文件(format=${JSON.stringify(data?.format)})`);
52
+ if (data.version !== 1) throw new Error(`不支持的导出版本 ${JSON.stringify(data.version)}(支持 1)`);
53
+ if (!Array.isArray(data.exchanges)) throw new Error('导出文件缺少 exchanges 数组');
54
+ return data;
55
+ }
56
+
57
+ export const exportDoc = (exchanges) => ({ format: EXPORT_FORMAT, version: 1, exported_at: Date.now(), exchanges });
58
+
59
+ /** Local file name stamp: YYYYMMDD-HHMMSS. */
60
+ export function stamp(ms = Date.now()) {
61
+ const d = new Date(ms);
62
+ const p = (n) => String(n).padStart(2, '0');
63
+ return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
64
+ }
package/ui/app.css ADDED
@@ -0,0 +1,379 @@
1
+ /* codeks-bridge inspector. Dense debug layout; colors follow prefers-color-scheme. */
2
+ :root {
3
+ --bg: #ffffff; --bg2: #f6f7f9; --bg3: #eaedf1; --fg: #1f2328; --fg2: #626b76;
4
+ --line: #d3d9e0; --line2: #eceff3; --accent: #0b67d0; --sel: #dcecff;
5
+ --ok: #1a7f37; --ok-bg: #dcf7e3; --bad: #c9222c; --bad-bg: #ffe8e6;
6
+ --warn: #8f5f00; --warn-bg: #fff3c4; --info: #0b67d0; --info-bg: #e2f0ff;
7
+ --sub: #7a45d6; --sub-bg: #f3eaff; --key: #8a3a00; --str: #0b3d78; --num: #0550ae; --bool: #9a3412;
8
+ --hue-l: 92%; --hue-s: 70%; --hue-fg-l: 28%;
9
+ --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
10
+ --shadow: 0 6px 24px rgb(0 0 0 / 0.16);
11
+ color-scheme: light dark;
12
+ }
13
+ @media (prefers-color-scheme: dark) {
14
+ :root {
15
+ --bg: #0f1216; --bg2: #161a20; --bg3: #222830; --fg: #e3e8ee; --fg2: #8e98a4;
16
+ --line: #313843; --line2: #1f242b; --accent: #4c97f8; --sel: #1b3352;
17
+ --ok: #46c060; --ok-bg: #12291b; --bad: #f6635b; --bad-bg: #331517; --warn: #d9a43a; --warn-bg: #2e2410;
18
+ --info: #63aaff; --info-bg: #13283f; --sub: #bf95ff; --sub-bg: #281d3b;
19
+ --key: #f0a868; --str: #a8d4ff; --num: #82c2ff; --bool: #f3a07a;
20
+ --hue-l: 24%; --hue-s: 32%; --hue-fg-l: 82%;
21
+ --shadow: 0 6px 24px rgb(0 0 0 / 0.5);
22
+ }
23
+ }
24
+
25
+ * { box-sizing: border-box; }
26
+ [hidden] { display: none !important; }
27
+ html, body { height: 100%; }
28
+ body {
29
+ margin: 0; display: flex; flex-direction: column; overflow: hidden;
30
+ font: 13px/1.4 system-ui, -apple-system, "Segoe UI", "PingFang SC", "Noto Sans CJK SC", sans-serif;
31
+ background: var(--bg); color: var(--fg);
32
+ }
33
+ button, input, select { font: inherit; color: inherit; }
34
+ input[type="search"], input[type="password"], input[type="text"], select {
35
+ background: var(--bg); border: 1px solid var(--line); border-radius: 4px; padding: 1px 6px; min-height: 22px;
36
+ }
37
+ input:focus-visible, select:focus-visible, button:focus-visible, .list:focus-visible { outline: 2px solid var(--accent); outline-offset: -1px; }
38
+ pre { margin: 0; }
39
+ table { border-collapse: collapse; }
40
+ h3, h4 { margin: 0; }
41
+
42
+ .mono, .tree, .raw, .sv-text, .ev-list, .diff-tree, .fields, .kv td { font-family: var(--mono); font-size: 12px; }
43
+ .dim { color: var(--fg2); }
44
+ .err { color: var(--bad); }
45
+ .grow { flex: 1 1 auto; }
46
+ .note { padding: 4px 8px; margin: 0 0 8px; background: var(--info-bg); border-left: 3px solid var(--info); border-radius: 3px; font-size: 12px; }
47
+ .server-transformations { margin-bottom: 12px; }
48
+ .transformation-report { margin: 4px 0; }
49
+ .transformation-entry { padding: 3px 0; border-bottom: 1px solid var(--line2); }
50
+ .transformation-entry > summary { cursor: pointer; overflow-wrap: anywhere; }
51
+ .empty { padding: 48px 24px; text-align: center; color: var(--fg2); }
52
+ .empty.small { padding: 14px 4px; text-align: left; }
53
+ .link { color: var(--accent); text-decoration: none; }
54
+ .link:hover { text-decoration: underline; }
55
+ .clickable { cursor: pointer; }
56
+ .check { display: inline-flex; align-items: center; gap: 3px; font-size: 12px; cursor: pointer; white-space: nowrap; }
57
+ .str { color: var(--str); }
58
+ .num, .mono.number { color: var(--num); }
59
+ .bool, .mono.boolean { color: var(--bool); }
60
+ .null { color: var(--fg2); font-style: italic; }
61
+
62
+ /* ---------- buttons and chips ---------- */
63
+ .btn {
64
+ display: inline-flex; align-items: center; gap: 3px; padding: 1px 8px; min-height: 22px;
65
+ border: 1px solid var(--line); border-radius: 4px; background: var(--bg); cursor: pointer;
66
+ white-space: nowrap; font-size: 12px; line-height: 18px;
67
+ }
68
+ .btn:hover:not(:disabled) { background: var(--bg3); }
69
+ .btn:disabled { opacity: 0.45; cursor: default; }
70
+ .btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
71
+ .btn.primary:hover { filter: brightness(1.08); background: var(--accent); }
72
+ .btn.toggle.on { background: var(--info-bg); border-color: var(--info); color: var(--info); }
73
+ .btn.capture { font-weight: 600; color: var(--ok); border-color: var(--ok); }
74
+ .btn.capture.on { background: var(--bad); border-color: var(--bad); color: #fff; }
75
+ .btn.capture.on:hover { background: var(--bad); filter: brightness(1.1); }
76
+ .btn.more { display: flex; margin: 6px 0; }
77
+ .btn.close2 { color: var(--fg2); }
78
+ .sv-tools .btn, .tn-acts .btn, .list-head .btn, .ev-tools .btn, .pane-info .btn, .dn-row .btn, td .btn, .ann-detail .btn {
79
+ padding: 0 6px; min-height: 18px; font-size: 11px; line-height: 16px;
80
+ }
81
+
82
+ .chip {
83
+ display: inline-flex; align-items: center; gap: 3px; max-width: 46em; height: 18px; padding: 0 5px;
84
+ border: 1px solid var(--line); border-radius: 3px; background: var(--bg2); color: var(--fg);
85
+ font-size: 11px; line-height: 16px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; vertical-align: middle; flex: none;
86
+ }
87
+ .chip.sm { height: 16px; padding: 0 4px; font-size: 10.5px; line-height: 14px; }
88
+ .chip.ok { color: var(--ok); background: var(--ok-bg); border-color: transparent; }
89
+ .chip.bad { color: var(--bad); background: var(--bad-bg); border-color: transparent; }
90
+ .chip.warn { color: var(--warn); background: var(--warn-bg); border-color: transparent; }
91
+ .chip.info { color: var(--info); background: var(--info-bg); border-color: transparent; }
92
+ .chip.dim { color: var(--fg2); background: var(--bg3); border-color: transparent; }
93
+ .chip.sub { color: var(--sub); background: var(--sub-bg); border-color: transparent; }
94
+ .chip.id { font-family: var(--mono); }
95
+ .chip.kind { font-weight: 600; }
96
+ .chip.k-turn { color: var(--info); background: var(--info-bg); border-color: transparent; }
97
+ .chip.k-compaction { color: var(--sub); background: var(--sub-bg); border-color: transparent; }
98
+ .chip.k-invalid { color: var(--bad); background: var(--bad-bg); border-color: transparent; }
99
+ .chip.hue { color: hsl(var(--hue) 60% var(--hue-fg-l)); background: hsl(var(--hue) var(--hue-s) var(--hue-l)); border-color: transparent; font-weight: 600; }
100
+ .chip.phase { color: var(--warn); border-color: var(--warn); background: transparent; }
101
+ .chip.name { font-family: var(--mono); color: var(--key); background: transparent; }
102
+ .chip.parts { color: var(--fg2); background: transparent; border-style: dashed; }
103
+ .chip.cache { color: var(--ok); border-color: var(--ok); background: transparent; }
104
+ .chip.enc { color: var(--sub); border-color: var(--sub); background: transparent; font-family: var(--mono); }
105
+ .chip.ann { max-width: 60em; font-weight: 600; }
106
+ .chip.ann[title] { cursor: help; }
107
+ .chip.ann.click { cursor: pointer; }
108
+ .link-chip { cursor: pointer; color: var(--accent); background: transparent; }
109
+ .link-chip:hover { text-decoration: underline; }
110
+
111
+ /* ---------- top bar ---------- */
112
+ .top {
113
+ display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding: 5px 10px;
114
+ border-bottom: 1px solid var(--line); background: var(--bg2); flex: none;
115
+ }
116
+ body.capturing .top { box-shadow: inset 0 3px 0 var(--bad); }
117
+ .brand { font-weight: 700; margin-right: 4px; }
118
+ .mem { display: inline-flex; align-items: center; gap: 5px; font-family: var(--mono); font-size: 11px; color: var(--fg2); white-space: nowrap; }
119
+ .mem meter { width: 70px; height: 10px; }
120
+ .filter { width: 280px; }
121
+ .chips { display: inline-flex; gap: 4px; flex-wrap: wrap; }
122
+ .pop { position: relative; }
123
+ .pop > summary { list-style: none; }
124
+ .pop > summary::-webkit-details-marker { display: none; }
125
+ .pop[open] > summary { background: var(--bg3); }
126
+ .pop-body {
127
+ position: absolute; top: calc(100% + 4px); left: 0; z-index: 30; min-width: 380px; max-width: 80vw; max-height: 60vh; overflow: auto;
128
+ padding: 6px; background: var(--bg); border: 1px solid var(--line); border-radius: 6px; box-shadow: var(--shadow);
129
+ }
130
+ .pop-body table { width: 100%; font-size: 12px; }
131
+ .pop-body th, .pop-body td { text-align: left; padding: 2px 6px; border-bottom: 1px solid var(--line2); white-space: nowrap; }
132
+ .pop-body tbody tr { cursor: pointer; }
133
+ .pop-body tbody tr:hover { background: var(--bg2); }
134
+ .banner { padding: 5px 12px; background: var(--warn-bg); color: var(--warn); border-bottom: 1px solid var(--line); flex: none; font-size: 12px; }
135
+ .banner.bad { background: var(--bad-bg); color: var(--bad); }
136
+ .banner .btn { margin-left: 8px; }
137
+
138
+ /* ---------- layout ---------- */
139
+ .main { flex: 1 1 auto; min-height: 0; display: grid; grid-template-columns: var(--list-w, 440px) 5px minmax(0, 1fr); }
140
+ .list-pane { display: flex; flex-direction: column; min-height: 0; min-width: 0; }
141
+ .list-info { padding: 3px 10px; font-size: 11px; color: var(--fg2); border-bottom: 1px solid var(--line); flex: none; display: flex; gap: 8px; flex-wrap: wrap; }
142
+ .list { flex: 1 1 auto; overflow: auto; min-height: 0; }
143
+ .list:focus { outline: none; }
144
+ .splitter { background: var(--line2); border-left: 1px solid var(--line); cursor: col-resize; }
145
+ .splitter:hover, .splitter.drag { background: var(--accent); }
146
+ .detail { display: flex; flex-direction: column; min-width: 0; min-height: 0; overflow: hidden; }
147
+
148
+ /* ---------- exchange list ---------- */
149
+ .xrow { padding: 4px 8px 4px 7px; border-bottom: 1px solid var(--line2); border-left: 3px solid transparent; cursor: pointer; }
150
+ .xrow.hued { border-left-color: hsl(var(--thue) 55% 55%); }
151
+ .xrow:hover { background: var(--bg2); }
152
+ .xrow.sel { background: var(--sel); }
153
+ .x1, .x2 { display: flex; align-items: center; gap: 5px; min-width: 0; }
154
+ .x2 { flex-wrap: wrap; margin-top: 2px; }
155
+ .x3 { margin-top: 2px; font-size: 11.5px; color: var(--fg2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
156
+ .x3.err { color: var(--bad); }
157
+ .x-time { font-family: var(--mono); font-size: 11.5px; color: var(--fg2); flex: none; }
158
+ .x-model { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
159
+ .x-model .dim { font-weight: 400; }
160
+ .x-dur { font-family: var(--mono); font-size: 11.5px; flex: none; min-width: 3.5em; text-align: right; }
161
+ .x-usage, .x-size { font-family: var(--mono); font-size: 11px; color: var(--fg2); white-space: nowrap; }
162
+ .x-size { margin-left: auto; }
163
+ .xrow.active .x-dur { color: var(--info); animation: pulse 1.2s ease-in-out infinite; }
164
+ @keyframes pulse { 50% { opacity: 0.35; } }
165
+ .tgroup {
166
+ position: sticky; top: 0; z-index: 2; display: flex; flex-wrap: wrap; align-items: center; gap: 2px 6px; padding: 3px 8px;
167
+ background: var(--bg3); border-bottom: 1px solid var(--line); font-size: 12px; cursor: pointer; white-space: nowrap;
168
+ }
169
+ .tgroup > * { flex: none; }
170
+ .tgroup .dim { flex: 0 1 auto; min-width: 0; max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
171
+ .swatch { width: 10px; height: 10px; border-radius: 2px; flex: none; background: var(--fg2); }
172
+ .tgroup.hued .swatch { background: hsl(var(--thue) 55% 55%); }
173
+
174
+ /* ---------- detail header, pipeline, panes ---------- */
175
+ .d-head { padding: 6px 10px; border-bottom: 1px solid var(--line); flex: none; }
176
+ .d-line { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; }
177
+ .d-line + .d-line { margin-top: 4px; }
178
+ .d-seq { font-weight: 700; }
179
+ .d-model { font-weight: 600; }
180
+ .d-actions { display: inline-flex; gap: 4px; }
181
+ .pipe { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; padding: 6px 10px; border-bottom: 1px solid var(--line); background: var(--bg2); flex: none; }
182
+ .card {
183
+ display: inline-flex; flex-direction: column; align-items: flex-start; gap: 1px; padding: 3px 9px; min-width: 112px;
184
+ border: 1px solid var(--line); border-left: 3px solid var(--card-c, var(--line)); border-radius: 5px;
185
+ background: var(--bg); cursor: pointer; text-align: left;
186
+ }
187
+ .card:hover { background: var(--bg3); }
188
+ .card.ok { --card-c: var(--ok); }
189
+ .card.bad { --card-c: var(--bad); }
190
+ .card.warn { --card-c: var(--warn); }
191
+ .card.info { --card-c: var(--info); }
192
+ .card.dim { --card-c: var(--fg2); }
193
+ .card.sel { background: var(--sel); border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
194
+ .card.sel2 { border-style: dashed; border-color: var(--accent); }
195
+ .card-main { font-size: 12px; font-weight: 600; white-space: nowrap; }
196
+ .card-sub { font-size: 11px; color: var(--fg2); font-family: var(--mono); white-space: nowrap; max-width: 22em; overflow: hidden; text-overflow: ellipsis; }
197
+ .card.narrow .card-sub { max-width: 19em; }
198
+ .pipe-side { display: flex; flex-direction: column; gap: 3px; }
199
+ .card.mini { min-width: 64px; padding: 1px 9px; }
200
+ .pipe-calls { display: flex; flex-direction: column; gap: 4px; }
201
+ .pipe-call { display: flex; align-items: center; gap: 5px; }
202
+ .arrow { color: var(--fg2); flex: none; }
203
+ .call-label { display: inline-block; padding: 0 6px; border-radius: 3px; font-size: 11px; line-height: 17px; white-space: nowrap; background: var(--bg3); color: var(--fg2); min-width: 8em; text-align: center; }
204
+ .call-label.l-main { background: var(--info-bg); color: var(--info); }
205
+ .call-label.l-summary { background: var(--warn-bg); color: var(--warn); }
206
+ .call-label.l-native { background: var(--sub-bg); color: var(--sub); }
207
+ .panes { flex: 1 1 auto; min-height: 0; display: flex; }
208
+ .pane { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; min-height: 0; }
209
+ .panes.split .pane + .pane { border-left: 2px solid var(--line); }
210
+ .pane-head { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; padding: 4px 10px; border-bottom: 1px solid var(--line); flex: none; }
211
+ .pane-title { font-weight: 600; white-space: nowrap; }
212
+ .tabs { display: inline-flex; border: 1px solid var(--line); border-radius: 4px; overflow: hidden; }
213
+ .tab { border: 0; border-right: 1px solid var(--line); padding: 1px 10px; background: var(--bg); color: var(--fg2); cursor: pointer; font-size: 12px; }
214
+ .tab:last-child { border-right: 0; }
215
+ .tab:hover { background: var(--bg3); }
216
+ .tab.on { background: var(--accent); color: #fff; }
217
+ .pane-info { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 6px; font-size: 11.5px; min-width: 0; }
218
+ .pane-info .mono { max-width: 40em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
219
+ .pane-body { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 8px 10px 40px; }
220
+
221
+ /* ---------- JSON tree ---------- */
222
+ .tree { line-height: 1.45; }
223
+ .tn-row { padding: 0 2px; border-radius: 2px; white-space: pre-wrap; overflow-wrap: anywhere; }
224
+ .tn-click { cursor: pointer; }
225
+ .tn-row:hover { background: var(--bg2); }
226
+ .tn-kids { margin-left: 5px; padding-left: 11px; border-left: 1px dotted var(--line); }
227
+ .tn-tog { display: inline-block; width: 12px; text-align: center; color: var(--fg2); user-select: none; flex: none; }
228
+ .tn-key { color: var(--key); }
229
+ .tn-idx { color: var(--fg2); }
230
+ .tn-sep { color: var(--fg2); }
231
+ .tn-sum { color: var(--fg2); margin-right: 6px; }
232
+ .tn-pv { color: var(--fg2); font-style: italic; }
233
+ .tn-acts { display: none; margin-left: 8px; gap: 3px; }
234
+ .tn-row:hover > .tn-acts { display: inline-flex; }
235
+ .tn-str { margin-left: 17px; }
236
+ .tn-row img.thumb, .item-row img.thumb { vertical-align: middle; }
237
+
238
+ /* ---------- long strings ---------- */
239
+ .sv { border: 1px solid var(--line); border-radius: 4px; margin: 2px 0 5px; background: var(--bg2); min-width: 0; }
240
+ .sv-tools { display: flex; align-items: center; flex-wrap: wrap; gap: 4px; padding: 2px 5px; border-bottom: 1px solid var(--line2); font-family: system-ui, sans-serif; font-size: 11px; }
241
+ .sv-text { padding: 4px 7px; white-space: pre-wrap; overflow-wrap: anywhere; max-height: 60vh; overflow: auto; }
242
+ .sv-omit { display: inline; padding: 0 4px; margin: 0 2px; border-radius: 3px; background: var(--warn-bg); color: var(--warn); cursor: pointer; font-style: italic; }
243
+ .sv-omit:hover { text-decoration: underline; }
244
+ .sv-extra:not(:empty) { padding: 4px 7px; border-top: 1px solid var(--line2); }
245
+ img.thumb { border: 1px solid var(--line); border-radius: 3px; background: repeating-conic-gradient(var(--bg3) 0 25%, var(--bg) 0 50%) 0 0 / 10px 10px; cursor: zoom-in; vertical-align: middle; }
246
+ img.thumb.sm { height: 28px; max-width: 64px; object-fit: contain; }
247
+ img.thumb.md { max-height: 120px; max-width: 240px; }
248
+ img.thumb.big { height: auto; max-height: 70vh; max-width: min(90%, 900px); cursor: zoom-out; }
249
+
250
+ /* ---------- structure view ---------- */
251
+ .struct { min-width: 0; }
252
+ table.fields { width: 100%; table-layout: auto; }
253
+ .fields th { text-align: left; font: 600 11px system-ui, sans-serif; color: var(--fg2); padding: 2px 6px; border-bottom: 1px solid var(--line); }
254
+ .fields td { padding: 2px 6px; border-bottom: 1px solid var(--line2); vertical-align: top; }
255
+ .fields tr.clickable:hover > td { background: var(--bg2); }
256
+ .fkey { white-space: nowrap; color: var(--key); width: 1%; }
257
+ .ftype { white-space: nowrap; color: var(--fg2); width: 1%; font-size: 11px; }
258
+ .fval { overflow-wrap: anywhere; }
259
+ .fval .chip { margin-left: 4px; }
260
+ tr.field-detail > td { background: var(--bg2); padding: 6px 8px 8px 22px; border-bottom: 1px solid var(--line); }
261
+ .item-list { margin-top: 14px; }
262
+ .list-head {
263
+ position: sticky; top: -8px; z-index: 3; display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
264
+ padding: 5px 0; background: var(--bg); border-bottom: 1px solid var(--line);
265
+ }
266
+ .list-title { font-family: var(--mono); font-weight: 700; }
267
+ .list-filter { width: 230px; }
268
+ .item { border-bottom: 1px solid var(--line2); }
269
+ .item-row { display: flex; align-items: center; gap: 6px; padding: 2px 4px; min-height: 24px; cursor: pointer; white-space: nowrap; overflow: hidden; }
270
+ .item-row:hover { background: var(--bg2); }
271
+ .item.open > .item-row { background: var(--bg3); }
272
+ .idx { font-family: var(--mono); font-size: 11.5px; color: var(--fg2); min-width: 3.2em; flex: none; }
273
+ .badges { display: inline-flex; gap: 3px; flex: none; }
274
+ .pv { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; color: var(--fg2); font-family: var(--mono); font-size: 12px; }
275
+ .metrics { flex: none; color: var(--fg2); font-family: var(--mono); font-size: 11px; }
276
+ .item-body { padding: 4px 8px 8px 26px; background: var(--bg2); border-top: 1px solid var(--line2); }
277
+ .item-anns { padding: 4px 8px 4px 26px; display: grid; gap: 4px; }
278
+ .flash { animation: flash 1.8s ease-out; }
279
+ @keyframes flash { 0%, 30% { background: var(--warn-bg); box-shadow: inset 3px 0 0 var(--warn); } }
280
+
281
+ /* ---------- annotations, kv tables, overview ---------- */
282
+ .ann-detail { padding: 6px 8px; background: var(--bg2); border: 1px solid var(--line); border-radius: 4px; min-width: 0; }
283
+ .sub-title { font-weight: 600; font-size: 11.5px; color: var(--fg2); margin: 8px 0 3px; }
284
+ table.kv th { text-align: left; font-weight: 400; color: var(--fg2); padding: 2px 10px 2px 0; vertical-align: top; white-space: nowrap; }
285
+ table.kv td { padding: 2px 0; overflow-wrap: anywhere; vertical-align: top; }
286
+ table.kv.headers th { font-family: var(--mono); font-size: 12px; color: var(--key); }
287
+ table.kv.fields-kv { width: 100%; }
288
+ table.kv.fields-kv th { width: 1%; }
289
+ table.kv.fields-kv tr { border-bottom: 1px solid var(--line2); }
290
+ .hval { overflow-wrap: anywhere; }
291
+ .overview h4 { margin: 16px 0 5px; font-size: 12px; color: var(--fg2); }
292
+ .overview h4:first-child { margin-top: 0; }
293
+ table.calls, table.anns { width: 100%; font-size: 12px; }
294
+ .calls th, .calls td, .anns th, .anns td { text-align: left; padding: 3px 6px; border-bottom: 1px solid var(--line2); vertical-align: top; }
295
+ .calls th, .anns th { font-weight: 600; color: var(--fg2); border-bottom-color: var(--line); white-space: nowrap; }
296
+ .calls tr.clickable:hover > td, .anns tr.clickable:hover > td { background: var(--bg2); }
297
+ .anns tr.field-detail > td { padding: 4px 8px 10px; }
298
+ .timeline { display: grid; gap: 3px; max-width: 1100px; }
299
+ .tl-row { display: grid; grid-template-columns: 130px minmax(120px, 1fr) 270px; gap: 8px; align-items: center; padding: 1px 2px; border-radius: 3px; }
300
+ .tl-row:hover { background: var(--bg2); }
301
+ .tl-label { font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
302
+ .tl-track { position: relative; height: 12px; background: var(--bg3); border-radius: 3px; overflow: hidden; }
303
+ .tl-bar { position: absolute; top: 2px; bottom: 2px; border-radius: 2px; background: var(--fg2); min-width: 2px; }
304
+ .tl-bar.ok { background: var(--ok); }
305
+ .tl-bar.bad { background: var(--bad); }
306
+ .tl-bar.warn { background: var(--warn); }
307
+ .tl-bar.info { background: var(--info); }
308
+ .tl-head { position: absolute; top: 0; bottom: 0; width: 2px; background: var(--fg); opacity: 0.7; }
309
+ .tl-num { font-size: 11px; color: var(--fg2); white-space: nowrap; }
310
+ .tl-axis { font-size: 11px; margin-top: 2px; }
311
+
312
+ /* ---------- raw ---------- */
313
+ .rawbox .sv-tools { position: sticky; top: -8px; z-index: 2; background: var(--bg); border: 1px solid var(--line); border-radius: 4px 4px 0 0; }
314
+ .raw { padding: 6px 8px; white-space: pre-wrap; overflow-wrap: anywhere; background: var(--bg2); border: 1px solid var(--line); border-top: 0; border-radius: 0 0 4px 4px; }
315
+
316
+ /* ---------- events ---------- */
317
+ .ev-tools { position: sticky; top: -8px; z-index: 3; display: flex; align-items: center; flex-wrap: wrap; gap: 8px; padding: 5px 0; background: var(--bg); border-bottom: 1px solid var(--line); }
318
+ .ev { border-bottom: 1px solid var(--line2); }
319
+ .ev.keep { opacity: 0.6; }
320
+ .ev-row { display: flex; align-items: center; gap: 8px; padding: 1px 4px; min-height: 21px; cursor: pointer; white-space: nowrap; overflow: hidden; }
321
+ .ev-row:hover { background: var(--bg2); }
322
+ .ev.group > .ev-row { background: color-mix(in srgb, var(--info-bg) 55%, transparent); }
323
+ .ev.group > .ev-row:hover { background: var(--info-bg); }
324
+ .ev-t { min-width: 8.5em; text-align: right; color: var(--fg2); flex: none; }
325
+ .ev-i { min-width: 6.5em; color: var(--fg2); flex: none; }
326
+ .ev-type { color: var(--accent); font-weight: 600; flex: none; }
327
+ .ev-type.err { color: var(--bad); }
328
+ .ev-body { padding: 4px 8px 8px 28px; background: var(--bg2); border-top: 1px solid var(--line2); font-family: system-ui, sans-serif; }
329
+ .ev-sub { margin-top: 4px; border-left: 2px solid var(--line); padding-left: 6px; }
330
+ .ev-body .ann-detail { margin-bottom: 6px; }
331
+
332
+ /* ---------- diff ---------- */
333
+ .diffpane { display: flex; flex-direction: column; gap: 8px; }
334
+ .diff-tools { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
335
+ .diff-side { display: inline-flex; align-items: center; gap: 4px; padding: 3px 6px; border: 1px solid var(--line); border-radius: 5px; background: var(--bg2); }
336
+ .diff-side > b { font-family: var(--mono); }
337
+ .ex-sel { max-width: 380px; }
338
+ .diff-legend { font-size: 11.5px; color: var(--fg2); }
339
+ .diff-stats { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; margin: 4px 0 8px; }
340
+ .diff-tree { line-height: 1.45; }
341
+ .dn-row { padding: 0 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; border-radius: 2px; }
342
+ .dn-row.tn-click:hover { background: var(--bg3); }
343
+ .sign { display: inline-block; width: 1.3em; font-weight: 700; color: var(--fg2); text-align: center; }
344
+ .dn.add > .dn-row { background: var(--ok-bg); }
345
+ .dn.add > .dn-row .sign { color: var(--ok); }
346
+ .dn.del > .dn-row { background: var(--bad-bg); }
347
+ .dn.del > .dn-row .sign { color: var(--bad); }
348
+ .dn.ch > .dn-row .sign, .dn.mod > .dn-row .sign { color: var(--warn); }
349
+ .dn.eq > .dn-row { color: var(--fg2); }
350
+ .dn .old { color: var(--bad); text-decoration: line-through; }
351
+ .dn .new { color: var(--ok); }
352
+ .dn-body { padding: 2px 0 4px 26px; }
353
+ .eqrun { padding: 0 2px 0 1.6em; color: var(--fg2); font-style: italic; cursor: pointer; border-radius: 2px; }
354
+ .eqrun:hover { background: var(--bg3); }
355
+ .sdiff { margin: 2px 0 6px 26px; }
356
+ .sdiff .sv-text { border: 1px solid var(--line); border-radius: 4px; background: var(--bg2); }
357
+ .sdiff .old { background: var(--bad-bg); color: var(--bad); text-decoration: line-through; }
358
+ .sdiff .new { background: var(--ok-bg); color: var(--ok); }
359
+ .sdiff .ctx { color: var(--fg2); }
360
+
361
+ /* ---------- overlays ---------- */
362
+ .toast {
363
+ position: fixed; left: 50%; bottom: 18px; transform: translateX(-50%); z-index: 100; max-width: 80vw;
364
+ padding: 6px 14px; border-radius: 6px; background: var(--fg); color: var(--bg); box-shadow: var(--shadow);
365
+ opacity: 0; pointer-events: none; transition: opacity 0.2s;
366
+ }
367
+ .toast.show { opacity: 1; }
368
+ .toast.err { background: var(--bad); color: #fff; }
369
+ .drop {
370
+ position: fixed; inset: 12px; z-index: 90; display: flex; align-items: center; justify-content: center;
371
+ border: 3px dashed var(--accent); border-radius: 12px; background: color-mix(in srgb, var(--bg) 85%, transparent);
372
+ font-size: 20px; font-weight: 600; color: var(--accent); pointer-events: none;
373
+ }
374
+ .token-dialog { width: 380px; max-width: 92vw; padding: 16px 18px; border: 1px solid var(--line); border-radius: 8px; background: var(--bg); color: var(--fg); box-shadow: var(--shadow); }
375
+ .token-dialog::backdrop { background: rgb(0 0 0 / 0.35); }
376
+ .token-dialog h3 { font-size: 15px; margin-bottom: 6px; }
377
+ .token-dialog p { margin: 6px 0; font-size: 12px; }
378
+ .token-dialog input { width: 100%; min-height: 28px; }
379
+ .dialog-buttons { display: flex; justify-content: flex-end; gap: 6px; margin-top: 12px; }