diff --git a/assets/desktop_pet/DESIGN.md b/assets/desktop_pet/DESIGN.md index 06dee4ab..3c23574f 100644 --- a/assets/desktop_pet/DESIGN.md +++ b/assets/desktop_pet/DESIGN.md @@ -108,7 +108,7 @@ components: ### 交接 -成员加入、完成和 agent 间消息用走路与信封表达:信封带两格残影的抛物线和黄色标签(任务 / 回报 / 消息 / 新任务),落地时收件人头顶冒「!」。走路沿 2 单位网格寻路、不穿家具。 +成员加入、完成和 agent 间消息用走路与信封表达。进门时门先向屋里打开,人在门槛上淡入后走进来;坐下时沿用原作新人入职的效果——跳一跳、头顶冒星星、说「报到!」。离开时先说「再见」,再以约 1.7 倍速走到门口、在门槛淡出,门随后关上。信封带两格残影的抛物线和黄色标签(任务 / 回报 / 消息 / 新任务),落地时收件人头顶冒「!」。走路沿 2 单位网格寻路、不穿家具。 ### 溢出成员 diff --git a/assets/desktop_pet/agent_office_pet.html b/assets/desktop_pet/agent_office_pet.html index d33f22e0..cf24d6b2 100644 --- a/assets/desktop_pet/agent_office_pet.html +++ b/assets/desktop_pet/agent_office_pet.html @@ -758,7 +758,7 @@ } /* ---------- 6b. 走路:2 单位一格的占地图 + 8 向 BFS,家具外扩 1 单位不贴边走 ---------- */ -const GRID = 2, GW = RW / GRID, GH = RD / GRID, DOOR = [2, 59]; +const GRID = 2, GW = RW / GRID, GH = RD / GRID, DOOR = [3, 59], DOOR_SILL = [0.4, 59]; // 门内一步 / 门槛(左墙 y 54–64) let BLOCK = new Uint8Array(GW * GH); function buildBlockMap() { BLOCK = new Uint8Array(GW * GH); @@ -966,7 +966,15 @@ const step = ((simNow / 150) | 0) % 2; const spr = charSprite(a.look, pose, w.moving ? (step ? 'walkA' : 'walkB') : 'stand', flip); const [sx, sy] = P(VIEW, w.x, w.y, 0), bob = w.moving && step ? 1 : 0; + // 进门第一段从门槛淡入,出门最后一段淡出到门槛 + const gap = (px, py) => Math.hypot(w.x - px, w.y - py) / 2.5; + let alpha = 1; + if (w.fadeIn && w.i <= 1) alpha = Math.min(alpha, gap(...w.path[0].p)); + if (w.fadeOut && w.i >= w.path.length - 1) alpha = Math.min(alpha, gap(...w.path[w.path.length - 1].p)); + a.fade = Math.max(0, Math.min(1, alpha)); + c.globalAlpha = a.fade; c.drawImage(spr.cv, Math.round(sx - spr.ax), Math.round(sy - spr.ay - bob)); blits++; + c.globalAlpha = 1; a.hx = Math.round(sx); a.hy = Math.round(sy - spr.ay - bob); } function leds(c, rx) { @@ -1026,6 +1034,10 @@ const F = d.wall === 'y' ? (a, b, z0, z1, col) => faceY(c, o, 0, a, b, z0, z1, col) : (a, b, z0, z1, col) => faceX(c, o, 0, a, b, z0, z1, col); WALL_KINDS[d.kind].draw(F, d.a, d); } + if (doorOpen > .04) { // 门开着:门洞是暗的,门板绕 y=54 一侧的铰链转进屋里 + faceX(c, o, 0, 55, 63, 0, 24, '#2b2230'); faceX(c, o, 0, 55, 63, 16, 24, '#3a2f3f'); + box(c, o, 0, 54, 0, Math.max(1, Math.round(7 * doorOpen)), 1, 24, { top: '#c4884f', left: '#b07342', right: '#8d5a33' }); + } } // 机器人:场景里唯一会走动的角色(也是 box() 拼的) @@ -1052,13 +1064,25 @@ // 走动的组员:进门领任务、回工位、交结果、出门;占用与机器人同样的深度排序 const WALK_SPEED = 0.032; // 世界单位 / 毫秒,穿过房间约 2–3 秒 +const LEAVE_SPEED = WALK_SPEED * 1.7; // 离场快步出门 +const DOORWAY_SPEED = 0.008; // 跨门槛那一步放慢(约 0.3 秒),淡入淡出看得见 const walkers = new Set(); -function startWalk(a, path, done) { +// opts.fadeIn:从门槛淡入(进门);opts.fadeOut:走到终点(门槛)时淡出(出门) +function startWalk(a, path, done, opts = {}) { const [x, y] = path[0].p; - a.walk = { path, i: 1, x, y, dir: a.walk ? a.walk.dir : '+x', moving: false, pause: 0, done, + a.walk = { path, i: 1, x, y, dir: a.walk ? a.walk.dir : '+x', moving: false, done, + speed: opts.speed || WALK_SPEED, fadeIn: !!opts.fadeIn, fadeOut: !!opts.fadeOut, + pause: opts.delay || 0, obj: { h: 20, minX: 0, maxX: 0, minY: 0, maxY: 0, draw: c => a.walk && drawWalker(c, a) } }; walkers.add(a); } +// 门:有人从门槛进出时打开,人走远后关上 +let doorOpen = 0; +function updDoor(dt) { + const near = [...walkers].some(a => a.walk && (a.walk.fadeIn || a.walk.fadeOut) && + Math.hypot(a.walk.x - DOOR_SILL[0], a.walk.y - DOOR_SILL[1]) < 10); + doorOpen += ((near ? 1 : 0) - doorOpen) * Math.min(1, dt / 110); +} const faceDir = (fx, fy, tx, ty) => Math.abs(tx - fx) > Math.abs(ty - fy) ? (tx > fx ? '+x' : '-x') : (ty > fy ? '+y' : '-y'); function updWalkers(dt) { for (const a of [...walkers]) { @@ -1066,7 +1090,7 @@ if (!w) { walkers.delete(a); continue; } if (w.pause > 0) { w.pause -= dt; w.moving = false; continue; } if (w.i >= w.path.length) { walkers.delete(a); a.walk = null; w.done && w.done(); continue; } - const stop = w.path[w.i], [tx, ty] = stop.p, dx = tx - w.x, dy = ty - w.y, dist = Math.hypot(dx, dy), sp = dt * WALK_SPEED; + const stop = w.path[w.i], [tx, ty] = stop.p, dx = tx - w.x, dy = ty - w.y, dist = Math.hypot(dx, dy), sp = dt * (stop.speed || w.speed); w.moving = true; if (dist > .01) w.dir = faceDir(w.x, w.y, tx, ty); if (dist <= sp) { @@ -1127,6 +1151,7 @@ if (!a.hx) continue; let n = null, col = '#ffe14d'; if (simNow < a.pingUntil) n = 'ex'; + else if (simNow < a.arriveUntil) n = 'star'; else if (a.state === 'question') n = 'q'; else if (a.state === 'error') { n = 'ex'; col = '#ff5a4f'; } else if (a.state === 'done') n = 'star'; @@ -1192,7 +1217,7 @@ function layoutBubbles(k) { const W = innerWidth, minTop = k * 30, items = []; for (const a of Object.values(AG)) { - const c = a.hx ? bubbleContent(a) : null; + const c = a.hx && !(a.walk && a.fade < .5) ? bubbleContent(a) : null; // 门口还没完全显形时不说话 if (!c) { if (!a.bub.hidden) { a.bub.hidden = true; a.bubKey = ''; } continue; } renderBubble(a, c); items.push([c.prio, a]); } @@ -1249,13 +1274,18 @@ /* ---------- 9. 时间 & 主循环 ---------- */ const waiters = []; const wait = ms => new Promise(r => waiters.push({ t: simNow + ms, r })); +// 同步的模拟时间定时器:回调在推进时间的同一步里执行(Promise 回调要等到微任务) +const timers = []; +const after = (ms, fn) => timers.push({ t: simNow + ms, fn }); const FRAME_MS = 33; // 常驻桌面:像素动画本来就是 150ms 级步进,约 30fps 足够,省 CPU/GPU let last = performance.now(), sinceDraw = FRAME_MS; function advance(dt) { simNow += dt; for (let i = waiters.length - 1; i >= 0; i--) if (waiters[i].t <= simNow) { const w = waiters[i]; waiters.splice(i, 1); w.r(); } + for (let i = timers.length - 1; i >= 0; i--) if (timers[i].t <= simNow) { const t = timers[i]; timers.splice(i, 1); t.fn(); } updRobot(dt); updWalkers(dt); + updDoor(dt); } function loop(ts) { const dt = Math.min(64, ts - last); last = ts; @@ -1428,21 +1458,29 @@ return boss&&AG[boss.id]&&boss.id!==item.id?AG[boss.id]:null; } const posOf=a=>a.walk?[a.walk.x,a.walk.y]:STN[a.seat]?[STN[a.seat].x,STN[a.seat].y]:DOOR; -// 新成员:进门 → 到派活的人桌前领信封 → 绕到自己椅子后面 → 坐下 -function enterOffice(a,boss,from=DOOR) { +// 报到:在工位上跳一跳、头顶冒星星(原桌宠新人入职的效果) +function arrive(a) { + if(!AG[a.id]||a.leaving)return; + a.hopUntil=simNow+1600;a.arriveUntil=simNow+1600;say(a.id,'报到!',1600); +} +// 道别:气泡留到人走出门为止 +const farewell=a=>{if(AG[a.id]===a&&a.leaving)say(a.id,'再见',60000);}; +// 新成员:开门从门槛淡入 → 到派活的人桌前领信封 → 绕到自己椅子后面 → 坐下报到 +function enterOffice(a,boss,from=null) { const S=STN[a.seat]; if(!S)return; - if(!motionOK()) { // 减少动态效果:不走路,信封直接从派活的人飞到工位 - if(boss&&liveReady){send(boss.id,a.id,'任务');say(boss.id,'派给 '+shortName(a.name),2200);} + if(!motionOK()) { // 减少动态效果:不走路,信封直接从派活的人飞到工位,人在工位上报到 + if(liveReady){if(boss){send(boss.id,a.id,'任务');say(boss.id,'派给 '+shortName(a.name),2200);}arrive(a);} return; } - const stops=[{p:from}]; + const stops=[{p:from||DOOR_SILL}]; + if(!from)stops.push({p:DOOR,direct:true,speed:DOORWAY_SPEED}); const B=boss&&!boss.walk&&STN[boss.seat]; if(B) stops.push({p:deskFront(B),wait:1000,face:[B.x,B.y],call:()=>{send(boss.id,a.id,'任务');say(boss.id,'交给你了',1500);}}); stops.push({p:seatBehind(S)},{p:[S.x,S.y],direct:true}); - startWalk(a,route(stops),null); + startWalk(a,route(stops),()=>arrive(a),{fadeIn:!from,delay:from?0:220}); // 先开门再迈进来 } -// 离开:完成的人先到上级桌前交回报,再出门;失败/停止的人直接出门 +// 离开:完成的人先到上级桌前交回报,说「再见」,快步走出大门;失败/停止的人直接道别出门 function leaveOffice(a,boss,report) { const S=STN[a.seat]; if(S&&S.agent===a.id)S.agent=null; @@ -1450,13 +1488,17 @@ const from=posOf(a),stops=[{p:from}]; if(S&&!a.walk)stops.push({p:seatBehind(S),direct:true}); const B=report&&boss&&!boss.walk&&STN[boss.seat]; - if(B) stops.push({p:deskFront(B),wait:1000,face:[B.x,B.y],call:()=>{send(a.id,boss.id,'回报');say(boss.id,'收到 '+shortName(a.name)+' 的结果',2600);}}); - stops.push({p:DOOR}); - startWalk(a,route(stops),()=>removeAgent(a.id)); + if(B) stops.push({p:deskFront(B),wait:800,face:[B.x,B.y],call:()=>{ + send(a.id,boss.id,'回报');say(boss.id,'收到 '+shortName(a.name)+' 的结果',2600); + after(600,()=>farewell(a)); + }}); + stops.push({p:DOOR},{p:DOOR_SILL,direct:true,speed:DOORWAY_SPEED}); + startWalk(a,route(stops),()=>removeAgent(a.id),{fadeOut:true,speed:LEAVE_SPEED}); + if(!B)farewell(a); } // 正在往外走的人又接到任务:掉头回工位 function returnToSeat(a,boss) { - const seat=freeSeat(a.seat); + const seat=STN[a.seat]&&STN[a.seat].agent===a.id?a.seat:freeSeat(a.seat); if(!seat){removeAgent(a.id);return false;} a.leaving=false;a.seat=seat;STN[seat].agent=a.id; enterOffice(a,boss,posOf(a)); @@ -1489,10 +1531,11 @@ if(ids.has(id)||a.leaving)continue; const item={id,parentPath:a.parentPath||''}, boss=bossOf(item,visible), report=completed.has(id); if(motionOK()) {a.state=report?'done':a.state;a.activityLabel=report?'已完成':a.activityLabel;a.detail='';leaveOffice(a,boss,report);} - else { - if(liveReady&&report&&boss){send(id,boss.id,'回报');say(boss.id,'收到 '+shortName(a.name)+' 的结果',2600);} - removeAgent(id); - } + else if(liveReady) { // 减少动态效果:原地道别片刻后离开 + if(report&&boss){send(id,boss.id,'回报');say(boss.id,'收到 '+shortName(a.name)+' 的结果',2600);} + a.leaving=true;farewell(a); + after(900,()=>{if(AG[id]===a&&a.leaving)removeAgent(id);}); + } else removeAgent(id); } for(const item of visible) { let a=AG[item.id]; @@ -1585,6 +1628,7 @@ get agents(){return Object.values(AG).map(a=>({id:a.id,name:a.name,state:a.state,contextRatio:a.contextRatio,look:a.look.key,seat:a.seat, walking:!!a.walk,leaving:a.leaving,bubble:a.bub.hidden?'':a.bub.textContent,compact:a.bub.classList.contains('compact')}));}, get envelopes(){return envelopes.map(e=>({from:e.a.id,to:e.b.id,label:e.el?e.el.textContent:''}));}, + get door(){return doorOpen;}, get layoutSignature(){return JSON.stringify({theme:THEME,seats:Object.values(STN).map(s=>[s.id,s.x,s.y,s.axis,s.s])});}, // 测试用:按 32ms 步长推进模拟时间(走路、信封、等待)后重画一帧 advance(ms){for(let left=ms;left>0;left-=32)advance(Math.min(32,left));render();presentUpscale();}, diff --git a/assets/desktop_pet/surface.md b/assets/desktop_pet/surface.md index 7c9da9ad..b390b6ac 100644 --- a/assets/desktop_pet/surface.md +++ b/assets/desktop_pet/surface.md @@ -24,12 +24,12 @@ | 等成员回报 | 沙漏;气泡显示「等成员回报」 | | 等待授权、等待回答、重试 | 保留各自的明确状态文案 | | 主 agent 成功完成 | 留在主工位,显示 `zzz`;下一轮开始后恢复执行状态 | -| 子 agent 加入 | 从门口走进,到派活的人桌前领「任务」信封,再回工位 | -| 子 agent 成功完成 | 走到派活的人桌前递「回报」信封后出门;后续任务出现时再按真实成员恢复 | +| 子 agent 加入 | 门先打开,人从门槛淡入走进来,到派活的人桌前领「任务」信封,回工位坐下后跳一跳、冒星星、说「报到!」 | +| 子 agent 成功完成 | 走到派活的人桌前递「回报」信封,说「再见」,快步走到门口、开门淡出;后续任务出现时再按真实成员恢复 | | 执行失败或停止 | 本轮保留失败或停止状态,不能表现为成功休息 | | 整理上下文 | 显示整理状态及机器人提示;文件堆随后跟随新上下文比例 | -文件堆反映当前上下文使用量相对窗口容量的比例,不使用会话累计消耗量。容量未知时不绘制文件堆,不能把未知当作零用量。消息信封由真实成员变化或消息记录触发;减少动态效果偏好下不走路,信封仍然飞过(它表达谁和谁在交接,不是装饰)。 +文件堆反映当前上下文使用量相对窗口容量的比例,不使用会话累计消耗量。容量未知时不绘制文件堆,不能把未知当作零用量。消息信封由真实成员变化或消息记录触发;减少动态效果偏好下不走路,信封仍然飞过(它表达谁和谁在交接,不是装饰),新成员在工位上「报到!」,离开的成员原地说「再见」后消失。 状态气泡常驻于忙碌的 agent 头顶,按「求助 > 主 agent > 正在输出 > 刚变化」的优先级摆放:撞上就左右错开或抬高(引线连回头顶),再放不下只显示动作。有人忙时机器人不闲聊。 diff --git a/docs/desktop-agent-office.md b/docs/desktop-agent-office.md index 9313c619..a19c3841 100644 --- a/docs/desktop-agent-office.md +++ b/docs/desktop-agent-office.md @@ -10,7 +10,7 @@ Windows 与 macOS Desktop 会显示一个透明的像素办公室。单会话有 - “跟随”默认开启,办公室随主窗口当前会话切换。点击门牌会关闭跟随并固定办公室;再次打开跟随后恢复跟随。查看最近五个之外的当前会话不改变门牌排序。 - 主 agent 成功完成后留在座位显示 `zzz`,下一轮自动醒来;成功完成的子 agent 立即离座,重新工作时以同样的外观返回。 - 忙碌的 agent 头顶常驻状态气泡:「图标 动作 · 细节」。正在输出时显示「撰写回复」和最新的几个字(打字动画 + 光标),推理时显示思考点和推理尾巴,调用工具时显示动作(读取、运行命令、修改代码…)和调用预览(路径、命令)。默认「用于编程」模式下服务端不发正文进度,桌宠由 token 事件推断。气泡互相避让:先左右错开,再抬高并用引线连回头顶,放不下时只留动作。 -- 子 agent 出现时从门口走进来,先到派活的人桌前领「任务」信封再回工位;完成后走到派活的人桌前递「回报」信封(派活的人显示「收到…的结果」)再出门;失败或停止的成员直接出门。网状协作中 agent 间消息以带「消息 / 新任务 / 回报」标签的信封在工位间飞过。系统开启「减少动态效果」时不走路,交接信封仍然显示。 +- 子 agent 出现时门先打开,人从门槛淡入走进来,先到派活的人桌前领「任务」信封再回工位,坐下后跳一跳、冒星星、说「报到!」;完成后走到派活的人桌前递「回报」信封(派活的人显示「收到…的结果」),说「再见」后快步出门、在门槛淡出;失败或停止的成员直接道别出门。网状协作中 agent 间消息以带「消息 / 新任务 / 回报」标签的信封在工位间飞过。系统开启「减少动态效果」时不走路,交接信封、「报到!」和原地「再见」仍然显示。 - 等待授权、等待回答、重试、压缩、失败和取消有各自状态。超过七个子角色时点击 `+N` 查看成员并打开对应会话;列表随状态更新,Escape 收起。选中房间人物后双击可打开其会话。 - 文件堆随当前上下文占用增长,压缩后依据实际新统计变薄。工具符号与聊天工具行共用 SVG 来源。 - 拖动房间空地移动窗口;滚轮、右下角把手和右键菜单可缩放。窗口不抢主界面焦点,透明角落可穿透点击;尺寸按原实现保存。 diff --git a/docs/images/agent-office/office-controls-hover.png b/docs/images/agent-office/office-controls-hover.png new file mode 100644 index 00000000..8b55cd1c Binary files /dev/null and b/docs/images/agent-office/office-controls-hover.png differ diff --git a/openspec/changes/enhance-desktop-agent-office-motion/verification.md b/openspec/changes/enhance-desktop-agent-office-motion/verification.md index 4edfc3eb..1b436624 100644 --- a/openspec/changes/enhance-desktop-agent-office-motion/verification.md +++ b/openspec/changes/enhance-desktop-agent-office-motion/verification.md @@ -13,3 +13,7 @@ | Windows 原生 | 隔离用户目录 + 本机假模型(OpenAI 兼容流式,无真实模型调用)运行开发版 Desktop,经 WebView2 调试端口采样:主 agent 推理尾巴 → 撰写回复实时文字 → 派发任务、两名成员进门领「任务」信封入座 → 成员思考、流式回复 → 完成后递「回报」信封、主 agent「收到…的结果」→ zzz。窗口扩展样式保持 TOPMOST/NOACTIVATE/TOOLWINDOW;WindowFromPoint 确认气泡可点、透明角落穿透,注入的浮层矩形并入窗口区域、清除后恢复穿透。 | 未执行:macOS 编译与实机验收(本机为 Windows;`desktop_pet_mac.mm` 仅经代码审查);Linux 保持无桌宠。 + +## 进出场过渡(2026-10-07 追加) + +用户反馈新成员出现太突然、离场要快步出门并道别。改为:门先打开,人在门槛淡入后走进(跨门槛一步约 0.3 秒);坐下后沿用原桌宠新人入职效果——跳一跳、冒星星、说「报到!」;离开时交完回报说「再见」,约 1.7 倍速走到门口在门槛淡出,门随后关上;减少动态效果时原地「报到!」/「再见」。浏览器自动化 47 项通过;本机假模型驱动的 Windows 原生运行采样确认开门、报到、再见、出门顺序。 diff --git a/openspec/changes/hover-desktop-office-controls/verification.md b/openspec/changes/hover-desktop-office-controls/verification.md index 118f3d16..0cead385 100644 --- a/openspec/changes/hover-desktop-office-controls/verification.md +++ b/openspec/changes/hover-desktop-office-controls/verification.md @@ -32,3 +32,15 @@ Temporary evidence: `C:/Users/shao/AppData/Local/Temp/ace-office-controls-native - macOS floating/normal level and deferred close paths were updated and reviewed from source; no macOS compiler or runtime validation was performed on Windows. - Installed Desktop packages are not updated by this local build. The pet HTML is embedded directly by CMake, independently of `web/dist`; the user can run the current development build to see the change. - Existing dirty changes in AGENTS.md, ChannelsSettings.jsx, ComposerSessionControls.jsx and globals.css were left outside this change. No commit or push. + +## Release integration — 0.9.36 + +After implementation, the user authorized release if both Claude App and Codex App had no other active ACECode turn. Claude's local ACECode process reported `idle`; Codex's current runtime had no other in-progress ACECode turn. The earlier retained working-tree changes were reviewed and included through PR #106. + +The fixed audit window is 2026-10-05 01:51:29 through 2026-10-07 01:51:29 (Asia/Taipei). All 46 worktrees were inspected. Claude's completed `38eeec9651aa2698d84f1a4817573d841af7bd3b` adds door entrances, report-in hops and faster goodbyes; its animation code merges with the new window controls. Only the browser script's reported check count conflicted; both sets of assertions were retained and the combined 64-check run passed. + +The two recent preserved upgrade branches (`6d45f1ce`, `8d64540d`) are already present as patches: reverse apply checks pass outside the combined HTTP test file, and all five introduced HTTP tests are byte-identical in master. No duplicate merge is needed. Remaining worktrees are outside the window, and the nine explicitly retained migration refs remain untouched. + +Seed assets, seeding code and tests are unchanged from v0.9.35; revision and manifest remain `2026-10-04.1`. Release binaries are built from the final synchronized master and the release script verifies upgrade behavior and the complete GitHub/aupdate inventory. The final version/commit, CI, hashes and public downloads are recorded in the external release verification JSON rather than adding a post-tag source commit. + +![Office controls after animation integration](../../../docs/images/agent-office/office-controls-hover.png) diff --git a/web/scripts/test-desktop-office.mjs b/web/scripts/test-desktop-office.mjs index bb62329f..f190c8fe 100644 --- a/web/scripts/test-desktop-office.mjs +++ b/web/scripts/test-desktop-office.mjs @@ -95,14 +95,19 @@ try { assert.ok(await page.evaluate(()=>AgentOffice.agents.filter(a=>a.leaving).every(a=>a.walking&&a.state==='done'))); assert.ok(await advanceUntil("AgentOffice.envelopes.some(e=>e.label==='回报'&&e.to==='root')"),'report envelope flies to the dispatcher'); assert.ok(await advanceUntil("AgentOffice.agents.some(a=>a.id==='root'&&/收到.*的结果/.test(a.bubble))"),'dispatcher acknowledges the report'); + assert.ok(await advanceUntil("AgentOffice.agents.some(a=>a.leaving&&a.bubble.includes('再见'))"),'leaving workers say goodbye'); + assert.ok(await advanceUntil("AgentOffice.door>0.6"),'the door opens for the exit'); assert.ok(await advanceUntil("AgentOffice.agents.length===1"),'workers leave through the door'); + assert.ok(await advanceUntil("AgentOffice.door<0.1"),'the door closes again'); assert.deepEqual(await page.evaluate(()=>AgentOffice.agents.map(a=>a.state)),['sleep']); await page.screenshot({path:path.join(output,'office-sleep-688.png')}); // A new worker walks in, receives a task envelope at the dispatcher's desk, then sits down. await apply(data()); assert.ok(await page.evaluate(()=>AgentOffice.agents.filter(a=>!a.root&&a.id!=='root').every(a=>a.walking)),'new workers enter on foot'); + assert.ok(await advanceUntil("AgentOffice.door>0.6",1000),'the door opens as workers step in'); assert.ok(await advanceUntil("AgentOffice.envelopes.some(e=>e.label==='任务'&&e.from==='root')"),'task envelope leaves the dispatcher'); assert.ok(await advanceUntil("AgentOffice.agents.every(a=>!a.walking)"),'workers reach their seats'); + assert.ok(await page.evaluate(()=>AgentOffice.agents.some(a=>a.bubble.includes('报到'))),'arrivals hop and report in at their desks'); assert.equal(await page.evaluate(()=>AgentOffice.agents.find(a=>a.id==='代码审查').look),look); assert.equal(await page.evaluate(()=>AgentOffice.layoutSignature),originalLayout); const toolImage=await page.locator('.office-tool:not([hidden])').first().getAttribute('src'); @@ -130,6 +135,10 @@ try { await apply(data({agents:[actor('root'),actor('代码审查',{path:'/root/review'}),actor('回归测试',{path:'/root/test'}),actor('文档')]})); assert.equal(await page.evaluate(()=>AgentOffice.agents.find(a=>a.id==='文档').walking),false); assert.ok(await page.evaluate(()=>AgentOffice.envelopes.some(e=>e.to==='文档'&&e.label==='任务')),'reduced motion keeps the hand-off'); + assert.match(await page.evaluate(()=>AgentOffice.agents.find(a=>a.id==='文档').bubble),/报到/,'reduced motion still reports in'); + await apply(data({agents:[actor('root'),actor('代码审查',{path:'/root/review'}),actor('回归测试',{path:'/root/test'})]})); + assert.match(await page.evaluate(()=>AgentOffice.agents.find(a=>a.id==='文档')?.bubble||''),/再见/,'reduced motion says goodbye in place'); + assert.ok(await advanceUntil("!AgentOffice.agents.some(a=>a.id==='文档')",2000),'then leaves without walking'); await page.emulateMedia({reducedMotion:'no-preference'}); assert.equal(await page.locator('#officeMembers').isHidden(),true); assert.ok(await page.evaluate(()=>officeActions.some(raw=>{try{const m=JSON.parse(raw);return m.type==='overlay'&&m.rect===null;}catch{return false;}}))); @@ -151,5 +160,5 @@ try { assert.match(await page.locator('#officeNotice').textContent(),/发送消息后/); assert.ok(await page.evaluate(()=>{const notices=officeActions.map(raw=>{try{return JSON.parse(raw);}catch{return {};}}).filter(m=>m.type==='overlay');return Array.isArray(notices.at(-1)?.rect);}), 'native region includes empty-state notice'); assert.deepEqual(errors,[]);assert.deepEqual(external,[]); - console.log(JSON.stringify({output,checks:57,errors,external},null,2)); + console.log(JSON.stringify({output,checks:64,errors,external},null,2)); } finally {await browser.close();}