Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion assets/desktop_pet/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ components:

### 交接

成员加入、完成和 agent 间消息用走路与信封表达:信封带两格残影的抛物线和黄色标签(任务 / 回报 / 消息 / 新任务),落地时收件人头顶冒「!」。走路沿 2 单位网格寻路、不穿家具。
成员加入、完成和 agent 间消息用走路与信封表达。进门时门先向屋里打开,人在门槛上淡入后走进来;坐下时沿用原作新人入职的效果——跳一跳、头顶冒星星、说「报到!」。离开时先说「再见」,再以约 1.7 倍速走到门口、在门槛淡出,门随后关上。信封带两格残影的抛物线和黄色标签(任务 / 回报 / 消息 / 新任务),落地时收件人头顶冒「!」。走路沿 2 单位网格寻路、不穿家具。

### 溢出成员

Expand Down
84 changes: 64 additions & 20 deletions assets/desktop_pet/agent_office_pet.html
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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() 拼的)
Expand All @@ -1052,21 +1064,33 @@

// 走动的组员:进门领任务、回工位、交结果、出门;占用与机器人同样的深度排序
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]) {
const w = a.walk;
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) {
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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]);
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1428,35 +1458,47 @@
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;
a.leaving=true;
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));
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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();},
Expand Down
6 changes: 3 additions & 3 deletions assets/desktop_pet/surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@
| 等成员回报 | 沙漏;气泡显示「等成员回报」 |
| 等待授权、等待回答、重试 | 保留各自的明确状态文案 |
| 主 agent 成功完成 | 留在主工位,显示 `zzz`;下一轮开始后恢复执行状态 |
| 子 agent 加入 | 从门口走进,到派活的人桌前领「任务」信封,再回工位 |
| 子 agent 成功完成 | 走到派活的人桌前递「回报」信封后出门;后续任务出现时再按真实成员恢复 |
| 子 agent 加入 | 门先打开,人从门槛淡入走进来,到派活的人桌前领「任务」信封,回工位坐下后跳一跳、冒星星、说「报到!」 |
| 子 agent 成功完成 | 走到派活的人桌前递「回报」信封,说「再见」,快步走到门口、开门淡出;后续任务出现时再按真实成员恢复 |
| 执行失败或停止 | 本轮保留失败或停止状态,不能表现为成功休息 |
| 整理上下文 | 显示整理状态及机器人提示;文件堆随后跟随新上下文比例 |

文件堆反映当前上下文使用量相对窗口容量的比例,不使用会话累计消耗量。容量未知时不绘制文件堆,不能把未知当作零用量。消息信封由真实成员变化或消息记录触发;减少动态效果偏好下不走路,信封仍然飞过(它表达谁和谁在交接,不是装饰)。
文件堆反映当前上下文使用量相对窗口容量的比例,不使用会话累计消耗量。容量未知时不绘制文件堆,不能把未知当作零用量。消息信封由真实成员变化或消息记录触发;减少动态效果偏好下不走路,信封仍然飞过(它表达谁和谁在交接,不是装饰),新成员在工位上「报到!」,离开的成员原地说「再见」后消失。

状态气泡常驻于忙碌的 agent 头顶,按「求助 > 主 agent > 正在输出 > 刚变化」的优先级摆放:撞上就左右错开或抬高(引线连回头顶),再放不下只显示动作。有人忙时机器人不闲聊。

Expand Down
2 changes: 1 addition & 1 deletion docs/desktop-agent-office.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Windows 与 macOS Desktop 会显示一个透明的像素办公室。单会话有
- “跟随”默认开启,办公室随主窗口当前会话切换。点击门牌会关闭跟随并固定办公室;再次打开跟随后恢复跟随。查看最近五个之外的当前会话不改变门牌排序。
- 主 agent 成功完成后留在座位显示 `zzz`,下一轮自动醒来;成功完成的子 agent 立即离座,重新工作时以同样的外观返回。
- 忙碌的 agent 头顶常驻状态气泡:「图标 动作 · 细节」。正在输出时显示「撰写回复」和最新的几个字(打字动画 + 光标),推理时显示思考点和推理尾巴,调用工具时显示动作(读取、运行命令、修改代码…)和调用预览(路径、命令)。默认「用于编程」模式下服务端不发正文进度,桌宠由 token 事件推断。气泡互相避让:先左右错开,再抬高并用引线连回头顶,放不下时只留动作。
- 子 agent 出现时从门口走进来,先到派活的人桌前领「任务」信封再回工位;完成后走到派活的人桌前递「回报」信封(派活的人显示「收到…的结果」)再出门;失败或停止的成员直接出门。网状协作中 agent 间消息以带「消息 / 新任务 / 回报」标签的信封在工位间飞过。系统开启「减少动态效果」时不走路,交接信封仍然显示。
- 子 agent 出现时门先打开,人从门槛淡入走进来,先到派活的人桌前领「任务」信封再回工位,坐下后跳一跳、冒星星、说「报到!」;完成后走到派活的人桌前递「回报」信封(派活的人显示「收到…的结果」),说「再见」后快步出门、在门槛淡出;失败或停止的成员直接道别出门。网状协作中 agent 间消息以带「消息 / 新任务 / 回报」标签的信封在工位间飞过。系统开启「减少动态效果」时不走路,交接信封、「报到!」和原地「再见」仍然显示。
- 等待授权、等待回答、重试、压缩、失败和取消有各自状态。超过七个子角色时点击 `+N` 查看成员并打开对应会话;列表随状态更新,Escape 收起。选中房间人物后双击可打开其会话。
- 文件堆随当前上下文占用增长,压缩后依据实际新统计变薄。工具符号与聊天工具行共用 SVG 来源。
- 拖动房间空地移动窗口;滚轮、右下角把手和右键菜单可缩放。窗口不抢主界面焦点,透明角落可穿透点击;尺寸按原实现保存。
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -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 原生运行采样确认开门、报到、再见、出门顺序。
12 changes: 12 additions & 0 deletions openspec/changes/hover-desktop-office-controls/verification.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Loading
Loading