@y9931666/dsh-plugin-crossfire 0.1.0 → 0.1.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 (38) hide show
  1. package/README.md +2 -0
  2. package/lib/client.js +637 -204
  3. package/lib/index.js +339 -165
  4. package/lib/{state-CXNvzj_T.js → state-B2anbn4l.js} +2 -2
  5. package/lib/{state-DzrgVdrB.js → state-DvF_Jqcg.js} +13 -1
  6. package/package.json +56 -20
  7. package/release-notes/v0.1.1.md +58 -0
  8. package/release-notes/v0.1.2.md +44 -0
  9. package/release-notes/v0.1.3.md +21 -0
  10. package/lib/types/budget.d.ts +0 -86
  11. package/lib/types/client/ReviewCard.d.ts +0 -41
  12. package/lib/types/client/SettingsView.d.ts +0 -34
  13. package/lib/types/client/brand.d.ts +0 -55
  14. package/lib/types/client/index.d.ts +0 -21
  15. package/lib/types/client/locales.d.ts +0 -19
  16. package/lib/types/client/merge.d.ts +0 -27
  17. package/lib/types/client/slot-boundary.d.ts +0 -46
  18. package/lib/types/client/wire.d.ts +0 -192
  19. package/lib/types/dimensions.d.ts +0 -47
  20. package/lib/types/engine.d.ts +0 -153
  21. package/lib/types/etag.d.ts +0 -40
  22. package/lib/types/expert-events.d.ts +0 -110
  23. package/lib/types/export-md.d.ts +0 -21
  24. package/lib/types/harness-compat.d.ts +0 -104
  25. package/lib/types/index.d.ts +0 -76
  26. package/lib/types/members.d.ts +0 -76
  27. package/lib/types/noise.d.ts +0 -29
  28. package/lib/types/preferences.d.ts +0 -134
  29. package/lib/types/prompt.d.ts +0 -16
  30. package/lib/types/redteam.d.ts +0 -115
  31. package/lib/types/review.d.ts +0 -144
  32. package/lib/types/rpc.d.ts +0 -176
  33. package/lib/types/split.d.ts +0 -142
  34. package/lib/types/state.d.ts +0 -74
  35. package/lib/types/tools.d.ts +0 -51
  36. package/lib/types/types.d.ts +0 -287
  37. package/lib/types/version.d.ts +0 -18
  38. package/lib/types/web-guard.d.ts +0 -55
package/lib/client.js CHANGED
@@ -76,7 +76,7 @@ async function fetchReviews(session, signal) {
76
76
  try {
77
77
  const query = session === "" ? "" : `?session=${encodeURIComponent(session)}`;
78
78
  const response = await fetch(`${STATE_ROUTE}${query}`, {
79
- cache: "no-store",
79
+ cache: "no-cache",
80
80
  ...signal === void 0 ? {} : { signal }
81
81
  });
82
82
  const body = await response.json();
@@ -222,9 +222,243 @@ function routeText(provider, model) {
222
222
  return [provider, model].filter((part) => part !== void 0 && part.trim() !== "").join(" / ");
223
223
  }
224
224
 
225
+ //#endregion
226
+ //#region src/client/defectTitle.ts
227
+ /**
228
+ * 观点标题:一条缺陷卡片的"抬头"。
229
+ *
230
+ * 红队的报告信封要求每条发言以 `观点 N(指向 X,维度Y):正文` 开头,而这个前缀
231
+ * 恰恰就是最有信息量的一行(编号 + 定位 + 维度)。用户反馈:"观点虽然说我可以理解,
232
+ * 把完整内容给写出来了,但我觉得还是要在最开始那里立一个标题,这样目的明确"。
233
+ *
234
+ * ## 为什么单独一个模块
235
+ *
236
+ * 标题按钮是**折叠态唯一可见的内容**,所以这里的两条边界(空括号信封、内嵌换行)
237
+ * 一旦退化,肉眼看到的是"整张卡片塌成一团",而不是一条错误文案。它是纯字符串函数,
238
+ * 因此抽出来直接单测 —— 不需要 jsdom / react 测试环境(本仓库只有 `node:test`)。
239
+ *
240
+ * ## 标题从哪里来(三层,逐层兜底)
241
+ *
242
+ * 1. **压缩信封**:`观点 N(指向 dsh-plugin-crossfire/src/client/ReviewCard.module.css:106-176,维度 交互)`
243
+ * → `观点 N · ReviewCard.module.css:106-176 · 交互`
244
+ * 红队写**完整路径**是对的(审计要能定位到文件),但它不该占满标题 —— 用户实测反馈:
245
+ * "标题过长,导致后面无法显示;且有很多无用信息,比如路径的位置,这个可以放在内容里"。
246
+ * 所以完整路径仍留在缺陷正文(可追溯),标题只留**文件名 + 行号 + 维度**。
247
+ * 2. **原始信封**(压缩不出来时,例如括号里没有 `指向`/`维度` 的旧格式)→ 原样返回前缀。
248
+ * 3. **首句兜底**:未按格式提交时取首句(按换行、再按中英文句末标点切,限长)。
249
+ *
250
+ * **绝不**返回空串 —— 标题位空了比不显示更糟。
251
+ */
252
+ /** 标题硬上限(字符)。超过即截断并加省略号。 */
253
+ const TITLE_MAX = 80;
254
+ /** 压缩后的单个片段上限(文件名或维度名过长时截断,避免任一片段独吞整行)。 */
255
+ const PART_MAX = 40;
256
+ /**
257
+ * 信封:`观点 N(…)` / `观点N(…)`。
258
+ *
259
+ * 括号内必须**至少有一个既非空白、也非右括号的字符**:`观点 1()`、`观点 1( )`、
260
+ * `观点 1(\u3000)` 是"格式没填",不是"合法信封"。旧写法 `[^))]*` 会把它们当成命中
261
+ * 直接返回,于是标题退化成无信息串、还绕过了首句兜底。
262
+ *
263
+ * 首个字符的字符类是 `[^))\s]` 而不是 `\S`:`\S` 能匹配**右括号本身**,于是
264
+ * `观点 1()真正的首句。` 会靠回溯让 `\S` 吃掉 `)`、再由 `[^))]*[))]` 一路吃到
265
+ * 下一个 `)`(跨过右括号),把一条没按格式提交的发言重新判成合法信封。
266
+ * 两侧的 `\s*` 只作用于**括号内首尾的空白**,正文与冒号之间仍允许空格(`观点 1(指向 y):`)。
267
+ */
268
+ const ENVELOPE = /^(观点\s*\d+\s*[((]\s*[^))\s]\s*[^))]*[))])\s*[::]?/;
269
+ /**
270
+ * 信封内部:`观点 N(指向 X,维度 Y)` 的三段。
271
+ *
272
+ * `指向` 后取到**逗号或右括号**为止(中英文逗号、顿号都认);`维度` 后取到右括号为止。
273
+ * 用 `[\s\S]` 而不是 `.`:定位串里可能出现换行(红队偶尔换行排版)。
274
+ */
275
+ const ENVELOPE_PARTS = /^观点\s*(\d+)\s*[((]\s*指向\s*([\s\S]*?)\s*[,,、]\s*维度\s*([\s\S]*?)\s*[))]/;
276
+ /**
277
+ * 从一条缺陷正文里导出标题。见模块注释的三层兜底。
278
+ *
279
+ * @param content 缺陷原文(可以是任意多行文本)。
280
+ * @returns 非空标题,最长 {@link TITLE_MAX} 字符。
281
+ */
282
+ function defectTitle(content) {
283
+ const text = content.trim();
284
+ const envelope = ENVELOPE.exec(text);
285
+ if (envelope !== null && envelope[1] !== void 0) {
286
+ const raw = envelope[1].trim();
287
+ const compact = compactEnvelope(raw);
288
+ if (compact !== void 0) return take(compact, TITLE_MAX);
289
+ const index = /观点\s*(\d+)/.exec(raw)?.[1];
290
+ const tail = text.slice(envelope[0].length).replace(/^[\s::]+/, "");
291
+ const firstLine$1 = (tail.split(/\r?\n/)[0] ?? tail).trim();
292
+ const firstSentence = (firstLine$1.split(/(?<=[。..;;!!??])/)[0] ?? firstLine$1).trim();
293
+ if (index !== void 0 && firstSentence !== "") return take(`观点 ${index} · ${firstSentence}`, TITLE_MAX);
294
+ if (index !== void 0) {
295
+ const dimension = /维度\s*([^))]*)/.exec(raw)?.[1]?.trim();
296
+ if (dimension !== void 0 && dimension !== "") return take(`观点 ${index} · ${dimension}`, TITLE_MAX);
297
+ return `观点 ${index}`;
298
+ }
299
+ return take(raw, TITLE_MAX);
300
+ }
301
+ const firstLine = text.split(/\r?\n/)[0] ?? text;
302
+ const title = (firstLine.split(/(?<=[。..;;!!??])/)[0] ?? firstLine).trim();
303
+ if (title === "") return take(text, TITLE_MAX);
304
+ return take(title, TITLE_MAX);
305
+ }
306
+ /**
307
+ * 把信封压缩成 `观点 N · 文件名:行号 · 维度`。
308
+ *
309
+ * @returns 压缩结果;**形状不认识时返回 `undefined`**(调用方退回原始前缀),
310
+ * 而不是硬拼一个残缺标题。
311
+ */
312
+ function compactEnvelope(envelope) {
313
+ const parts = ENVELOPE_PARTS.exec(envelope);
314
+ if (parts === null) return void 0;
315
+ const index = parts[1] ?? "";
316
+ const pointer = shortPointer(parts[2] ?? "");
317
+ const dimension = (parts[3] ?? "").trim().replace(/\s+/g, " ");
318
+ if (index === "" || pointer === "" || dimension === "") return void 0;
319
+ return `观点 ${index} · ${take(pointer, PART_MAX)} · ${take(dimension, PART_MAX)}`;
320
+ }
321
+ /**
322
+ * 把"指向"里的定位串压成**文件名 + 行号**:
323
+ *
324
+ * `dsh-plugin-crossfire/src/client/ReviewCard.module.css:106-176` → `ReviewCard.module.css:106-176`
325
+ * `src/a.ts` → `a.ts`
326
+ * `README.md` → `README.md`
327
+ *
328
+ * 只认 `/` 与 `\` 两种分隔符(Windows 路径也要能压)。文件名与行号之间的冒号保留 ——
329
+ * 行号是"跳到哪一行"的关键信息,不能丢。
330
+ */
331
+ function shortPointer(pointer) {
332
+ const compact = pointer.trim().replace(/\s+/g, " ");
333
+ if (compact === "") return "";
334
+ return (compact.split(/[/\\]/).pop() ?? compact).trim();
335
+ }
336
+ /**
337
+ * 截断到 `max` 个字符;被截断时补省略号。
338
+ *
339
+ * 省略号只在**真的截掉了内容**时加:`text` 恰好 `max` 字时不该显示一个假的"还有更多"。
340
+ */
341
+ function take(text, max) {
342
+ return text.length > max ? `${text.slice(0, max)}…` : text;
343
+ }
344
+
345
+ //#endregion
346
+ //#region src/client/session-cache.ts
347
+ /**
348
+ * 会话身份的**浏览器侧缓存**(设置页保存失败的真实修复)。
349
+ *
350
+ * ## 它修的是什么
351
+ *
352
+ * `crossfire/prefs.set` 在宿主侧要求"必须声称一个身份"(`judgeActor(undefined, session)`)。
353
+ * 而宿主的 slot 契约决定了设置页**结构上拿不到** `sessionId` —— 运行时 slot 目录里:
354
+ *
355
+ * conversation.view → standardProps 含 `sessionId: SessionId`
356
+ * tool.call.toolview → 含
357
+ * settings.section → **不含**(只有 useResource / useWorkspaces / usePanelInfo /
358
+ * useSessions / useSessionStatus / useSessionRetainInfo)
359
+ *
360
+ * 于是卡片(conversation.view / toolcall)保存偏好成功,而设置页的每一次保存都被
361
+ * 拒成"保存失败"—— Bug 的真实形态:**角色预设 / 阵容预设 / 强度 / 预算全部存不进去**。
362
+ *
363
+ * ## 为什么用一个模块级缓存而不是给设置页造假身份
364
+ *
365
+ * 设置页确实是全局的(偏好也是全局的),但它不能在**不知道自己是谁**的情况下写入。
366
+ * 而会话标签页(`conversation.view`)与轨迹卡片(`tool.call.toolview`)都拿到了宿主
367
+ * 注入的真实 `sessionId` —— 用户打开过任意一个,就等于把身份告诉了插件。这里把它记下来,
368
+ * 供设置页在提交时声明。
369
+ *
370
+ * ## 诚实的边界
371
+ *
372
+ * 冷启动后**直接**打开设置页、且本次会话从未渲染过上述任一入口时,缓存是空的,
373
+ * 保存仍会被宿主拒绝(这是刻意的:插件不编造身份)。绕开方式很简单——先看一眼
374
+ * 「针锋」标签页或对话卡片,再回来保存。
375
+ */
376
+ /** 最近一次从宿主注入里观察到的会话 id(空串 = 还不知道)。 */
377
+ let lastKnownSessionId = "";
378
+ /**
379
+ * 记下宿主注入的会话 id。
380
+ *
381
+ * 只接受非空字符串;重复值不产生副作用。返回是否**发生了变化**,便于调用方避免
382
+ * 无意义的重渲染。
383
+ */
384
+ function rememberSessionId(value) {
385
+ if (typeof value !== "string") return false;
386
+ const trimmed = value.trim();
387
+ if (trimmed === "" || trimmed === lastKnownSessionId) return false;
388
+ lastKnownSessionId = trimmed;
389
+ return true;
390
+ }
391
+ /** 当前已知的会话 id(空串 = 尚未观察到,调用方不得据此编造一个)。 */
392
+ function knownSessionId() {
393
+ return lastKnownSessionId;
394
+ }
395
+ /**
396
+ * 把一个 payload 补上会话身份(已有 `session` 时不覆盖)。
397
+ *
398
+ * 这是设置页真正用得上的那个函数:它只在**确实知道**身份时才附加字段,否则原样返回,
399
+ * 让宿主按它自己的规则拒绝 —— 而不是伪造一个 `session: ''` 去骗过存在性检查。
400
+ */
401
+ function withSessionIdentity(payload) {
402
+ if ("session" in payload) return payload;
403
+ const known = knownSessionId();
404
+ if (known === "") return payload;
405
+ return {
406
+ ...payload,
407
+ session: known
408
+ };
409
+ }
410
+
411
+ //#endregion
412
+ //#region src/client/promptView.ts
413
+ /**
414
+ * 点击"查看原提示词":无条目 → 写入 `loading` 并要求发请求;有条目 → 删除该条目
415
+ * (收起),`start` 为 false。
416
+ *
417
+ * 三态一律同样对待:`ready` / `error` / 正在 `loading` 的条目,再点都是收起。
418
+ * 失败后保留 `error` 条目正是为此 —— 用户再点即收起,而不是无限重拉。
419
+ */
420
+ function togglePromptView(table, key) {
421
+ if (table[key] !== void 0) {
422
+ const next = { ...table };
423
+ delete next[key];
424
+ return {
425
+ table: next,
426
+ start: false
427
+ };
428
+ }
429
+ return {
430
+ table: {
431
+ ...table,
432
+ [key]: { status: "loading" }
433
+ },
434
+ start: true
435
+ };
436
+ }
437
+ /**
438
+ * 把一次 RPC 结果写回展开表。
439
+ *
440
+ * **只有该键仍处于 `loading` 时才写回**。用户已收起(键不存在)、或该键已经处于
441
+ * 终态时,返回原表**引用不变** —— 这样 `setState` 不会触发一次无意义的重渲染。
442
+ */
443
+ function applyPromptResult(table, key, result) {
444
+ const current = table[key];
445
+ if (current === void 0 || current.status !== "loading") return table;
446
+ const next = result.ok ? {
447
+ status: "ready",
448
+ persona: result.persona
449
+ } : {
450
+ status: "error",
451
+ message: result.message
452
+ };
453
+ return {
454
+ ...table,
455
+ [key]: next
456
+ };
457
+ }
458
+
225
459
  //#endregion
