magic-agent 2.4.2 → 2.6.0

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 (156) hide show
  1. package/CLIENT-CONTRACT.md +290 -30
  2. package/README.md +15 -14
  3. package/client-ui/pc/app.css +32 -0
  4. package/client-ui/pc/js/04-task-list.js +5 -1
  5. package/client-ui/pc/js/07-chat-stream-render.js +3 -3
  6. package/client-ui/pc/js/90-glue.js +37 -2
  7. package/client-ui/pc/js/92-composer.js +19 -4
  8. package/client-ui/pc/js/93-rail.js +16 -5
  9. package/client-ui/shared/conv/02-media-kinds.js +2 -2
  10. package/client-ui/shared/conv/03-chat-message.js +241 -23
  11. package/client-ui/shared/conv/06-ask-answer.js +75 -1
  12. package/client-ui/shared/conv/09-stream-apply.js +63 -7
  13. package/client-ui/shared/conv/19-attachments.js +157 -6
  14. package/client-ui/shared/conv/27-conv-ledger.js +5 -2
  15. package/client-ui/shared/conv/29-msg-variants.js +81 -0
  16. package/client-ui/shared/conv/conv.css +79 -19
  17. package/client-ui/shared/conv/dom-map.md +1 -0
  18. package/client-ui/shared/conversation.css +13 -2
  19. package/client-ui/shared/conversation.js +336 -50
  20. package/client-ui/shared/transport-http.js +14 -0
  21. package/client-ui-mobile/index.html +2932 -333
  22. package/client-ui-mobile/reqs.css +102 -0
  23. package/client-ui-mobile/reqs.js +485 -70
  24. package/client-ui-mobile/reqshot.js +505 -0
  25. package/client-ui-mobile/scripts.css +21 -0
  26. package/client-ui-mobile/scripts.js +539 -0
  27. package/client-ui-mobile/vendor/cose-base.js +3214 -0
  28. package/client-ui-mobile/vendor/cytoscape-fcose.js +1549 -0
  29. package/client-ui-mobile/vendor/cytoscape.min.js +32 -0
  30. package/client-ui-mobile/vendor/layout-base.js +5230 -0
  31. package/cmd/magic-client/browsectl.go +199 -0
  32. package/cmd/magic-client/clientctl.go +33 -9
  33. package/cmd/magic-client/main.go +57 -26
  34. package/internal/agent/ask.go +45 -0
  35. package/internal/agent/ask_test.go +39 -0
  36. package/internal/agent/attachments_test.go +6 -2
  37. package/internal/agent/capability.go +7 -1
  38. package/internal/agent/claude.go +20 -15
  39. package/internal/agent/codebuddy.go +125 -34
  40. package/internal/agent/codebuddy_account_test.go +54 -0
  41. package/internal/agent/codebuddy_stream_test.go +103 -0
  42. package/internal/agent/credits.go +713 -0
  43. package/internal/agent/credits_minimax_test.go +97 -0
  44. package/internal/agent/credits_test.go +211 -0
  45. package/internal/agent/custom_models.go +155 -0
  46. package/internal/agent/custom_models_test.go +125 -0
  47. package/internal/agent/dsh.go +123 -49
  48. package/internal/agent/dsh_resume.go +13 -2
  49. package/internal/agent/dsh_test.go +108 -6
  50. package/internal/agent/engine.go +37 -1
  51. package/internal/agent/engine_base.go +21 -17
  52. package/internal/agent/engine_base_test.go +2 -2
  53. package/internal/agent/engine_test.go +47 -86
  54. package/internal/agent/fallback.go +313 -0
  55. package/internal/agent/fallback_test.go +340 -0
  56. package/internal/agent/launch.go +43 -1
  57. package/internal/agent/llmhttp.go +46 -4
  58. package/internal/agent/llmhttp_test.go +157 -0
  59. package/internal/agent/models.go +53 -1
  60. package/internal/agent/models_test.go +20 -5
  61. package/internal/agent/pidalive_windows_test.go +40 -0
  62. package/internal/agent/runcmd.go +26 -0
  63. package/internal/agent/runcmd_windows.go +36 -5
  64. package/internal/agent/stream.go +136 -6
  65. package/internal/agent/stream_test.go +25 -38
  66. package/internal/agent/stream_timeline_test.go +2 -37
  67. package/internal/agent/streamjson.go +108 -6
  68. package/internal/agent/tools_suffix_test.go +6 -3
  69. package/internal/agent/trae.go +50 -243
  70. package/internal/agent/trae_exec.go +433 -0
  71. package/internal/agent/trae_exec_test.go +181 -0
  72. package/internal/agent/wraplaunch_test.go +113 -0
  73. package/internal/cli/ask.go +160 -37
  74. package/internal/cli/ask_test.go +1 -1
  75. package/internal/cli/checkin.go +77 -0
  76. package/internal/cli/clientcmd.go +3 -2
  77. package/internal/cli/control.go +46 -5
  78. package/internal/cli/control_test.go +65 -0
  79. package/internal/cli/events.go +4 -0
  80. package/internal/cli/quotas.go +125 -0
  81. package/internal/cli/requirements.go +51 -0
  82. package/internal/cli/root.go +122 -5
  83. package/internal/cli/session.go +7 -10
  84. package/internal/cli/suggest.go +7 -5
  85. package/internal/cli/team.go +37 -9
  86. package/internal/cli/team_test.go +39 -0
  87. package/internal/client/ab.go +372 -0
  88. package/internal/client/ab_test.go +287 -0
  89. package/internal/client/browse.go +84 -36
  90. package/internal/client/browse_test.go +107 -9
  91. package/internal/client/browseinput.go +481 -0
  92. package/internal/client/browseinput_test.go +216 -0
  93. package/internal/client/corecmd.go +23 -0
  94. package/internal/client/corecmd_other.go +8 -0
  95. package/internal/client/corecmd_windows.go +24 -0
  96. package/internal/client/daemon.go +90 -17
  97. package/internal/client/daemon_test.go +33 -0
  98. package/internal/client/engines_checkin_test.go +57 -0
  99. package/internal/client/fallback_flags_test.go +64 -0
  100. package/internal/client/jobs.go +294 -21
  101. package/internal/client/jobs_test.go +299 -2
  102. package/internal/client/knowledge.go +854 -0
  103. package/internal/client/knowledge_test.go +510 -0
  104. package/internal/client/lan.go +193 -7
  105. package/internal/client/lan_test.go +110 -0
  106. package/internal/client/quotas_merge_test.go +107 -0
  107. package/internal/client/requirements.go +198 -1
  108. package/internal/client/requirements_shots_test.go +200 -0
  109. package/internal/client/restart.go +134 -0
  110. package/internal/client/restart_test.go +151 -0
  111. package/internal/client/scripts.go +1047 -0
  112. package/internal/client/scripts_decode_other.go +27 -0
  113. package/internal/client/scripts_decode_test.go +125 -0
  114. package/internal/client/scripts_decode_windows.go +206 -0
  115. package/internal/client/scripts_test.go +726 -0
  116. package/internal/client/service.go +322 -18
  117. package/internal/client/service_test.go +51 -0
  118. package/internal/client/suggest.go +105 -2
  119. package/internal/client/suggest_test.go +86 -0
  120. package/internal/client/team_flags.go +25 -11
  121. package/internal/client/team_flags_test.go +39 -0
  122. package/internal/requirements/bootstrap.go +16 -0
  123. package/internal/requirements/consolidate.go +100 -9
  124. package/internal/requirements/doc.go +154 -3
  125. package/internal/requirements/graph.go +138 -0
  126. package/internal/requirements/memory.go +7 -0
  127. package/internal/requirements/modules.go +312 -0
  128. package/internal/requirements/modules_test.go +373 -0
  129. package/internal/requirements/relations.go +155 -0
  130. package/internal/requirements/relations_test.go +231 -0
  131. package/internal/requirements/requirements.go +10 -0
  132. package/internal/requirements/shots.go +418 -0
  133. package/internal/requirements/shots_test.go +387 -0
  134. package/internal/session/session.go +92 -0
  135. package/internal/session/sessionlog.go +23 -0
  136. package/internal/session/sessionlog_read_test.go +39 -0
  137. package/internal/suggest/suggest.go +23 -1
  138. package/internal/team/event.go +3 -1
  139. package/internal/team/execute.go +21 -6
  140. package/internal/team/fallback_route_test.go +80 -0
  141. package/internal/team/planner.go +15 -3
  142. package/internal/team/route.go +89 -17
  143. package/internal/team/team.go +18 -4
  144. package/npm/apk/magic-agent-mobile-release.apk +0 -0
  145. package/npm/apk/version.json +1 -5
  146. package/npm/dist/darwin-arm64/magic-agent +0 -0
  147. package/npm/dist/darwin-arm64/magic-client +0 -0
  148. package/npm/dist/darwin-x64/magic-agent +0 -0
  149. package/npm/dist/darwin-x64/magic-client +0 -0
  150. package/npm/dist/linux-arm64/magic-agent +0 -0
  151. package/npm/dist/linux-arm64/magic-client +0 -0
  152. package/npm/dist/linux-x64/magic-agent +0 -0
  153. package/npm/dist/linux-x64/magic-client +0 -0
  154. package/npm/dist/win32-x64/magic-agent.exe +0 -0
  155. package/npm/dist/win32-x64/magic-client.exe +0 -0
  156. package/package.json +8 -2
