magic-agent 2.1.5 → 2.3.2
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.
- package/CLIENT-CONTRACT.md +147 -11
- package/README.md +10 -4
- package/client-ui/pc/app.css +17 -0
- package/client-ui/pc/index.html +5 -0
- package/client-ui/pc/js/05-task-list-render.js +28 -4
- package/client-ui/pc/js/07-chat-stream-render.js +19 -8
- package/client-ui/pc/js/93-rail.js +24 -2
- package/client-ui/shared/conv/03-chat-message.js +63 -3
- package/client-ui/shared/conv/06-ask-answer.js +30 -7
- package/client-ui/shared/conv/09-stream-apply.js +16 -0
- package/client-ui/shared/conv/19-attachments.js +75 -11
- package/client-ui/shared/conv/27-conv-ledger.js +1 -1
- package/client-ui/shared/conv/29-msg-variants.js +56 -3
- package/client-ui/shared/conv/README.md +6 -0
- package/client-ui/shared/conv/conv.css +33 -1
- package/client-ui/shared/conversation.css +42 -4
- package/client-ui/shared/conversation.js +801 -102
- package/client-ui/shared/transport-http.js +199 -38
- package/client-ui-mobile/index.html +731 -581
- package/client-ui-mobile/reqs.css +39 -0
- package/client-ui-mobile/reqs.js +520 -0
- package/cmd/magic-client/main.go +1 -1
- package/internal/agent/capability.go +8 -1
- package/internal/agent/claude.go +5 -3
- package/internal/agent/codebuddy.go +6 -2
- package/internal/agent/codex.go +7 -2
- package/internal/agent/dsh_sdk.go +8 -0
- package/internal/agent/dsh_sdk_test.go +17 -0
- package/internal/agent/engine.go +20 -0
- package/internal/agent/engine_test.go +52 -0
- package/internal/agent/llmengine.go +14 -0
- package/internal/agent/llmengine_test.go +24 -0
- package/internal/agent/llmhttp.go +6 -0
- package/internal/agent/llmhttp_test.go +27 -0
- package/internal/agent/llmmodels.go +48 -0
- package/internal/agent/permission.go +5 -0
- package/internal/cli/ask.go +38 -17
- package/internal/cli/requirements.go +2 -0
- package/internal/cli/root.go +5 -0
- package/internal/cli/session.go +16 -14
- package/internal/cli/sessionlog.go +5 -0
- package/internal/cli/sessionlog_user_test.go +49 -0
- package/internal/cli/suggest.go +98 -0
- package/internal/cli/suggest_test.go +54 -0
- package/internal/cli/team.go +111 -24
- package/internal/client/requirements.go +27 -7
- package/internal/client/requirements_test.go +51 -0
- package/internal/client/service.go +35 -3
- package/internal/client/service_test.go +35 -0
- package/internal/client/suggest.go +131 -0
- package/internal/client/suggest_test.go +116 -0
- package/internal/requirements/consolidate.go +86 -24
- package/internal/requirements/requirements.go +9 -0
- package/internal/requirements/requirements_test.go +129 -0
- package/internal/requirements/store.go +11 -3
- package/internal/session/delete.go +58 -11
- package/internal/session/delete_test.go +140 -0
- package/internal/session/session.go +25 -0
- package/internal/session/session_mark_test.go +62 -0
- package/internal/suggest/suggest.go +532 -0
- package/internal/suggest/suggest_test.go +268 -0
- package/internal/team/board.go +107 -15
- package/internal/team/execute.go +1 -1
- package/internal/team/team.go +28 -19
- package/internal/team/team_test.go +68 -15
- package/npm/apk/magic-agent-mobile-release.apk +0 -0
- package/npm/apk/version.json +3 -3
- package/npm/dist/darwin-arm64/magic-agent +0 -0
- package/npm/dist/darwin-arm64/magic-client +0 -0
- package/npm/dist/darwin-x64/magic-agent +0 -0
- package/npm/dist/darwin-x64/magic-client +0 -0
- package/npm/dist/linux-arm64/magic-agent +0 -0
- package/npm/dist/linux-arm64/magic-client +0 -0
- package/npm/dist/linux-x64/magic-agent +0 -0
- package/npm/dist/linux-x64/magic-client +0 -0
- package/npm/dist/win32-x64/magic-agent.exe +0 -0
- package/npm/dist/win32-x64/magic-client.exe +0 -0
- package/package.json +6 -2
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
const QA_RAIL_MAX = 60; // 一屏最多画多少行(CLI 的登记表可能有几百条)
|
|
21
|
+
/* 运行中那枚**自动**置顶图钉(与移动端/旧左栏同一图形):行内说明「它凭什么在最前」,
|
|
22
|
+
跑完状态一变就消失 —— 别让用户以为自己置顶过它。 */
|
|
23
|
+
const QA_RAIL_PIN = '<span class="rail-pin" role="img" aria-label="运行中 · 自动置顶" '
|
|
24
|
+
+ 'title="运行中 · 自动置顶(跑完自动归位)">'
|
|
25
|
+
+ '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M16 9V4h1a1 1 0 000-2H7a1 1 0 000 2h1v5a3 3 0 01-3 3v2h5.97v7l1 1 1-1v-7H19v-2a3 3 0 01-3-3z"/></svg></span>';
|
|
21
26
|
const QA_RAIL_POLL_MS = 30000; // 有会话在跑时的兜底轮询(其余时候靠「一轮结束」触发)
|
|
22
27
|
const QA_RAIL_TAIL_MS = 2500; // 打开一条 running 会话时的进度同步间隔(见 qaRailTailStart)
|
|
23
28
|
/* 一条会话「这一轮结束了」的事件名(与 core 的归一化词表同名)。
|
|
@@ -66,6 +71,18 @@ function qaRailGroupLabel(s){
|
|
|
66
71
|
if(st === "interrupted") return "已打断";
|
|
67
72
|
return "已完成";
|
|
68
73
|
}
|
|
74
|
+
/* 运行中的会话**自动临时置顶**(2026-10-22 用户:「运行中的会话自动临时置顶」):
|
|
75
|
+
判据与左栏「进行中」同一口径(`qaRailGroup === "live"`,等审批也算 —— 它同样需要被看见),
|
|
76
|
+
**不改任何数据、不落盘**:只在渲染取行时把它们排到最前,跑完下一次刷新自动归位。
|
|
77
|
+
为什么 PC 没有「置顶」按钮也要这条:左栏按 CLI 给的顺序平铺(最近活动倒序),
|
|
78
|
+
一个跑几十分钟的长任务会被后来新建的会话一路压到下面,用户得往下翻才能找到「正在跑的那条」。
|
|
79
|
+
⚠️ 稳定分区,不按时间重排 —— CLI 的顺序是既有契约,这里只做「提到最前」,不动同类先后。 */
|
|
80
|
+
function qaRailAutoPinned(s){ return qaRailGroup(s) === "live"; }
|
|
81
|
+
function qaRailOrder(rows){
|
|
82
|
+
const live = [], rest = [];
|
|
83
|
+
(rows || []).forEach((s)=>{ (qaRailAutoPinned(s) ? live : rest).push(s); });
|
|
84
|
+
return live.concat(rest);
|
|
85
|
+
}
|
|
69
86
|
/** 时间列:今天只给 时:分,其余给 月/日(与旧左栏同一口径)。 */
|
|
70
87
|
function qaRailTime(ts){
|
|
71
88
|
const n = Number(ts);
|
|
@@ -145,7 +162,9 @@ function qaRailRender(){
|
|
|
145
162
|
`<span class="n">${counts[k]}</span></button>`;
|
|
146
163
|
tabs.innerHTML = tab("all", "全部") + tab("live", "进行中") + tab("done", "已完成") + tab("failed", "失败");
|
|
147
164
|
}
|
|
148
|
-
|
|
165
|
+
/* 取行:**运行中先自动临时置顶**,再截 QA_RAIL_MAX —— 长任务不会被 60 条上限挤掉
|
|
166
|
+
(除非同时跑着 60 条以上,那种情况本来就只能显示最近这些)。 */
|
|
167
|
+
const rows = qaRailOrder(qaRailAll.filter(qaRailHit)).slice(0, QA_RAIL_MAX);
|
|
149
168
|
if(!qaRailAll.length){
|
|
150
169
|
box.innerHTML = `<div class="tk-empty" data-testid="rail-empty">${qaRailLoaded
|
|
151
170
|
? (qaRailErr ? escHtml(qaRailErr) : "还没有会话 —— 在右边直接提问就会开一条")
|
|
@@ -167,6 +186,7 @@ function qaRailRender(){
|
|
|
167
186
|
return `<button class="tk-item${s.id === qaActiveId ? " active" : ""}" data-sid="${escHtml(s.id)}" type="button" title="${escHtml(tip)}" data-testid="rail-item">
|
|
168
187
|
<span class="tico"><i class="rail-dot ${g}" aria-hidden="true"></i></span>
|
|
169
188
|
<span class="tt">${escHtml(title)}</span>
|
|
189
|
+
${qaRailAutoPinned(s) ? QA_RAIL_PIN : ""}
|
|
170
190
|
${noLog(s) ? `<span class="rail-nolog" title="没有可回放的历史">无历史</span>` : ""}
|
|
171
191
|
<span class="tm">${escHtml(qaRailTime(s.ts))}</span>
|
|
172
192
|
<span class="tk-dots" data-rail-menu="${escHtml(s.id)}" role="button" tabindex="-1" data-testid="rail-dots"
|
|
@@ -238,7 +258,9 @@ function qaRailApply(ctx, events){
|
|
|
238
258
|
if(Number.isFinite(sq) && sq > ctx.seq) ctx.seq = sq;
|
|
239
259
|
if(kind === "user"){
|
|
240
260
|
ctx.h = null;
|
|
241
|
-
|
|
261
|
+
/* 移动端「会话托管」自动发的提问带 hosted:true:PC 回放同一条历史时也画成
|
|
262
|
+
「托管发送」,否则同一会话在两端会长成两张脸。 */
|
|
263
|
+
qaRenderMine(String(ev.text || ""), [], { hosted: !!ev.hosted });
|
|
242
264
|
continue;
|
|
243
265
|
}
|
|
244
266
|
if(!ctx.h){ ctx.h = qaRenderAnswer(); if(!ctx.h) break; }
|
|
@@ -460,9 +460,16 @@ function qaMsgRetry(msg){
|
|
|
460
460
|
toast("正在重试 · " + escHtml(text.slice(0, 24)) + (text.length > 24 ? "…" : ""));
|
|
461
461
|
qaSend(text, { followup: true, echo: false });
|
|
462
462
|
}
|
|
463
|
-
|
|
463
|
+
/* opts(可选,2026-10-23 只增参数):
|
|
464
|
+
· hosted=true —— 这条是「会话托管」自动选的第一条建议,不是用户亲手打的。
|
|
465
|
+
仍然是右侧的用户轮次(留 `.qa-mine`,排队 / 重试 / 取文都照旧),但头像换成
|
|
466
|
+
数字分身的机器人、气泡虚线描边,并在正文上方挂一枚「托管发送」小标 ——
|
|
467
|
+
用户 2026-10-23 报:「是托管发送的要标识 显示时区别于“我”发送」。
|
|
468
|
+
老调用方一律不传第三参,画出来与以前逐像素一致。 */
|
|
469
|
+
function qaRenderMine(text, atts, opts){
|
|
464
470
|
const s = $("#qa-stream"); if(!s) return;
|
|
465
471
|
s.hidden = false;
|
|
472
|
+
const hosted = !!(opts && opts.hosted);
|
|
466
473
|
/* 空态那句「还没有事件」得先让位:它只在画布一条都还没有时该在。
|
|
467
474
|
⚠️ 以前只有**事件流**那条路径会摘它(conversation.js 的 ingest),而用户气泡
|
|
468
475
|
是宿主直接调这里画的、不经过事件流 —— 于是发出第一句之后,画布上同时挂着
|
|
@@ -471,8 +478,18 @@ function qaRenderMine(text, atts){
|
|
|
471
478
|
const empty = s.querySelector(".conv__empty");
|
|
472
479
|
if(empty) empty.remove();
|
|
473
480
|
const div = document.createElement("div");
|
|
474
|
-
div.className = "qa-msg qa-mine";
|
|
475
|
-
div.innerHTML = `<div class="qa-ico">${qaAvatarHtml("mine")}</div><div class="qa-bd"><div class="qa-t md-body"></div></div>`;
|
|
481
|
+
div.className = "qa-msg qa-mine" + (hosted ? " qa-hosted" : "");
|
|
482
|
+
div.innerHTML = `<div class="qa-ico">${qaAvatarHtml(hosted ? "sim" : "mine")}</div><div class="qa-bd"><div class="qa-t md-body"></div></div>`;
|
|
483
|
+
/* 小标放在 .qa-t **前面**(正文之外):取文 / 重试只看 .qa-t,不会把这四个字
|
|
484
|
+
当成用户说的话再发一遍。 */
|
|
485
|
+
if(hosted){
|
|
486
|
+
const bd0 = div.querySelector(".qa-bd");
|
|
487
|
+
const tag = document.createElement("div");
|
|
488
|
+
tag.className = "qa-hosted-tag";
|
|
489
|
+
tag.textContent = "托管发送";
|
|
490
|
+
tag.title = "会话托管自动选择的第一条建议,不是你亲手输入的";
|
|
491
|
+
bd0.insertBefore(tag, bd0.firstChild);
|
|
492
|
+
}
|
|
476
493
|
// 用户消息按 markdown 渲染:marked + DOMPurify(沿用产品文档同一管线,输入是用户可控,
|
|
477
494
|
// 但消毒仍保留以防恶意粘贴)。
|
|
478
495
|
const host = div.querySelector(".qa-t");
|
|
@@ -1443,10 +1460,53 @@ function qaRenderAnswer(host){
|
|
|
1443
1460
|
}else{
|
|
1444
1461
|
qaTodoFill(host, rows, { title: "任务列表" });
|
|
1445
1462
|
}
|
|
1463
|
+
/* 子任务产物的落点(2026-10-22 用户:「主会话产物要显示子任务的产物」)。
|
|
1464
|
+
以前它们只出现在整轮结束那份 result 信封里,主会话画布要等到最后才看得到;
|
|
1465
|
+
现在某个子任务一落定,core 就把它的产物挂到任务行上,这里立刻画出来。
|
|
1466
|
+
复用正文 / 工具输出同一条产物链(19-attachments::qaArtMount):存在性照样由主进程
|
|
1467
|
+
探针判,探针不可用(纯预览)时一张不画,绝不画出一枚必然裂掉的卡。 */
|
|
1468
|
+
mountTaskArts(host, rows);
|
|
1446
1469
|
qaScrollIntoView();
|
|
1447
1470
|
return host;
|
|
1448
1471
|
}
|
|
1449
1472
|
|
|
1473
|
+
/* mountTaskArts 把「这一轮所有子任务交出的产物」画在任务列表卡正下方。
|
|
1474
|
+
· 一次性汇总成一区(不按任务分子标题):用户要的是「这轮产出了什么」,
|
|
1475
|
+
重复的路径(交接单 / 多个任务引用同一份文件)在这里去重;
|
|
1476
|
+
· 幂等:同一份快照反复到达时把上一块摘掉重画 —— 内存里只记**当前**这份清单,
|
|
1477
|
+
走的是「整块替换」而不是「越叠越多」;
|
|
1478
|
+
· rows 里没带 arts(引擎的 TodoWrite 待办清单 / 老 core)时什么都不做。 */
|
|
1479
|
+
function mountTaskArts(panel, rows){
|
|
1480
|
+
const all = [];
|
|
1481
|
+
const seen = Object.create(null);
|
|
1482
|
+
(Array.isArray(rows) ? rows : []).forEach((r)=>{
|
|
1483
|
+
(r && Array.isArray(r.arts) ? r.arts : []).forEach((p)=>{
|
|
1484
|
+
/* 同一个文件可能被两个子任务共用(交接单 / 引用上游产物),写法还可能是
|
|
1485
|
+
绝对路径或相对路径 —— 按规范化路径判重(qaArtCanon,见 19-attachments)。
|
|
1486
|
+
它是运行时的全局,取不到就退回原串。 */
|
|
1487
|
+
const k = (typeof qaArtCanon === "function") ? qaArtCanon(p) : String(p || "");
|
|
1488
|
+
if(!p || !k || seen[k]) return;
|
|
1489
|
+
seen[k] = 1;
|
|
1490
|
+
all.push(p);
|
|
1491
|
+
});
|
|
1492
|
+
});
|
|
1493
|
+
let box = panel.nextElementSibling;
|
|
1494
|
+
if(!(box && box.classList && box.classList.contains("qa-task-arts"))) box = null;
|
|
1495
|
+
if(!box){
|
|
1496
|
+
if(!all.length) return;
|
|
1497
|
+
box = document.createElement("div");
|
|
1498
|
+
box.className = "qa-task-arts";
|
|
1499
|
+
panel.parentNode.insertBefore(box, panel.nextSibling);
|
|
1500
|
+
}
|
|
1501
|
+
/* 整块替换:快照是**全量**的(每次状态变化都整份重发),产物清单也跟着替换;
|
|
1502
|
+
不 append 才不会越叠越多。上一轮已画好的卡先摘掉,免得探针回包前留着旧图。 */
|
|
1503
|
+
box.innerHTML = "";
|
|
1504
|
+
if(!all.length){ if(box.parentNode) box.parentNode.removeChild(box); return; }
|
|
1505
|
+
/* ⚠️ 取**运行时**的全局:qaArtMount 定义在 19-attachments 分片里,本文件装配在前、
|
|
1506
|
+
直接引用会在定义前求值(typeof 判定为 "undefined",产物区于是静默变空)。 */
|
|
1507
|
+
if(typeof qaArtMount === "function") qaArtMount(box, all, { label: "子任务产物" });
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1450
1510
|
/* ===== 「任务耗时」分组:一轮收尾时,把这一轮的**历史过程**并进一枚折叠组 =====
|
|
1451
1511
|
(2026-10-12 用户:「历史的思考和工具最终又合并在一个任务总耗时的分组里」,形制对齐 TraeWork)
|
|
1452
1512
|
· 收的是**思考块 + 工具组**(`.qa-think` / `.qa-tools`),正文留在外面 —— 那是结论,过程才是历史;
|
|
@@ -263,8 +263,13 @@ function qaAskFlush(taskId){
|
|
|
263
263
|
(主进程 `desk:ask` 是单会话槽)。剩下的等那一轮收尾再放 —— 队列自己就串起来了。
|
|
264
264
|
⚠️ 提问卡答案优先:qaAskQueue 里还有没发出去的答案时先不发队列消息(那是引擎正等着的
|
|
265
265
|
回答,抢在它前面发会把这一问的上下文冲掉)。
|
|
266
|
-
⚠️
|
|
267
|
-
|
|
266
|
+
⚠️ 只能在**用户正看着这条会话**时发(画布只有一份):在别的会话的画布里替它发就是
|
|
267
|
+
张冠李戴 —— 与 qaAskFlush 同一条判据。切回来即发。
|
|
268
|
+
⚠️ 2026-10-23 修(用户:「排队的和自动建议发送串会话了」):判据只看 qaActiveId **不够** ——
|
|
269
|
+
`qaTaskSwitch` 已经把 `qaActiveId` 换成新会话,但旧会话那条 `qaMsgFlush(id)` 的入参
|
|
270
|
+
还是旧的;`qaSend` 内部再读 `qaTaskRunning(qaActiveId)` / `qaActiveId` 落笔,就发进新会话了。
|
|
271
|
+
改法:`qaMsgFlush` 把**自己那条会话**(`owner`)显式交给 qaSend(`{ sid }`),由 qaSend
|
|
272
|
+
按 sid 落笔、发现画布已不是它就连一条都不发、把条目原样退回队列(见 qaMsgFlush)。
|
|
268
273
|
⚠️ 队列条目只存正文(`{ text }`):处理中贴的图留在**附件条**上(2026-10-19 起处理中
|
|
269
274
|
贴图不再被拦,见 19-attachments 的 askAttBind),收尾后 qaMsgFlush 走 qaSend
|
|
270
275
|
(from = 本实例)时才被取走 —— 贴图与排队的那条一起发出。 */
|
|
@@ -336,17 +341,35 @@ function qaMsgEnqueue(taskId, item){
|
|
|
336
341
|
toast("已排队 · 这一轮结束后自动发送<br><b>" + escHtml((item && item.text) || "") + "</b>", true);
|
|
337
342
|
}
|
|
338
343
|
/** 排队的下一条该不该现在发:这条会话没有一轮在跑 + 用户正看着它 + 没有待发的提问卡答案
|
|
339
|
-
* (见上面三条 ⚠️)。发一条即返回 —— 剩下的等这一轮收尾再来。
|
|
344
|
+
* (见上面三条 ⚠️)。发一条即返回 —— 剩下的等这一轮收尾再来。
|
|
345
|
+
* 返回 true = 已经交出去了(`qaSend` 认了这条),false = 还在队列里没动。
|
|
346
|
+
* ⚠️ 「交出去」是**异步**的(qaSend 是 async,且可能因为会话已不可用而把条目退回来):
|
|
347
|
+
* 这里按乐观口径先出队,退回由 qaMsgRequeue 放回队首 —— 顺序不会乱,也不会丢。 */
|
|
340
348
|
function qaMsgFlush(taskId){
|
|
341
349
|
const id = taskId || "";
|
|
342
350
|
const q = qaMsgQueue.get(id);
|
|
343
|
-
if(!q || !q.length || qaTaskRunning(id)) return;
|
|
344
|
-
if(id !== (qaActiveId || "")) return;
|
|
345
|
-
if((qaAskQueue.get(id) || []).length) return;
|
|
351
|
+
if(!q || !q.length || qaTaskRunning(id)) return false;
|
|
352
|
+
if(id !== (qaActiveId || "")) return false;
|
|
353
|
+
if((qaAskQueue.get(id) || []).length) return false;
|
|
346
354
|
const item = q.shift();
|
|
347
355
|
if(!q.length) qaMsgQueue.delete(id);
|
|
348
356
|
qaMsgQueueRender(id);
|
|
349
|
-
|
|
357
|
+
/* `sid` = 这条队列自己的会话,**不是** qaActiveId(上面已判过相等,但 qaSend 是 async 的:
|
|
358
|
+
真要并发切走,判据在 qaSend 里还会按 sid 再核一次 —— 只认这一条,不认调用瞬间的全局)。 */
|
|
359
|
+
const ret = qaSend(item.text, { followup: true, from: QA_QUEUE_INSTANCE, sid: id });
|
|
360
|
+
if(ret && typeof ret.then === "function"){
|
|
361
|
+
ret.then((ok)=>{ if(ok === false) qaMsgRequeue(id, item); }).catch(()=>{ qaMsgRequeue(id, item); });
|
|
362
|
+
}
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
/** 没发出去的那条退回**队首**(qaSend 发现画布已经不是这条会话 / 空正文时回 false)。
|
|
366
|
+
* 放队首而不是队尾:用户排的先后顺序不能因为一次「发不出去」被打乱。 */
|
|
367
|
+
function qaMsgRequeue(taskId, item){
|
|
368
|
+
const id = taskId || ""; if(!id || !item) return;
|
|
369
|
+
const q = qaMsgQueue.get(id) || [];
|
|
370
|
+
q.unshift(item);
|
|
371
|
+
qaMsgQueue.set(id, q);
|
|
372
|
+
qaMsgQueueRender(id);
|
|
350
373
|
}
|
|
351
374
|
/** 会话删了 → 它排的队也没意义了(不留悬挂条目)。 */
|
|
352
375
|
function qaMsgQueueDropFor(taskId){ qaMsgQueue.delete(taskId || ""); qaMsgQueueRender(); }
|
|
@@ -425,8 +425,24 @@ function qaStreamApplyOne(ev, ctx){
|
|
|
425
425
|
status: st === "running" ? "in_progress"
|
|
426
426
|
: st === "done" ? "completed"
|
|
427
427
|
: (st === "failed" || st === "skipped") ? st : "pending",
|
|
428
|
+
/* 行尾「状态 · 耗时」:2026-10-22 用户「应该显示子任务标题和状态耗时」。
|
|
429
|
+
文案由这里给(与状态同一处映射,免得两端各写一套词),耗时取 core 给的
|
|
430
|
+
绝对值 runtime_ms(running 行随快照增长、落定后冻结)。 */
|
|
431
|
+
st: st === "running" ? "运行中"
|
|
432
|
+
: st === "done" ? "已完成"
|
|
433
|
+
: st === "failed" ? "失败"
|
|
434
|
+
: st === "skipped" ? "已跳过" : "等待中",
|
|
435
|
+
ms: Number(t.runtime_ms) || 0,
|
|
436
|
+
/* 这一行已经交出来的产物(绝对路径)。画在任务列表卡**下面**那一块
|
|
437
|
+
「子任务产物」区里(见 03-chat-message 的 todoList),不塞进行里 ——
|
|
438
|
+
一行里放缩略图会把列表撑垮。 */
|
|
439
|
+
arts: Array.isArray(t.arts) ? t.arts.filter(x=>typeof x === "string" && x) : [],
|
|
428
440
|
/* 行尾标注「引擎 · 角色」:主会话的任务列表卡要能一眼看出这一行派给了谁 */
|
|
429
441
|
meta: [t.engine, t.role || t.kind].filter(Boolean).join(" · "),
|
|
442
|
+
/* 行内进度(团队心跳):core 只给「跑了很久没动静」的那一行挂它,
|
|
443
|
+
如「已 40 秒无新输出」。2026-10-22 用户:「子任务进度也不需要显示多行
|
|
444
|
+
在任务列表中显示即可」—— 以前它是主会话里一行行 notice。 */
|
|
445
|
+
active: st === "running" ? String(t.active || "").trim() : "",
|
|
430
446
|
};
|
|
431
447
|
}).filter(r=>r.text);
|
|
432
448
|
if(typeof h.todoList === "function") h.todoList(rows);
|
|
@@ -650,6 +650,23 @@ function qaArtNode(it){
|
|
|
650
650
|
return card;
|
|
651
651
|
}
|
|
652
652
|
|
|
653
|
+
/** 产物路径的**归一化**(判「是不是同一个文件」的唯一口径)。
|
|
654
|
+
*
|
|
655
|
+
* 同一份文件在事件里有三种写法 —— 绝对路径、workspace 相对路径、以及 `t2/cover.png`
|
|
656
|
+
* 这类不带 `./` 的写法;只按字符串比对就会把它们当成三个文件画三张卡
|
|
657
|
+
* (2026-10-22 用户:「子任务产物同一个文件不要重复显示」)。
|
|
658
|
+
* 相对路径按会话 workspace 拼成绝对路径再比对(拼不出来就保留原串),顺手吃掉
|
|
659
|
+
* `./`、重复斜杠与末尾斜杠。 */
|
|
660
|
+
function qaArtCanon(p){
|
|
661
|
+
let s = String(p == null ? "" : p).trim().replace(/\\/g, "/");
|
|
662
|
+
if(!s) return "";
|
|
663
|
+
if(!/^\//.test(s) && !/^[A-Za-z]:\//.test(s)){
|
|
664
|
+
const base = String(qaArtBase() || "").replace(/\/+$/, "");
|
|
665
|
+
if(base) s = base + "/" + s.replace(/^\.\//, "");
|
|
666
|
+
}
|
|
667
|
+
return s.replace(/\/{2,}/g, "/").replace(/\/\.\//g, "/").replace(/\/+$/, "");
|
|
668
|
+
}
|
|
669
|
+
|
|
653
670
|
/** 把挑出的产物路径画成一排卡(**只画真的落在盘上的**)。
|
|
654
671
|
* ⚠️ 存在性问主进程(`desk:path-probe`,一次批量)——渲染层拿不到 fs。
|
|
655
672
|
* 正文里提到的 `app.js`、远端 URL、示例路径都不会被误画成卡。
|
|
@@ -657,15 +674,41 @@ function qaArtNode(it){
|
|
|
657
674
|
* `opts.label`(2026-10-15)= 在这一排卡上面先落一行小标题(「本轮产物」用,见 qaTurnArtsMount);
|
|
658
675
|
* `opts.detached` = 允许画进**不在文档树上**的宿主:后台那一轮的画布被整份摘进 holder,
|
|
659
676
|
* 节点还在、只是不在树上(`isConnected` 为假)—— 收尾时往它末尾补产物正是这种情形。
|
|
677
|
+
* `opts.skipShown:false` = 不做「已经画过的跳过」(默认做,见 qaTaskArtsShown)。
|
|
660
678
|
* 返回画出来的张数(Promise):收尾那条链要**等它画完**再落 HTML 快照(见 qaTurnArtsMount)。 */
|
|
661
679
|
function qaArtMount(host, paths, opts){
|
|
662
680
|
const o = opts || {};
|
|
663
681
|
const api = qaDeskFn("pathProbe");
|
|
664
682
|
if(!host || !api || !Array.isArray(paths) || !paths.length) return Promise.resolve(0);
|
|
665
|
-
const
|
|
683
|
+
const dup = new Set();
|
|
684
|
+
const list = [];
|
|
685
|
+
paths.forEach((p)=>{
|
|
686
|
+
const s = qaArtCanon(p);
|
|
687
|
+
if(!s || dup.has(s)) return;
|
|
688
|
+
dup.add(s);
|
|
689
|
+
list.push(String(p).trim());
|
|
690
|
+
});
|
|
691
|
+
if(!list.length) return Promise.resolve(0);
|
|
666
692
|
return Promise.resolve(api({ base: qaArtBase(), paths: list })).then((r)=>{
|
|
667
693
|
const items = (r && r.ok && Array.isArray(r.items)) ? r.items : [];
|
|
668
|
-
|
|
694
|
+
/* 探针回的是**解析后的绝对路径**(相对路径已按 workspace 展开),它才是最终落盘位置 ——
|
|
695
|
+
归一化去重也以它为准:上面那轮只挡得住「同一批里的重复」,跨两次挂载
|
|
696
|
+
(子任务产物区 vs 正文 / 本轮产物)的重复要靠这里。 */
|
|
697
|
+
const shown = new Set();
|
|
698
|
+
let hit = items.filter((x)=>{
|
|
699
|
+
if(!x || !x.exists || !x.abs) return false;
|
|
700
|
+
const k = qaArtCanon(x.abs);
|
|
701
|
+
if(shown.has(k)) return false;
|
|
702
|
+
shown.add(k);
|
|
703
|
+
return true;
|
|
704
|
+
});
|
|
705
|
+
/* 已经在本条消息的任务列表下方展示过的文件,别在正文 / 本轮产物里再画一遍
|
|
706
|
+
(2026-10-22 用户:「子任务产物同一个文件不要重复显示」)——
|
|
707
|
+
汇总正文末尾常有一份「产物」清单,子任务产物区里也是同一批文件。 */
|
|
708
|
+
const skip = o.skipShown === false ? null : qaTaskArtsShown(host);
|
|
709
|
+
if(skip && skip.size){
|
|
710
|
+
hit = hit.filter((x)=> !skip.has(qaArtCanon(x.abs)));
|
|
711
|
+
}
|
|
669
712
|
if(!hit.length) return 0; // 一个都不存在 → 不画
|
|
670
713
|
if(!o.detached && !host.isConnected) return 0; // 这条消息已经换掉了 → 不画
|
|
671
714
|
if(o.label){
|
|
@@ -695,6 +738,31 @@ function qaArtMount(host, paths, opts){
|
|
|
695
738
|
}).catch(()=> 0); // 探针失败 = 当作「都不是产物」,正文照旧
|
|
696
739
|
}
|
|
697
740
|
|
|
741
|
+
/** 这条应答消息里**已经画出来**的产物(规范化绝对路径集合)。
|
|
742
|
+
*
|
|
743
|
+
* 为什么要一问一答式地与 qaArtMount 合流(2026-10-22 用户:「子任务产物同一个文件不要
|
|
744
|
+
* 重复显示」):同一轮里产物有三条来路 ——
|
|
745
|
+
* ① 子任务产物区(任务列表卡下方,core 的 `arts`,最先画出来);
|
|
746
|
+
* ② 工具的「输出」段(结果正文里挑出来的路径);
|
|
747
|
+
* ③ 收尾的「本轮产物」区 + 正文里的产物卡。
|
|
748
|
+
* 同一份 cover.png 三条来路都会挑到。先画出来的那条留下,后面的跳过 —— 判据取 DOM 上
|
|
749
|
+
* 真实的 `data-path`(快照恢复后照样成立),不另记内存状态。
|
|
750
|
+
* 宿主容器不在树上时(后台那一轮被摘进 holder)退回 `host.closest` 拿不到消息 →
|
|
751
|
+
* 只看 host 自己,宁可少挡一次也不误挡。 */
|
|
752
|
+
function qaTaskArtsShown(host){
|
|
753
|
+
const out = new Set();
|
|
754
|
+
if(!host || !host.closest) return out;
|
|
755
|
+
const msg = (host.classList && host.classList.contains("qa-bd")) ? host : host.closest(".qa-bd");
|
|
756
|
+
const scope = msg || host;
|
|
757
|
+
try{
|
|
758
|
+
scope.querySelectorAll(".qa-task-arts .qa-art[data-path], .qa-arts .qa-art[data-path]").forEach((n)=>{
|
|
759
|
+
const p = String(n.dataset.path || "").trim();
|
|
760
|
+
if(p) out.add(qaArtCanon(p));
|
|
761
|
+
});
|
|
762
|
+
}catch(e){ /* 取不到就当没画过 —— 顶多多画一张,不会漏 */ }
|
|
763
|
+
return out;
|
|
764
|
+
}
|
|
765
|
+
|
|
698
766
|
/* ── 破图收尾(2026-10-20 用户:「是图片不存在吗 不存在的不显示」)────────────
|
|
699
767
|
探针只说「这会儿在盘上」,图却仍可能取不回来:手机连的是另一台/另一个实例、
|
|
700
768
|
快照里存的是换过的旧令牌、文件在渲染后被删。别留一张 alt + 碎图标的空卡:
|
|
@@ -821,15 +889,11 @@ function qaTurnArtsMount(run, h){
|
|
|
821
889
|
const el = h && h.el;
|
|
822
890
|
const host = (el && el.querySelector) ? el.querySelector(".qa-bd") : null;
|
|
823
891
|
if(!host || !paths.length) return Promise.resolve(0);
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
}catch(e){ /* 取不到就当没画过 —— 顶多多画一张,不会漏 */ }
|
|
830
|
-
const want = paths.filter((p)=>{ const b = String(p).split("/").pop(); return !seen.has(p) && !seen.has(b); });
|
|
831
|
-
if(!want.length) return Promise.resolve(0);
|
|
832
|
-
return qaArtMount(host, want, { label: "本轮产物", detached: true });
|
|
892
|
+
/* 「正文里已经画过的跳过」不再在这里判:qaArtMount 统一按规范化绝对路径比对
|
|
893
|
+
DOM 上真实的 `data-path`(含子任务产物区),相对 / 绝对 / 带不带 `./` 三种写法
|
|
894
|
+
都能对上(2026-10-22 用户:「子任务产物同一个文件不要重复显示」)。
|
|
895
|
+
这里只把「本轮产物」这个标题带下去 —— 一张都画不出来时它自己不会出现。 */
|
|
896
|
+
return qaArtMount(host, paths, { label: "本轮产物", detached: true });
|
|
833
897
|
}
|
|
834
898
|
/** 回放 / 续传专用(2026-10-20 方案 A):按**轮**收产物,封口时补挂到这轮消息末尾。
|
|
835
899
|
*
|
|
@@ -313,7 +313,7 @@ function qaSessionPlayer(){
|
|
|
313
313
|
}catch(e){ /* 收尾失败不影响回放 */ }
|
|
314
314
|
h = null;
|
|
315
315
|
}else if(arts){ arts.flush(null); } // 没有答复卡的一轮:清掉残留,别串到下一轮
|
|
316
|
-
qaRenderMine(String(ev.text || ""), []);
|
|
316
|
+
qaRenderMine(String(ev.text || ""), [], { hosted: !!ev.hosted });
|
|
317
317
|
t0 = atMs(ev);
|
|
318
318
|
continue;
|
|
319
319
|
}
|
|
@@ -349,6 +349,19 @@ function qaClip(s, n){
|
|
|
349
349
|
const max = n || 72;
|
|
350
350
|
return t.length > max ? t.slice(0, max) + "…" : t;
|
|
351
351
|
}
|
|
352
|
+
/** 毫秒 → 任务列表行尾的耗时文案(0 / 非数 → "",不写 "0s" 这种噪音)。
|
|
353
|
+
* 口径与收尾行「总耗时」一致:<1 分钟按秒、<1 小时按分、再往上按小时。 */
|
|
354
|
+
function qaDur(ms){
|
|
355
|
+
const v = Number(ms);
|
|
356
|
+
if(!Number.isFinite(v) || v <= 0) return "";
|
|
357
|
+
if(v < 60000) return Math.round(v / 1000) + "s";
|
|
358
|
+
if(v < 3600000){
|
|
359
|
+
const m = Math.floor(v / 60000), s = Math.round((v % 60000) / 1000);
|
|
360
|
+
return s ? m + "m" + s + "s" : m + "m";
|
|
361
|
+
}
|
|
362
|
+
const h = Math.floor(v / 3600000), m = Math.round((v % 3600000) / 60000);
|
|
363
|
+
return m ? h + "h" + m + "m" : h + "h";
|
|
364
|
+
}
|
|
352
365
|
function qaBaseName(p){
|
|
353
366
|
const t = String(p || "").replace(/\\/g, "/").replace(/\/+$/, "");
|
|
354
367
|
const i = t.lastIndexOf("/");
|
|
@@ -485,8 +498,10 @@ function qaTodoFill(host, rows, opts){
|
|
|
485
498
|
}
|
|
486
499
|
const sum = document.createElement("span");
|
|
487
500
|
sum.className = "qa-todo-sum";
|
|
488
|
-
|
|
489
|
-
|
|
501
|
+
/* 标题栏只报总账(「待办 1/2」),**不再**塞一行「已 41 秒无新输出」:
|
|
502
|
+
2026-10-22 用户截图 —— 标题栏和任务行同时写着同一句秒数,看着像两件事。
|
|
503
|
+
每一行各自的进度就在那一行里(见下方 .qa-todo-st)。 */
|
|
504
|
+
sum.textContent = "待办 " + done + "/" + arr.length;
|
|
490
505
|
hd.appendChild(sum);
|
|
491
506
|
if(o.title){
|
|
492
507
|
/* ⚠️ 开合态只落**类**(.qa-todo-panel 上的 open),不写 data-* / 不写行内样式:
|
|
@@ -515,9 +530,39 @@ function qaTodoFill(host, rows, opts){
|
|
|
515
530
|
else if(t.status === "failed") ico.innerHTML = QA_V_ICONS.alert;
|
|
516
531
|
const txt = document.createElement("span");
|
|
517
532
|
txt.className = "qa-todo-t";
|
|
518
|
-
|
|
533
|
+
/* 标题**永远是标题**(2026-10-22 用户截图:整行只剩「已 41 秒无新输出」,
|
|
534
|
+
任务叫什么完全看不见了 —— 此前这里是 `in_progress && active` 就把 text 换掉)。 */
|
|
535
|
+
txt.textContent = t.text;
|
|
519
536
|
r.appendChild(ico);
|
|
520
537
|
r.appendChild(txt);
|
|
538
|
+
/* 行尾:状态 · 耗时 ·(可选)此刻在干什么 —— 全在一行里,右对齐。
|
|
539
|
+
2026-10-22 用户:「状态重新显示 应该显示子任务标题和状态耗时」。
|
|
540
|
+
团队的每一行(running / 已完成 / 失败 / 跳过)都带状态与耗时;待办清单
|
|
541
|
+
(引擎的 TodoWrite)不带 st,走下面那两档旧形态,形态不变。 */
|
|
542
|
+
const dur = qaDur(t.ms);
|
|
543
|
+
if(t.st){
|
|
544
|
+
const st = document.createElement("span");
|
|
545
|
+
st.className = "qa-todo-st is-" + t.status;
|
|
546
|
+
st.textContent = String(t.st);
|
|
547
|
+
r.appendChild(st);
|
|
548
|
+
if(dur){
|
|
549
|
+
const ms = document.createElement("span");
|
|
550
|
+
ms.className = "qa-todo-dur";
|
|
551
|
+
ms.textContent = dur;
|
|
552
|
+
r.appendChild(ms);
|
|
553
|
+
}
|
|
554
|
+
/* 状态说明(等授权 / 长时间无输出)接在「状态 耗时」后面 —— 它是**次要**信息,
|
|
555
|
+
所以排在最后、字更小。只有真正要用户注意的时候 core 才会给 active
|
|
556
|
+
(等授权 / 静默超 30 秒),平时这一行就只有「标题 … 运行中 41s」。 */
|
|
557
|
+
if(t.status === "in_progress" && t.active){
|
|
558
|
+
const ac = document.createElement("span");
|
|
559
|
+
ac.className = "qa-todo-active";
|
|
560
|
+
ac.textContent = String(t.active);
|
|
561
|
+
r.appendChild(ac);
|
|
562
|
+
}
|
|
563
|
+
list.appendChild(r);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
521
566
|
/* 行尾标注(可选,`引擎 · 角色`):只有团队任务列表给这一档,待办清单没有 */
|
|
522
567
|
if(t.meta){
|
|
523
568
|
const mt = document.createElement("span");
|
|
@@ -525,6 +570,14 @@ function qaTodoFill(host, rows, opts){
|
|
|
525
570
|
mt.textContent = String(t.meta);
|
|
526
571
|
r.appendChild(mt);
|
|
527
572
|
}
|
|
573
|
+
if(t.status === "in_progress" && t.active){
|
|
574
|
+
const ac = document.createElement("span");
|
|
575
|
+
ac.className = "qa-todo-active";
|
|
576
|
+
ac.textContent = String(t.active);
|
|
577
|
+
const mt = r.querySelector(".qa-todo-meta");
|
|
578
|
+
if(mt) mt.remove();
|
|
579
|
+
r.appendChild(ac);
|
|
580
|
+
}
|
|
528
581
|
list.appendChild(r);
|
|
529
582
|
});
|
|
530
583
|
host.appendChild(list);
|
|
@@ -145,6 +145,12 @@ node tests/check-conv-shards.mjs --write # 按代
|
|
|
145
145
|
② **移动端宿主页有了装配冒烟**(`check:conv-comp` 第二段,按打包后的 `shared/` 关系把
|
|
146
146
|
`client-ui-mobile/index.html` 原样跑一遍):组件挂得上、画布在 `#conv` 里、共用皮肤与
|
|
147
147
|
两个基线令牌都在、零脚本报错。
|
|
148
|
+
**2026-10-22 补了行为级的那一层**:`npm run check:conv-mobile`
|
|
149
|
+
(`tests/check-conv-mobile-host.mjs`,21 条)按 `magic-client` 的 `/m/` 路由镜像起服务,
|
|
150
|
+
真跑 `client-ui-mobile/index.html`(390×844 手机视口),验团队一轮的
|
|
151
|
+
任务行(标题 + 状态 + 耗时)、`.qa-task-arts` 子任务产物区(缩略图走 `/desk/media`、
|
|
152
|
+
同一文件去重、重发不叠加),以及**移动端宿主的 `pathProbe` / `mediaUrl` 桥真的装上了**
|
|
153
|
+
—— 之前只有装配冒烟,桥断掉时内核只是静默不画,闸门全绿。
|
|
148
154
|
③ [[conv.css]] 里 magic-test 独有的结构件规则**刻意保留**(本壳没有那些节点 → 匹配不到,
|
|
149
155
|
无害;内核里有运行时拼出来的类名,静态删会误判)—— 理由写在文件头。
|
|
150
156
|
- **2026-10-17 修的两个「看得见吗 / 画了吗」问题**(移动端报障:「看不到会话历史」):
|
|
@@ -446,6 +446,21 @@
|
|
|
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
|
+
/* 托管自动发送的用户气泡(2026-10-23 用户:「是托管发送的要标识 显示时区别于“我”发送」):
|
|
450
|
+
还是右侧的用户轮次,但头像换成数字分身的机器人(qaRenderMine 的 opts.hosted),
|
|
451
|
+
气泡虚线描边 + 朱砂淡底,正文上方一枚「托管发送」小标 —— 一眼能看出这句不是手打的。 */
|
|
452
|
+
.qa-msg.qa-hosted .qa-bd{border-style:dashed;border-color:var(--cinnabar-hi);
|
|
453
|
+
background:linear-gradient(180deg,#FFF8F5,#FDF0EA)}
|
|
454
|
+
.qa-msg.qa-hosted .qa-ico{color:var(--cinnabar-hi);--h:var(--halo-c-red)}
|
|
455
|
+
.qa-hosted-tag{display:inline-flex;align-items:center;gap:4px;margin:0 0 6px;
|
|
456
|
+
padding:2px 7px;border-radius:var(--r-full,999px);
|
|
457
|
+
background:rgba(240,105,83,.10);color:var(--cinnabar-hi);
|
|
458
|
+
font-size:10.5px;line-height:15px;font-weight:600;letter-spacing:.02em;
|
|
459
|
+
user-select:none;white-space:nowrap}
|
|
460
|
+
.qa-hosted-tag::before{content:"";width:4px;height:4px;border-radius:50%;
|
|
461
|
+
background:currentColor;flex:0 0 auto}
|
|
462
|
+
[data-theme="dark"] .qa-msg.qa-hosted .qa-bd{background:#241B18;border-color:#8A5144}
|
|
463
|
+
[data-theme="dark"] .qa-hosted-tag{background:rgba(240,105,83,.16);color:#F2A08E}
|
|
449
464
|
/* 排队中的用户气泡(`is-queued`):已经发出去、但还在等前一轮跑完。
|
|
450
465
|
气泡照常可读(用户要能确认自己写的是什么),只把底色压淡 + 虚线边 + 底下补一行
|
|
451
466
|
「排队中 · 上一轮结束后自动发送」—— **不隐藏、不挪走**,否则就是这次报障的
|
|
@@ -516,6 +531,10 @@
|
|
|
516
531
|
|
|
517
532
|
.qa-arts-cap + .qa-arts{margin-top:4px}
|
|
518
533
|
|
|
534
|
+
/* 子任务产物区(任务列表卡正下方,见 03-chat-message::mountTaskArts):
|
|
535
|
+
与列表卡之间留一点缝,标题跟着卡一起对齐;卡本身沿用 .qa-arts / .qa-art 那一套。 */
|
|
536
|
+
.qa-task-arts{margin:0 0 8px}
|
|
537
|
+
|
|
519
538
|
.qa-art{border:1px solid var(--line);border-radius:10px;background:#FCFBF9;
|
|
520
539
|
overflow:hidden;cursor:pointer;transition:border-color .15s,box-shadow .15s}
|
|
521
540
|
|
|
@@ -1018,6 +1037,8 @@
|
|
|
1018
1037
|
[data-theme="dark"] .qa-t.md-body blockquote p{color:#A8A29E}
|
|
1019
1038
|
|
|
1020
1039
|
[data-theme="dark"] .qa-msg.qa-mine .qa-bd{background:#1C1C1E;border-color:#3B3835}
|
|
1040
|
+
/* 放在上面那条**之后**:同为 4 个类级选择器,靠后才会赢(托管气泡的暗色底不被盖回灰)。 */
|
|
1041
|
+
[data-theme="dark"] .qa-msg.qa-hosted .qa-bd{background:#241B18;border-color:#8A5144}
|
|
1021
1042
|
|
|
1022
1043
|
[data-theme="dark"] .qa-av-txt{background:linear-gradient(180deg,#2B2B2E,#1C1C1E)}
|
|
1023
1044
|
|
|
@@ -1645,9 +1666,20 @@
|
|
|
1645
1666
|
.qa-todo-row.is-skipped{color:var(--paper-mute)}
|
|
1646
1667
|
.qa-todo-row.is-skipped .qa-todo-ico{border-style:dashed}
|
|
1647
1668
|
.qa-todo-row.is-skipped .qa-todo-t{text-decoration:line-through}
|
|
1648
|
-
.qa-todo-t{min-width:0}
|
|
1669
|
+
.qa-todo-t{min-width:0;flex:1 1 auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1670
|
+
/* 行尾「状态 · 耗时」:团队的每一行都有(2026-10-22 用户:「应该显示子任务标题和状态耗时」)。
|
|
1671
|
+
标题占满剩余宽度并省略,状态与耗时靠右不换行 —— 一行永远是一行。 */
|
|
1672
|
+
.qa-todo-st{flex:none;font-family:var(--mono);font-size:10px;color:var(--paper-mute);white-space:nowrap}
|
|
1673
|
+
.qa-todo-st.is-in_progress{color:var(--amber)}
|
|
1674
|
+
.qa-todo-st.is-failed{color:var(--cinnabar-hi)}
|
|
1675
|
+
.qa-todo-dur{flex:none;font-family:var(--mono);font-size:10px;color:var(--paper-mute);
|
|
1676
|
+
white-space:nowrap;font-variant-numeric:tabular-nums}
|
|
1649
1677
|
.qa-todo-meta{margin-left:auto;flex:none;max-width:46%;font-family:var(--mono);font-size:10px;
|
|
1650
1678
|
color:var(--paper-mute);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
1679
|
+
/* 行内进度(团队心跳:「等待授权」「已 40 秒无新输出」):接在「状态 耗时」之后,
|
|
1680
|
+
是这一行里**最次要**的一档(字最小、允许收缩),标题永远优先占宽度。 */
|
|
1681
|
+
.qa-todo-active{flex:0 1 auto;min-width:0;max-width:40%;font-size:10px;
|
|
1682
|
+
color:var(--amber);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
1651
1683
|
.qa-todo-panel{min-width:0;margin:0 0 8px;border:1px solid var(--line);
|
|
1652
1684
|
border-radius:var(--r-md);overflow:hidden;background:var(--panel,#FFF)}
|
|
1653
1685
|
.qa-todo-panel .qa-todo-hd{gap:7px}
|
|
@@ -161,6 +161,37 @@
|
|
|
161
161
|
|
|
162
162
|
.composer{flex:0 0 auto;padding:0 clamp(12px,4vw,20px) 16px}
|
|
163
163
|
|
|
164
|
+
/* 下一轮建议(2026-10-23):一轮收尾后出现在**输入框正上方**,点一下直接发送。
|
|
165
|
+
形制照用户给的参考图:一枚枚圆角描边的短语,贴在输入框外沿、左对齐。
|
|
166
|
+
· 只占一行,横向滚动(不换行)—— 三条建议在手机上也不会把输入框顶下去;
|
|
167
|
+
· 宽/居中与 .composer__box、.composer__tools 逐值对齐,左边缘不参差;
|
|
168
|
+
· 默认 hidden 且**不预留高度**:没有建议时输入框位置一个像素都不动;
|
|
169
|
+
· ⚠️ display:flex 会盖掉 [hidden] 的 UA 规则,所以显式补一条。 */
|
|
170
|
+
.sugbar{display:flex;align-items:center;gap:8px;max-width:860px;margin:0 auto 8px;
|
|
171
|
+
overflow-x:auto;overscroll-behavior-x:contain;scrollbar-width:none;
|
|
172
|
+
-webkit-overflow-scrolling:touch;padding:1px 1px 2px}
|
|
173
|
+
.sugbar[hidden]{display:none}
|
|
174
|
+
.sugbar::-webkit-scrollbar{display:none}
|
|
175
|
+
.sugchip{flex:0 0 auto;max-width:min(78vw,320px);height:34px;padding:0 14px;
|
|
176
|
+
border:1px solid var(--border);border-radius:var(--r-full);background:var(--raised);
|
|
177
|
+
color:var(--ink);font:13.5px/32px var(--font);white-space:nowrap;overflow:hidden;
|
|
178
|
+
text-overflow:ellipsis;-webkit-tap-highlight-color:transparent}
|
|
179
|
+
.sugchip:active{background:var(--subtle);border-color:var(--accent)}
|
|
180
|
+
/* 托管中:紧挨着建议行的一枚状态标记(点它 = 暂停托管)。它与第一枚「待发」建议
|
|
181
|
+
同时出现,用户看到的是「要自动发这句」而不是「又多了几个按钮」。 */
|
|
182
|
+
.sugtag{flex:0 0 auto;height:34px;padding:0 11px;border:1px solid var(--accent);
|
|
183
|
+
border-radius:var(--r-full);background:transparent;color:var(--accent);
|
|
184
|
+
font:12px/32px var(--font);white-space:nowrap;letter-spacing:.02em}
|
|
185
|
+
.sugtag::before{content:"";display:inline-block;width:6px;height:6px;margin-right:6px;
|
|
186
|
+
border-radius:50%;background:currentColor;vertical-align:1px}
|
|
187
|
+
.sugtag:active{background:var(--subtle)}
|
|
188
|
+
/* 提问卡头的托管标记:卡头只有 9px 高,按建议条那枚 34px 会撑破;缩到 20px,仍保留圆点。 */
|
|
189
|
+
.qa-ask-hd .sugtag{height:20px;padding:0 8px;font:10px/18px var(--font);border-radius:var(--r-full)}
|
|
190
|
+
.qa-ask-hd .sugtag::before{width:5px;height:5px;margin-right:5px;vertical-align:1px}
|
|
191
|
+
/* 第一条建议 = 托管马上要发的那句:给一圈同色描边,别让它和另外两枚看起来一样重。 */
|
|
192
|
+
.sugchip--next{border-color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent)}
|
|
193
|
+
@media (prefers-reduced-motion:no-preference){.sugchip{transition:background .12s,border-color .12s}}
|
|
194
|
+
|
|
164
195
|
.composer__box{max-width:860px;margin:0 auto;border:1px solid var(--border);border-radius:var(--r-card);
|
|
165
196
|
background:var(--raised);display:flex;flex-direction:column;gap:2px;padding:10px 12px 8px}
|
|
166
197
|
|
|
@@ -212,13 +243,20 @@
|
|
|
212
243
|
|
|
213
244
|
/* 排队追问(2026-10-21:会话运行中允许先提交追问,类似 magic-test PC 端):
|
|
214
245
|
运行中提交的那几条先停在这里,等当前轮收尾后自动发出 —— 不打断、也不画成
|
|
215
|
-
用户气泡(还没跑,画上去会让人以为已经发出去了)。
|
|
216
|
-
|
|
246
|
+
用户气泡(还没跑,画上去会让人以为已经发出去了)。
|
|
247
|
+
⚠️ 2026-10-22 用户:「排队的任务列表是挂在对话框外面不是里面」—— 这条**挂在输入框
|
|
248
|
+
外面**(.composer__box 的上沿之上,DOM 里是它的前一个兄弟),不共享框的底色与描边。
|
|
249
|
+
所以在框外要自己站得住:宽度/居中与 .composer__box 逐值对齐;胶囊用 --raised 底 +
|
|
250
|
+
描边(页面底色上的 --subtle 几乎看不见)。出现在框外 → 顶上去的是参数行,框不被撑高。 */
|
|
251
|
+
.queuebar{display:flex;flex-direction:column;gap:6px;min-width:0;max-width:860px;
|
|
252
|
+
margin:0 auto 6px;padding:0 2px;max-height:132px;overflow-y:auto}
|
|
253
|
+
.queuebar[hidden]{display:none}
|
|
217
254
|
|
|
218
|
-
.queuebar__cap{font-size:11.5px;line-height:15px;color:var(--muted);padding-left:
|
|
255
|
+
.queuebar__cap{font-size:11.5px;line-height:15px;color:var(--muted);padding-left:4px}
|
|
219
256
|
|
|
220
257
|
.queuechip{display:flex;align-items:center;gap:6px;min-width:0;padding:5px 6px 5px 10px;
|
|
221
|
-
border-radius:var(--r-full);background:var(--
|
|
258
|
+
border-radius:var(--r-full);background:var(--raised);border:1px solid var(--border);
|
|
259
|
+
color:var(--ink);font-size:12px}
|
|
222
260
|
|
|
223
261
|
.queuechip__ico{flex:0 0 auto;color:var(--muted);display:flex}
|
|
224
262
|
|