226
460
  //#region \0dsh-css:F:\AI\DSH\1changyong\dsh-plugin-crossfire\src\client\ReviewCard.module.css.mjs
227
- const css$1 = "/*\n * Crossfire Chat card styles.\n *\n * 硬约束:**每个颜色都来自主题令牌**(`Theme.dsefkao_listTokens` 的 `--dsw-alias-*` 一族),\n * 所以浅色与深色主题都无需自己写 override。圆桌的设置页 CSS 用了硬编码\n * `#ffffff` / `rgba(0,0,0,.8)`,在深色主题下是坏的 —— 这里不重复那个错误。\n */\n\n.dsefkao_root {\n display: flex;\n flex-direction: column;\n gap: 10px;\n padding: 12px 14px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n font-size: 13px;\n line-height: 1.6;\n}\n\n.dsefkao_card {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.dsefkao_card + .dsefkao_card {\n margin-top: 10px;\n padding-top: 10px;\n border-top: 1px dashed var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n}\n\n/* 折叠的评审:一行摘要,点一下切换到它。卡片会列出本会话的全部评审,全展开\n 就是一堵墙(验收期间真实攒到 5 场、12 条缺陷)。 */\n.dsefkao_collapsed {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n margin-top: 6px;\n padding: 6px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n font: inherit;\n font-size: 12px;\n text-align: left;\n cursor: pointer;\n}\n\n.dsefkao_collapsed:hover {\n border-color: var(--dsw-alias-brand-primary, #4a6cf7);\n}\n\n.dsefkao_header {\n display: flex;\n align-items: baseline;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n.dsefkao_title {\n font-weight: 700;\n font-size: 13px;\n letter-spacing: 0.02em;\n}\n\n.dsefkao_subjectTitle {\n font-weight: 600;\n}\n\n.dsefkao_badge {\n font-size: 11px;\n padding: 1px 8px;\n border-radius: 999px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_badge_draft {\n border-color: var(--dsw-alias-state-idle-primary, rgba(128, 128, 128, 0.5));\n}\n\n.dsefkao_badge_attacking {\n border-color: var(--dsw-alias-state-warn-primary, rgba(200, 150, 0, 0.6));\n color: var(--dsw-alias-state-warn-primary, inherit);\n}\n\n.dsefkao_badge_judging {\n border-color: var(--dsw-alias-brand-primary, #4a6cf7);\n color: var(--dsw-alias-brand-primary, inherit);\n}\n\n.dsefkao_badge_settled {\n border-color: var(--dsw-alias-state-success-primary, #2e7d32);\n color: var(--dsw-alias-state-success-primary, inherit);\n}\n\n/* ---- 状态区:当前处于哪一态 + 下一步该做什么(唯一主行动) ----------------\n *\n * 这是卡片上**唯一**的强对比区域,其余段落一律保持低对比(标题灰、正文常规)。\n * 主行动是这一区里唯一的主色实心按钮,所以\"现在该干什么\"不需要读完全卡去找。\n */\n.dsefkao_statusStrip {\n display: flex;\n align-items: flex-start;\n gap: 12px;\n flex-wrap: wrap;\n padding: 10px 12px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n}\n\n/* 左侧信息块:态名 + 轮次 + 一句话说明;吃掉剩余宽度,主行动因此被推到右侧。 */\n.dsefkao_statusStripInfo {\n display: flex;\n flex-direction: column;\n gap: 2px;\n flex: 1 1 240px;\n min-width: 0;\n}\n\n.dsefkao_statusStripHead {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n/* 态名用反色实心药丸:它与主行动按钮同色,一眼就能配对(\"现在在这里 → 下一步在那里\")。 */\n.dsefkao_statusState {\n font-size: 12px;\n font-weight: 600;\n padding: 2px 10px;\n border-radius: 999px;\n background: var(--dsw-alias-brand-primary, #4a6cf7);\n color: var(--dsw-alias-bg-base, #ffffff);\n}\n\n.dsefkao_statusHint {\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n/* 右侧行动块:一个主按钮 + 一行\"下一步\"小标题。 */\n.dsefkao_statusStripAction {\n display: flex;\n flex-direction: column;\n align-items: flex-end;\n gap: 4px;\n margin-left: auto;\n}\n\n.dsefkao_statusNextLabel {\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.04em;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n/* 主行动:唯一的主色实心按钮。字号与内边距都比次要按钮大一档,视觉权重随之提高。 */\n.dsefkao_cta {\n font: inherit;\n font-size: 13px;\n font-weight: 600;\n padding: 7px 18px;\n border-radius: 8px;\n cursor: pointer;\n border: 1px solid var(--dsw-alias-brand-primary, #4a6cf7);\n background: var(--dsw-alias-brand-primary, #4a6cf7);\n color: var(--dsw-alias-bg-base, #ffffff);\n}\n\n.dsefkao_cta:disabled {\n cursor: progress;\n opacity: 0.6;\n}\n\n.dsefkao_statusError {\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-state-error-primary, #d9534f);\n word-break: break-word;\n}\n\n.dsefkao_statusOk {\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-state-success-primary, #2e7d32);\n word-break: break-word;\n}\n\n.dsefkao_section {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n/* ---- 品牌块(provider 缩写 + 品牌色) ----\n *\n * 底色是**第三方商标色**(深色主题下也一样),文字固定白色 —— 这是本插件里唯一\n * 不使用主题令牌的颜色。面积被刻意压到 18px 见方:它是身份标记,不是信息主体,\n * 主题(浅色/深色)不影响其可读性。\n */\n.dsefkao_brandBlock {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n width: 18px;\n height: 18px;\n border-radius: 5px;\n font-size: 9px;\n font-weight: 700;\n letter-spacing: -0.02em;\n color: #ffffff;\n line-height: 1;\n user-select: none;\n}\n\n.dsefkao_sectionTitle {\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.04em;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_subject,\n.dsefkao_stance {\n white-space: pre-wrap;\n word-break: break-word;\n padding: 8px 10px;\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-2, transparent);\n}\n\n.dsefkao_stance {\n border-left: 2px solid var(--dsw-alias-brand-primary, #4a6cf7);\n}\n\n/* 立场编辑框:左侧那根品牌色竖线是刻意的 —— 它是\"锋\"的视觉标记,提醒用户\n 这段文字会被红队逐条对攻。 */\n.dsefkao_stanceInput {\n font: inherit;\n font-size: 12px;\n line-height: 1.6;\n padding: 8px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n border-left: 2px solid var(--dsw-alias-brand-primary, #4a6cf7);\n background: var(--dsw-alias-bg-layer-2, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n resize: vertical;\n}\n\n.dsefkao_stanceInput:disabled {\n opacity: 0.7;\n}\n\n.dsefkao_chips {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n align-items: center;\n}\n\n.dsefkao_chip {\n font-size: 11px;\n padding: 1px 8px;\n border-radius: 999px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_chipWarn {\n font-size: 11px;\n padding: 1px 8px;\n border-radius: 999px;\n border: 1px solid var(--dsw-alias-state-warn-primary, rgba(200, 150, 0, 0.6));\n color: var(--dsw-alias-state-warn-primary, inherit);\n}\n\n.dsefkao_chipStrike {\n font-size: 11px;\n padding: 1px 8px;\n border-radius: 999px;\n border: 1px solid var(--dsw-alias-state-error-primary, rgba(217, 83, 79, 0.6));\n color: var(--dsw-alias-state-error-primary, inherit);\n}\n\n.dsefkao_expertRow {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n padding: 2px 0;\n}\n\n.dsefkao_expertName {\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_impactBar {\n display: flex;\n align-items: center;\n gap: 12px;\n flex-wrap: wrap;\n padding: 6px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n font-size: 12px;\n}\n\n.dsefkao_countEndorsed {\n color: var(--dsw-alias-state-success-primary, #2e7d32);\n font-weight: 600;\n}\n\n.dsefkao_countRejected {\n color: var(--dsw-alias-state-error-primary, #d9534f);\n font-weight: 600;\n}\n\n.dsefkao_countPending {\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_countStrike {\n color: var(--dsw-alias-state-error-primary, #d9534f);\n}\n\n.dsefkao_defect {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 10px 12px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-2, transparent);\n}\n\n.dsefkao_defectHead {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n font-size: 11px;\n}\n\n.dsefkao_quote {\n font-size: 12px;\n padding-left: 8px;\n border-left: 2px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_defectContent {\n white-space: pre-wrap;\n word-break: break-word;\n}\n\n.dsefkao_evidence {\n font-size: 12px;\n color: var(--dsw-alias-label-secondary, inherit);\n white-space: pre-wrap;\n word-break: break-word;\n}\n\n.dsefkao_evidenceLabel {\n font-weight: 600;\n color: var(--dsw-alias-label-primary, inherit);\n}\n\n/* 复审核对区:用虚线框与\"待裁决\"的实线框区分开 —— 它问的是另一个问题\n (修没修),而不是\"这条缺陷成不成立\"。 */\n.dsefkao_recheckBox {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 8px 10px;\n border-radius: 8px;\n border: 1px dashed var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.45));\n}\n\n.dsefkao_recheckHead {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n font-size: 11px;\n}\n\n.dsefkao_rejectReason {\n font-size: 12px;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_actions {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n.dsefkao_btnPrimary,\n.dsefkao_btnPrimaryOn,\n.dsefkao_btnDanger,\n.dsefkao_btnDangerOn,\n.dsefkao_btnGhost {\n font: inherit;\n font-size: 12px;\n padding: 4px 12px;\n border-radius: 8px;\n cursor: pointer;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n background: var(--dsw-alias-bg-layer-1, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n}\n\n.dsefkao_btnPrimaryOn {\n border-color: var(--dsw-alias-state-success-primary, #2e7d32);\n color: var(--dsw-alias-state-success-primary, inherit);\n font-weight: 600;\n}\n\n.dsefkao_btnDangerOn {\n border-color: var(--dsw-alias-state-error-primary, #d9534f);\n color: var(--dsw-alias-state-error-primary, inherit);\n font-weight: 600;\n}\n\n.dsefkao_btnGhost {\n border-color: transparent;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_btnPrimary:disabled,\n.dsefkao_btnPrimaryOn:disabled,\n.dsefkao_btnDanger:disabled,\n.dsefkao_btnDangerOn:disabled,\n.dsefkao_btnGhost:disabled {\n cursor: progress;\n opacity: 0.6;\n}\n\n.dsefkao_rejectBox {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 8px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-state-error-primary, rgba(217, 83, 79, 0.4));\n}\n\n.dsefkao_rejectPrompt {\n font-size: 12px;\n font-weight: 600;\n}\n\n.dsefkao_rejectInput {\n font: inherit;\n font-size: 12px;\n padding: 6px 8px;\n border-radius: 6px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n background: var(--dsw-alias-bg-base, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n resize: vertical;\n}\n\n.dsefkao_link {\n align-self: flex-start;\n border: none;\n background: none;\n padding: 0;\n font: inherit;\n font-size: 12px;\n color: var(--dsw-alias-brand-primary, #4a6cf7);\n cursor: pointer;\n}\n\n.dsefkao_footer {\n display: flex;\n align-items: center;\n gap: 10px;\n flex-wrap: wrap;\n padding-top: 4px;\n border-top: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.25));\n}\n\n.dsefkao_footerNote {\n font-size: 11px;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_muted {\n font-size: 12px;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_warn {\n font-size: 11px;\n color: var(--dsw-alias-state-warn-primary, inherit);\n}\n\n.dsefkao_notice {\n font-size: 12px;\n padding: 6px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-state-error-primary, rgba(217, 83, 79, 0.5));\n color: var(--dsw-alias-state-error-primary, inherit);\n}\n\n.dsefkao_empty {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n/* ==========================================================================\n * 用户反馈驱动的视觉改造(分区 / 三色表态 / 专家身份 / 可关闭说明 / 折叠按钮)\n *\n * 核心目标:**次要信息让位,主要信息抓得住**。手段只有三种 —— 分隔线、颜色语义、\n * 按钮形状。刻意不引入背景色块堆叠:那只会让\"一堵墙\"变成\"一堵彩色的墙\"。\n * 颜色仍然全部来自主题令牌(品牌色块是唯一例外,见 brand.dsefkao_ts)。\n * ========================================================================== */\n\n/* ---- 分区:带竖线的小标题 + 硬分隔线 ---- */\n\n.dsefkao_sectionDivider {\n border-top: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.28));\n padding-top: 10px;\n margin-top: 2px;\n}\n\n.dsefkao_sectionHeadRow {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n/* 分区标题:左侧品牌色竖线 + 加粗,成为扫读时的视觉锚点。 */\n.dsefkao_sectionTitleStrong {\n font-size: 12px;\n font-weight: 700;\n letter-spacing: 0.04em;\n padding-left: 8px;\n border-left: 3px solid var(--dsw-alias-brand-primary, #4a6cf7);\n color: var(--dsw-alias-label-primary, inherit);\n}\n\n.dsefkao_sectionCount {\n font-size: 11px;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n/* ---- 展开 / 折叠:做成按钮,不要看起来像正文链接 ---- */\n\n.dsefkao_foldButton {\n font: inherit;\n font-size: 11px;\n padding: 2px 10px;\n border-radius: 999px;\n cursor: pointer;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.45));\n background: var(--dsw-alias-bg-layer-2, transparent);\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_foldButton:hover {\n border-color: var(--dsw-alias-brand-primary, #4a6cf7);\n color: var(--dsw-alias-label-primary, inherit);\n}\n\n/* ---- 可关闭的解释性说明 ---- */\n\n.dsefkao_explain {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_explainText {\n flex: 1 1 auto;\n min-width: 0;\n}\n\n.dsefkao_explainClose {\n flex: none;\n font: inherit;\n font-size: 11px;\n line-height: 1;\n padding: 3px 7px;\n border-radius: 6px;\n cursor: pointer;\n border: 1px solid transparent;\n background: transparent;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_explainClose:hover {\n border-color: var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.45));\n color: var(--dsw-alias-label-primary, inherit);\n}\n\n/* ---- 专家:头像 + 名称 + 维度 + 状态 ---- */\n\n.dsefkao_expertCard {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 8px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n}\n\n.dsefkao_expertHead {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n/* 专家名用正文色与加粗:它是\"这条观点是谁提的\"的答案,必须比元数据更显眼。 */\n.dsefkao_expertNameStrong {\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-label-primary, inherit);\n}\n\n/* 缺陷子卡抬头里的作者标识(头像 + 名称)。 */\n.dsefkao_defectAuthor {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n/* ---- 三态表态:绿 / 红 / 灰 -----------------------------------------------\n *\n * 用户要求:三个都做成按钮形状并各对应颜色;点中之后整块填充该颜色、文字变白。\n * 未点中 = 该颜色的描边 + 同色文字(颜色语义仍在,但不抢主行动的视觉权重)。\n */\n.dsefkao_verdictButton {\n font: inherit;\n font-size: 12px;\n font-weight: 600;\n padding: 4px 14px;\n border-radius: 8px;\n cursor: pointer;\n background: transparent;\n}\n\n.dsefkao_verdictEndorse {\n border: 1px solid var(--dsw-alias-state-success-primary, #2e7d32);\n color: var(--dsw-alias-state-success-primary, #2e7d32);\n}\n\n.dsefkao_verdictEndorseOn {\n border: 1px solid var(--dsw-alias-state-success-primary, #2e7d32);\n background: var(--dsw-alias-state-success-primary, #2e7d32);\n color: var(--dsw-alias-bg-base, #ffffff);\n}\n\n.dsefkao_verdictReject {\n border: 1px solid var(--dsw-alias-state-error-primary, #d9534f);\n color: var(--dsw-alias-state-error-primary, #d9534f);\n}\n\n.dsefkao_verdictRejectOn {\n border: 1px solid var(--dsw-alias-state-error-primary, #d9534f);\n background: var(--dsw-alias-state-error-primary, #d9534f);\n color: var(--dsw-alias-bg-base, #ffffff);\n}\n\n.dsefkao_verdictPending {\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.5));\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_verdictPendingOn {\n border: 1px solid var(--dsw-alias-state-idle-primary, rgba(128, 128, 128, 0.85));\n background: var(--dsw-alias-state-idle-primary, rgba(128, 128, 128, 0.85));\n color: var(--dsw-alias-bg-base, #ffffff);\n}\n\n.dsefkao_verdictButton:disabled {\n cursor: progress;\n opacity: 0.6;\n}\n\n/* ---- 立场全貌(可滚动) ---- */\n\n.dsefkao_stanceFull {\n white-space: pre-wrap;\n word-break: break-word;\n max-height: 320px;\n overflow: auto;\n padding: 8px 10px;\n border-radius: 8px;\n border-left: 2px solid var(--dsw-alias-brand-primary, #4a6cf7);\n background: var(--dsw-alias-bg-layer-2, transparent);\n}\n\n/* ---- 角色定位编辑器 ---- */\n\n.dsefkao_roleEditor {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.dsefkao_roleInput {\n font: inherit;\n font-size: 12px;\n line-height: 1.6;\n padding: 6px 8px;\n border-radius: 6px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n background: var(--dsw-alias-bg-base, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n resize: vertical;\n}\n\n";
461
+ const css$1 = "/*\n * Crossfire Chat card styles.\n *\n * 硬约束:**每个颜色都来自主题令牌**(`Theme.dsefkao_listTokens` 的 `--dsw-alias-*` 一族),\n * 所以浅色与深色主题都无需自己写 override。圆桌的设置页 CSS 用了硬编码\n * `#ffffff` / `rgba(0,0,0,.8)`,在深色主题下是坏的 —— 这里不重复那个错误。\n */\n\n.dsefkao_root {\n display: flex;\n flex-direction: column;\n gap: 10px;\n padding: 12px 14px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n font-size: 13px;\n line-height: 1.6;\n}\n\n.dsefkao_card {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.dsefkao_card + .dsefkao_card {\n margin-top: 10px;\n padding-top: 10px;\n border-top: 1px dashed var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n}\n\n/* 折叠的评审:一行摘要,点一下切换到它。卡片会列出本会话的全部评审,全展开\n 就是一堵墙(验收期间真实攒到 5 场、12 条缺陷)。 */\n.dsefkao_collapsed {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n margin-top: 6px;\n padding: 6px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n font: inherit;\n font-size: 12px;\n text-align: left;\n cursor: pointer;\n}\n\n.dsefkao_collapsed:hover {\n border-color: var(--dsw-alias-brand-primary, #4a6cf7);\n}\n\n.dsefkao_header {\n display: flex;\n align-items: baseline;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n.dsefkao_title {\n font-weight: 700;\n font-size: 13px;\n letter-spacing: 0.02em;\n}\n\n.dsefkao_subjectTitle {\n font-weight: 600;\n}\n\n.dsefkao_badge {\n font-size: 11px;\n padding: 1px 8px;\n border-radius: 999px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_badge_draft {\n border-color: var(--dsw-alias-state-idle-primary, rgba(128, 128, 128, 0.5));\n}\n\n.dsefkao_badge_attacking {\n border-color: var(--dsw-alias-state-warn-primary, rgba(200, 150, 0, 0.6));\n color: var(--dsw-alias-state-warn-primary, inherit);\n}\n\n.dsefkao_badge_judging {\n border-color: var(--dsw-alias-brand-primary, #4a6cf7);\n color: var(--dsw-alias-brand-primary, inherit);\n}\n\n.dsefkao_badge_settled {\n border-color: var(--dsw-alias-state-success-primary, #2e7d32);\n color: var(--dsw-alias-state-success-primary, inherit);\n}\n\n/* ---- 状态区:当前处于哪一态 + 下一步该做什么(唯一主行动) ----------------\n *\n * 这是卡片上**唯一**的强对比区域,其余段落一律保持低对比(标题灰、正文常规)。\n * 主行动是这一区里唯一的主色实心按钮,所以\"现在该干什么\"不需要读完全卡去找。\n */\n.dsefkao_statusStrip {\n display: flex;\n align-items: flex-start;\n gap: 12px;\n flex-wrap: wrap;\n padding: 12px 14px;\n border-radius: 10px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n}\n\n/* 左侧信息块:态名 + 轮次 + 一句话说明;吃掉剩余宽度,主行动因此被推到右侧。 */\n.dsefkao_statusStripInfo {\n display: flex;\n flex-direction: column;\n gap: 2px;\n flex: 1 1 240px;\n min-width: 0;\n}\n\n.dsefkao_statusStripHead {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n/* 态名用反色实心药丸:它与主行动按钮同色,一眼就能配对(\"现在在这里 → 下一步在那里\")。\n *\n * 底色**按状态取色**,与抬头 badge 同源(draft=idle 灰 / attacking=warn 黄 /\n * judging=brand 蓝 / settled=success 绿)—— 此前这里恒为 brand 蓝,于是同一句\n * 「攻击中」在抬头是黄、在状态区是蓝,药丸对四态零区分,\"与主行动同色即可配对\"\n * 自然不成立。\n *\n * 缺省值就是 judging(品牌色,与主行动按钮同色);其余三态由下面的修饰类覆盖背景,\n * 文字色四态共用一个反色。四个状态名都来自调用点同一张\n * `styles[`statusState_${review.dsefkao_status}`]` 查表,颜色一律走 `--dsw-alias-*` 令牌。 */\n.dsefkao_statusState {\n font-size: 12px;\n font-weight: 600;\n padding: 2px 10px;\n border-radius: 999px;\n background: var(--dsw-alias-brand-primary, #4a6cf7);\n color: var(--dsw-alias-bg-base, #ffffff);\n}\n\n/* draft:与 .dsefkao_badge_draft 同源(idle 灰)。 */\n.dsefkao_statusState_draft {\n background: var(--dsw-alias-state-idle-primary, rgba(128, 128, 128, 0.85));\n}\n\n/* attacking:与 .dsefkao_badge_attacking 同源(warn 黄)。 */\n.dsefkao_statusState_attacking {\n background: var(--dsw-alias-state-warn-primary, rgba(200, 150, 0, 0.9));\n}\n\n/* settled:与 .dsefkao_badge_settled 同源(success 绿)。 */\n.dsefkao_statusState_settled {\n background: var(--dsw-alias-state-success-primary, rgba(46, 125, 50, 0.9));\n}\n\n.dsefkao_statusHint {\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n/* 右侧行动块:一个主按钮 + 一行\"下一步\"小标题。 */\n.dsefkao_statusStripAction {\n display: flex;\n flex-direction: column;\n align-items: flex-end;\n gap: 4px;\n margin-left: auto;\n}\n\n.dsefkao_statusNextLabel {\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.04em;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n/* 主行动:唯一的主色实心按钮。字号与内边距都比次要按钮大一档,视觉权重随之提高。 */\n.dsefkao_cta {\n font: inherit;\n font-size: 13px;\n font-weight: 600;\n padding: 7px 18px;\n border-radius: 8px;\n cursor: pointer;\n border: 1px solid var(--dsw-alias-brand-primary, #4a6cf7);\n background: var(--dsw-alias-brand-primary, #4a6cf7);\n color: var(--dsw-alias-bg-base, #ffffff);\n}\n\n.dsefkao_cta:disabled {\n cursor: progress;\n opacity: 0.6;\n}\n\n.dsefkao_statusError {\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-state-error-primary, #d9534f);\n word-break: break-word;\n}\n\n.dsefkao_statusOk {\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-state-success-primary, #2e7d32);\n word-break: break-word;\n}\n\n.dsefkao_section {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n/* ---- 品牌块(provider 缩写 + 品牌色) ----\n *\n * 底色是**第三方商标色**(深色主题下也一样),文字固定白色 —— 这是本插件里唯一\n * 不使用主题令牌的颜色。面积被刻意压到 18px 见方:它是身份标记,不是信息主体,\n * 主题(浅色/深色)不影响其可读性。\n */\n.dsefkao_brandBlock {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n width: 18px;\n height: 18px;\n border-radius: 5px;\n font-size: 9px;\n font-weight: 700;\n letter-spacing: -0.02em;\n color: #ffffff;\n line-height: 1;\n user-select: none;\n}\n\n.dsefkao_sectionTitle {\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.04em;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_subject,\n.dsefkao_stance {\n white-space: pre-wrap;\n word-break: break-word;\n padding: 8px 10px;\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-2, transparent);\n}\n\n.dsefkao_stance {\n border-left: 2px solid var(--dsw-alias-brand-primary, #4a6cf7);\n}\n\n/* 立场编辑框:左侧那根品牌色竖线是刻意的 —— 它是\"锋\"的视觉标记,提醒用户\n 这段文字会被红队逐条对攻。 */\n.dsefkao_stanceInput {\n font: inherit;\n font-size: 12px;\n line-height: 1.6;\n padding: 8px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n border-left: 2px solid var(--dsw-alias-brand-primary, #4a6cf7);\n background: var(--dsw-alias-bg-layer-2, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n resize: vertical;\n}\n\n.dsefkao_stanceInput:disabled {\n opacity: 0.7;\n}\n\n.dsefkao_chips {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n align-items: center;\n}\n\n.dsefkao_chip {\n font-size: 11px;\n padding: 1px 8px;\n border-radius: 999px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_chipWarn {\n font-size: 11px;\n padding: 1px 8px;\n border-radius: 999px;\n border: 1px solid var(--dsw-alias-state-warn-primary, rgba(200, 150, 0, 0.6));\n color: var(--dsw-alias-state-warn-primary, inherit);\n}\n\n.dsefkao_chipStrike {\n font-size: 11px;\n padding: 1px 8px;\n border-radius: 999px;\n border: 1px solid var(--dsw-alias-state-error-primary, rgba(217, 83, 79, 0.6));\n color: var(--dsw-alias-state-error-primary, inherit);\n}\n\n.dsefkao_expertRow {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n padding: 2px 0;\n}\n\n.dsefkao_expertName {\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_impactBar {\n display: flex;\n align-items: center;\n gap: 12px;\n flex-wrap: wrap;\n padding: 6px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n font-size: 12px;\n}\n\n.dsefkao_countEndorsed {\n color: var(--dsw-alias-state-success-primary, #2e7d32);\n font-weight: 600;\n}\n\n.dsefkao_countRejected {\n color: var(--dsw-alias-state-error-primary, #d9534f);\n font-weight: 600;\n}\n\n.dsefkao_countPending {\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_countStrike {\n color: var(--dsw-alias-state-error-primary, #d9534f);\n}\n\n/* 一条待裁决缺陷。\n *\n * 左侧 3px 竖条按表态状态着色(认定=绿 / 驳回=红 / 待定=灰):一条评审里常有\n * 十几条缺陷,用户最需要先看清\"哪些还没表态\",而这个信息此前只体现在底部按钮\n * 描边上 —— 必须把每条都读一遍才能得到全局分布。竖条让分布在一眼内可见。 */\n.dsefkao_defect {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 12px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n border-left-width: 3px;\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-2, transparent);\n}\n\n.dsefkao_defect_pending {\n border-left-color: var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n}\n\n.dsefkao_defect_endorsed {\n border-left-color: var(--dsw-alias-state-success-primary, #2e7d32);\n}\n\n.dsefkao_defect_rejected {\n border-left-color: var(--dsw-alias-state-error-primary, #d9534f);\n}\n\n/* 缺陷之间留出可辨认的呼吸:此前它们只隔 4px(父级 .dsefkao_section 的 gap),\n * 一条条读下来像一整块连续文本。 */\n.dsefkao_defect + .dsefkao_defect {\n margin-top: 8px;\n}\n\n.dsefkao_defectHead {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n font-size: 12px;\n}\n\n.dsefkao_quote {\n font-size: 12px;\n padding-left: 8px;\n border-left: 2px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_defectContent {\n white-space: pre-wrap;\n word-break: break-word;\n line-height: 1.65;\n}\n\n/* ---- 观点标题 + 折叠 ----\n *\n * 用户诉求:①\"每条观点都要在开头立一个标题,这样目的明确\";②\"每条观点都可以折叠,\n * 折叠后只显示标题和对应专家\"。\n *\n * 标题做成**按钮**而不是纯文字:整行可点即展开/收起,省掉一个额外的折叠控件;视觉上\n * 它是这一条的抬头(比正文重一档),折叠态下也就是唯一可见的内容。\n */\n.dsefkao_defectTitle {\n font: inherit;\n font-size: 13px;\n font-weight: 600;\n line-height: 1.5;\n text-align: left;\n width: 100%;\n padding: 4px 0;\n border: none;\n background: transparent;\n color: var(--dsw-alias-label-primary, inherit);\n cursor: pointer;\n /* 折叠态下标题是这一条**唯一可见**的内容,所以它必须永远是单行:标题由\n `defectTitle()` 生成、硬上限 80 字符,13px 下 80 个汉字约 1040px,远超卡片列宽,\n 不约束就会折成 2–4 行,折叠体随之长回大块。 */\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n /* 长英文 token(文件名 / URL)没有可断点:nowrap 下它只会被省略号吃掉,但补上\n `anywhere` 后即使将来放宽 nowrap 也不会横向溢出。 */\n overflow-wrap: anywhere;\n}\n\n.dsefkao_defectTitle:hover {\n color: var(--dsw-alias-brand-primary, #4a6cf7);\n}\n\n.dsefkao_evidence {\n font-size: 12px;\n color: var(--dsw-alias-label-secondary, inherit);\n white-space: pre-wrap;\n word-break: break-word;\n}\n\n.dsefkao_evidenceLabel {\n font-weight: 600;\n color: var(--dsw-alias-label-primary, inherit);\n}\n\n/* 复审核对区:用虚线框与\"待裁决\"的实线框区分开 —— 它问的是另一个问题\n (修没修),而不是\"这条缺陷成不成立\"。 */\n.dsefkao_recheckBox {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 8px 10px;\n border-radius: 8px;\n border: 1px dashed var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.45));\n}\n\n.dsefkao_recheckHead {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n font-size: 11px;\n}\n\n.dsefkao_rejectReason {\n font-size: 12px;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_actions {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n.dsefkao_btnPrimary,\n.dsefkao_btnPrimaryOn,\n.dsefkao_btnDanger,\n.dsefkao_btnDangerOn,\n.dsefkao_btnGhost {\n font: inherit;\n font-size: 12px;\n padding: 4px 12px;\n border-radius: 8px;\n cursor: pointer;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n background: var(--dsw-alias-bg-layer-1, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n}\n\n.dsefkao_btnPrimaryOn {\n border-color: var(--dsw-alias-state-success-primary, #2e7d32);\n color: var(--dsw-alias-state-success-primary, inherit);\n font-weight: 600;\n}\n\n.dsefkao_btnDangerOn {\n border-color: var(--dsw-alias-state-error-primary, #d9534f);\n color: var(--dsw-alias-state-error-primary, inherit);\n font-weight: 600;\n}\n\n.dsefkao_btnGhost {\n border-color: transparent;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_btnPrimary:disabled,\n.dsefkao_btnPrimaryOn:disabled,\n.dsefkao_btnDanger:disabled,\n.dsefkao_btnDangerOn:disabled,\n.dsefkao_btnGhost:disabled {\n cursor: progress;\n opacity: 0.6;\n}\n\n.dsefkao_rejectBox {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 8px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-state-error-primary, rgba(217, 83, 79, 0.4));\n}\n\n.dsefkao_rejectPrompt {\n font-size: 12px;\n font-weight: 600;\n}\n\n.dsefkao_rejectInput {\n font: inherit;\n font-size: 12px;\n padding: 6px 8px;\n border-radius: 6px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n background: var(--dsw-alias-bg-base, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n resize: vertical;\n}\n\n.dsefkao_link {\n align-self: flex-start;\n border: none;\n background: none;\n padding: 0;\n font: inherit;\n font-size: 12px;\n color: var(--dsw-alias-brand-primary, #4a6cf7);\n cursor: pointer;\n}\n\n.dsefkao_footer {\n display: flex;\n align-items: center;\n gap: 10px;\n flex-wrap: wrap;\n padding-top: 4px;\n border-top: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.25));\n}\n\n.dsefkao_footerNote {\n font-size: 11px;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_muted {\n font-size: 12px;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_warn {\n font-size: 11px;\n color: var(--dsw-alias-state-warn-primary, inherit);\n}\n\n.dsefkao_notice {\n font-size: 12px;\n padding: 6px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-state-error-primary, rgba(217, 83, 79, 0.5));\n color: var(--dsw-alias-state-error-primary, inherit);\n}\n\n/* 空态:没有评审时整卡只剩一行字,看起来像\"坏了\"。\n * 用一圈虚线 + 内边距把它读成\"这里还没有东西\",而不是\"渲染失败\"。 */\n.dsefkao_empty {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 6px;\n padding: 28px 16px;\n border: 1px dashed var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-2, transparent);\n text-align: center;\n}\n\n/* ==========================================================================\n * 用户反馈驱动的视觉改造(分区 / 三色表态 / 专家身份 / 可关闭说明 / 折叠按钮)\n *\n * 核心目标:**次要信息让位,主要信息抓得住**。手段只有三种 —— 分隔线、颜色语义、\n * 按钮形状。刻意不引入背景色块堆叠:那只会让\"一堵墙\"变成\"一堵彩色的墙\"。\n * 颜色仍然全部来自主题令牌(品牌色块是唯一例外,见 brand.dsefkao_ts)。\n * ========================================================================== */\n\n/* ---- 分区:带竖线的小标题 + 硬分隔线 ---- */\n\n.dsefkao_sectionDivider {\n border-top: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.28));\n padding-top: 10px;\n margin-top: 2px;\n}\n\n.dsefkao_sectionHeadRow {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n/* 分区标题:左侧品牌色竖线 + 加粗,成为扫读时的视觉锚点。 */\n.dsefkao_sectionTitleStrong {\n font-size: 12px;\n font-weight: 700;\n letter-spacing: 0.04em;\n padding-left: 8px;\n border-left: 3px solid var(--dsw-alias-brand-primary, #4a6cf7);\n color: var(--dsw-alias-label-primary, inherit);\n}\n\n.dsefkao_sectionCount {\n font-size: 11px;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n/* ---- 展开 / 折叠:做成按钮,不要看起来像正文链接 ---- */\n\n.dsefkao_foldButton {\n font: inherit;\n font-size: 11px;\n padding: 2px 10px;\n border-radius: 999px;\n cursor: pointer;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.45));\n background: var(--dsw-alias-bg-layer-2, transparent);\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_foldButton:hover {\n border-color: var(--dsw-alias-brand-primary, #4a6cf7);\n color: var(--dsw-alias-label-primary, inherit);\n}\n\n/* ---- 可关闭的解释性说明 ---- */\n\n.dsefkao_explain {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_explainText {\n flex: 1 1 auto;\n min-width: 0;\n}\n\n.dsefkao_explainClose {\n flex: none;\n font: inherit;\n font-size: 11px;\n line-height: 1;\n padding: 3px 7px;\n border-radius: 6px;\n cursor: pointer;\n border: 1px solid transparent;\n background: transparent;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_explainClose:hover {\n border-color: var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.45));\n color: var(--dsw-alias-label-primary, inherit);\n}\n\n/* ---- 专家:头像 + 名称 + 维度 + 状态 ---- */\n\n.dsefkao_expertCard {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 8px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n}\n\n.dsefkao_expertHead {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n/* 专家名用正文色与加粗:它是\"这条观点是谁提的\"的答案,必须比元数据更显眼。 */\n.dsefkao_expertNameStrong {\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-label-primary, inherit);\n}\n\n/* 缺陷子卡抬头里的作者标识(头像 + 名称)。 */\n.dsefkao_defectAuthor {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n/* ---- 三态表态:绿 / 红 / 灰 -----------------------------------------------\n *\n * 用户要求:三个都做成按钮形状并各对应颜色;点中之后整块填充该颜色、文字变白。\n * 未点中 = 该颜色的描边 + 同色文字(颜色语义仍在,但不抢主行动的视觉权重)。\n */\n.dsefkao_verdictButton {\n box-sizing: border-box;\n min-height: 28px;\n font: inherit;\n font-size: 12px;\n font-weight: 600;\n padding: 4px 14px;\n border-radius: 8px;\n cursor: pointer;\n background: transparent;\n}\n\n.dsefkao_verdictEndorse {\n border: 1px solid var(--dsw-alias-state-success-primary, #2e7d32);\n color: var(--dsw-alias-state-success-primary, #2e7d32);\n}\n\n.dsefkao_verdictEndorseOn {\n border: 1px solid var(--dsw-alias-state-success-primary, #2e7d32);\n background: var(--dsw-alias-state-success-primary, #2e7d32);\n color: var(--dsw-alias-bg-base, #ffffff);\n}\n\n.dsefkao_verdictReject {\n border: 1px solid var(--dsw-alias-state-error-primary, #d9534f);\n color: var(--dsw-alias-state-error-primary, #d9534f);\n}\n\n.dsefkao_verdictRejectOn {\n border: 1px solid var(--dsw-alias-state-error-primary, #d9534f);\n background: var(--dsw-alias-state-error-primary, #d9534f);\n color: var(--dsw-alias-bg-base, #ffffff);\n}\n\n.dsefkao_verdictPending {\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.5));\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_verdictPendingOn {\n border: 1px solid var(--dsw-alias-state-idle-primary, rgba(128, 128, 128, 0.85));\n background: var(--dsw-alias-state-idle-primary, rgba(128, 128, 128, 0.85));\n color: var(--dsw-alias-bg-base, #ffffff);\n}\n\n.dsefkao_verdictButton:disabled {\n cursor: progress;\n opacity: 0.6;\n}\n\n/* ---- 立场全貌(可滚动) ---- */\n\n.dsefkao_stanceFull {\n white-space: pre-wrap;\n word-break: break-word;\n max-height: 320px;\n overflow: auto;\n padding: 8px 10px;\n border-radius: 8px;\n border-left: 2px solid var(--dsw-alias-brand-primary, #4a6cf7);\n background: var(--dsw-alias-bg-layer-2, transparent);\n}\n\n/* ---- 角色定位编辑器 ---- */\n\n.dsefkao_roleEditor {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.dsefkao_roleInput {\n font: inherit;\n font-size: 12px;\n line-height: 1.6;\n padding: 6px 8px;\n border-radius: 6px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n background: var(--dsw-alias-bg-base, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n resize: vertical;\n}\n\n";
228
462
  const classMap$1 = {
229
463
  "listTokens": "dsefkao_listTokens",
230
464
  "root": "dsefkao_root",
@@ -241,7 +475,11 @@ const classMap$1 = {
241
475
  "statusStrip": "dsefkao_statusStrip",
242
476
  "statusStripInfo": "dsefkao_statusStripInfo",
243
477
  "statusStripHead": "dsefkao_statusStripHead",
478
+ "status": "dsefkao_status",
244
479
  "statusState": "dsefkao_statusState",
480
+ "statusState_draft": "dsefkao_statusState_draft",
481
+ "statusState_attacking": "dsefkao_statusState_attacking",
482
+ "statusState_settled": "dsefkao_statusState_settled",
245
483
  "statusHint": "dsefkao_statusHint",
246
484
  "statusStripAction": "dsefkao_statusStripAction",
247
485
  "statusNextLabel": "dsefkao_statusNextLabel",
@@ -266,9 +504,13 @@ const classMap$1 = {
266
504
  "countPending": "dsefkao_countPending",
267
505
  "countStrike": "dsefkao_countStrike",
268
506
  "defect": "dsefkao_defect",
507
+ "defect_pending": "dsefkao_defect_pending",
508
+ "defect_endorsed": "dsefkao_defect_endorsed",
509
+ "defect_rejected": "dsefkao_defect_rejected",
269
510
  "defectHead": "dsefkao_defectHead",
270
511
  "quote": "dsefkao_quote",
271
512
  "defectContent": "dsefkao_defectContent",
513
+ "defectTitle": "dsefkao_defectTitle",
272
514
  "evidence": "dsefkao_evidence",
273
515
  "evidenceLabel": "dsefkao_evidenceLabel",
274
516
  "recheckBox": "dsefkao_recheckBox",
@@ -344,6 +586,7 @@ function CrossfireCard(props) {
344
586
  * (见 rpc.ts 的 judgeActor)。
345
587
  */
346
588
  const sessionId = typeof props.sessionId === "string" ? props.sessionId : "";
589
+ rememberSessionId(sessionId);
347
590
  const [reviews, setReviews] = (0, react.useState)([]);
348
591
  const [note, setNote] = (0, react.useState)(void 0);
349
592
  const [notice, setNotice] = (0, react.useState)(void 0);
@@ -361,6 +604,8 @@ function CrossfireCard(props) {
361
604
  const [stanceExpanded, setStanceExpanded] = (0, react.useState)({});
362
605
  const [recheckApproval, setRecheckApproval] = (0, react.useState)(null);
363
606
  const [dismissedHints, setDismissedHints] = (0, react.useState)({});
607
+ const [expandedDefects, setExpandedDefects] = (0, react.useState)({});
608
+ const [promptViews, setPromptViews] = (0, react.useState)({});
364
609
  const [dismissBusy, setDismissBusy] = (0, react.useState)(false);
365
610
  const alive = (0, react.useRef)(true);
366
611
  /**
@@ -590,7 +835,7 @@ function CrossfireCard(props) {
590
835
  })
591
836
  }));
592
837
  };
593
- /** 导出为 Markdown 并触发下载(文件同时已落盘到评审目录)。 */
838
+ /** 导出为 Markdown 并触发下载(盘上同时落 `export.md` 与带 BOM 的 `export.txt`)。 */
594
839
  const exportReview = async (review) => {
595
840
  setBusy(true);
596
841
  const result = await rpc("crossfire/review.export", {
@@ -604,13 +849,13 @@ function CrossfireCard(props) {
604
849
  reportNotice(`${t("exportFailed")}:${result.error.message}`);
605
850
  return;
606
851
  }
607
- reportOk(`${t("exportDone")}:${result.value.path}`);
852
+ reportOk(result.value.textPath === void 0 || result.value.textPath === "" ? `${t("exportDone")}:${result.value.path}` : `${t("exportDone")}:${result.value.path} + ${result.value.textPath}`);
608
853
  try {
609
- const blob = new Blob([result.value.markdown], { type: "text/markdown;charset=utf-8" });
854
+ const blob = new Blob([result.value.markdown], { type: "text/plain;charset=utf-8" });
610
855
  const url = URL.createObjectURL(blob);
611
856
  const anchor = document.createElement("a");
612
857
  anchor.href = url;
613
- anchor.download = `${review.reviewId}.md`;
858
+ anchor.download = `${review.reviewId}.txt`;
614
859
  anchor.click();
615
860
  URL.revokeObjectURL(url);
616
861
  } catch {}
@@ -682,6 +927,29 @@ function CrossfireCard(props) {
682
927
  if (expert === void 0 || expert.provider === void 0) return fallback;
683
928
  return routeText(expert.provider, expert.model);
684
929
  }
930
+ /**
931
+ * 缺陷排序:**打穿立场的优先**,其次按维度分组,组内保持原始顺序。
932
+ *
933
+ * 这是 §5.3 方向 4 的落地:"按维度分组、按是否打穿立场排序,把最需要裁决的排在最前"。
934
+ * 用 `map + index` 稳定排序,保证同一组内的相对次序与收集时一致(否则每轮表态后
935
+ * 卡片会重排,用户会找不到刚看过的那条)。
936
+ */
937
+ function orderDefects(defects) {
938
+ return defects.map((defect, index) => ({
939
+ defect,
940
+ index
941
+ })).sort((left, right) => {
942
+ const strikeDelta = Number(right.defect.strikesStance === true) - Number(left.defect.strikesStance === true);
943
+ if (strikeDelta !== 0) return strikeDelta;
944
+ const dimensionDelta = left.defect.dimension.localeCompare(right.defect.dimension, "zh");
945
+ if (dimensionDelta !== 0) return dimensionDelta;
946
+ return left.index - right.index;
947
+ }).map((entry) => entry.defect);
948
+ }
949
+ /** 折叠状态的键:`${reviewId}::${defectId}`。 */
950
+ function defectFoldKey(reviewId, defectId) {
951
+ return `${reviewId}::${defectId}`;
952
+ }
685
953
  if (reviews.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
686
954
  className: ReviewCard_module_css_default.root,
687
955
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -777,12 +1045,12 @@ function CrossfireCard(props) {
777
1045
  className: ReviewCard_module_css_default.statusStripHead,
778
1046
  children: [
779
1047
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
780
- className: ReviewCard_module_css_default.statusState,
1048
+ className: `${ReviewCard_module_css_default.statusState} ${ReviewCard_module_css_default[`statusState_${review.status}`] ?? ""}`,
781
1049
  children: t(STATUS_LABEL_KEY[review.status])
782
1050
  }),
783
1051
  review.status === "judging" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
784
1052
  className: ReviewCard_module_css_default.muted,
785
- children: `${t("countPending")} ${review.counts.pending} / ${t("impactTitle")} ${review.counts.total}`
1053
+ children: t("countPendingOfTotal").replace("{pending}", String(review.counts.pending)).replace("{total}", String(review.counts.total))
786
1054
  }) : null,
787
1055
  review.status === "attacking" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
788
1056
  className: ReviewCard_module_css_default.muted,
@@ -1079,6 +1347,37 @@ function CrossfireCard(props) {
1079
1347
  setRoleEditing(roleKey);
1080
1348
  },
1081
1349
  children: t("expertRoleEdit")
1350
+ }),
1351
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1352
+ type: "button",
1353
+ className: ReviewCard_module_css_default.foldButton,
1354
+ onClick: () => {
1355
+ const viewKey = `${review.reviewId}::${expert.key}`;
1356
+ let start = false;
1357
+ setPromptViews((previous) => {
1358
+ const toggled = togglePromptView(previous, viewKey);
1359
+ start = toggled.start;
1360
+ return toggled.table;
1361
+ });
1362
+ if (!start) return;
1363
+ (async () => {
1364
+ const result = await rpc("crossfire/expert.prompt", {
1365
+ reviewId: review.reviewId,
1366
+ stateRoot: review.stateRoot,
1367
+ session: sessionId,
1368
+ expertKey: expert.key
1369
+ });
1370
+ if (!alive.current) return;
1371
+ setPromptViews((previous) => applyPromptResult(previous, viewKey, result.ok ? {
1372
+ ok: true,
1373
+ persona: result.value.persona
1374
+ } : {
1375
+ ok: false,
1376
+ message: result.error.message
1377
+ }));
1378
+ })();
1379
+ },
1380
+ children: t("expertPromptView")
1082
1381
  })
1083
1382
  ]
1084
1383
  }),