@@ -62,6 +62,52 @@ function attErrText(err){
62
62
  /** 缩略图地址:落盘后用真实 URL,落盘途中用本地 objectURL */
63
63
  function attThumbUrl(a){ return a.remoteUrl || a.objectUrl || ""; }
64
64
 
65
+ /* ── 历史附件(2026-10-25 用户报障:「点击历史会话时发送的参考图没了」)──────────
66
+ 根因:core 落盘的附件只有 **path + mime** 两个字段(internal/agent/attachments.go 的
67
+ Attachment),既没有 remoteUrl 也没有 objectUrl —— attThumbUrl 对它恒回空,
68
+ qaRenderMine 里「拿不到地址就跳过」的分支把整排缩略图全吞了。
69
+ 这里给历史那档一条**独立的取址链**,与实时附件互不干扰:
70
+ ① 实时字段(remoteUrl / objectUrl)优先 —— 两档同形时行为不变;
71
+ ② 宿主回读通道(desk.fileUrl / desk.mediaUrl。组件宿主在 installDeskProbe 里把
72
+ mediaUrl 挂成 transport.fileUrl = /desk/media?path=…,按绝对路径回读,上传目录
73
+ 内外的本机媒体文件都通);
74
+ ③ qaShotSrc(file:// 壳内直接拼 file://;产物库路径走内置后端);
75
+ ④ 都没有 → 空串,上层跳过 —— 宁可没有图,也不画一个必然裂的框(与 qaShotSrc
76
+ 预览态的口径一致)。 */
77
+ /** 附件显示名:实时那档有 name;历史附件只有 path → 取 basename,并去掉上传时
78
+ * 服务端加的 <UnixNano>- 前缀,还原成用户上传时的文件名(与组件宿主 attNode 同一口径)。 */
79
+ function attHistName(a){
80
+ const n = a && a.name ? String(a.name) : "";
81
+ if(n) return n;
82
+ const p = String((a && a.path) || "");
83
+ const base = p.split(/[\\/]/).pop() || "";
84
+ return base.replace(/^\d{13,}-/, "") || "截图";
85
+ }
86
+ /** 这条附件是不是图(历史附件只有 mime/path;QA_IMG_EXT 与缩略图同一份后缀表)。 */
87
+ function attHistIsImage(a){
88
+ if(!a) return false;
89
+ const mime = String(a.mime || "");
90
+ if(mime) return /^image\//i.test(mime);
91
+ return QA_IMG_EXT.test(attHistName(a));
92
+ }
93
+ /** 历史附件 → 可显示地址(取址链见上)。实时附件请继续走 attThumbUrl。 */
94
+ function attHistUrl(a){
95
+ if(!a) return "";
96
+ if(a.remoteUrl) return a.remoteUrl;
97
+ if(a.objectUrl) return a.objectUrl;
98
+ const p = String(a.path || "").trim();
99
+ if(!p) return "";
100
+ const via = (name)=>{
101
+ const f = qaDeskFn(name);
102
+ if(typeof f !== "function") return "";
103
+ try{ return f(p) || ""; }catch(e){ return ""; }
104
+ };
105
+ const u = via("fileUrl") || via("mediaUrl");
106
+ if(u) return u;
107
+ if(typeof qaShotSrc === "function"){ try{ return qaShotSrc(p) || ""; }catch(e){} }
108
+ return "";
109
+ }
110
+
65
111
  /** 落盘返回的 url → 浏览器可直接用的绝对地址。
66
112
  * 壳内与预览态都拿得到 prdBackendBase(同一个内置后端);
67
113
  * file:// 直开且后端未就绪时返回空 —— 那种情况继续用 objectURL,
@@ -393,6 +439,9 @@ function qaArtKind(p){
393
439
  if(QA_IMG_EXT.test(s)) return "image";
394
440
  if(/\.(mp4|mov|m4v|webm|mkv|avi)$/i.test(s)) return "video";
395
441
  if(/\.(mp3|wav|m4a|aac|ogg|flac)$/i.test(s)) return "audio";
442
+ if(/\.(?:html?|xhtml)$/i.test(s)) return "html"; // HTML 产物:产物卡里嵌 iframe 预览
443
+ //(是否「自包含可预览」由 qaHtmlStandalone
444
+ // 按内容判,见 qaArtMount 的换卡逻辑)
396
445
  if(QA_ART_EXT.test(s)) return "file";
397
446
  return "";
398
447
  }
@@ -608,10 +657,34 @@ function qaArtCollect(text){
608
657
  return out.slice(0, 12); // 一次最多画 12 张(多了就不是「产物」了)
609
658
  }
610
659
 
611
- /** 一张产物卡(按类型分流:图 / 视频 / 音频 / 其它文件)。 */
612
- function qaArtNode(it){
613
- const kind = qaArtKind(it.abs) || "file";
614
- const name = String((it.abs || "").split("/").pop() || "文件");
660
+ /** HTML 产物是否「自包含、可独立预览」(2026-10-24 用户:「index.html这种非静态页面要排除
661
+ * 不需要预览」「按文件名区分不行」→ 改**内容**判据):
662
+ * 读文件头(`/desk/path-read`,512KB 上限)扫 `<script src>` / `<link href>` ——
663
+ * 引用了**外部 js / css**(相对 / 绝对路径,非 data: / 远端)的就是应用入口或页面壳,
664
+ * 单塞一个 iframe 里预览出来是白板或半个页面 → 返 false,退回普通文件卡。
665
+ * ⚠️ 通道拿不到 / 读失败 / 太大读不了 → 返 true(宁可照画,不凭猜把卡降级)。 */
666
+ function qaHtmlStandalone(p){
667
+ const rd = qaDeskFn("pathRead");
668
+ if(!rd) return Promise.resolve(true);
669
+ return Promise.resolve(rd({ path: String(p), base: qaArtBase() })).then((r)=>{
670
+ if(!r || !r.ok || typeof r.text !== "string") return false; // tooBig / 读不到:多半是应用入口
671
+ const t = r.text.slice(0, 65536).toLowerCase();
672
+ const re = /(?:src|href)\s*=\s*["']([^"']+)["']/g;
673
+ let m;
674
+ while((m = re.exec(t))){
675
+ const u = m[1].trim();
676
+ if(!u || /^(?:data:|https?:|\/\/|#|mailto:)/i.test(u)) continue;
677
+ if(/\.(?:m?js|css)(?:[?#]|$)/i.test(u)) return false; // 引了外部脚本 / 样式 → 不自包含
678
+ }
679
+ return true;
680
+ }).catch(()=> true);
681
+ }
682
+
683
+ /** 一张产物卡(按类型分流:图 / 视频 / 音频 / HTML / 其它文件)。
684
+ * `force`(2026-10-24)= 强制按这一档画(HTML 换卡逻辑用:入口页降级成普通文件卡)。 */
685
+ function qaArtNode(it, force){
686
+ const kind = force || qaArtKind(it.abs) || "file";
687
+ const name = String((it.abs || "").split(/[\\/]/).pop() || "文件");
615
688
  const card = document.createElement("div");
616
689
  card.className = "qa-art qa-art-" + kind;
617
690
  card.dataset.path = it.abs || "";
@@ -627,6 +700,15 @@ function qaArtNode(it){
627
700
  if(kind === "video") m.playsInline = true;
628
701
  card.appendChild(m);
629
702
  }
703
+ if(kind === "html"){ // HTML:iframe 就地预览(2026-10-24 用户:
704
+ const f = document.createElement("iframe"); // 「会话产物如果有html 现在没显示预览效果」)
705
+ f.src = qaArtUrl(it.abs);
706
+ f.title = name;
707
+ f.setAttribute("sandbox", "allow-scripts allow-same-origin allow-popups allow-forms");
708
+ f.setAttribute("loading", "lazy");
709
+ f.setAttribute("draggable", "false");
710
+ card.appendChild(f);
711
+ }
630
712
  const cap = document.createElement("div");
631
713
  cap.className = "qa-art-cap";
632
714
  if(kind === "file"){ // 其它文件:后缀徽标 + 名字 + 大小
@@ -726,7 +808,7 @@ function qaArtMount(host, paths, opts){
726
808
  if(!buckets.has(k)) buckets.set(k, []);
727
809
  buckets.get(k).push(x);
728
810
  });
729
- ["image", "video", "audio", "file"].forEach((k)=>{
811
+ ["image", "video", "audio", "html", "file"].forEach((k)=>{
730
812
  const arr = buckets.get(k);
731
813
  if(!arr || !arr.length) return;
732
814
  const row = document.createElement("div");
@@ -734,6 +816,22 @@ function qaArtMount(host, paths, opts){
734
816
  arr.forEach((x)=> row.appendChild(qaArtNode(x)));
735
817
  host.appendChild(row);
736
818
  });
819
+ /* HTML 产物卡是**异步定形**的:先按 html 画(iframe 预览),随后读文件头判「自包含」,
820
+ 引用了外部 js / css 的(index.html 这类应用入口)降级成普通文件卡 —— 内容判据,
821
+ 不按文件名(2026-10-24 用户:「index.html这种非静态页面要排除」「按文件名区分不行」)。 */
822
+ hit.forEach((x)=>{
823
+ if(qaArtKind(x.abs) !== "html") return;
824
+ qaHtmlStandalone(x.abs).then((ok)=>{
825
+ if(ok) return;
826
+ const cards = host.querySelectorAll(".qa-art-html");
827
+ for(const c of cards){
828
+ if(qaArtCanon(c.dataset.path) === qaArtCanon(x.abs)){
829
+ c.parentNode.replaceChild(qaArtNode(x, "file"), c);
830
+ break;
831
+ }
832
+ }
833
+ }).catch(()=>{ /* 换不了就保持原样 */ });
834
+ });
737
835
  return hit.length;
738
836
  }).catch(()=> 0); // 探针失败 = 当作「都不是产物」,正文照旧
739
837
  }
@@ -803,6 +901,59 @@ function qaArtDrop(card){
803
901
  }, true);
804
902
  })();
805
903
 
904
+ /* ── HTML 产物 · 点击打开(2026-10-24 用户:「预览是缩略效果了 要支持点击查看」
905
+ → 「现在有内置浏览器啊 点击跳转打开啊」)────────────────────────────────
906
+ 点卡片:宿主有内置浏览器(`MagicHost.openBrowser`,移动端已挂)就**跳内置浏览器**;
907
+ 没有的宿主退回**内核自画的全屏预览层**(不依赖宿主能力,PC / magic-test 通吃),
908
+ Esc / 关闭按钮退出。
909
+ ⚠️ 缩略 iframe 设 `pointer-events:none`(CSS):iframe 内部的点击不会冒泡到卡片,
910
+ 不设的话「点卡片」只有边条能点中;代价是缩略态不能滚里面内容 —— 反正要看就点开。
911
+ ⚠️ 页面级一次性挂载,与上面 error 收尾同一个形制。 */
912
+ (function(){
913
+ if(typeof document === "undefined" || window.__qaArtHtmlPeek) return;
914
+ window.__qaArtHtmlPeek = true;
915
+ let layer = null;
916
+ const close = ()=>{
917
+ if(layer){ layer.parentNode && layer.parentNode.removeChild(layer); layer = null; }
918
+ document.removeEventListener("keydown", onKey);
919
+ };
920
+ function onKey(e){ if(e && e.key === "Escape") close(); }
921
+ function openPeek(p, u){
922
+ close();
923
+ layer = document.createElement("div");
924
+ layer.className = "qa-art-html-peek";
925
+ const bar = document.createElement("div");
926
+ bar.className = "qa-art-html-peek-bar";
927
+ const t = document.createElement("span");
928
+ t.className = "qa-art-html-peek-t";
929
+ t.textContent = String(p).split(/[\\/]/).pop() || p;
930
+ const x = document.createElement("button");
931
+ x.type = "button"; x.className = "qa-art-html-peek-x"; x.textContent = "关闭";
932
+ x.addEventListener("click", close);
933
+ bar.appendChild(t); bar.appendChild(x);
934
+ const f = document.createElement("iframe");
935
+ f.src = u; f.title = t.textContent;
936
+ f.setAttribute("sandbox", "allow-scripts allow-same-origin allow-popups allow-forms");
937
+ layer.appendChild(bar); layer.appendChild(f);
938
+ document.body.appendChild(layer);
939
+ document.addEventListener("keydown", onKey);
940
+ }
941
+ document.addEventListener("click", function(e){
942
+ const card = e.target && e.target.closest ? e.target.closest(".qa-art-html") : null;
943
+ if(!card || layer) return;
944
+ const p = String(card.dataset.path || "").trim();
945
+ if(!p || typeof qaArtUrl !== "function") return;
946
+ const u = qaArtUrl(p);
947
+ if(!u) return; // 拼不出地址(预览态)→ 不动
948
+ e.preventDefault();
949
+ try{
950
+ const ob = (typeof window !== "undefined" && window.MagicHost) ? window.MagicHost.openBrowser : null;
951
+ if(typeof ob === "function"){ ob(u); return; } // 有内置浏览器 → 跳浏览器
952
+ }catch(err){ /* 宿主通道不通 → 退全屏层 */ }
953
+ openPeek(p, u);
954
+ }, false);
955
+ })();
956
+
806
957
  /* ── 会话产物 · 消息末尾的「本轮产物」(2026-10-15)─────────────────────────
807
958
  用户原话:「会话有新的图片、视频文件产生 应该在消息结束时附加在末尾作为会话产物显示预览」。
808
959
  正文里写出来的产物路径早就有渲染(qaShotsMount / qaArtMount:图变缩略图、视频变就地播放器),
@@ -819,7 +970,7 @@ function qaArtDrop(card){
819
970
  ⚠️ 与正文里已经画过的**不重复**(按绝对路径与文件名双判):用户要的是「正文没提的也看得见」,
820
971
  正文里已经有的再画一遍就成了同一张图出现两次。
821
972
  ⚠️ 只**收**不摘、只**补**不改:正文与工具输出原文一个字都不动。 */
822
- const QA_TURN_ART_KIND = /^(?:image|video|file)$/;
973
+ const QA_TURN_ART_KIND = /^(?:image|video|file|html)$/;
823
974
  /** 记一条候选路径进**这一轮**的产物集(去重;留图 / 视频 / 文件三档)。 */
824
975
  function qaTurnArtsNote(run, p){
825
976
  const s = String(p == null ? "" : p).trim();
@@ -313,7 +313,10 @@ function qaSessionPlayer(){
313
313
  }catch(e){ /* 收尾失败不影响回放 */ }
314
314
  h = null;
315
315
  }else if(arts){ arts.flush(null); } // 没有答复卡的一轮:清掉残留,别串到下一轮
316
- qaRenderMine(String(ev.text || ""), [], { hosted: !!ev.hosted });
316
+ /* 历史附件(参考图)随 user 事件落盘({path, mime}):透传给内核的 attHistUrl
317
+ 按路径换回读地址 —— 与 93-rail 同一口径(2026-10-25 用户报障修复)。 */
318
+ qaRenderMine(String(ev.text || ""),
319
+ Array.isArray(ev.attachments) ? ev.attachments : [], { hosted: !!ev.hosted });
317
320
  t0 = atMs(ev);
318
321
  continue;
319
322
  }
@@ -349,7 +352,7 @@ function qaSessionPlayer(){
349
352
  qaRenderDone(!failed && !stopped,
350
353
  failed ? "调用失败 · " + String(ev.reason || ev.error || "未知错误").slice(0, 200)
351
354
  : stopped ? "已停止" : "已完成",
352
- null, turnMs || null);
355
+ null, turnMs || null, failed ? ev : null);
353
356
  if(arts) arts.flush(h); // 终态之后这一轮的产物补在消息末尾(与实时 onDone 同位)
354
357
  t0 = 0; // 这一轮收尾了:下一轮(若没有 user 事件)重新起算
355
358
  /* ⚠️ 句柄必须一起置空(2026-10-21 用户:「同一轮对话 显示成了两个」):
@@ -255,6 +255,87 @@ function qaMarkerNode(opts){
255
255
  return wrap;
256
256
  }
257
257
 
258
+ /* ---------- 错误显示卡(终态失败的统一落笔)----------
259
+ 用户 2026-10-23:「没有设计错误显示卡 现在是普通的 json 文本」—— 失败那一轮原来只落一行
260
+ `.qa-done.err` 等宽灰字(「调用失败 · <reason>」),没有卡片形态。这里补一张**真正的卡**:
261
+ 朱砂描边 + 淡红底的卡片面,抬头是告警图标 + 「调用失败」标题 + 「失败」状态徽章,
262
+ 主体是**根因**(reason,没被截断,人话那一句),底部可展开「错误详情」(完整错误链 /
263
+ JSON,走 qaCodeFrameNode,可复制)。
264
+
265
+ ⚠️ 与标记行(qaMarkerNode)的分工:control_error / command / notice 那种**过程性**的失败
266
+ 仍落标记行(它们不是这一轮的终态,只是过程里的一次失败通知);这张卡只给**这一轮真的
267
+ 以失败收场**的终态(error / turn_failed / 桥不可用 / 回放失败)。两条形态不重叠。
268
+ ⚠️ 快照重建安全:整张卡的状态(开合)都靠 class / data-*,点击走 #qa-stream 事件委托
269
+ (见 07-stream-delegation 的 `.qa-mk-row` 分支)—— 展开详情那块直接复用 `.qa-mk`
270
+ 那套词汇,所以委托不用为这张卡单独加逻辑,快照恢复出来的卡照样能点开。
271
+ ⚠️ 根节点**保留 `.qa-done` 类**:收尾行契约(`tests/check-conv-shared-host.mjs` 数 `.qa-done`)
272
+ 只认这个类,改类名会让那条断言失守。卡片形态靠 `.qa-errcard` 额外类实现。 */
273
+ function qaErrorCardNode(opts){
274
+ const o = opts || {};
275
+ const t = o.elapsedMs || "";
276
+ const det = o.detail;
277
+ let reason = "";
278
+ let chain = null;
279
+ /* 优先从结构化错误信封取根因与完整链(reason 是最内层原因、error 是带引擎包装的整条链);
280
+ 没有信封(桥异常 / 抛错)就退回 label 里那段,再不行给「未知错误」。 */
281
+ if(det && typeof det === "object"){
282
+ reason = String(det.reason || "").trim();
283
+ chain = det.error != null && String(det.error).trim() ? det.error
284
+ : det.message != null && String(det.message).trim() ? det.message
285
+ : null;
286
+ }
287
+ if(!reason && typeof o.label === "string"){
288
+ reason = String(o.label)
289
+ .replace(/^调用失败\s*·\s*/, "")
290
+ .replace(/\s*·\s*总耗时[\s\S]*$/, "")
291
+ .trim();
292
+ }
293
+ if(!reason) reason = "未知错误";
294
+
295
+ const wrap = document.createElement("div");
296
+ wrap.className = "qa-done err qa-errcard";
297
+
298
+ const hd = document.createElement("div");
299
+ hd.className = "qa-ec-hd";
300
+ hd.innerHTML =
301
+ '<span class="qa-ec-ico">' + (QA_V_ICONS.alert || "") + '</span>'
302
+ + '<span class="qa-ec-tt">调用失败</span>'
303
+ + qaStatusBadgeHtml("failed");
304
+ wrap.appendChild(hd);
305
+
306
+ const bd = document.createElement("div");
307
+ bd.className = "qa-ec-bd";
308
+ const rp = document.createElement("p");
309
+ rp.className = "qa-ec-reason";
310
+ rp.textContent = reason;
311
+ bd.appendChild(rp);
312
+ if(t){
313
+ const mt = document.createElement("div");
314
+ mt.className = "qa-ec-meta";
315
+ mt.textContent = "总耗时 " + t;
316
+ bd.appendChild(mt);
317
+ }
318
+ /* 完整错误链:复用 `.qa-mk` 词汇(行 + 明细),点击开合走委托,快照安全 */
319
+ if(chain != null && String(chain).trim()){
320
+ const mk = document.createElement("div");
321
+ mk.className = "qa-mk";
322
+ const row = document.createElement("div");
323
+ row.className = "qa-mk-row";
324
+ row.dataset.tog = "1";
325
+ row.innerHTML = '<span class="qa-mk-chev">' + (QA_V_ICONS.chev || "") + '</span>'
326
+ + '<span class="qa-mk-tt">错误详情</span>';
327
+ mk.appendChild(row);
328
+ const mkbd = document.createElement("div");
329
+ mkbd.className = "qa-mk-bd";
330
+ const isStr = typeof chain === "string";
331
+ mkbd.appendChild(qaCodeFrameNode("error", isStr ? String(chain) : JSON.stringify(chain, null, 2)));
332
+ mk.appendChild(mkbd);
333
+ bd.appendChild(mk);
334
+ }
335
+ wrap.appendChild(bd);
336
+ return wrap;
337
+ }
338
+
258
339
  /* ---------- ④ 分隔条(anywhere Marker variant="separator")----------
259
340
  用在「横切整条会话」的事件上 —— anywhere 那边是**会话压缩**(`MarkerContentKind="compact"`,
260
341
  进行中/已完成两种文案)。分隔条不是谁说的话,形状本身就在说「这里是一条分界」。
@@ -465,12 +465,12 @@
465
465
  气泡照常可读(用户要能确认自己写的是什么),只把底色压淡 + 虚线边 + 底下补一行
466
466
  「排队中 · 上一轮结束后自动发送」—— **不隐藏、不挪走**,否则就是这次报障的
467
467
  「追问没在对话区显示」。虚线边框同时说明它不是「已送达」那种实心状态。 */
468
- .qa-msg.qa-mine.is-queued .qa-bd{border-style:dashed;border-color:var(--queued,#8B5CF6);
468
+ .qa-msg.qa-mine.is-queued .qa-bd{border-style:dashed;border-color:var(--queued);
469
469
  background:linear-gradient(180deg,#FAF7FF,#F3EEFF)}
470
470
  .qa-msg.qa-mine.is-queued .qa-bd::after{content:"排队中 · 上一轮结束后自动发送";
471
- display:block;margin-top:6px;font-size:11.5px;line-height:15px;color:var(--queued,#8B5CF6)}
471
+ display:block;margin-top:6px;font-size:11.5px;line-height:15px;color:var(--queued)}
472
472
  [data-theme="dark"] .qa-msg.qa-mine.is-queued .qa-bd{background:#241E33;border-color:#7C5CE0}
473
- [data-theme="dark"] .qa-msg.qa-mine.is-queued .qa-bd::after{color:#A98CF5}
473
+ [data-theme="dark"] .qa-msg.qa-mine.is-queued .qa-bd::after{color:var(--queued)}
474
474
  /* 排队期间被取消(`is-cancelled`):气泡留着当回执,但明确说它不会被执行。
475
475
  用灰色虚线而不是红色 —— 用户自己撤的,不是失败,别用报错的颜色吓人。 */
476
476
  .qa-msg.qa-mine.is-cancelled .qa-bd{border-style:dashed;border-color:var(--line);
@@ -549,6 +549,33 @@
549
549
 
550
550
  .qa-art-video video{display:block;max-width:280px;max-height:190px;background:#000}
551
551
 
552
+ /* HTML 产物卡:iframe 就地预览(2026-10-24)—— 固定尺寸与视频同一档,可滚动看整页。
553
+ 缩略 iframe 关掉指针事件(点击不冒泡出 iframe,关了才能「点卡片看大图」),光标用放大镜。 */
554
+ .qa-art-html{cursor:zoom-in}
555
+
556
+ .qa-art-html iframe{display:block;width:280px;height:190px;border:0;background:#fff;pointer-events:none}
557
+
558
+ .qa-arts-html{display:grid;grid-template-columns:repeat(auto-fill,280px);gap:8px}
559
+
560
+ .qa-arts-html .qa-art-html{width:280px}
561
+
562
+ .qa-arts-html .qa-art-cap{max-width:none}
563
+
564
+ /* HTML 产物全屏预览层(点击卡片弹出,内核自画,见 19-attachments 的 __qaArtHtmlPeek):
565
+ 深蒙层 + 顶条(文件名 / 关闭)+ 撑满余下的 iframe。 */
566
+ .qa-art-html-peek{position:fixed;inset:0;z-index:9999;display:flex;flex-direction:column;
567
+ gap:8px;padding:20px;background:rgba(28,25,23,.62)}
568
+
569
+ .qa-art-html-peek-bar{display:flex;align-items:center;gap:10px}
570
+
571
+ .qa-art-html-peek-t{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
572
+ font-family:var(--mono);font-size:12px;color:#F5F4F3}
573
+
574
+ .qa-art-html-peek-x{flex:0 0 auto;padding:3px 12px;border-radius:8px;border:1px solid rgba(245,244,243,.35);
575
+ background:transparent;color:#F5F4F3;font-size:12px;cursor:pointer}
576
+
577
+ .qa-art-html-peek iframe{flex:1 1 auto;width:100%;border:0;border-radius:10px;background:#fff}
578
+
552
579
  .qa-art audio{display:block;width:260px;margin:8px}
553
580
 
554
581
  .qa-art-cap{display:flex;align-items:center;gap:7px;padding:6px 9px;max-width:280px;
@@ -864,7 +891,7 @@
864
891
  stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round}
865
892
 
866
893
  .qa-ask-tag{font-family:var(--mono);font-size:9px;letter-spacing:.06em;padding:1.5px 7px;
867
- border-radius:99px;border:1px solid rgba(79,70,229,.28);color:var(--indigo);background:var(--halo-indigo)}
894
+ border-radius:99px;border:1px solid color-mix(in srgb, var(--indigo) 28%, transparent);color:var(--indigo);background:var(--halo-indigo)}
868
895
 
869
896
  .qa-ask-ttl{font-family:var(--mono);font-size:9px;letter-spacing:.1em;color:var(--paper-mute);margin-left:auto}
870
897
 
@@ -883,7 +910,7 @@
883
910
  padding:7px 9px;border:1px solid var(--line);border-radius:8px;background:#fff;font:inherit;
884
911
  cursor:pointer;transition:border-color .18s,background .18s}
885
912
 
886
- .qa-ask-opt:hover{border-color:rgba(79,70,229,.45);background:var(--halo-indigo)}
913
+ .qa-ask-opt:hover{border-color:color-mix(in srgb, var(--indigo) 45%, transparent);background:var(--halo-indigo)}
887
914
 
888
915
  .qa-ask-opt .mk{width:15px;height:15px;flex:none;margin-top:1px;border-radius:5px;
889
916
  border:1.5px solid var(--line);display:grid;place-items:center;color:var(--paper-mute)}
@@ -910,18 +937,18 @@
910
937
  .qa-ask-other input{flex:1;min-width:0;padding:6px 9px;border:1px solid var(--line);
911
938
  border-radius:8px;background:#fff;font-family:var(--serif);font-size:12px;color:var(--paper)}
912
939
 
913
- .qa-ask-other input:focus{outline:none;border-color:rgba(79,70,229,.5)}
940
+ .qa-ask-other input:focus{outline:none;border-color:color-mix(in srgb, var(--indigo) 50%, transparent)}
914
941
 
915
942
  .qa-ask-other button{flex:none;padding:6px 12px;border:1px solid var(--line);border-radius:8px;
916
943
  background:#fff;font-family:var(--serif);font-size:12px;color:var(--paper-dim);cursor:pointer}
917
944
 
918
- .qa-ask-other button:hover{border-color:rgba(79,70,229,.45);color:var(--indigo)}
945
+ .qa-ask-other button:hover{border-color:color-mix(in srgb, var(--indigo) 45%, transparent);color:var(--indigo)}
919
946
 
920
947
  .qa-ask-ft{display:flex;align-items:center;gap:8px;margin-top:8px}
921
948
 
922
949
  .qa-ask-ft[hidden],.qa-ask-ans[hidden]{display:none}
923
950
 
924
- .qa-ask-sub{flex:none;padding:5px 14px;border:1px solid rgba(79,70,229,.4);border-radius:99px;
951
+ .qa-ask-sub{flex:none;padding:5px 14px;border:1px solid color-mix(in srgb, var(--indigo) 40%, transparent);border-radius:99px;
925
952
  background:var(--halo-indigo);font-family:var(--serif);font-size:12px;color:var(--indigo);cursor:pointer}
926
953
 
927
954
  .qa-ask-sub:disabled{opacity:.4;cursor:default;border-color:var(--line);background:transparent;
@@ -988,6 +1015,39 @@
988
1015
 
989
1016
  .qa-done.err{color:var(--cinnabar-hi)}
990
1017
 
1018
+ /* 错误显示卡(终态失败):朱砂描边 + 淡红底的卡片面(见 29-msg-variants 的 qaErrorCardNode)。
1019
+ ⚠️ 根节点带 `.qa-done` 类(收尾行契约要数它),这里把它从「等宽灰字行」翻成「块级卡片」:
1020
+ display:block 盖掉 `.qa-done` 的 flex 行布局;颜色交给子元素,根只留卡片面。 */
1021
+ .qa-errcard{display:block;margin:8px 0 4px;padding:10px 12px;
1022
+ border:1px solid rgba(214,64,42,.38);border-radius:var(--r-md);background:rgba(214,64,42,.06)}
1023
+
1024
+ .qa-errcard + .qa-errcard{margin-top:0}
1025
+
1026
+ .qa-ec-hd{display:flex;align-items:center;gap:8px}
1027
+
1028
+ .qa-ec-ico{width:16px;height:16px;flex:none;display:grid;place-items:center;color:var(--cinnabar-hi)}
1029
+
1030
+ .qa-ec-ico svg{width:15px;height:15px;display:block;stroke:currentColor;fill:none;
1031
+ stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
1032
+
1033
+ .qa-ec-tt{flex:1;min-width:0;font-family:var(--font);font-size:13px;font-weight:600;color:var(--cinnabar-hi)}
1034
+
1035
+ .qa-ec-bd{margin-top:8px}
1036
+
1037
+ .qa-ec-reason{margin:0;font-size:12.5px;line-height:1.6;color:var(--paper-dim);
1038
+ white-space:pre-wrap;word-break:break-word}
1039
+
1040
+ .qa-ec-meta{margin-top:6px;font-family:var(--mono);font-size:10px;letter-spacing:.04em;color:var(--paper-mute)}
1041
+
1042
+ /* 卡片里的可展开详情(复用 .qa-mk 词汇);把它的左缩进让出来,贴着卡片内边距更顺 */
1043
+ .qa-errcard .qa-mk{margin:8px 0 0}
1044
+
1045
+ .qa-errcard .qa-mk-bd{padding-left:2px}
1046
+
1047
+ [data-theme="dark"] .qa-errcard{background:rgba(248,113,113,.10);border-color:rgba(248,113,113,.42)}
1048
+
1049
+ [data-theme="dark"] .qa-ec-reason{color:#E7E5E4}
1050
+
991
1051
  .qa-reqacts{display:flex;flex-wrap:wrap;gap:8px;padding:10px 2px 2px;justify-content:flex-start}
992
1052
 
993
1053
  .qa-reqacts button{font-family:var(--mono);font-size:11px;letter-spacing:.04em;padding:5px 12px;
@@ -1104,7 +1164,7 @@
1104
1164
 
1105
1165
  [data-theme="dark"] .qa-ask-hd{color:#E5E5E5}
1106
1166
 
1107
- [data-theme="dark"] .qa-ask-tag{color:#818CF8;border-color:rgba(129,140,248,.4);background:rgba(129,140,248,.1)}
1167
+ [data-theme="dark"] .qa-ask-tag{color:var(--indigo);border-color:color-mix(in srgb, var(--indigo) 40%, transparent);background:color-mix(in srgb, var(--indigo) 10%, transparent)}
1108
1168
 
1109
1169
  [data-theme="dark"] .qa-ask-ttl{color:#525252}
1110
1170
 
@@ -1114,7 +1174,7 @@
1114
1174
 
1115
1175
  [data-theme="dark"] .qa-ask-opt{border-color:#3B3835;background:transparent}
1116
1176
 
1117
- [data-theme="dark"] .qa-ask-opt:hover{border-color:rgba(129,140,248,.45);background:rgba(129,140,248,.08)}
1177
+ [data-theme="dark"] .qa-ask-opt:hover{border-color:color-mix(in srgb, var(--indigo) 45%, transparent);background:color-mix(in srgb, var(--indigo) 8%, transparent)}
1118
1178
 
1119
1179
  [data-theme="dark"] .qa-ask-opt .lb{color:#E5E5E5}
1120
1180
 
@@ -1122,7 +1182,7 @@
1122
1182
 
1123
1183
  [data-theme="dark"] .qa-ask-opt .mk{border-color:#525252;background:transparent}
1124
1184
 
1125
- [data-theme="dark"] .qa-ask-opt.sel{border-color:#6366F1;background:rgba(99,102,241,.1)}
1185
+ [data-theme="dark"] .qa-ask-opt.sel{border-color:var(--indigo);background:color-mix(in srgb, var(--indigo) 10%, transparent)}
1126
1186
 
1127
1187
  [data-theme="dark"] .qa-ask-opt.sel .lb{color:#F5F4F3}
1128
1188
 
@@ -1130,9 +1190,9 @@
1130
1190
 
1131
1191
  [data-theme="dark"] .qa-ask-other button{background:#1C1C1E;border-color:#3B3835;color:#A8A29E}
1132
1192
 
1133
- [data-theme="dark"] .qa-ask-sub{background:rgba(99,102,241,.1);border-color:rgba(99,102,241,.4);color:#818CF8}
1193
+ [data-theme="dark"] .qa-ask-sub{background:color-mix(in srgb, var(--indigo) 10%, transparent);border-color:color-mix(in srgb, var(--indigo) 40%, transparent);color:var(--indigo)}
1134
1194
 
1135
- [data-theme="dark"] .qa-ask-sub:hover:not(:disabled){background:rgba(99,102,241,.18)}
1195
+ [data-theme="dark"] .qa-ask-sub:hover:not(:disabled){background:color-mix(in srgb, var(--indigo) 18%, transparent)}
1136
1196
 
1137
1197
  [data-theme="dark"] .qa-ask-hint{color:#525252}
1138
1198
 
@@ -1154,7 +1214,7 @@
1154
1214
 
1155
1215
  [data-theme="dark"] .qa-run::after{background:#1C1C1E}
1156
1216
 
1157
- [data-theme="dark"] .qa-spinner{border-color:#3B3835;border-top-color:#818CF8}
1217
+ [data-theme="dark"] .qa-spinner{border-color:#3B3835;border-top-color:var(--indigo)}
1158
1218
 
1159
1219
  [data-theme="dark"] .qa-run-pill{background:rgba(245,244,243,.05);border-color:#3B3835}
1160
1220
 
@@ -1515,7 +1575,7 @@
1515
1575
 
1516
1576
  .qa-diff-row.del .s{color:#9E3120}
1517
1577
 
1518
- .qa-diff-row.hunk{background:rgba(79,70,229,.07);color:#4338CA}
1578
+ .qa-diff-row.hunk{background:color-mix(in srgb, var(--indigo) 7%, transparent);color:var(--indigo)}
1519
1579
 
1520
1580
  .qa-diff-row.file{background:rgba(28,25,23,.035);color:var(--paper-mute)}
1521
1581
 
@@ -1596,7 +1656,7 @@
1596
1656
 
1597
1657
  [data-theme="dark"] .qa-diff-row.del .s{color:#FCA5A5}
1598
1658
 
1599
- [data-theme="dark"] .qa-diff-row.hunk{background:rgba(129,140,248,.12);color:#A5B4FC}
1659
+ [data-theme="dark"] .qa-diff-row.hunk{background:color-mix(in srgb, var(--indigo) 12%, transparent);color:var(--indigo)}
1600
1660
 
1601
1661
  [data-theme="dark"] .qa-diff-row.file{background:rgba(245,244,243,.045);color:#A8A29E}
1602
1662
 
@@ -1624,9 +1684,9 @@
1624
1684
  /* 排队气泡上的「取消排队」:这里要的是**文字**按钮(用户得看懂点它会发生什么),
1625
1685
  所以不复用上面 22×22 的图标尺寸,单开一档。 */
1626
1686
  .qa-msg.qa-mine .qa-acts .qa-act--cancel{width:auto;height:22px;padding:0 8px;border-radius:var(--r-full);
1627
- font-size:11.5px;line-height:22px;color:var(--queued,#8B5CF6);white-space:nowrap}
1628
- .qa-msg.qa-mine .qa-acts .qa-act--cancel:hover{background:rgba(139,92,246,.12);color:var(--queued,#8B5CF6)}
1629
- [data-theme="dark"] .qa-msg.qa-mine .qa-acts .qa-act--cancel{color:#A98CF5}
1687
+ font-size:11.5px;line-height:22px;color:var(--queued);white-space:nowrap}
1688
+ .qa-msg.qa-mine .qa-acts .qa-act--cancel:hover{background:color-mix(in srgb, var(--queued) 12%, transparent);color:var(--queued)}
1689
+ [data-theme="dark"] .qa-msg.qa-mine .qa-acts .qa-act--cancel{color:var(--queued)}
1630
1690
 
1631
1691
  .qa-act svg{width:13px;height:13px;display:block;stroke:currentColor;fill:none;
1632
1692
  stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round}
@@ -130,6 +130,7 @@ setWho setRunLabel setResult // 角色标识 / 轮次标
130
130
  | `command`(斜杠命令生命周期) | 以前走 default | **已补**:`case "command"` → 标记行(图标 terminal + 进行中/已完成/失败 + 可展开原始载荷) |
131
131
  | `control_error` | 以前走 default(话说反了) | **已补**:`case "control_error"` → 「控制失败 · …」失败标记行 |
132
132
  | `notice` | 已有 `case "notice"` ✓ | 无需动 |
133
+ | `fallback`(主备降级,2026-10-26) | 以前没有这条 kind(core 也不发) | **已补**:`case "fallback"` → 标记行「已由备用引擎「…」接上」(icon sep + 可展开的 from/to 载荷) |
133
134
  | `answered` / `answer_queued` | 在内核的**刻意忽略**清单里 ✓(画面由收尾那条表达) | 无需动 |
134
135
  | `user`(历史里用户那条) | 不在内核:宿主在回放时调 `qaRenderMine`(magic-test 就是这么做的) | 归宿主 |
135
136
  | `interrupted` / `stopped` | 不在内核:收尾那条 `onDone({stopped:true})` → `qaRenderDone(…, "已停止")` | 归宿主 |
@@ -484,15 +484,21 @@
484
484
  .pick__cols{display:flex;gap:0;height:min(56vh,56dvh,440px);border:1px solid var(--border);
485
485
  border-radius:var(--r);overflow:hidden}
486
486
 
487
- .pick__col{flex:1;min-width:0;overflow-y:auto;-webkit-overflow-scrolling:touch;padding:4px}
487
+ /* 引擎列窄、模型列宽:引擎名短(codebuddy-ai 已是极值),模型名长得多
488
+ (deepseek-v4.1-flash-1017 + 倍率),均分会让右列全被省略号截掉
489
+ (2026-10-24 用户:「减少引擎宽度增加模型宽度 让模型显示全」)。 */
490
+ .pick__col{flex:0 0 36%;min-width:0;overflow-y:auto;-webkit-overflow-scrolling:touch;padding:4px}
488
491
 
489
- .pick__col--r{flex:1.5;border-left:1px solid var(--border)}
492
+ .pick__col--r{flex:1 1 auto;border-left:1px solid var(--border)}
490
493
 
491
494
  .pick__citem{display:flex;flex-direction:column;gap:1px;width:100%;padding:9px 10px;
492
495
  border-radius:var(--r);text-align:left}
493
496
 
494
497
  .pick__citem.is-on{background:var(--activity)}
495
498
 
499
+ /* 历史会话锁:引擎项置灰不可点(模型列不受影响) */
500
+ .pick__citem[disabled]{opacity:.55;cursor:not-allowed}
501
+
496
502
  .pick__cl{font-size:14px;line-height:20px;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
497
503
 
498
504
  .pick__citem.is-on .pick__cl{font-weight:600}
@@ -509,6 +515,11 @@
509
515
  font-variant-numeric:tabular-nums;white-space:nowrap}
510
516
 
511
517
  .pick__cd{font-size:11px;line-height:15px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
518
+ /* 账号剩余积分(codebuddy 系,--engines 的 credit_left):单独一行、用强调色,
519
+ 与「N 个模型」区分开 —— 它会随对话消耗变化,是选择引擎时的第二个判据。 */
520
+ .pick__cd--credit{color:var(--accent)}
521
+ /* 「领取每日积分」:不是导航项,点它只领积分(阻止冒泡到引擎按钮)。 */
522
+ .pick__claim{cursor:pointer;text-decoration:underline;text-underline-offset:2px}
512
523
 
513
524
  @keyframes pick-fade{from{opacity:0}to{opacity:1}}
514
525