magic-agent 1.0.49 → 1.0.109

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 (94) hide show
  1. package/CLIENT-CONTRACT.md +514 -7
  2. package/README.md +31 -2
  3. package/client-ui/pc/app.css +3 -41
  4. package/client-ui/pc/js/04-task-list.js +6 -2
  5. package/client-ui/pc/js/90-glue.js +23 -4
  6. package/client-ui/pc/js/92-composer.js +11 -4
  7. package/client-ui/pc/js/93-rail.js +178 -19
  8. package/client-ui/shared/conv/03-chat-message.js +145 -25
  9. package/client-ui/shared/conv/09-stream-apply.js +55 -6
  10. package/client-ui/shared/conv/19-attachments.js +70 -6
  11. package/client-ui/shared/conv/27-conv-ledger.js +18 -0
  12. package/client-ui/shared/conv/29-msg-variants.js +10 -0
  13. package/client-ui/shared/conv/README.md +6 -0
  14. package/client-ui/shared/conv/conv.css +77 -0
  15. package/client-ui/shared/conversation.css +229 -43
  16. package/client-ui/shared/conversation.js +1316 -131
  17. package/client-ui/shared/transport-http.js +164 -12
  18. package/client-ui-mobile/index.html +2443 -178
  19. package/internal/agent/capability.go +16 -2
  20. package/internal/agent/claude.go +3 -2
  21. package/internal/agent/codebuddy.go +7 -1
  22. package/internal/agent/codex_test.go +3 -0
  23. package/internal/agent/custom_models_test.go +5 -0
  24. package/internal/agent/dsh.go +18 -2
  25. package/internal/agent/dsh_test.go +53 -2
  26. package/internal/agent/engine_test.go +34 -19
  27. package/internal/agent/env_test.go +14 -9
  28. package/internal/agent/models_test.go +9 -0
  29. package/internal/agent/runcmd_test.go +24 -3
  30. package/internal/agent/stream.go +33 -0
  31. package/internal/cli/ask.go +94 -4
  32. package/internal/cli/contract_test.go +26 -0
  33. package/internal/cli/events.go +18 -7
  34. package/internal/cli/keepalive_test.go +69 -0
  35. package/internal/cli/requirements.go +119 -0
  36. package/internal/cli/requirements_test.go +36 -0
  37. package/internal/cli/root.go +56 -4
  38. package/internal/cli/session.go +64 -25
  39. package/internal/cli/sessionlog.go +76 -7
  40. package/internal/cli/sessionlog_test.go +58 -0
  41. package/internal/cli/team.go +374 -0
  42. package/internal/cli/team_session.go +156 -0
  43. package/internal/cli/team_test.go +360 -0
  44. package/internal/client/plugins.go +7 -4
  45. package/internal/client/requirements.go +309 -0
  46. package/internal/client/requirements_test.go +239 -0
  47. package/internal/client/service.go +545 -19
  48. package/internal/client/service_test.go +47 -2
  49. package/internal/client/session_events_test.go +320 -0
  50. package/internal/client/team_flags.go +204 -0
  51. package/internal/client/team_flags_test.go +254 -0
  52. package/internal/requirements/bootstrap.go +423 -0
  53. package/internal/requirements/consolidate.go +908 -0
  54. package/internal/requirements/doc.go +473 -0
  55. package/internal/requirements/memory.go +324 -0
  56. package/internal/requirements/requirements.go +169 -0
  57. package/internal/requirements/requirements_test.go +530 -0
  58. package/internal/requirements/store.go +289 -0
  59. package/internal/session/delete.go +8 -4
  60. package/internal/session/session.go +194 -17
  61. package/internal/session/session_append.go +4 -1
  62. package/internal/session/session_append_test.go +19 -0
  63. package/internal/session/session_mark_test.go +42 -3
  64. package/internal/session/session_test.go +43 -0
  65. package/internal/session/sessionlog.go +50 -34
  66. package/internal/session/sessionlog_read_test.go +69 -11
  67. package/internal/session/status.go +205 -0
  68. package/internal/session/status_test.go +144 -0
  69. package/internal/team/adaptive.go +293 -0
  70. package/internal/team/board.go +216 -0
  71. package/internal/team/event.go +28 -0
  72. package/internal/team/execute.go +648 -0
  73. package/internal/team/plan.go +903 -0
  74. package/internal/team/planner.go +294 -0
  75. package/internal/team/reduce.go +192 -0
  76. package/internal/team/role.go +334 -0
  77. package/internal/team/route.go +206 -0
  78. package/internal/team/schedule.go +241 -0
  79. package/internal/team/schedule_test.go +286 -0
  80. package/internal/team/subsession.go +27 -0
  81. package/internal/team/team.go +401 -0
  82. package/internal/team/team_test.go +1997 -0
  83. package/npm/bin/magic-client.js +0 -0
  84. package/npm/dist/darwin-arm64/magic-agent +0 -0
  85. package/npm/dist/darwin-arm64/magic-client +0 -0
  86. package/npm/dist/darwin-x64/magic-agent +0 -0
  87. package/npm/dist/darwin-x64/magic-client +0 -0
  88. package/npm/dist/linux-arm64/magic-agent +0 -0
  89. package/npm/dist/linux-arm64/magic-client +0 -0
  90. package/npm/dist/linux-x64/magic-agent +0 -0
  91. package/npm/dist/linux-x64/magic-client +0 -0
  92. package/npm/dist/win32-x64/magic-agent.exe +0 -0
  93. package/npm/dist/win32-x64/magic-client.exe +0 -0
  94. package/package.json +1 -1