@@ -1086,6 +1385,28 @@ function CrossfireCard(props) {
1086
1385
  className: ReviewCard_module_css_default.muted,
1087
1386
  children: `${t("expertRoleLabel")}:${expert.role}`
1088
1387
  }),
1388
+ (() => {
1389
+ const view = promptViews[`${review.reviewId}::${expert.key}`];
1390
+ if (view === void 0) return null;
1391
+ if (view.status === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1392
+ className: ReviewCard_module_css_default.statusError,
1393
+ children: view.message
1394
+ });
1395
+ if (view.status === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1396
+ className: ReviewCard_module_css_default.muted,
1397
+ children: t("loading")
1398
+ });
1399
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1400
+ className: ReviewCard_module_css_default.section,
1401
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1402
+ className: ReviewCard_module_css_default.sectionTitle,
1403
+ children: t("expertPromptTitle")
1404
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1405
+ className: ReviewCard_module_css_default.stanceFull,
1406
+ children: view.persona
1407
+ })]
1408
+ });
1409
+ })(),
1089
1410
  roleEditing === roleKey ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1090
1411
  className: ReviewCard_module_css_default.roleEditor,
1091
1412
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
@@ -1183,210 +1504,240 @@ function CrossfireCard(props) {
1183
1504
  review.defects.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1184
1505
  className: ReviewCard_module_css_default.muted,
1185
1506
  children: t("defectEmpty")
1186
- }) : review.defects.map((defect) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1187
- className: ReviewCard_module_css_default.defect,
1188
- children: [
1189
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1190
- className: ReviewCard_module_css_default.defectHead,
1191
- children: [
1192
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1193
- className: ReviewCard_module_css_default.chip,
1194
- children: defect.dimension
1507
+ }) : orderDefects(review.defects).map((defect) => {
1508
+ const foldKey = defectFoldKey(review.reviewId, defect.id);
1509
+ const isOpen = expandedDefects[foldKey] === true;
1510
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1511
+ className: `${ReviewCard_module_css_default.defect} ${ReviewCard_module_css_default[`defect_${defect.status}`] ?? ""}`,
1512
+ children: [
1513
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1514
+ className: ReviewCard_module_css_default.defectHead,
1515
+ children: [
1516
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1517
+ className: ReviewCard_module_css_default.chip,
1518
+ children: defect.dimension
1519
+ }),
1520
+ defect.dimensionFallback === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1521
+ className: ReviewCard_module_css_default.chipWarn,
1522
+ children: t("dimensionFallback")
1523
+ }) : null,
1524
+ defect.strikesStance === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1525
+ className: ReviewCard_module_css_default.chipStrike,
1526
+ children: t("strikesStance")
1527
+ }) : null,
1528
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: ReviewCard_module_css_default.spacer }),
1529
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1530
+ className: ReviewCard_module_css_default.defectAuthor,
1531
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1532
+ className: ReviewCard_module_css_default.brandBlock,
1533
+ style: { background: providerBrand(authorOf(defect.expertKey, review.experts)?.provider).color },
1534
+ title: routeOf(authorOf(defect.expertKey, review.experts), t("modelInherit")),
1535
+ children: providerBrand(authorOf(defect.expertKey, review.experts)?.provider).abbr
1536
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1537
+ className: ReviewCard_module_css_default.expertNameStrong,
1538
+ children: defect.expertKey
1539
+ })]
1540
+ }),
1541
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1542
+ type: "button",
1543
+ className: ReviewCard_module_css_default.foldButton,
1544
+ "aria-expanded": isOpen,
1545
+ onClick: () => setExpandedDefects((previous) => {
1546
+ const next = { ...previous };
1547
+ if (next[foldKey] === true) delete next[foldKey];
1548
+ else next[foldKey] = true;
1549
+ return next;
1550
+ }),
1551
+ children: isOpen ? t("collapseSection") : t("expandSection")
1552
+ })
1553
+ ]
1554
+ }),
1555
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1556
+ type: "button",
1557
+ className: ReviewCard_module_css_default.defectTitle,
1558
+ "aria-expanded": isOpen,
1559
+ onClick: () => setExpandedDefects((previous) => {
1560
+ const next = { ...previous };
1561
+ if (next[foldKey] === true) delete next[foldKey];
1562
+ else next[foldKey] = true;
1563
+ return next;
1195
1564
  }),
