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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Canonical root docs are [README.md](README.md), [README_CN.md](README_CN.md), [A
- `acecode_testable` is an INTERFACE aggregate of the production static libraries. All TUI implementations belong to `acecode_tui`; do not add testable subset lists. Executable entry points and the desktop WebView shell stay outside the aggregate. See `cmake/acecode_layer_libraries.cmake` for source ownership.
- Daemon/API work usually touches [src/apps/daemon/](src/apps/daemon), [src/apps/web/](src/apps/web), and [src/domain/session/](src/domain/session). Update [docs/daemon-api.md](docs/daemon-api.md) when protocol behavior changes.
- React/Vite/Tailwind frontend work stays under [web/src/](web/src). Do not edit generated build output directly; regenerate it with the web build.
- 新增弹框默认规则:有关闭按钮时,点击弹框外部不得关闭(使用 `Modal` 时显式设置 `dismissOnBackdrop={false}`);没有关闭按钮时,默认允许点击外部关闭。用户特殊指定的行为优先(例如设置对话框)。此规则面向今后的新增弹框,不追溯修改其他既有弹框;2026-10-07 指定的消息通道连接向导与管理弹框按此规则调整。
- Avoid modifying vendored or submodule trees such as [external/](external), `hermes-agent/`, or `claudecodehaha/` unless the task explicitly targets them.

## OpenSpec Workflow
Expand Down
2 changes: 2 additions & 0 deletions assets/desktop_pet/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ components:

浅纸色紧凑按钮,选中时整面变色。悬停改变底色,键盘焦点有独立轮廓;跟随开关用方形选中标记,并同步 `aria-checked`。

顶部控制条平时隐藏,鼠标进入办公室时显示,离开后保留 1 秒;重入取消隐藏,键盘焦点保持可见。右侧依次放置 pin 与关闭 SVG 按钮,使用同组按钮的颜色、边框与焦点轮廓;pin 的选中底色与 `aria-pressed` 同步原生置顶状态。隐藏控件不截获透明区域的点击,最小窗口仍保留所有按钮。

### 场景状态

角色动作、气泡和工具图标共同呈现状态。忙碌期间气泡常驻,一行「图标 动作 · 细节」:动作用短的现在进行时动词,细节用系统界面字体(路径、命令、英文推理在像素字体里难以辨认),放不下时从开头裁掉,保留最新的字和文件名;正在输出的气泡带闪烁光标。等待授权、等待回答、失败、停止均有明确文案;工具图标只在执行或准备工具时显示。上下文文件堆属于相应工位,依据当前上下文使用率呈现。
Expand Down
68 changes: 63 additions & 5 deletions assets/desktop_pet/agent_office_pet.html
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,18 @@

:root{--office-paper:#fff9e9;--office-ink:#27324f;--office-line:#a8a6a4;--office-selected:#dce7f6}
.office-controls{position:absolute;z-index:5;top:calc(var(--k)*2px);left:calc(var(--k)*4px);right:calc(var(--k)*4px);height:calc(var(--k)*26px);display:flex;align-items:center;gap:4px;font:12px/1.3 -apple-system,BlinkMacSystemFont,"PingFang SC","Segoe UI",sans-serif;color:var(--office-ink)}
.office-controls{opacity:0;pointer-events:none}
.office-controls.is-visible{opacity:1;pointer-events:auto}
.office-tabs{display:flex;gap:3px;flex:1;min-width:0}
.office-tab,.office-follow,.office-overflow{font:inherit;color:inherit;background:var(--office-paper);border:1px solid var(--office-line);border-radius:4px;cursor:pointer;min-width:0;padding:5px 7px;line-height:1.3}
.office-tab,.office-follow,.office-overflow,.office-window-button{font:inherit;color:inherit;background:var(--office-paper);border:1px solid var(--office-line);border-radius:4px;cursor:pointer;min-width:0;padding:5px 7px;line-height:1.3}
.office-tab{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.office-tab[aria-selected="true"]{background:var(--office-selected);border-color:#526a91}
.office-tab:hover,.office-follow:hover,.office-overflow:hover{background:#e6ecf5}
.office-tab:focus-visible,.office-follow:focus-visible,.office-overflow:focus-visible{outline:2px solid #4378be;outline-offset:1px}
.office-tab:hover,.office-follow:hover,.office-overflow:hover,.office-window-button:hover{background:#e6ecf5}
.office-tab:focus-visible,.office-follow:focus-visible,.office-overflow:focus-visible,.office-window-button:focus-visible{outline:2px solid #4378be;outline-offset:1px}
.office-window-button{flex:none;display:flex;align-items:center;justify-content:center;width:26px;height:26px;padding:4px}
.office-window-button svg{width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
.office-pin[aria-pressed="true"]{background:var(--office-selected);border-color:#526a91}
.office-pin[aria-pressed="false"] svg{transform:rotate(45deg)}
.office-follow{flex:none;display:flex;align-items:center;gap:4px;white-space:nowrap}
.office-follow::before{content:"";display:block;width:8px;height:8px;border:1px solid #65738b;border-radius:2px}
.office-follow[aria-checked="true"]::before{background:#456b9c;border-color:#456b9c}
Expand All @@ -62,14 +68,22 @@
.office-members button:hover{background:var(--office-selected)}
@media(max-width:400px){.office-controls{font-size:11px;gap:2px}.office-tab,.office-follow{padding:2px 3px}.office-tabs{gap:2px}.office-follow{gap:2px}.office-follow::before{width:6px;height:6px}}
@media(max-width:250px){.office-tab,.office-follow{padding:0 1px;line-height:1}.office-follow::before{width:5px;height:5px}}
@media(max-width:400px){.office-window-button{width:22px;height:22px;padding:3px}}
@media(max-width:250px){.office-window-button{width:16px;height:14px;padding:0}.office-window-button svg{width:10px;height:10px}}
@media(prefers-reduced-motion:reduce){.grip{transition:none}.bub.type .bub-detail::after{animation:none}}
</style>
</head>
<body>
<div class="scene" id="scene">
<div class="office-controls" aria-label="办公室切换">
<div class="office-controls" id="officeControls" aria-label="办公室控制">
<div class="office-tabs" id="officeTabs" role="tablist" aria-label="最近发消息的五个会话"></div>
<button class="office-follow" id="officeFollow" type="button" role="switch" aria-checked="true" title="跟随主窗口当前会话">跟随</button>
<button class="office-window-button office-pin" id="officePin" type="button" aria-pressed="true" aria-label="置顶办公室" title="取消置顶">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M16 9V3H8v6l-3 4v3h14v-3l-3-4ZM12 16v5"/></svg>
</button>
<button class="office-window-button" id="officeClose" type="button" aria-label="关闭办公室" title="关闭办公室">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m6 6 12 12M18 6 6 18"/></svg>
</button>
</div>
<div class="office-notice" id="officeNotice" role="status">正在连接会话</div>
<button class="office-overflow" id="officeOverflow" type="button" hidden></button>
Expand Down Expand Up @@ -1252,6 +1266,42 @@

/* ---------- 10. 桌面宠物交互(原页面的面板 / 按钮 / 花名册都去掉了) ---------- */
const post = msg => { try { if (window.chrome && chrome.webview) chrome.webview.postMessage(msg); } catch (e) { /* 普通浏览器里打开时没有宿主 */ } };
const officeControls = $('officeControls'), officeScene = $('scene');
const CONTROLS_HIDE_DELAY_MS = 1000;
let controlsHideTimer = 0, officeHovered = false, keyboardNavigation = false, officePinned = true;
const hasKeyboardFocus = () => keyboardNavigation && officeScene.contains(document.activeElement);
function showOfficeControls() {
clearTimeout(controlsHideTimer);
if (officeControls.classList.contains('is-visible')) return;
officeControls.classList.add('is-visible');
reportOfficeRegions();
}
function scheduleControlsHide() {
clearTimeout(controlsHideTimer);
if (officeHovered || hasKeyboardFocus()) return;
controlsHideTimer = setTimeout(() => {
if (officeHovered || hasKeyboardFocus()) return;
officeControls.classList.remove('is-visible');
reportOfficeRegions();
}, CONTROLS_HIDE_DELAY_MS);
}
officeScene.addEventListener('pointerenter', () => { officeHovered = true; showOfficeControls(); });
officeScene.addEventListener('pointerleave', () => { officeHovered = false; scheduleControlsHide(); });
document.addEventListener('keydown', () => { keyboardNavigation = true; if (hasKeyboardFocus()) showOfficeControls(); });
document.addEventListener('pointerdown', () => { keyboardNavigation = false; scheduleControlsHide(); }, true);
officeScene.addEventListener('focusin', () => { if (hasKeyboardFocus()) showOfficeControls(); });
officeScene.addEventListener('focusout', e => {
if (!officeScene.contains(e.relatedTarget)) { keyboardNavigation = false; scheduleControlsHide(); }
});
window.addEventListener('blur', () => { keyboardNavigation = false; officeHovered = false; scheduleControlsHide(); });
window.addEventListener('pagehide', () => clearTimeout(controlsHideTimer));
function syncOfficePin(pinned) {
officePinned = pinned;
$('officePin').setAttribute('aria-pressed', String(pinned));
$('officePin').title = pinned ? '取消置顶' : '置顶办公室';
}
$('officePin').addEventListener('click', () => action({type:'pin', pinned:!officePinned}));
$('officeClose').addEventListener('click', () => action({type:'close'}));
let readySent = false;
function notifyReady() { if (!readySent) { readySent = true; post('ready'); } } // 第一帧画完再让原生把窗口挪进屏幕
function renderTask() {}
Expand Down Expand Up @@ -1341,6 +1391,10 @@
badgeTimer = setTimeout(() => { b.hidden = true; }, 900);
}
if (hostAttached()) chrome.webview.addEventListener('message', e => {
if (e.data?.type === 'pet-window-state') {
if (typeof e.data.pinned === 'boolean') syncOfficePin(e.data.pinned);
return;
}
if (e.data && typeof e.data === 'object') {applyOfficeSnapshot(e.data);return;}
const m = String(e.data || '');
if (m.startsWith('size ')) showBadge(m.slice(5) + '%');
Expand Down Expand Up @@ -1503,7 +1557,11 @@
const rects=[$('officeMembers'),$('officeNotice')].filter(node=>node&&!node.hidden).map(node=>node.getBoundingClientRect());
const rect=rects.length?[Math.min(...rects.map(r=>r.left))/innerWidth,Math.min(...rects.map(r=>r.top))/innerHeight,
Math.max(...rects.map(r=>r.right))/innerWidth,Math.max(...rects.map(r=>r.bottom))/innerHeight]:null;
action({type:'overlay',rect,bubbles:bubbleRegions});
const bounds=officeControls.getBoundingClientRect();
const controls=officeControls.classList.contains('is-visible')
? [Math.max(0,bounds.left-3)/innerWidth,Math.max(0,bounds.top-3)/innerHeight,
Math.min(innerWidth,bounds.right+3)/innerWidth,Math.min(innerHeight,bounds.bottom+3)/innerHeight] : null;
action({type:'overlay',rect,controls,bubbles:bubbleRegions});
}
function syncOfficeMembers() {
const list=$('officeMembers');
Expand Down
4 changes: 2 additions & 2 deletions assets/desktop_pet/surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## 视觉与首屏

透明等距房间占据主体,顶部仅保留最近会话和跟随开关。原作家具、五套布局模板及像素角色保持;场景不扩展为管理页面。首次连接前显示空房间,连接或读取提示位于控制条下方。
透明等距房间占据主体,顶部菜单包含最近会话、跟随开关及右侧的 pin / 关闭按钮,鼠标进入时显示、移出 1 秒后隐藏,键盘焦点保持可见。原作家具、五套布局模板及像素角色保持;场景不扩展为管理页面。首次连接前显示空房间,连接或读取提示位于控制条下方。

## 会话与成员

Expand Down Expand Up @@ -37,7 +37,7 @@

- 拖动场景移动窗口;单击角色查看名称和状态,双击打开对应会话。
- 滚轮或把手调整窗口大小,把手双击恢复默认大小;控件和成员列表中的滚动不触发场景缩放。
- 右键交给原生菜单。桌面窗口保持透明、置顶及不抢焦点约束;主动打开会话属于独立操作。
- 右键交给原生菜单。桌面窗口保持透明及不抢焦点,默认置顶;pin 可切换到普通窗口层级,缩放和停靠不改变选择。关闭只影响桌宠;主动打开会话属于独立操作。
- 成员列表独立滚动,点击成员打开会话并关闭列表,Escape 关闭列表;列表区域与原生命中区域保持同步。
- 连接中断时保留最后快照并显示重连提示,不能继续伪造实时进展。

Expand Down
2 changes: 2 additions & 0 deletions docs/desktop-agent-office.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Windows 与 macOS Desktop 会显示一个透明的像素办公室。单会话有

## 操作

- 顶部菜单在鼠标移入办公室时显示,移出后保留 1 秒再隐藏;重新移入会取消隐藏,键盘操作时保持可见。隐藏菜单的透明区域可穿透点击。
- 菜单右侧 pin 控制办公室是否始终置顶:默认置顶,取消后可被其他普通窗口覆盖,拖动、缩放和停靠均保持当前选择;重新启动桌宠恢复默认置顶。叉号只关闭办公室,不退出 ACECode,也不停止 agent 任务。
- 顶部显示跨工作区最近发过消息的五个主会话。顺序只看用户最后发送消息的时间,打开、切换和 agent 输出不会把会话顶到最前面。
- “跟随”默认开启,办公室随主窗口当前会话切换。点击门牌会关闭跟随并固定办公室;再次打开跟随后恢复跟随。查看最近五个之外的当前会话不改变门牌排序。
- 主 agent 成功完成后留在座位显示 `zzz`,下一轮自动醒来;成功完成的子 agent 立即离座,重新工作时以同样的外观返回。
Expand Down
2 changes: 2 additions & 0 deletions openspec/changes/hover-desktop-office-controls/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-07
23 changes: 23 additions & 0 deletions openspec/changes/hover-desktop-office-controls/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
## Context

顶部 `.office-controls` 常驻;Windows 创建窗口与 `apply_placement` 都使用 TOPMOST,macOS 使用 floating NSPanel。两端目前无条件把顶部 32 场景像素计入命中区域。共享页面通过 overlay 消息上报气泡与成员列表。

## Goals / Non-Goals

实现用户指定的悬停菜单、置顶切换与关闭。保留房间、动画、会话切换、跟随和拖动缩放。不改 daemon 协议,不增加用户设置页面;pin 状态在当前桌宠生命周期内保留,重启默认置顶。

## Decisions

- 页面管理唯一一个 1000ms 隐藏计时器。进入场景、菜单或键盘焦点时取消计时;离开后计时。鼠标点击留下的焦点不会永久锁住菜单,键盘焦点则保持可见。
- 控件隐藏使用 opacity 与 pointer-events,不移除键盘顺序;焦点进入时显现。保留现有浅纸色按钮,pin 以 aria-pressed 和选中底色表达状态,两个按钮都有中文可访问名称。
- overlay 新增独立 `controls` 矩形,隐藏时为 null;原生只合并实际显示的控件矩形与房间/气泡,移除固定顶部区域。几何解析仍执行边界、类型和数量限制。
- `pin` 消息携带 boolean;宿主以 `pet-window-state` 回传实际 pinned 状态,ready 时同样回传。Windows 用 SetWindowPos TOPMOST/NOTOPMOST,普通布局调用改为 SWP_NOZORDER;macOS 切换 floating/normal level。
- `close` 通过宿主下一轮窗口消息/主队列调用现有 close,避免在 WebView 回调中销毁自身。主界面和 agent 任务继续运行。

## Risks / Trade-offs

菜单与房间之间有透明空隙,延迟隐藏允许鼠标跨越空隙。最小 172px 宽度仍须容纳五个会话、跟随、pin 和关闭,按钮尺寸随现有紧凑断点收紧。Windows 本机编译与浏览器自动化验证;macOS 修改只做源码核对,无法在 Windows 宣称实测。

## Validation

扩展现有浏览器回归,检查默认隐藏、进入/离开/重新进入、鼠标与键盘焦点、pin 消息/回执、关闭和窄窗口。原生几何回归验证隐藏区域穿透、显示区域命中和浮层限制。Windows 增量构建并运行 DesktopPetLayout 测试;记录源码验证与运行中 Desktop 验证边界。
22 changes: 22 additions & 0 deletions openspec/changes/hover-desktop-office-controls/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
## Why

办公室顶部控件常驻会遮住桌面,且当前原生窗口始终置顶。用户希望控件按需出现,并能直接切换置顶或关闭桌宠。

## What Changes

- 鼠标进入办公室立即显示顶部菜单;移出后延迟 1000ms 隐藏,重新进入取消隐藏。键盘操作保留可见焦点。
- 菜单最右侧增加 SVG pin 与关闭按钮。默认保持置顶,取消 pin 后按普通窗口层级显示;缩放、停靠和 DPI 更新不重置层级。
- 隐藏的菜单不占用原生点击区域;关闭仅影响桌宠。

## Capabilities

### New Capabilities
- `desktop-office-window-controls`: 桌宠菜单显隐、置顶开关及关闭操作。

### Modified Capabilities

无。

## Impact

共享桌宠 HTML、Windows/WebView2 与 macOS/WKWebView 宿主、原生命中区域解析、现有办公室浏览器和几何测试。保持 Claude 已增强的场景动画、会话快照与美术实现。
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
## ADDED Requirements

### Requirement: Hover controls with delayed hiding
The office SHALL hide its top controls at rest, reveal them immediately on hover, and wait 1000ms after pointer exit before hiding. Re-entry SHALL cancel pending hiding, and keyboard focus SHALL keep controls visible.

#### Scenario: Cross the transparent gap to the menu
- **WHEN** the pointer leaves the room and enters the menu within 1000ms
- **THEN** the menu remains visible and usable

#### Scenario: Pointer leaves after clicking a control
- **WHEN** a mouse click leaves focus on a menu button and the pointer exits the office
- **THEN** the menu hides after 1000ms and its native hit region is removed

#### Scenario: Keyboard navigation
- **WHEN** keyboard focus enters the controls
- **THEN** controls become visible and remain visible while keyboard focus stays within the office controls

### Requirement: Native pin state
The office SHALL provide a pin button at the right side of its menu, default to pinned, and acknowledge the native state through the bridge. Unpinned windows SHALL be coverable by normal windows and remain unpinned during size and placement updates.

#### Scenario: Unpin and resize
- **WHEN** the user unpins the office and then zooms, resets size or docks it
- **THEN** it retains the normal window level

#### Scenario: Pin and reload
- **WHEN** the user pins the office or the office page reloads
- **THEN** the button reflects the actual native pinned state

### Requirement: Close only the office
The office SHALL provide an accessible close icon after the pin button and close only the pet window using the native close lifecycle.

#### Scenario: Close from the top menu
- **WHEN** the user activates the close button
- **THEN** the pet closes while the main Desktop and agent sessions continue
10 changes: 10 additions & 0 deletions openspec/changes/hover-desktop-office-controls/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
## 1. Implementation

- [x] 1.1 实现菜单延迟显隐、键盘焦点、pin/关闭按钮与桥回执。
- [x] 1.2 接通 Windows/macOS 窗口层级及关闭,修正动态命中区域。
- [x] 1.3 扩展浏览器与几何回归,同步用户文档及局部视觉规则。

## 2. Validation

- [x] 2.1 运行办公室浏览器回归、Windows 增量编译及 DesktopPetLayout 测试。
- [x] 2.2 完成 OpenSpec strict 校验、差异检查,记录平台实测边界。
Loading
Loading