@@ -446,6 +446,22 @@
446
446
  .qa-msg.qa-mine .qa-bd{padding:10px 14px;
447
447
  border:1px solid var(--line);border-radius:var(--qa-bubble-r,14px 5px 14px 14px);
448
448
  background:linear-gradient(180deg,#FBF9F7,#F5F1ED)}
449
+ /* 排队中的用户气泡(`is-queued`):已经发出去、但还在等前一轮跑完。
450
+ 气泡照常可读(用户要能确认自己写的是什么),只把底色压淡 + 虚线边 + 底下补一行
451
+ 「排队中 · 上一轮结束后自动发送」—— **不隐藏、不挪走**,否则就是这次报障的
452
+ 「追问没在对话区显示」。虚线边框同时说明它不是「已送达」那种实心状态。 */
453
+ .qa-msg.qa-mine.is-queued .qa-bd{border-style:dashed;border-color:var(--queued,#8B5CF6);
454
+ background:linear-gradient(180deg,#FAF7FF,#F3EEFF)}
455
+ .qa-msg.qa-mine.is-queued .qa-bd::after{content:"排队中 · 上一轮结束后自动发送";
456
+ display:block;margin-top:6px;font-size:11.5px;line-height:15px;color:var(--queued,#8B5CF6)}
457
+ [data-theme="dark"] .qa-msg.qa-mine.is-queued .qa-bd{background:#241E33;border-color:#7C5CE0}
458
+ [data-theme="dark"] .qa-msg.qa-mine.is-queued .qa-bd::after{color:#A98CF5}
459
+ /* 排队期间被取消(`is-cancelled`):气泡留着当回执,但明确说它不会被执行。
460
+ 用灰色虚线而不是红色 —— 用户自己撤的,不是失败,别用报错的颜色吓人。 */
461
+ .qa-msg.qa-mine.is-cancelled .qa-bd{border-style:dashed;border-color:var(--line);
462
+ background:none;opacity:.72}
463
+ .qa-msg.qa-mine.is-cancelled .qa-bd::after{content:"已取消排队 · 这一句没有发送";
464
+ display:block;margin-top:6px;font-size:11.5px;line-height:15px;color:var(--muted)}
449
465
 
450
466
  .qa-msg.qa-review .qa-bd{padding-top:5px}
451
467
 
@@ -795,6 +811,12 @@
795
811
 
796
812
  .qa-agent-chip.ok{color:var(--jade);border-color:rgba(90,143,104,.4);background:rgba(90,143,104,.07)}
797
813
 
814
+ /* 子会话徽标(2026-10-21,团队模式):子任务自己的引擎会话 id 绑定在主会话下面,
815
+ 与状态胶囊并排显示。虚线边框 + 淡字色 —— 它是「出处」,不是状态,不该抢状态的视觉。 */
816
+ .qa-agent-sess{font-family:var(--mono);font-size:9px;letter-spacing:.06em;padding:1.5px 7px;
817
+ border-radius:99px;border:1px dashed var(--line);color:var(--paper-mute);
818
+ background:transparent;white-space:nowrap;cursor:default}
819
+
798
820
  .qa-sub{margin:8px 0 2px;padding:2px 0 2px 10px;border-left:2px solid var(--line)}
799
821
 
800
822
  .qa-sub-h{font-family:var(--mono);font-size:9px;color:var(--paper-mute);
@@ -1131,6 +1153,8 @@
1131
1153
 
1132
1154
  [data-theme="dark"] .qa-agent-chip.ok{color:#34D399;background:rgba(52,211,153,.1);border-color:rgba(52,211,153,.35)}
1133
1155
 
1156
+ [data-theme="dark"] .qa-agent-sess{color:#71717A;border-color:#3B3835}
1157
+
1134
1158
  [data-theme="dark"] .qa-sub-h{color:#525252}
1135
1159
 
1136
1160
  [data-theme="dark"] .qa-sub-n{color:#525252}
@@ -1576,6 +1600,12 @@
1576
1600
  transition:background .15s,color .15s}
1577
1601
 
1578
1602
  .qa-act:hover{background:rgba(28,25,23,.07);color:var(--paper)}
1603
+ /* 排队气泡上的「取消排队」:这里要的是**文字**按钮(用户得看懂点它会发生什么),
1604
+ 所以不复用上面 22×22 的图标尺寸,单开一档。 */
1605
+ .qa-msg.qa-mine .qa-acts .qa-act--cancel{width:auto;height:22px;padding:0 8px;border-radius:var(--r-full);
1606
+ font-size:11.5px;line-height:22px;color:var(--queued,#8B5CF6);white-space:nowrap}
1607
+ .qa-msg.qa-mine .qa-acts .qa-act--cancel:hover{background:rgba(139,92,246,.12);color:var(--queued,#8B5CF6)}
1608
+ [data-theme="dark"] .qa-msg.qa-mine .qa-acts .qa-act--cancel{color:#A98CF5}
1579
1609
 
1580
1610
  .qa-act svg{width:13px;height:13px;display:block;stroke:currentColor;fill:none;
1581
1611
  stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round}
@@ -1588,6 +1618,53 @@
1588
1618
 
1589
1619
  [data-theme="dark"] .qa-act:hover{background:rgba(245,244,243,.09);color:#F5F4F3}
1590
1620
 
1621
+ /* ---- 任务列表(qaTodoNode / qaTodoFill,见 29-msg-variants)----
1622
+ 一族两形:工具卡明细里的「待办清单」与一条应答里唯一那块「任务列表」面板。
1623
+ ⚠️ 这一族原来只长在 client-ui/pc/app.css(抽取时漏了),移动端没有皮肤 →
1624
+ SVG 无尺寸约束,图标撑成 457×457(整块卡变成一个黑色巨圆)。
1625
+ 现在收进共用皮肤:三端同一份,PC 侧的重复副本已删。 */
1626
+ .qa-todo{min-width:0}
1627
+ .qa-todo-hd{display:flex;align-items:center;height:26px;padding:0 8px;
1628
+ border-bottom:1px solid var(--line);background:var(--subtle,rgba(28,25,23,.028))}
1629
+ .qa-todo-sum{font-family:var(--mono);font-size:10px;color:var(--paper-mute);
1630
+ white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
1631
+ .qa-todo-list{padding:5px 8px 7px}
1632
+ .qa-todo-row{display:flex;align-items:baseline;gap:7px;padding:2.5px 0;
1633
+ font-size:11.5px;line-height:1.5;color:var(--paper-dim)}
1634
+ .qa-todo-ico{width:12px;height:12px;flex:none;align-self:center;border-radius:50%;
1635
+ border:1.3px solid var(--line);display:grid;place-items:center;color:#FFF}
1636
+ .qa-todo-ico svg{width:8px;height:8px;display:block;stroke:currentColor;fill:none;
1637
+ stroke-width:3;stroke-linecap:round;stroke-linejoin:round}
1638
+ .qa-todo-row.is-in_progress{color:var(--paper);font-weight:600}
1639
+ .qa-todo-row.is-in_progress .qa-todo-ico{border-color:var(--amber);color:var(--amber)}
1640
+ .qa-todo-row.is-completed{color:var(--paper-mute)}
1641
+ .qa-todo-row.is-completed .qa-todo-ico{border-color:var(--jade);background:var(--jade)}
1642
+ .qa-todo-row.is-completed .qa-todo-t{text-decoration:line-through}
1643
+ .qa-todo-row.is-failed{color:var(--cinnabar-hi)}
1644
+ .qa-todo-row.is-failed .qa-todo-ico{border-color:var(--cinnabar-hi);color:var(--cinnabar-hi)}
1645
+ .qa-todo-row.is-skipped{color:var(--paper-mute)}
1646
+ .qa-todo-row.is-skipped .qa-todo-ico{border-style:dashed}
1647
+ .qa-todo-row.is-skipped .qa-todo-t{text-decoration:line-through}
1648
+ .qa-todo-t{min-width:0}
1649
+ .qa-todo-meta{margin-left:auto;flex:none;max-width:46%;font-family:var(--mono);font-size:10px;
1650
+ color:var(--paper-mute);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
1651
+ .qa-todo-panel{min-width:0;margin:0 0 8px;border:1px solid var(--line);
1652
+ border-radius:var(--r-md);overflow:hidden;background:var(--panel,#FFF)}
1653
+ .qa-todo-panel .qa-todo-hd{gap:7px}
1654
+ .qa-todo-tt{flex:none;font-size:11.5px;font-weight:600;color:var(--paper)}
1655
+ .qa-todo-panel .qa-todo-sum{margin-left:auto}
1656
+ .qa-todo-toggle{flex:none;display:grid;place-items:center;width:16px;height:16px;
1657
+ border-radius:4px;color:var(--paper-mute);cursor:pointer}
1658
+ .qa-todo-toggle:hover{background:var(--subtle,rgba(28,25,23,.05))}
1659
+ .qa-todo-toggle svg{width:11px;height:11px;fill:none;stroke:currentColor;stroke-width:2.4;
1660
+ stroke-linecap:round;stroke-linejoin:round;transition:transform .15s}
1661
+ .qa-todo-panel.open .qa-todo-toggle svg{transform:rotate(180deg)}
1662
+ .qa-todo-panel:not(.open) .qa-todo-list{display:none}
1663
+ [data-theme="dark"] .qa-todo-hd{background:rgba(245,244,243,.045);border-color:#3B3835}
1664
+ [data-theme="dark"] .qa-todo-ico{border-color:#57534E}
1665
+ [data-theme="dark"] .qa-todo-row.is-in_progress{color:#F5F4F3}
1666
+ [data-theme="dark"] .qa-todo-row.is-completed{color:#A8A29E}
1667
+
1591
1668
  /* ============================================================================
1592
1669
  * 令牌桥:magic-test 的调色板名字 → 各端宿主已有的令牌(**只用不定义**)
1593
1670
  *
@@ -23,9 +23,21 @@
23
23
  * 尺寸:--r-card --r --r-full --font --mono
24
24
  *
25
25
  * ── 可选令牌(不给就用下面的兜底值)──
26
- * `--q-bottle*` 是掌天瓶绿液的配色(瓶身描边 / 液面亮色 / 液色 / 深液 / 瓶塞 / 光晕)。
27
- * 兜底那套绿是**自带的**,宿主不定义也能画;想跟品牌色对齐的宿主(PC 的 --accent 是墨绿系)
28
- * 在自己页里覆盖这 6 个名字即可。
26
+ * `--q-bottle*` 是掌天瓶的配色(瓶是**玉**不是玻璃,见 conversation.js 的形制说明),
27
+ * 分玉胎与灵液两组,共 10 个名字:
28
+ * 玉胎(整只瓶的底色,**空处也是绿的**——薄玉,不是透明)
29
+ * --q-bottle-jade-hi / --q-bottle-jade / --q-bottle-jade-lo 上薄下厚
30
+ * ⚠️ 这三个是 rgba,**浅底上必须更浓**才不发白成瓷:兜底按深底给,
31
+ * 浅色主题的宿主请覆盖(移动端在 prefers-color-scheme:light 与
32
+ * html[data-theme=light] 里各给了一份 —— 两处都要给,只给一处会在另一条路径上发白)。
33
+ * 灵液(液位以下的浓翠)
34
+ * --q-bottle-hi / --q-bottle / --q-bottle-lo / --q-bottle-deep
35
+ * 结构与高光
36
+ * --q-bottle-line 瓶身轮廓(比玉胎深一档的**绿**,不是灰)
37
+ * --q-bottle-rim 边光 / 液面亮线 / 气泡(必须比 hi 更白,否则整只瓶「发光过度」)
38
+ * --q-bottle-cork 木塞
39
+ * --q-bottle-glow 瓶后那团绿雾(loading 与启动闪屏共用)
40
+ * 兜底那份自带,宿主不定义也能画;想跟品牌色对齐的宿主在自己页里覆盖即可。
29
41
  */
30
42
  /* ⚠️ min-width:0 两处都不能省(2026-10-17):本壳是 flex 链上的中间层,
31
43
  默认的 `min-width:auto` 会让「内容里最宽的那一小块」决定整条链的宽度 ——
@@ -154,6 +166,11 @@
154
166
 
155
167
  .composer__box:focus-within{border-color:var(--accent)}
156
168
 
169
+ /* 拖入图片的落点反馈(粘贴 / 拖拽共用一条上传管线,见 conversation.js 的 clipImageFiles):
170
+ 只改边框与底色,不动布局 —— 拖拽过程中不能因为加一个内边距把输入框顶得跳一下。 */
171
+ .composer__box.is-drop{border-color:var(--accent);background:var(--subtle);
172
+ outline:2px solid var(--accent);outline-offset:-2px}
173
+
157
174
  .composer textarea{display:block;width:100%;border:0;outline:0;resize:none;background:none;
158
175
  color:var(--ink);font:16px/24px var(--font);min-height:24px;max-height:200px;padding:2px}
159
176
 
@@ -174,15 +191,49 @@
174
191
 
175
192
  .attachbar{display:flex;flex-wrap:wrap;gap:6px;padding:2px 2px 8px;min-width:0}
176
193
 
194
+ /* 插件胶囊(2026-10-21):**这一轮用哪个 CLI** —— 独立一条,不混进附件条
195
+ (见 conversation.js 的 .plugbar 注释)。用主色描边,与附件的灰底 chip 区分开:
196
+ 一个是「带上这份材料」,一个是「换一种干法」,不该长得一样。 */
197
+ .plugbar{display:flex;flex-wrap:wrap;gap:6px;padding:2px 2px 6px;min-width:0}
198
+
199
+ .plugchip{display:inline-flex;align-items:center;gap:6px;max-width:100%;
200
+ padding:5px 6px 5px 10px;border-radius:var(--r-full);
201
+ background:var(--activity);border:1px solid var(--primary-action);color:var(--ink);font-size:12px}
202
+
203
+ .plugchip__n{max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:600}
204
+
205
+ .plugchip__x{color:var(--muted);font-size:12px;padding:0 2px}
206
+
177
207
  .attachchip{display:inline-flex;align-items:center;gap:6px;max-width:100%;padding:4px 6px 4px 10px;border-radius:var(--r-full);background:var(--subtle);color:var(--ink);font-size:12px}
178
208
 
179
209
  .attachchip__n{max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
180
210
 
181
211
  .attachchip__x{color:var(--muted);font-size:12px;padding:0 2px}
182
212
 
183
- .attachthumb{position:relative;flex:0 0 auto;width:56px;height:56px;border-radius:var(--r);overflow:hidden;background:var(--subtle)}
213
+ /* 排队追问(2026-10-21:会话运行中允许先提交追问,类似 magic-test PC 端):
214
+ 运行中提交的那几条先停在这里,等当前轮收尾后自动发出 —— 不打断、也不画成
215
+ 用户气泡(还没跑,画上去会让人以为已经发出去了)。 */
216
+ .queuebar{display:flex;flex-direction:column;gap:6px;padding:2px 2px 6px;min-width:0}
217
+
218
+ .queuebar__cap{font-size:11.5px;line-height:15px;color:var(--muted);padding-left:2px}
219
+
220
+ .queuechip{display:flex;align-items:center;gap:6px;min-width:0;padding:5px 6px 5px 10px;
221
+ border-radius:var(--r-full);background:var(--subtle);color:var(--ink);font-size:12px}
222
+
223
+ .queuechip__ico{flex:0 0 auto;color:var(--muted);display:flex}
224
+
225
+ .queuechip__t{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
226
+
227
+ .queuechip__x{flex:0 0 auto;color:var(--muted);font-size:12px;padding:0 2px}
184
228
 
185
- .attachthumb__img{display:block;width:100%;height:100%;object-fit:cover}
229
+ /* 附件缩略图:**按原图比例**给框(尺寸在图片加载后由 fitThumb 写死,见 conversation.js),
230
+ 不再一律 56×56 硬裁 —— 粘贴的截图多是 16:9,塞进正方形只剩中间一块,看着像挂了张破图。
231
+ 框最长 148、最高 96,短边不强制 56:比例与最小尺寸冲突时**比例优先**。
232
+ inset 描边让白底截图在输入框上也有边界。 */
233
+ .attachthumb{position:relative;flex:0 0 auto;width:56px;height:56px;max-width:100%;
234
+ border-radius:var(--r);overflow:hidden;background:var(--subtle);box-shadow:inset 0 0 0 1px var(--border)}
235
+
236
+ .attachthumb__img{display:block;width:100%;height:100%;object-fit:contain}
186
237
 
187
238
  .attachthumb__mask{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.45);color:#fff;font-size:11px}
188
239
 
@@ -202,6 +253,50 @@
202
253
 
203
254
  .sheetrow:active{background:var(--subtle)}
204
255
 
256
+ /* 插件抽屉(2026-10-21):加号 →「插件」→ 列表 / 详情。
257
+ 列表行沿用 .sheetrow 的节奏,但要多两行说明与一枚状态徽标 —— 所以单独一套。
258
+ 徽标两态用色块区分(不是只靠文字),扫一眼就能看出哪个还没装。 */
259
+ .plglist{display:flex;flex-direction:column}
260
+
261
+ .plgrow{display:flex;align-items:center;gap:10px;width:100%;padding:12px 9px;
262
+ border-radius:var(--r);text-align:left}
263
+
264
+ .plgrow:active{background:var(--subtle)}
265
+
266
+ .plgrow__n{flex:0 0 auto;font-size:14px;font-weight:600;color:var(--ink)}
267
+
268
+ .plgrow__s{flex:1;min-width:0;font-size:12px;color:var(--muted);
269
+ overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
270
+
271
+ .plgrow__b{flex:0 0 auto;padding:2px 8px;border-radius:var(--r-full);font-size:11px;
272
+ background:var(--subtle);color:var(--muted);white-space:nowrap}
273
+
274
+ .plgrow__b.is-on{background:var(--tint-done, var(--activity));color:var(--success)}
275
+
276
+ .plg__tag{padding:2px 9px 6px;font-size:12.5px;color:var(--ink-soft);line-height:1.45}
277
+
278
+ .plg__p{padding:6px 9px;font-size:13px;color:var(--ink);line-height:1.55}
279
+
280
+ .plg__note{padding:4px 9px;font-size:11.5px;color:var(--muted);line-height:1.5}
281
+
282
+ .plg__cmd{margin:2px 9px 8px;padding:10px;border-radius:var(--r);background:var(--subtle);
283
+ color:var(--ink-soft);font-size:11.5px;line-height:1.55;white-space:pre-wrap;overflow-wrap:anywhere}
284
+
285
+ .plg__log{margin:6px 9px 2px;padding:9px;border-radius:var(--r);background:var(--subtle);
286
+ color:var(--ink-soft);font-size:11px;line-height:1.5;max-height:180px;overflow-y:auto;
287
+ white-space:pre-wrap;overflow-wrap:anywhere}
288
+
289
+ .plg__btns{display:flex;gap:8px;padding:2px 9px 10px}
290
+
291
+ .plg__btn{flex:1;height:38px;border-radius:var(--r);background:var(--subtle);
292
+ color:var(--ink);font-size:13.5px}
293
+
294
+ /* 主按钮用宿主定义的「主操作」配色(--primary-action / --on-primary-action)——
295
+ 别用 --ink 反白:深色主题下 --ink 是浅色,反白会变成浅底白字,整枚按钮消失。 */
296
+ .plg__btn--go{background:var(--primary-action);color:var(--on-primary-action)}
297
+
298
+ .plg__btn:disabled{opacity:.45}
299
+
205
300
  .permchip{display:inline-flex;align-items:center;gap:6px;height:30px;padding:0 11px;
206
301
  border-radius:var(--r-full);border:1px solid var(--border);background:var(--subtle);
207
302
  color:var(--ink-soft);font-size:12.5px;max-width:190px;white-space:nowrap;overflow:hidden}
@@ -224,22 +319,65 @@
224
319
 
225
320
  .casc__sep{color:var(--muted)}
226
321
 
227
- /* ── 「生成中」提示行:合并后那枚胶囊(.casc)**下面**的一行 ──────────────
228
- 2026-10-20 用户:「两个胶囊合并成1个 在下面显示生成中效果即可」——
229
- 画布底部内核那枚「引擎 · 模型 · 生成中」胶囊(.qa-run)与输入区上方那枚
322
+ /* ── 运行态:**「生成中」并进这枚胶囊,且不配文字** ────────────────────────
323
+ 2026-10-20 用户:「两个胶囊合并成1个」→「是图中胶囊下方的生成中 应该跟胶囊合并」
324
+ →「不需要文字了 用其他动画效果代替」。
325
+ 画布底部内核那枚「引擎 · 模型 · 生成中」胶囊(.qa-run)与输入区上方这枚
230
326
  「引擎 · 模型」胶囊(.casc)显示的是**同一串字**、上下挨着,像同一件事说了两遍。
231
- 本壳的做法:画布那枚不画(见下面 .conv__thread .qa-run),「这一轮在跑」
232
- 改由 .casc 下面这一行小字表达(转圈 + 「生成中」)。
233
- ⚠️ 宽度/居中与 .composer__tools 逐值对齐 —— 左边缘与那枚胶囊同一条线。 */
234
- .composer__gen{display:none;align-items:center;gap:6px;max-width:860px;margin:0 auto 8px;
235
- font-size:11.5px;line-height:16px;color:var(--muted)}
236
- .composer__gen.on{display:flex}
237
- .composer__gen__sp{width:11px;height:11px;flex:none;border-radius:50%;
238
- border:1.5px solid var(--faint);border-top-color:var(--muted);
239
- animation:work-spin .8s linear infinite}
327
+ 本壳的做法:画布那枚不画(见下面 .conv__thread .qa-run);运行态由**这枚胶囊自己**表达 ——
328
+ 前面那枚「引擎」小图标换成**绿液瓶**(掌天瓶那套加载图形,图形见 conversation.js 的 bottleSVG),
329
+ 绿液缓缓涨落 + 瓶后那团绿雾脉动。
330
+ ⚠️ **不要文字**(用户明说):一枚会动的小瓶比「生成中」三个字更轻,也不占宽度。
331
+ ⚠️ 绿液涨落是「不确定进度」的诚实表达:不知道还差多久,就无限涨落,不假装百分比。
332
+ ⚠️ 胶囊**仍然是**引擎·模型选择器(点它开抽屉):只换图标不换整枚胶囊,
333
+ 换掉整枚会让人以为运行态不能点。 */
334
+ .casc__lead{display:inline-flex;align-items:center;justify-content:center;flex:none;width:14px;height:18px}
335
+ /* 瓶子:与读历史那枚同比例缩到小图标大小(viewBox 64:104 → 11:18)。
336
+ ⚠️ 尺寸必须与 bottleSVG 的 viewBox 同比例,否则 preserveAspectRatio 会在盒子里留白
337
+ —— 看上去就是「瓶子比预期的位置偏了一点」(同 .conv__loading__bottle 的教训)。
338
+ ⚠️ 缩到这个尺寸,瓶身那圈描边只剩 0.2px(亚像素)→ 瓶形读不出来,只剩一团绿。
339
+ 所以这里给液体加一圈 drop-shadow:**绿**的部分自己立得住,瓶形是锦上添花。 */
340
+ .casc__bottle{display:none;position:relative;width:11px;height:18px}
341
+ .casc__bottle::before{content:"";position:absolute;left:50%;top:54%;width:20px;height:20px;
342
+ margin:-10px 0 0 -10px;border-radius:50%;pointer-events:none;
343
+ background:radial-gradient(circle,var(--q-bottle-glow, rgba(52,210,123,.30)) 0%,transparent 68%);
344
+ animation:qbBottleGlow 2.8s ease-in-out infinite}
345
+ .casc__bottle .qbottle{display:block;width:11px;height:18px;
346
+ color:var(--q-bottle-line, #43A06E);
347
+ filter:drop-shadow(0 0 2px var(--q-bottle, rgba(52,210,123,.9)))}
348
+ .casc.is-running .casc__cpu{display:none}
349
+ .casc.is-running .casc__bottle{display:block}
350
+
351
+ /* 胶囊边框的流动绿光(2026-10-20 用户:「胶囊边框也可以有动画」):
352
+ 运行态时这枚胶囊的边框换成**一道绕着胶囊口转的绿光** —— 灵液沿瓶壁爬升的同一种语言。
353
+ 实现:两层背景叠出「1px 环」——
354
+ ① 顶层 = 胶囊原本的底色,只留 padding-box → 盖住内部,只露一圈边;
355
+ ② 底层 = conic 渐变(整圈浓绿 + 一道亮带),只留 border-box → 亮带绕着圈走。
356
+ ⚠️ 边框自身要 transparent:否则那 1px 实色边框会盖住渐变环。
357
+ ⚠️ conic 的角度靠 @property 才能补间(Chrome 85+/Safari 16.4+);老 WebView 没注册
358
+ 这个属性时走 var(--casc-angle,0deg) 兜底 = 亮带停在顶部(不转,但整圈仍是绿的,
359
+ 加上里面的绿液瓶,运行态照样读得出来)。
360
+ ⚠️ 背景层多值 background-clip 在很老的上游上只认第一值 —— 那种情况下顶层底色
361
+ 仍会盖住内部,最多是没有那圈渐变,不会露出 conic 的花色(安全退化)。 */
362
+ @property --casc-angle{syntax:'<angle>';inherits:false;initial-value:0deg}
363
+ .casc.is-running{
364
+ border-color:transparent;
365
+ background-image:
366
+ linear-gradient(var(--subtle),var(--subtle)),
367
+ conic-gradient(from var(--casc-angle,0deg),
368
+ var(--q-bottle-lo,#0F7B3C) 0deg,
369
+ var(--q-bottle,#22C55E) 296deg,
370
+ var(--q-bottle-hi,#7DF3A8) 330deg,
371
+ var(--q-bottle-lo,#0F7B3C) 360deg);
372
+ background-origin:border-box;
373
+ background-clip:padding-box,border-box;
374
+ -webkit-background-clip:padding-box,border-box;
375
+ animation:cascRimSpin 2.6s linear infinite}
376
+
377
+ @keyframes cascRimSpin{to{--casc-angle:360deg}}
240
378
 
241
379
  /* 内核那枚「引擎 · 模型 · 生成中」胶囊:组件这条线上**不画**。
242
- 理由与 .composer__gen 那一段相同(同一串字说了两遍)—— 它在**画布**里,
380
+ 理由与上面那一段相同(同一串字说了两遍)—— 它在**画布**里,
243
381
  而画布里另有本壳自己的运行态表达(并进 bot 头像那一行的呼吸 + 计时)。
244
382
  ⚠️ 引擎 · 模型的真值仍在 .casc 上(降级换模型时 'start' 事件改写它),
245
383
  所以不丢信息;PC 工作台那条线用的是自己的皮肤(client-ui/pc/app.css),
@@ -251,13 +389,32 @@
251
389
 
252
390
  .btn:disabled{opacity:.45;cursor:not-allowed}
253
391
 
254
- .sendbtn{width:36px;height:36px;border-radius:var(--r-full);background:var(--primary-action);
255
- color:var(--on-primary-action);display:flex;align-items:center;justify-content:center;flex:0 0 auto}
392
+ /* 尺寸与左侧「+」逐值一致(32×32):同一行两个按钮大小不同会显得右边「重」 */
393
+ .sendbtn{width:32px;height:32px;border-radius:var(--r-full);background:var(--primary-action);
394
+ color:var(--on-primary-action);display:flex;align-items:center;justify-content:center;flex:0 0 auto;
395
+ transition:background .18s,color .18s,box-shadow .18s,transform .12s}
256
396
 
257
397
  .sendbtn:active{transform:scale(.94)}
258
398
 
259
- .sendbtn--stop{background:var(--error-surface);color:var(--error-text);
260
- border:1px solid var(--error-border)}
399
+ /* ── 停止态(2026-10-22 重做)──
400
+ 旧版是「浅粉底 + 红色时钟」,两处不对:① 时钟读起来是「定时 / 等待」,
401
+ 按下去却是**立刻掐断**;② 浅底红图标在这个位置的分量和「+」差不多,
402
+ 看不出它是当前唯一的破坏性动作。现在改成实心朱砂圆 + 反白圆角方块
403
+ (与 PC 的 --cinnabar 同源),外圈再套一道呼吸光晕:这一轮还在跑时它在呼吸,
404
+ 收尾(class 摘掉)后光晕随动画一起消失,不需要额外的「停止中」文案。
405
+ 可选令牌:--stop-action / --stop-action-hi(宿主想换色时才给)。 */
406
+ .sendbtn--stop{background:var(--stop-action,#D6402A);color:#fff;
407
+ animation:sendStopBreath 2s ease-out infinite}
408
+
409
+ .sendbtn--stop:active{background:var(--stop-action-hi,#C23A22)}
410
+
411
+ @keyframes sendStopBreath{
412
+ 0%{box-shadow:0 0 0 0 rgba(214,64,42,.38)}
413
+ 70%{box-shadow:0 0 0 7px rgba(214,64,42,0)}
414
+ 100%{box-shadow:0 0 0 0 rgba(214,64,42,0)}}
415
+
416
+ @media (prefers-reduced-motion:reduce){
417
+ .sendbtn--stop{animation:none;box-shadow:0 0 0 3px rgba(214,64,42,.16)}}
261
418
 
262
419
  .pick-scrim{position:fixed;inset:0;z-index:1000;background:rgba(0,0,0,.42);animation:pick-fade .16s ease}
263
420
 
@@ -302,6 +459,17 @@
302
459
 
303
460
  .pick__citem.is-on .pick__cl{font-weight:600}
304
461
 
462
+ /* 模型行:名字 + 倍率同行(`--engines` 的 `model_credits`,「×0.34」)。
463
+ 名字 min-width:0 才能让省略号生效;倍率定宽靠右、不参与收缩 ——
464
+ 长模型名只挤自己的那一格,整列倍率仍然对齐。 */
465
+ .pick__crow{display:flex;align-items:baseline;gap:6px;width:100%;min-width:0}
466
+
467
+ .pick__crow .pick__cl{flex:1 1 auto;min-width:0}
468
+
469
+ /* 倍率用等宽数字:同一列的 ×0.34 / ×1.20 宽度一致,扫读时不会跳。 */
470
+ .pick__cr{flex:0 0 auto;font-size:11px;line-height:15px;color:var(--muted);
471
+ font-variant-numeric:tabular-nums;white-space:nowrap}
472
+
305
473
  .pick__cd{font-size:11px;line-height:15px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
306
474
 
307
475
  @keyframes pick-fade{from{opacity:0}to{opacity:1}}
@@ -358,7 +526,7 @@
358
526
  跟着冻住 —— 那正是「看起来卡在读取」的来源(同 magic-test 那份 qa-hist-loading 的教训)。
359
527
  ⚠️ 节点由 conversation.js 的 histLoadingNode 建,类名与这里成对,改名要两边一起改。 */
360
528
  .conv__loading{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;
361
- align-items:center;justify-content:center;gap:16px;
529
+ align-items:center;justify-content:center;gap:10px;
362
530
  /* 「撑满宽高」:画布有 20/12/28/12 的内边距,光靠 `flex:1` 只能填满**内边距以内**那块
363
531
  (实测 366×582,画布 390×630)。用负边距把这层内边距吃回来,盒子才真正贴到画布的
364
532
  四边 —— 四边数值必须与 `.conv__thread` 的 padding 逐位对齐(上 20 / 右 12 / 下 28 / 左 12),
@@ -368,56 +536,74 @@
368
536
  (首屏还没拿到 `more` 时它是隐的,但用户二次进入可能是亮的)—— 用 flex 参与布局时
369
537
  两者不重叠;绝对定位则会把那块入口盖在 loading 底下。 */
370
538
 
371
- /* 瓶的光晕:一团绿雾垫在瓶子后面,让「绿」在这一屏有存在感(只动 opacity) */
372
- .conv__loading__bottle{position:relative;flex:none;width:132px;height:165px}
373
-
374
- .conv__loading__bottle::before{content:"";position:absolute;left:50%;top:56%;width:180px;height:180px;
375
- margin:-90px 0 0 -90px;border-radius:50%;pointer-events:none;
376
- background:radial-gradient(circle,var(--q-bottle-glow, rgba(52,210,123,.30)) 0%,transparent 68%);
539
+ /* 瓶的光晕:一团绿雾垫在瓶子后面,让「绿」在这一屏有存在感(只动 opacity)。
540
+ ⚠️ 尺寸要与 bottleSVG 的 viewBox 同比例(64:104 = 0.615),否则 preserveAspectRatio
541
+ 会在盒子里留出空白 —— 看上去就是「瓶子比预期的位置偏了一点」。
542
+ 22/36 = 0.6111 ✓(取偶数像素,缩小后不会出现半像素毛边)
543
+ ⚠️ 尺寸一路在缩(2026-10-21 用户:「瓶子太大 参考一下凡人修仙传的小绿瓶」
544
+ →「瓶子显示小一倍」→「再小一倍」):132×175 → 88×143 → **22×36**。
545
+ 它是「一枚小瓶」,一屏只有这一件东西时**也不要放大**——放大就成摆件了。
546
+ 22px 宽是这枚图形的下限:再小木塞(4.4 单位 ≈ 1.5px)与口沿那道暗圈会糊成一团,
547
+ 瓶型就散掉了。真要更小,得换一份「小尺寸专用」的简化图形,不能靠缩放。
548
+ 宿主若要更醒目,请调**光晕**(--q-bottle-glow 的浓度)而不是调瓶身。 */
549
+ .conv__loading__bottle{position:relative;flex:none;width:22px;height:36px}
550
+
551
+ /* 光晕按瓶身缩放:瓶 22 宽时,雾圈 40(约 1.8 倍)才裹得住;
552
+ 跟着一起缩,否则小瓶子会陷进一大团绿雾里,看着像「糊了一块绿」。 */
553
+ .conv__loading__bottle::before{content:"";position:absolute;left:50%;top:54%;width:40px;height:40px;
554
+ margin:-20px 0 0 -20px;border-radius:50%;pointer-events:none;
555
+ background:radial-gradient(circle,var(--q-bottle-glow, rgba(45,200,120,.30)) 0%,transparent 68%);
377
556
  animation:qbBottleGlow 2.8s ease-in-out infinite}
378
557
 
379
- .conv__loading__bottle .qbottle{display:block;width:132px;height:165px;
380
- color:var(--q-bottle-line, var(--muted))}
558
+ /* ⚠️ 轮廓色兜底写 `#43A06E` 而不是 `var(--muted)`:`--muted` 只有部分宿主定义,
559
+ 没定义时 SVG 的 `stroke` 会退回**纯黑**,瓶子会变成一圈黑线稿(实测过,很难看)。
560
+ 小绿瓶的轮廓本来就不是灰的,是**比玉胎深一档的绿**。 */
561
+ .conv__loading__bottle .qbottle{display:block;width:22px;height:36px;
562
+ color:var(--q-bottle-line, #43A06E)}
381
563
 
382
564
  .conv__loading__label{font-size:13.5px;line-height:1.5;color:var(--muted);text-align:center}
383
565
 
384
566
  @keyframes qbBottleGlow{0%,100%{opacity:.45;transform:scale(.94)}50%{opacity:.9;transform:scale(1.04)}}
385
567
 
386
568
  /* ══════════ 掌天瓶:绿液本体(conversation.js 的 bottleSVG 产出的 DOM)══════════
387
- 液面位置已经**烘进 path 的 d**(脚本按 `fill` 算好顶边 y),这里只叠加动作:
388
- 涨液做 `translateY`、波浪做 `translateX`、气泡做位移 —— 全是 transform,
389
- 主线程被回放占住时也不会触发重排。
390
- ⚠️ 波浪的横移距离必须**等于一个波长**(bottleSVG 里 `q24 …48` = 48),否则循环处会跳一下。 */
391
- .qbottle__wave{animation:qbWaveX 3.2s linear infinite}
569
+ 液面位置已经**烘进 path 的 d**(脚本按 `fill` 算好液面 y),这里只叠加动作:
570
+ 涨液做 `translateY`、气泡做位移 —— 全是 transform,主线程被回放占住时也不会触发重排。
571
+ ⚠️ 这里**没有**波浪横移动画:瓶型的液面是凹液面(静态曲线),不是正弦波。
572
+ 正弦波配圆腹会露出「水波在玉胎里横着走」的假;凹液面 + 整体涨落更像真的在灌。 */
392
573
 
393
574
  /* 气泡:在腹部深处升起、升到液面之下就淡出(循环)。三个错开相位 = 一直在冒泡。
394
- 位移上限 16px 是配合 bottleSVG 里气泡的坐标(y 94→78):最浅也还在液面(y≈88)以下,
575
+ 位移上限 11px 是配合 bottleSVG 里气泡的坐标(y 92→81):最浅也还在液面(y≈90)以下,
395
576
  不会出现「气泡跑到空气里」。 */
396
- .qbottle__bubble{fill:#EAFFF2;opacity:0;animation:qbBubble 3s ease-in infinite}
577
+ .qbottle__bubble{fill:var(--q-bottle-rim, #EAFFF2);opacity:0;animation:qbBubble 3s ease-in infinite}
397
578
  .qbottle__bubble--b{animation-duration:3.7s;animation-delay:.9s}
398
579
  .qbottle__bubble--c{animation-duration:4.3s;animation-delay:1.8s}
399
580
 
400
581
  /* 涨落:整块液体上下推 ±8px。看着是「液面在呼吸」,而不是「瓶子在倒来倒去」。 */
401
582
  .qbottle--hist .qbottle__liquid{animation:qbRise 3.6s ease-in-out infinite}
402
583
 
403
- @keyframes qbWaveX{0%{transform:translateX(0)}100%{transform:translateX(-48px)}}
404
584
  @keyframes qbBubble{
405
585
  0%{transform:translateY(0) scale(.7);opacity:0}
406
586
  20%{opacity:.8}
407
587
  70%{opacity:.55}
408
- 100%{transform:translateY(-16px) scale(1);opacity:0}}
588
+ 100%{transform:translateY(-11px) scale(1);opacity:0}}
409
589
  @keyframes qbRise{
410
590
  0%,100%{transform:translateY(8px)}
411
591
  50%{transform:translateY(-8px)}}
412
592
 
413
593
  /* 一次性灌满(App 启动闪屏用):从空瓶灌到满,停住不回退 —— 启动是**有终点**的,
414
594
  反复涨落会让人以为「还在启动」。`forwards` 让它停在满瓶那一刻。
595
+ 位移量取 `--qb-dy`(bottleSVG 按 fill 算好写进 style):硬编码会跟液位脱钩。
415
596
  ⚠️ 曲线必须**前快后缓**(ease-out):试过 ease-in-out,前 300ms 几乎不动,
416
- 用户看到的是一枚空瓶子在发光 —— 第一眼没有「绿」就白做了这一屏。 */
417
- .qbottle--rise .qbottle__liquid{animation:qbFill .95s cubic-bezier(.16,.72,.28,1) forwards}
597
+ 用户看到的是一枚空瓶子在发光 —— 第一眼没有「绿」就白做了这一屏。
598
+ ⚠️ `animation-delay:-.16s` 不能省(2026-10-21 实测):启动那 300ms 主线程在铺首屏,
599
+ 浏览器会**丢掉前几帧**,`getComputedStyle` 里 translateY 一直是 54px —— 也就是
600
+ 第一眼看到的仍是空瓶。负延迟让动画「从中段开始」,首帧就已经灌了一半,
601
+ 即便帧被丢也一定有绿。(按 .16/.95 = 17% 时间点算,ease-out 下约 50% 液位。) */
602
+ .qbottle--rise .qbottle__liquid{
603
+ animation:qbFill .95s cubic-bezier(.16,.72,.28,1) -.16s forwards}
418
604
 
419
605
  @keyframes qbFill{
420
- 0%{transform:translateY(84px)}
606
+ 0%{transform:translateY(var(--qb-dy, 46px))}
421
607
  100%{transform:translateY(0)}}
422
608
 
423
609
  @media (prefers-reduced-motion:reduce){