1196
- defect.dimensionFallback === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1197
- className: ReviewCard_module_css_default.chipWarn,
1198
- children: t("dimensionFallback")
1199
- }) : null,
1200
- defect.strikesStance === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1201
- className: ReviewCard_module_css_default.chipStrike,
1202
- children: t("strikesStance")
1203
- }) : null,
1204
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: ReviewCard_module_css_default.spacer }),
1205
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1206
- className: ReviewCard_module_css_default.defectAuthor,
1207
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1208
- className: ReviewCard_module_css_default.brandBlock,
1209
- style: { background: providerBrand(authorOf(defect.expertKey, review.experts)?.provider).color },
1210
- title: routeOf(authorOf(defect.expertKey, review.experts), t("modelInherit")),
1211
- children: providerBrand(authorOf(defect.expertKey, review.experts)?.provider).abbr
1212
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1213
- className: ReviewCard_module_css_default.expertNameStrong,
1214
- children: defect.expertKey
1215
- })]
1216
- })
1217
- ]
1218
- }),
1219
- defect.quote === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1220
- className: ReviewCard_module_css_default.quote,
1221
- children: [
1222
- "“",
1223
- defect.quote,
1224
- "”"
1225
- ]
1226
- }),
1227
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1228
- className: ReviewCard_module_css_default.defectContent,
1229
- children: defect.content
1230
- }),
1231
- defect.evidence === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1232
- className: ReviewCard_module_css_default.evidence,
1233
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1234
- className: ReviewCard_module_css_default.evidenceLabel,
1235
- children: defect.evidence.kind === "repro" ? `[${t("evidenceRepro")}]` : `[${t("evidenceArgument")}]`
1236
- }), defect.evidence.text]
1237
- }),
1238
- defect.recheck === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1239
- className: ReviewCard_module_css_default.recheckBox,
1240
- children: [
1241
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1242
- className: ReviewCard_module_css_default.recheckHead,
1565
+ children: defectTitle(defect.content)
1566
+ }),
1567
+ isOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1568
+ defect.quote === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1569
+ className: ReviewCard_module_css_default.quote,
1570
+ children: [
1571
+ "“",
1572
+ defect.quote,
1573
+ "”"
1574
+ ]
1575
+ }),
1576
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1577
+ className: ReviewCard_module_css_default.defectContent,
1578
+ children: defect.content
1579
+ }),
1580
+ defect.evidence === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1581
+ className: ReviewCard_module_css_default.evidence,
1243
1582
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1244
1583
  className: ReviewCard_module_css_default.evidenceLabel,
1245
- children: t("recheckLabel")
1246
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1247
- className: ReviewCard_module_css_default.muted,
1248
- children: `#${String(defect.recheck.pass)}`
1249
- })]
1584
+ children: defect.evidence.kind === "repro" ? `[${t("evidenceRepro")}]` : `[${t("evidenceArgument")}]`
1585
+ }), defect.evidence.text]
1250
1586
  }),
1251
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1252
- className: ReviewCard_module_css_default.actions,
1253
- children: [RECHECK_VERDICTS.map((verdict) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1587
+ defect.recheck === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1588
+ className: ReviewCard_module_css_default.recheckBox,
1589
+ children: [
1590
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1591
+ className: ReviewCard_module_css_default.recheckHead,
1592
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1593
+ className: ReviewCard_module_css_default.evidenceLabel,
1594
+ children: t("recheckLabel")
1595
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1596
+ className: ReviewCard_module_css_default.muted,
1597
+ children: `#${String(defect.recheck.pass)}`
1598
+ })]
1599
+ }),
1600
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1601
+ className: ReviewCard_module_css_default.actions,
1602
+ children: [RECHECK_VERDICTS.map((verdict) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1603
+ type: "button",
1604
+ disabled: busy,
1605
+ className: defect.recheck?.verdict === verdict ? ReviewCard_module_css_default.btnPrimaryOn : ReviewCard_module_css_default.btnGhost,
1606
+ onClick: () => {
1607
+ setVerdict(review, defect, verdict, recheckNotes[defect.id] ?? "");
1608
+ },
1609
+ children: t(VERDICT_KEY[verdict] ?? "verdictUnfixed")
1610
+ }, verdict)), defect.recheck?.verdict === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1611
+ className: ReviewCard_module_css_default.muted,
1612
+ children: t("verdictUndecided")
1613
+ }) : null]
1614
+ }),
1615
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1616
+ className: ReviewCard_module_css_default.actions,
1617
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1618
+ className: ReviewCard_module_css_default.rejectInput,
1619
+ disabled: busy,
1620
+ value: recheckNotes[defect.id] ?? defect.recheck?.note ?? "",
1621
+ placeholder: t("recheckNote"),
1622
+ onChange: (event) => {
1623
+ const value = event.target.value;
1624
+ setRecheckNotes((previous) => ({
1625
+ ...previous,
1626
+ [defect.id]: value
1627
+ }));
1628
+ }
1629
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1630
+ type: "button",
1631
+ disabled: busy || defect.recheck?.verdict === void 0 || (recheckNotes[defect.id] ?? defect.recheck?.note ?? "") === (defect.recheck?.note ?? ""),
1632
+ title: defect.recheck?.verdict === void 0 ? t("recheckNoteNeedsVerdict") : void 0,
1633
+ className: ReviewCard_module_css_default.btnGhost,
1634
+ onClick: () => {
1635
+ const verdict = defect.recheck?.verdict;
1636
+ if (verdict === void 0) return;
1637
+ setVerdict(review, defect, verdict, recheckNotes[defect.id] ?? defect.recheck?.note ?? "");
1638
+ },
1639
+ children: t("presetSave")
1640
+ })]
1641
+ }),
1642
+ defect.recheck.note === void 0 || defect.recheck.note === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1643
+ className: ReviewCard_module_css_default.muted,
1644
+ children: defect.recheck.note
1645
+ })
1646
+ ]
1647
+ })
1648
+ ] }) : null,
1649
+ defect.status === "rejected" && defect.rejectReason !== void 0 && defect.rejectReason !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1650
+ className: ReviewCard_module_css_default.rejectReason,
1651
+ children: [
1652
+ t("btnReject"),
1653
+ ":",
1654
+ defect.rejectReason
1655
+ ]
1656
+ }) : null,
1657
+ review.status === "settled" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1658
+ className: ReviewCard_module_css_default.actions,
1659
+ children: [
1660
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1254
1661
  type: "button",
1255
1662
  disabled: busy,
1256
- className: defect.recheck?.verdict === verdict ? ReviewCard_module_css_default.btnPrimaryOn : ReviewCard_module_css_default.btnGhost,
1257
- onClick: () => {
1258
- setVerdict(review, defect, verdict, recheckNotes[defect.id] ?? "");
1259
- },
1260
- children: t(VERDICT_KEY[verdict] ?? "verdictUnfixed")
1261
- }, verdict)), defect.recheck?.verdict === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1262
- className: ReviewCard_module_css_default.muted,
1263
- children: t("verdictUndecided")
1264
- }) : null]
1265
- }),
1266
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1267
- className: ReviewCard_module_css_default.actions,
1268
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1269
- className: ReviewCard_module_css_default.rejectInput,
1270
- disabled: busy,
1271
- value: recheckNotes[defect.id] ?? defect.recheck?.note ?? "",
1272
- placeholder: t("recheckNote"),
1273
- onChange: (event) => {
1274
- const value = event.target.value;
1275
- setRecheckNotes((previous) => ({
1276
- ...previous,
1277
- [defect.id]: value
1278
- }));
1279
- }
1280
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1281
- type: "button",
1282
- disabled: busy || defect.recheck?.verdict === void 0 || (recheckNotes[defect.id] ?? defect.recheck?.note ?? "") === (defect.recheck?.note ?? ""),
1283
- title: defect.recheck?.verdict === void 0 ? t("recheckNoteNeedsVerdict") : void 0,
1284
- className: ReviewCard_module_css_default.btnGhost,
1663
+ className: defect.status === "endorsed" ? `${ReviewCard_module_css_default.verdictButton} ${ReviewCard_module_css_default.verdictEndorseOn}` : `${ReviewCard_module_css_default.verdictButton} ${ReviewCard_module_css_default.verdictEndorse}`,
1285
1664
  onClick: () => {
1286
- const verdict = defect.recheck?.verdict;
1287
- if (verdict === void 0) return;
1288
- setVerdict(review, defect, verdict, recheckNotes[defect.id] ?? defect.recheck?.note ?? "");
1665
+ setStatus(review, defect, defect.status === "endorsed" ? "pending" : "endorsed");
1289
1666
  },
1290
- children: t("presetSave")
1291
- })]
1292
- }),
1293
- defect.recheck.note === void 0 || defect.recheck.note === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1294
- className: ReviewCard_module_css_default.muted,
1295
- children: defect.recheck.note
1296
- })
1297
- ]
1298
- }),
1299
- defect.status === "rejected" && defect.rejectReason !== void 0 && defect.rejectReason !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1300
- className: ReviewCard_module_css_default.rejectReason,
1301
- children: [
1302
- t("btnReject"),
1303
- ":",
1304
- defect.rejectReason
1305
- ]
1306
- }) : null,
1307
- review.status === "settled" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1308
- className: ReviewCard_module_css_default.actions,
1309
- children: [
1310
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1311
- type: "button",
1312
- disabled: busy,
1313
- className: defect.status === "endorsed" ? `${ReviewCard_module_css_default.verdictButton} ${ReviewCard_module_css_default.verdictEndorseOn}` : `${ReviewCard_module_css_default.verdictButton} ${ReviewCard_module_css_default.verdictEndorse}`,
1314
- onClick: () => {
1315
- setStatus(review, defect, defect.status === "endorsed" ? "pending" : "endorsed");
1316
- },
1317
- children: t("btnEndorse")
1318
- }),
1319
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1320
- type: "button",
1321
- disabled: busy,
1322
- className: defect.status === "rejected" ? `${ReviewCard_module_css_default.verdictButton} ${ReviewCard_module_css_default.verdictRejectOn}` : `${ReviewCard_module_css_default.verdictButton} ${ReviewCard_module_css_default.verdictReject}`,
1323
- onClick: () => {
1324
- if (defect.status === "rejected") {
1325
- setStatus(review, defect, "pending");
1326
- return;
1327
- }
1328
- setRejectingId(defect.id);
1329
- setRejectDraft("");
1330
- },
1331
- children: t("btnReject")
1332
- }),
1333
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1334
- type: "button",
1335
- disabled: busy,
1336
- className: defect.status === "pending" ? `${ReviewCard_module_css_default.verdictButton} ${ReviewCard_module_css_default.verdictPendingOn}` : `${ReviewCard_module_css_default.verdictButton} ${ReviewCard_module_css_default.verdictPending}`,
1337
- onClick: () => {
1338
- setStatus(review, defect, "pending");
1339
- },
1340
- children: t("btnPending")
1341
- })
1342
- ]
1343
- }),
1344
- rejectingId === defect.id ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1345
- className: ReviewCard_module_css_default.rejectBox,
1346
- children: [
1347
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1348
- className: ReviewCard_module_css_default.rejectPrompt,
1349
- children: t("rejectPrompt")
1350
- }),
1351
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1352
- className: ReviewCard_module_css_default.rejectInput,
1353
- value: rejectDraft,
1354
- autoFocus: true,
1355
- rows: 2,
1356
- placeholder: t("rejectPlaceholder"),
1357
- onChange: (event) => setRejectDraft(event.target.value)
1358
- }),
1359
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1360
- className: ReviewCard_module_css_default.actions,
1361
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1667
+ children: t("btnEndorse")
1668
+ }),
1669
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1362
1670
  type: "button",
1363
1671
  disabled: busy,
1364
- className: ReviewCard_module_css_default.btnDangerOn,
1672
+ className: defect.status === "rejected" ? `${ReviewCard_module_css_default.verdictButton} ${ReviewCard_module_css_default.verdictRejectOn}` : `${ReviewCard_module_css_default.verdictButton} ${ReviewCard_module_css_default.verdictReject}`,
1365
1673
  onClick: () => {
1366
- const reason = rejectDraft.trim();
1367
- if (reason === "") {
1368
- reportNotice(t("rejectPrompt"));
1674
+ if (defect.status === "rejected") {
1675
+ setStatus(review, defect, "pending");
1369
1676
  return;
1370
1677
  }
1371
- setStatus(review, defect, "rejected", reason);
1372
- setRejectingId(null);
1678
+ setRejectingId(defect.id);
1373
1679
  setRejectDraft("");
1374
1680
  },
1375
- children: t("rejectConfirm")
1376
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1681
+ children: t("btnReject")
1682
+ }),
1683
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1377
1684
  type: "button",
1378
- className: ReviewCard_module_css_default.btnGhost,
1685
+ disabled: busy,
1686
+ className: defect.status === "pending" ? `${ReviewCard_module_css_default.verdictButton} ${ReviewCard_module_css_default.verdictPendingOn}` : `${ReviewCard_module_css_default.verdictButton} ${ReviewCard_module_css_default.verdictPending}`,
1379
1687
  onClick: () => {
1380
- setRejectingId(null);
1381
- setRejectDraft("");
1688
+ setStatus(review, defect, "pending");
1382
1689
  },
1383
- children: t("rejectCancel")
1384
- })]
1385
- })
1386
- ]
1387
- }) : null
1388
- ]
1389
- }, defect.id)),
1690
+ children: t("btnPending")
1691
+ })
1692
+ ]
1693
+ }),
1694
+ rejectingId === defect.id ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1695
+ className: ReviewCard_module_css_default.rejectBox,
1696
+ children: [
1697
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1698
+ className: ReviewCard_module_css_default.rejectPrompt,
1699
+ children: t("rejectPrompt")
1700
+ }),
1701
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1702
+ className: ReviewCard_module_css_default.rejectInput,
1703
+ value: rejectDraft,
1704
+ autoFocus: true,
1705
+ rows: 2,
1706
+ placeholder: t("rejectPlaceholder"),
1707
+ onChange: (event) => setRejectDraft(event.target.value)
1708
+ }),
1709
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1710
+ className: ReviewCard_module_css_default.actions,
1711
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1712
+ type: "button",
1713
+ disabled: busy,
1714
+ className: ReviewCard_module_css_default.btnDangerOn,
1715
+ onClick: () => {
1716
+ const reason = rejectDraft.trim();
1717
+ if (reason === "") {
1718
+ reportNotice(t("rejectPrompt"));
1719
+ return;
1720
+ }
1721
+ setStatus(review, defect, "rejected", reason);
1722
+ setRejectingId(null);
1723
+ setRejectDraft("");
1724
+ },
1725
+ children: t("rejectConfirm")
1726
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1727
+ type: "button",
1728
+ className: ReviewCard_module_css_default.btnGhost,
1729
+ onClick: () => {
1730
+ setRejectingId(null);
1731
+ setRejectDraft("");
1732
+ },
1733
+ children: t("rejectCancel")
1734
+ })]
1735
+ })
1736
+ ]
1737
+ }) : null
1738
+ ]
1739
+ }, defect.id);
1740
+ }),
1390
1741
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1391
1742
  className: ReviewCard_module_css_default.footer,
1392
1743
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
@@ -1423,9 +1774,82 @@ function mergeSquad(list, squad) {
1423
1774
  return list.some((item) => item.id === squad.id) ? list.map((item) => item.id === squad.id ? squad : item) : [...list, squad];
1424
1775
  }
1425
1776
 
1777
+ //#endregion
1778
+ //#region src/client/icons.tsx
1779
+ const SHAPES = {
1780
+ layers: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1781
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 2.2 2.4 5 8 7.8 13.6 5 8 2.2Z" }),
1782
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M2.4 8 8 10.8 13.6 8" }),
1783
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M2.4 11 8 13.8 13.6 11" })
1784
+ ] }),
1785
+ gauge: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1786
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M2.2 11.5a5.8 5.8 0 1 1 11.6 0" }),
1787
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 11.5 10.8 6.5" }),
1788
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1789
+ cx: "8",
1790
+ cy: "11.5",
1791
+ r: "0.9"
1792
+ })
1793
+ ] }),
1794
+ role: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1795
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
1796
+ x: "2.2",
1797
+ y: "3.2",
1798
+ width: "11.6",
1799
+ height: "9.6",
1800
+ rx: "2"
1801
+ }),
1802
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1803
+ cx: "6",
1804
+ cy: "7.2",
1805
+ r: "1.4"
1806
+ }),
1807
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8.6 6.4h3.2M8.6 8.4h2.4" })
1808
+ ] }),
1809
+ squad: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1810
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1811
+ cx: "6",
1812
+ cy: "6",
1813
+ r: "2"
1814
+ }),
1815
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M2.6 13c.3-2.1 1.7-3.2 3.4-3.2s3.1 1.1 3.4 3.2" }),
1816
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1817
+ cx: "11.4",
1818
+ cy: "6.6",
1819
+ r: "1.5"
1820
+ }),
1821
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M11.4 9.4c1.5 0 2.5 1 2.8 2.6" })
1822
+ ] }),
1823
+ panel: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
1824
+ x: "2.2",
1825
+ y: "3.2",
1826
+ width: "11.6",
1827
+ height: "9.6",
1828
+ rx: "2"
1829
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M6.2 3.2v9.6" })] }),
1830
+ skill: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M9.2 2 4.6 8.8h3L6.8 14l4.6-6.8h-3L9.2 2Z" }),
1831
+ message: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 3.4h10a1.6 1.6 0 0 1 1.6 1.6v5.2A1.6 1.6 0 0 1 13 11.8H8.4L5 14v-2.2H3A1.6 1.6 0 0 1 1.4 10.2V5A1.6 1.6 0 0 1 3 3.4Z" })
1832
+ };
1833
+ /** 渲染一个 16×16 线性图标,颜色跟随 `currentColor`。 */
1834
+ function InlineIcon({ name, size = 14 }) {
1835
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1836
+ width: size,
1837
+ height: size,
1838
+ viewBox: "0 0 16 16",
1839
+ fill: "none",
1840
+ stroke: "currentColor",
1841
+ strokeWidth: "1.4",
1842
+ strokeLinecap: "round",
1843
+ strokeLinejoin: "round",
1844
+ "aria-hidden": "true",
1845
+ focusable: "false",
1846
+ children: SHAPES[name]
1847
+ });
1848
+ }
1849
+
1426
1850
  //#endregion
1427
1851
  //#region \0dsh-css:F:\AI\DSH\1changyong\dsh-plugin-crossfire\src\client\SettingsView.module.css.mjs
1428
- const css = "/*\n * Crossfire settings page styles.\n *\n * 硬约束:**每一个颜色都必须来自主题令牌**(运行时 `Theme.ddxi2lc_listTokens` 的\n * `--dsw-alias-*` 一族),因此浅色与深色主题都无需自己写 override。\n *\n * 这里刻意不照抄圆桌会议的 `RoundTableSettings.ddxi2lc_module.ddxi2lc_css`:那份用的是硬编码\n * `#ffffff` / `rgba(0, 0, 0, 0.8)`,在深色主题下会呈现出白底黑字的割裂块。\n * 令牌已经由宿主按主题解析过,直接用才是正确的做法。\n */\n\n.ddxi2lc_root {\n display: flex;\n flex-direction: column;\n gap: 20px;\n max-width: 620px;\n padding: 8px 0;\n color: var(--dsw-alias-label-primary, inherit);\n}\n\n.ddxi2lc_notice {\n font-size: 12px;\n line-height: 1.6;\n}\n\n.ddxi2lc_noticeOk {\n color: var(--dsw-alias-state-success-primary, #2e7d32);\n}\n\n.ddxi2lc_noticeErr {\n color: var(--dsw-alias-state-error-primary, #d9534f);\n}\n\n.ddxi2lc_section {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.ddxi2lc_sectionTitle {\n font-size: 14px;\n font-weight: 600;\n}\n\n/* 区块抬头行:标题在左,\"新建\"这类**区块主操作**在右。\n *\n * 之前\"+ 新建预设\"与\"编辑/删除\"这类次要操作同级、且淹没在列表下方,用户实测\n * 反馈\"不够显眼\"。提为区块主操作后,它在本区里是唯一的实心按钮。 */\n.ddxi2lc_sectionHead {\n display: flex;\n align-items: center;\n gap: 10px;\n flex-wrap: wrap;\n}\n\n.ddxi2lc_sectionHead .ddxi2lc_sectionTitle {\n flex: 0 1 auto;\n}\n\n/* 把主操作推到右侧;窄屏换行时它自然塌成整行。 */\n.ddxi2lc_sectionHeadGap {\n flex: 1 1 auto;\n}\n\n/* ---- 区块主操作(品牌色实心) ---- */\n\n.ddxi2lc_primaryButton {\n font-size: 13px;\n font-weight: 600;\n padding: 6px 14px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-brand-primary, #4a6cf7);\n background: var(--dsw-alias-brand-primary, #4a6cf7);\n color: var(--dsw-alias-bg-base, #ffffff);\n cursor: pointer;\n}\n\n.ddxi2lc_primaryButton:disabled {\n cursor: not-allowed;\n border-color: var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n color: var(--dsw-alias-state-idle-primary, rgba(128, 128, 128, 0.7));\n}\n\n/* ---- 品牌块(provider 缩写 + 品牌色) ----\n *\n * 与 ReviewCard.ddxi2lc_module.ddxi2lc_css 的同名规则刻意各写一份:CSS Modules 的 classMap 是\n * 按模块生成的,跨模块复用类名只能靠把 classMap 传来传去,不值得。两处的大小\n * 差异也是真实的(这里是列表行,那里是密集的专家行)。\n */\n.ddxi2lc_brandBlock {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n width: 22px;\n height: 22px;\n border-radius: 6px;\n font-size: 10px;\n font-weight: 700;\n letter-spacing: -0.02em;\n color: #ffffff;\n line-height: 1;\n user-select: none;\n}\n\n/* 行内的品牌块 + 路由文本:块给身份,文本给可读的全名(块的 title 在触屏上读不到)。 */\n.ddxi2lc_routeRow {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n.ddxi2lc_hint {\n font-size: 12px;\n line-height: 1.7;\n color: var(--dsw-alias-label-secondary, rgba(128, 128, 128, 0.9));\n}\n\n.ddxi2lc_field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.ddxi2lc_label {\n font-size: 13px;\n font-weight: 600;\n}\n\n.ddxi2lc_input,\n.ddxi2lc_select {\n font-size: 13px;\n padding: 6px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n background: var(--dsw-alias-bg-layer-1, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n max-width: 260px;\n}\n\n.ddxi2lc_input:focus,\n.ddxi2lc_select:focus {\n outline: 2px solid var(--dsw-alias-brand-primary, #4a6cf7);\n outline-offset: 1px;\n}\n\n/* ---- 强度:三选一的分段控件 ---- */\n\n.ddxi2lc_segmented {\n display: inline-flex;\n gap: 0;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n border-radius: 8px;\n overflow: hidden;\n width: fit-content;\n}\n\n.ddxi2lc_segment {\n font-size: 13px;\n padding: 6px 14px;\n border: none;\n background: var(--dsw-alias-bg-layer-1, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n cursor: pointer;\n}\n\n.ddxi2lc_segment + .ddxi2lc_segment {\n border-left: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n}\n\n.ddxi2lc_segmentActive {\n background: var(--dsw-alias-brand-primary, #4a6cf7);\n color: var(--dsw-alias-bg-base, #ffffff);\n font-weight: 600;\n}\n\n.ddxi2lc_segment:disabled {\n cursor: progress;\n color: var(--dsw-alias-state-idle-primary, rgba(128, 128, 128, 0.7));\n}\n\n/* ---- 维度池 ---- */\n\n.ddxi2lc_chips {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n}\n\n.ddxi2lc_chip {\n font-size: 12px;\n padding: 3px 10px;\n border-radius: 999px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n/* 带品牌块的成员 chip:左边品牌色块,右边成员 key。 */\n.ddxi2lc_chipWithBrand {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-size: 12px;\n padding: 3px 10px 3px 4px;\n border-radius: 999px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n/* 成员 chip 里的品牌块比列表行的再小一点:chip 本身只有 24px 高。 */\n.ddxi2lc_brandBlockSmall {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n width: 18px;\n height: 18px;\n border-radius: 5px;\n font-size: 9px;\n font-weight: 700;\n letter-spacing: -0.02em;\n color: #ffffff;\n line-height: 1;\n user-select: none;\n}\n\n.ddxi2lc_chipCustom {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-size: 12px;\n padding: 3px 6px 3px 10px;\n border-radius: 999px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n background: var(--dsw-alias-bg-layer-1, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n}\n\n.ddxi2lc_chipRemove {\n border: none;\n background: transparent;\n color: var(--dsw-alias-state-error-primary, #d9534f);\n cursor: pointer;\n font-size: 12px;\n padding: 0 2px;\n line-height: 1;\n}\n\n.ddxi2lc_addRow {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n.ddxi2lc_addButton {\n font-size: 13px;\n padding: 6px 12px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n background: var(--dsw-alias-bg-layer-1, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n cursor: pointer;\n}\n\n.ddxi2lc_addButton:disabled {\n cursor: not-allowed;\n color: var(--dsw-alias-state-idle-primary, rgba(128, 128, 128, 0.7));\n}\n\n/* ---- 边界说明 ---- */\n\n.ddxi2lc_boundary {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 12px 14px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n font-size: 12px;\n line-height: 1.7;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.ddxi2lc_boundaryTitle {\n font-size: 13px;\n font-weight: 600;\n color: var(--dsw-alias-label-primary, inherit);\n}\n\n.ddxi2lc_batchNote {\n font-size: 12px;\n line-height: 1.6;\n padding: 10px 12px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-state-warn-primary, rgba(200, 150, 0, 0.5));\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n/* ------------------------------------------------------------------ *\n * 角色预设 / 阵容预设(后补的类,颜色同样只用主题令牌)\n * ------------------------------------------------------------------ */\n\n/* 比 .ddxi2lc_input / .ddxi2lc_select 的 260px 宽一些:角色说明与模型名更长。 */\n.ddxi2lc_inputWide {\n font-size: 13px;\n padding: 6px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n background: var(--dsw-alias-bg-layer-1, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n width: 100%;\n max-width: 420px;\n}\n\n.ddxi2lc_textarea {\n font-size: 13px;\n font-family: inherit;\n line-height: 1.5;\n padding: 6px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n background: var(--dsw-alias-bg-layer-1, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n width: 100%;\n max-width: 420px;\n resize: vertical;\n}\n\n.ddxi2lc_inputWide:focus,\n.ddxi2lc_textarea:focus {\n outline: 2px solid var(--dsw-alias-brand-primary, #4a6cf7);\n outline-offset: 1px;\n}\n\n/* ---- 列表 ---- */\n\n.ddxi2lc_list {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.ddxi2lc_listRow {\n display: flex;\n flex-direction: column;\n gap: 4px;\n padding: 8px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n}\n\n.ddxi2lc_rowHead {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n.ddxi2lc_rowName {\n font-size: 13px;\n font-weight: 600;\n}\n\n.ddxi2lc_rowMeta {\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-secondary, inherit);\n word-break: break-word;\n}\n\n/* 把行内按钮推到右侧;列表换行时它自然塌成 0 宽。 */\n.ddxi2lc_spacer {\n flex: 1 1 auto;\n}\n\n.ddxi2lc_rowButton {\n font-size: 12px;\n padding: 3px 10px;\n border-radius: 6px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n background: var(--dsw-alias-bg-layer-1, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n cursor: pointer;\n}\n\n.ddxi2lc_rowDanger {\n font-size: 12px;\n padding: 3px 10px;\n border-radius: 6px;\n border: 1px solid var(--dsw-alias-state-error-primary, #d9534f);\n background: transparent;\n color: var(--dsw-alias-state-error-primary, #d9534f);\n cursor: pointer;\n}\n\n.ddxi2lc_rowButton:disabled,\n.ddxi2lc_rowDanger:disabled {\n cursor: not-allowed;\n color: var(--dsw-alias-state-idle-primary, rgba(128, 128, 128, 0.7));\n border-color: var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n}\n\n/* ---- 内联编辑器 ---- */\n\n.ddxi2lc_editor {\n display: flex;\n flex-direction: column;\n gap: 10px;\n padding: 12px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n background: var(--dsw-alias-bg-overlay, var(--dsw-alias-bg-layer-1, transparent));\n}\n\n.ddxi2lc_gridTwo {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 10px;\n}\n\n/* ---- 阵容成员行 ---- */\n\n.ddxi2lc_member {\n display: flex;\n align-items: flex-end;\n gap: 8px;\n flex-wrap: wrap;\n padding: 8px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n}\n\n.ddxi2lc_memberFields {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));\n gap: 8px;\n flex: 1 1 320px;\n}\n\n/* 上限提示:与 .ddxi2lc_notice 同字号,但用 warn 令牌(浅深色都可读)。 */\n.ddxi2lc_warn {\n color: var(--dsw-alias-state-warn-primary, rgba(200, 150, 0, 0.9));\n}\n\n";
1852
+ const css = "/*\n * Crossfire settings page styles.\n *\n * 硬约束:**每一个颜色都必须来自主题令牌**(运行时 `Theme.ddxi2lc_listTokens` 的\n * `--dsw-alias-*` 一族),因此浅色与深色主题都无需自己写 override。\n *\n * 这里刻意不照抄圆桌会议的 `RoundTableSettings.ddxi2lc_module.ddxi2lc_css`:那份用的是硬编码\n * `#ffffff` / `rgba(0, 0, 0, 0.8)`,在深色主题下会呈现出白底黑字的割裂块。\n * 令牌已经由宿主按主题解析过,直接用才是正确的做法。\n */\n\n/* ---- 本地设计令牌(这一批改动的全部依据) ----\n *\n * 字号四档、圆角三档、间距 4px 栅格。圆桌设置页有同值的一份:\n * CSS Module 的 classMap 按模块生成,跨包无法共享类名,只能共享**数值**。\n * 这两份文件是同一套规格的两个副本 —— 改一处时另一处必须同步,否则两个\n * 插件的设置页会重新分叉。\n */\n.ddxi2lc_root {\n --cf-fs-11: 11px;\n --cf-fs-12: 12px;\n --cf-fs-13: 13px;\n --cf-fs-14: 14px;\n --cf-radius-s: 6px;\n --cf-radius-m: 8px;\n --cf-radius-l: 12px;\n --cf-space-1: 4px;\n --cf-space-2: 8px;\n --cf-space-3: 12px;\n --cf-space-4: 16px;\n --cf-control-h: 32px;\n --cf-border: var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n --cf-border-strong: var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n --cf-bg-soft: var(--dsw-alias-bg-layer-2, transparent);\n --cf-bg-card: var(--dsw-alias-bg-layer-1, transparent);\n --cf-fg: var(--dsw-alias-label-primary, inherit);\n --cf-fg-2: var(--dsw-alias-label-secondary, inherit);\n --cf-brand: var(--dsw-alias-brand-primary, #4a6cf7);\n --cf-on-brand: var(--dsw-alias-bg-base, #ffffff);\n\n display: flex;\n flex-direction: column;\n gap: var(--cf-space-4);\n max-width: 720px;\n padding: 8px 0;\n color: var(--cf-fg);\n font-size: var(--cf-fs-13);\n line-height: 1.6;\n}\n\n.ddxi2lc_notice {\n font-size: 12px;\n line-height: 1.6;\n}\n\n.ddxi2lc_noticeOk {\n color: var(--dsw-alias-state-success-primary, #2e7d32);\n}\n\n.ddxi2lc_noticeErr {\n color: var(--dsw-alias-state-error-primary, #d9534f);\n}\n\n/* 每个区块是一张卡片:边框 + 圆角 + 浅底把\"一组设置\"圈起来。\n * 之前所有区块靠 20px 空隙分隔,长页面滚到一半就分不清哪些控件属于同一组。 */\n.ddxi2lc_section {\n display: flex;\n flex-direction: column;\n gap: var(--cf-space-3);\n padding: var(--cf-space-4);\n border: 1px solid var(--cf-border);\n border-radius: var(--cf-radius-l);\n background: var(--cf-bg-card);\n}\n\n/* 第一个区块是整页标题 + 导语,不做卡片:给它加边框会变成\"一个框里的标题\"。 */\n.ddxi2lc_root > .ddxi2lc_section:first-child {\n padding: 0 2px var(--cf-space-1);\n border: none;\n background: transparent;\n gap: var(--cf-space-1);\n}\n\n.ddxi2lc_sectionTitle {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: var(--cf-fs-14);\n font-weight: 650;\n letter-spacing: 0.01em;\n}\n\n/* 图标降一档饱和度:它是\"目录\"记号,不和文字抢。 */\n.ddxi2lc_sectionTitle svg {\n flex: none;\n color: var(--cf-fg-2);\n}\n\n/* 区块抬头行:标题在左,\"新建\"这类**区块主操作**在右。\n *\n * 之前\"+ 新建预设\"与\"编辑/删除\"这类次要操作同级、且淹没在列表下方,用户实测\n * 反馈\"不够显眼\"。提为区块主操作后,它在本区里是唯一的实心按钮。 */\n.ddxi2lc_sectionHead {\n display: flex;\n align-items: center;\n gap: 10px;\n flex-wrap: wrap;\n}\n\n.ddxi2lc_sectionHead .ddxi2lc_sectionTitle {\n flex: 0 1 auto;\n}\n\n/* 把主操作推到右侧;窄屏换行时它自然塌成整行。 */\n.ddxi2lc_sectionHeadGap {\n flex: 1 1 auto;\n}\n\n/* ---- 区块主操作(品牌色实心) ---- */\n\n.ddxi2lc_primaryButton {\n box-sizing: border-box;\n min-height: var(--cf-control-h);\n font-size: var(--cf-fs-13);\n font-weight: 600;\n padding: 6px 14px;\n border-radius: var(--cf-radius-m);\n border: 1px solid var(--cf-brand);\n background: var(--cf-brand);\n color: var(--cf-on-brand);\n cursor: pointer;\n}\n\n.ddxi2lc_primaryButton:hover:not(:disabled) {\n filter: brightness(1.06);\n}\n\n.ddxi2lc_primaryButton:disabled {\n cursor: not-allowed;\n border-color: var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n color: var(--dsw-alias-state-idle-primary, rgba(128, 128, 128, 0.7));\n}\n\n/* ---- 品牌块(provider 缩写 + 品牌色) ----\n *\n * 与 ReviewCard.ddxi2lc_module.ddxi2lc_css 的同名规则刻意各写一份:CSS Modules 的 classMap 是\n * 按模块生成的,跨模块复用类名只能靠把 classMap 传来传去,不值得。两处的大小\n * 差异也是真实的(这里是列表行,那里是密集的专家行)。\n */\n.ddxi2lc_brandBlock {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n width: 22px;\n height: 22px;\n border-radius: 6px;\n font-size: 10px;\n font-weight: 700;\n letter-spacing: -0.02em;\n color: #ffffff;\n line-height: 1;\n user-select: none;\n}\n\n/* 行内的品牌块 + 路由文本:块给身份,文本给可读的全名(块的 title 在触屏上读不到)。 */\n.ddxi2lc_routeRow {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n.ddxi2lc_hint {\n font-size: 12px;\n line-height: 1.7;\n color: var(--dsw-alias-label-secondary, rgba(128, 128, 128, 0.9));\n}\n\n.ddxi2lc_field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.ddxi2lc_label {\n font-size: 13px;\n font-weight: 600;\n}\n\n.ddxi2lc_input,\n.ddxi2lc_select {\n box-sizing: border-box;\n min-height: var(--cf-control-h);\n width: 100%;\n max-width: 320px;\n font-size: var(--cf-fs-13);\n padding: 6px 10px;\n border-radius: var(--cf-radius-m);\n border: 1px solid var(--cf-border-strong);\n background: var(--cf-bg-soft);\n color: var(--cf-fg);\n}\n\n.ddxi2lc_input:focus,\n.ddxi2lc_select:focus {\n outline: 2px solid var(--dsw-alias-brand-primary, #4a6cf7);\n outline-offset: 1px;\n}\n\n/* ---- 强度:三选一的分段控件 ---- */\n\n.ddxi2lc_segmented {\n display: inline-flex;\n gap: 2px;\n padding: 2px;\n border: 1px solid var(--cf-border);\n border-radius: var(--cf-radius-l);\n background: var(--cf-bg-soft);\n width: fit-content;\n}\n\n.ddxi2lc_segment {\n box-sizing: border-box;\n min-height: 28px;\n font-size: var(--cf-fs-13);\n padding: 5px 14px;\n border: none;\n border-radius: var(--cf-radius-m);\n background: transparent;\n color: var(--cf-fg);\n cursor: pointer;\n}\n\n/* 组内不再画分隔线:间隙 + 圆角已经能把两段分开,再加竖线会显得像表格。 */\n.ddxi2lc_segment + .ddxi2lc_segment {\n border-left: none;\n}\n\n.ddxi2lc_segmentActive {\n background: var(--cf-brand);\n color: var(--cf-on-brand);\n font-weight: 600;\n}\n\n.ddxi2lc_segment:disabled {\n cursor: progress;\n color: var(--dsw-alias-state-idle-primary, rgba(128, 128, 128, 0.7));\n}\n\n/* ---- 维度池 ---- */\n\n.ddxi2lc_chips {\n display: flex;\n flex-wrap: wrap;\n gap: var(--cf-space-2);\n}\n\n.ddxi2lc_chip {\n box-sizing: border-box;\n display: inline-flex;\n align-items: center;\n min-height: 24px;\n font-size: var(--cf-fs-12);\n padding: 2px 10px;\n border-radius: 999px;\n border: 1px solid var(--cf-border);\n background: var(--cf-bg-soft);\n color: var(--cf-fg-2);\n}\n\n/* 带品牌块的成员 chip:左边品牌色块,右边成员 key。 */\n.ddxi2lc_chipWithBrand {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-size: 12px;\n padding: 3px 10px 3px 4px;\n border-radius: 999px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n/* 成员 chip 里的品牌块比列表行的再小一点:chip 本身只有 24px 高。 */\n.ddxi2lc_brandBlockSmall {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n width: 18px;\n height: 18px;\n border-radius: 5px;\n font-size: 9px;\n font-weight: 700;\n letter-spacing: -0.02em;\n color: #ffffff;\n line-height: 1;\n user-select: none;\n}\n\n.ddxi2lc_chipCustom {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-size: 12px;\n padding: 3px 6px 3px 10px;\n border-radius: 999px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n background: var(--dsw-alias-bg-layer-1, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n}\n\n.ddxi2lc_chipRemove {\n border: none;\n background: transparent;\n color: var(--dsw-alias-state-error-primary, #d9534f);\n cursor: pointer;\n font-size: 12px;\n padding: 0 2px;\n line-height: 1;\n}\n\n.ddxi2lc_addRow {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n.ddxi2lc_addButton {\n box-sizing: border-box;\n min-height: var(--cf-control-h);\n font-size: var(--cf-fs-13);\n padding: 6px 12px;\n border-radius: var(--cf-radius-m);\n border: 1px solid var(--cf-border-strong);\n background: transparent;\n color: var(--cf-fg);\n cursor: pointer;\n}\n\n.ddxi2lc_addButton:hover:not(:disabled) {\n background: var(--cf-bg-soft);\n}\n\n.ddxi2lc_addButton:disabled {\n cursor: not-allowed;\n color: var(--dsw-alias-state-idle-primary, rgba(128, 128, 128, 0.7));\n}\n\n/* ---- 边界说明 ---- */\n\n.ddxi2lc_boundary {\n display: flex;\n flex-direction: column;\n gap: var(--cf-space-2);\n padding: 14px 16px;\n border-radius: var(--cf-radius-l);\n border: 1px solid var(--cf-border);\n background: var(--cf-bg-soft);\n font-size: var(--cf-fs-12);\n line-height: 1.7;\n color: var(--cf-fg-2);\n}\n\n.ddxi2lc_boundaryTitle {\n font-size: var(--cf-fs-13);\n font-weight: 600;\n color: var(--cf-fg);\n}\n\n.ddxi2lc_batchNote {\n font-size: var(--cf-fs-12);\n line-height: 1.6;\n padding: 10px 12px;\n border-radius: var(--cf-radius-m);\n border: 1px solid var(--dsw-alias-state-warn-primary, rgba(200, 150, 0, 0.5));\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary, #c89600) 8%, transparent);\n color: var(--cf-fg-2);\n}\n\n/* ------------------------------------------------------------------ *\n * 角色预设 / 阵容预设(后补的类,颜色同样只用主题令牌)\n * ------------------------------------------------------------------ */\n\n/* 比 .ddxi2lc_input / .ddxi2lc_select 的 320px 宽一些:角色说明与模型名更长。 */\n.ddxi2lc_inputWide {\n box-sizing: border-box;\n min-height: var(--cf-control-h);\n font-size: var(--cf-fs-13);\n padding: 6px 10px;\n border-radius: var(--cf-radius-m);\n border: 1px solid var(--cf-border-strong);\n background: var(--cf-bg-soft);\n color: var(--cf-fg);\n width: 100%;\n max-width: 480px;\n}\n\n.ddxi2lc_textarea {\n box-sizing: border-box;\n font-size: var(--cf-fs-13);\n font-family: inherit;\n line-height: 1.5;\n padding: 8px 10px;\n border-radius: var(--cf-radius-m);\n border: 1px solid var(--cf-border-strong);\n background: var(--cf-bg-soft);\n color: var(--cf-fg);\n width: 100%;\n max-width: 480px;\n resize: vertical;\n}\n\n.ddxi2lc_inputWide:focus,\n.ddxi2lc_textarea:focus {\n outline: 2px solid var(--dsw-alias-brand-primary, #4a6cf7);\n outline-offset: 1px;\n}\n\n/* ---- 列表 ---- */\n\n.ddxi2lc_list {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.ddxi2lc_listRow {\n display: flex;\n flex-direction: column;\n gap: var(--cf-space-1);\n padding: 10px 12px;\n border-radius: var(--cf-radius-m);\n border: 1px solid var(--cf-border);\n background: var(--cf-bg-soft);\n}\n\n.ddxi2lc_rowHead {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n.ddxi2lc_rowName {\n font-size: 13px;\n font-weight: 600;\n}\n\n.ddxi2lc_rowMeta {\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-secondary, inherit);\n word-break: break-word;\n}\n\n/* 把行内按钮推到右侧;列表换行时它自然塌成 0 宽。 */\n.ddxi2lc_spacer {\n flex: 1 1 auto;\n}\n\n.ddxi2lc_rowButton {\n box-sizing: border-box;\n min-height: 28px;\n font-size: var(--cf-fs-12);\n padding: 3px 10px;\n border-radius: var(--cf-radius-s);\n border: 1px solid var(--cf-border-strong);\n background: transparent;\n color: var(--cf-fg);\n cursor: pointer;\n}\n\n.ddxi2lc_rowButton:hover:not(:disabled) {\n background: var(--cf-bg-soft);\n}\n\n.ddxi2lc_rowDanger {\n box-sizing: border-box;\n min-height: 28px;\n font-size: var(--cf-fs-12);\n padding: 3px 10px;\n border-radius: var(--cf-radius-s);\n border: 1px solid var(--dsw-alias-state-error-primary, #d9534f);\n background: transparent;\n color: var(--dsw-alias-state-error-primary, #d9534f);\n cursor: pointer;\n}\n\n.ddxi2lc_rowDanger:hover:not(:disabled) {\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #d9534f) 7%, transparent);\n}\n\n.ddxi2lc_rowButton:disabled,\n.ddxi2lc_rowDanger:disabled {\n cursor: not-allowed;\n color: var(--dsw-alias-state-idle-primary, rgba(128, 128, 128, 0.7));\n border-color: var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n}\n\n/* ---- 内联编辑器 ---- */\n\n.ddxi2lc_editor {\n display: flex;\n flex-direction: column;\n gap: var(--cf-space-3);\n padding: 14px;\n border-radius: var(--cf-radius-l);\n border: 1px solid var(--cf-border-strong);\n background: var(--cf-bg-soft);\n}\n\n.ddxi2lc_gridTwo {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 10px;\n}\n\n/* ---- 阵容成员行 ---- */\n\n.ddxi2lc_member {\n display: flex;\n align-items: flex-end;\n gap: var(--cf-space-2);\n flex-wrap: wrap;\n padding: 10px;\n border-radius: var(--cf-radius-l);\n border: 1px solid var(--cf-border);\n background: var(--cf-bg-soft);\n}\n\n.ddxi2lc_memberFields {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));\n gap: 8px;\n flex: 1 1 320px;\n}\n\n/* 上限提示:与 .ddxi2lc_notice 同字号,但用 warn 令牌(浅深色都可读)。 */\n.ddxi2lc_warn {\n color: var(--dsw-alias-state-warn-primary, rgba(200, 150, 0, 0.9));\n}\n\n";
1429
1853
  const classMap = {
1430
1854
  "listTokens": "ddxi2lc_listTokens",
1431
1855
  "module": "ddxi2lc_module",
@@ -1947,9 +2371,9 @@ function CrossfireSettings(props) {
1947
2371
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1948
2372
  className: SettingsView_module_css_default.section,
1949
2373
  children: [
1950
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2374
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1951
2375
  className: SettingsView_module_css_default.sectionTitle,
1952
- children: t("dimensionsTitle")
2376
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(InlineIcon, { name: "layers" }), t("dimensionsTitle")]
1953
2377
  }),
1954
2378
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1955
2379
  className: SettingsView_module_css_default.hint,
@@ -2024,9 +2448,9 @@ function CrossfireSettings(props) {
2024
2448
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2025
2449
  className: SettingsView_module_css_default.section,
2026
2450
  children: [
2027
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2451
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2028
2452
  className: SettingsView_module_css_default.sectionTitle,
2029
- children: t("budgetTitle")
2453
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(InlineIcon, { name: "gauge" }), t("budgetTitle")]
2030
2454
  }),
2031
2455
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2032
2456
  className: SettingsView_module_css_default.hint,
@@ -2061,9 +2485,9 @@ function CrossfireSettings(props) {
2061
2485
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2062
2486
  className: SettingsView_module_css_default.sectionHead,
2063
2487
  children: [
2064
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2488
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2065
2489
  className: SettingsView_module_css_default.sectionTitle,
2066
- children: t("settingsPresetsTitle")
2490
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(InlineIcon, { name: "role" }), t("settingsPresetsTitle")]
2067
2491
  }),
2068
2492
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: SettingsView_module_css_default.sectionHeadGap }),
2069
2493
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
@@ -2278,9 +2702,9 @@ function CrossfireSettings(props) {
2278
2702
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2279
2703
  className: SettingsView_module_css_default.sectionHead,
2280
2704
  children: [
2281
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2705
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2282
2706
  className: SettingsView_module_css_default.sectionTitle,
2283
- children: t("settingsSquadsTitle")
2707
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(InlineIcon, { name: "squad" }), t("settingsSquadsTitle")]
2284
2708
  }),
2285
2709
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: SettingsView_module_css_default.sectionHeadGap }),
2286
2710
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
@@ -2696,6 +3120,7 @@ const zh = {
2696
3120
  countEndorsed: "认定",
2697
3121
  countRejected: "驳回",
2698
3122
  countPending: "待定",
3123
+ countPendingOfTotal: "待定 {pending} / 共 {total} 条",
2699
3124
  defectEmpty: "还没有可裁决的缺陷。",
2700
3125
  defectsTitle: "待裁决缺陷",
2701
3126
  evidenceRepro: "可复现步骤",
@@ -2738,6 +3163,8 @@ const zh = {
2738
3163
  expertRoleEdit: "改角色定位",
2739
3164
  expertRoleSaved: "角色定位已保存",
2740
3165
  expertRoleFromRoster: "(来自阵容预设)",
3166
+ expertPromptView: "查看原提示词",
3167
+ expertPromptTitle: "这位专家的原提示词(只读)",
2741
3168
  stanceSectionTitle: "我的立场与理由",
2742
3169
  stanceExpand: "展开看全文",
2743
3170
  stanceCollapse: "收起立场",
@@ -2871,6 +3298,7 @@ const en = {
2871
3298
  countEndorsed: "Endorsed",
2872
3299
  countRejected: "Rejected",
2873
3300
  countPending: "Pending",
3301
+ countPendingOfTotal: "Pending {pending} / {total} total",
2874
3302
  defectEmpty: "No defects to adjudicate yet.",
2875
3303
  defectsTitle: "Defects to adjudicate",
2876
3304
  evidenceRepro: "Repro steps",
@@ -2913,6 +3341,8 @@ const en = {
2913
3341
  expertRoleEdit: "Edit role",
2914
3342
  expertRoleSaved: "Role saved",
2915
3343
  expertRoleFromRoster: "(from a squad preset)",
3344
+ expertPromptView: "View original prompt",
3345
+ expertPromptTitle: "This expert's original prompt (read-only)",
2916
3346
  stanceSectionTitle: "Your position and reasons",
2917
3347
  stanceExpand: "Expand full text",
2918
3348
  stanceCollapse: "Collapse stance",
@@ -2994,8 +3424,11 @@ function apply(ctx) {
2994
3424
  return direct;
2995
3425
  }
2996
3426
  };
3427
+ const settingsRpc = async (endpoint, payload) => {
3428
+ return await rpc(endpoint, payload !== null && typeof payload === "object" && !Array.isArray(payload) ? withSessionIdentity(payload) : payload);
3429
+ };
2997
3430
  const settingsInjected = () => ({
2998
- rpc,
3431
+ rpc: settingsRpc,
2999
3432
  t: ctx.locale.bind(NS)
3000
3433
  });
3001
3434
  slots.inject("settings.section", () => slots.register({