Skip to content

fix(window): stabilize pet dragging and decouple agent integrations - #36

Merged
LRainner merged 3 commits into
masterfrom
codex/fix-windows-drag-state
Aug 1, 2026
Merged

fix(window): stabilize pet dragging and decouple agent integrations#36
LRainner merged 3 commits into
masterfrom
codex/fix-windows-drag-state

Conversation

@LRainner

@LRainner LRainner commented Aug 1, 2026

Copy link
Copy Markdown
Owner

背景

Windows 原生窗口拖动与 WebView 指针事件并不共享同一套可靠的结束时序。此前拖动状态散落在 main.ts 中,并依赖 pointerup、blur 和多个延迟计时器进行清理,容易产生以下问题:

  • 拖动动画只播放一瞬间便恢复
  • 窗口移动事件反复强制渲染,导致动画不断从首帧重播
  • 松开后偶发无法清除 dragging/running 状态
  • Agent 事件在拖动期间更新时,也可能穿透拖动层并重启动画

同时,实时状态链路以 Codex 事件和 Codex 配置为中心。后续接入其他 Agent 时,需要同时修改事件通道、状态控制器、状态窗口和会话账本,存在明显的耦合风险。

设计与实现

独立的拖动状态机

  • 新增 PetDragController,集中管理 idle、pressed、active 三种状态
  • 使用 dragId 配对每一次拖动,忽略迟到或不属于当前会话的结束事件
  • main.ts 只负责 DOM 与 Tauri 事件接线,不再维护拖动布尔变量和清理计时器
  • 位置持久化异步执行,不阻塞松开后的视觉状态恢复
  • 点击抑制只覆盖拖动完成后产生的合成点击,不影响正常交互

Windows 原生生命周期

  • 将平台逻辑隔离到 window_drag 模块
  • Windows 通过窗口子类化监听 WM_ENTERSIZEMOVE 和 WM_EXITSIZEMOVE
  • 以系统原生移动循环作为权威拖动生命周期,不再猜测 pointerup/blur 的触发顺序
  • 非 Windows 平台继续使用 WebView 指针释放完成拖动

稳定的动画优先级

  • ReactionController.setDragging() 改为幂等,同方向更新不会重新渲染
  • 方向判断使用累计水平位移阈值,过滤坐标抖动,同时支持缓慢反向拖动
  • 拖动成为最高优先级视觉层
  • 拖动期间 Agent 状态仍正常更新,但不能强制拖动动画回到首帧
  • 松开后统一恢复最新的 working、waiting、stalled 或 idle 状态

通用 Agent 接入层

  • 后端统一通过 agent-event 事件中心发布和缓存 Agent 事件
  • Hook 服务只负责 Codex 输入解析,不再直接控制状态窗口
  • 前端新增 AgentAdapterRegistry,将各 Agent 的原始事件规范化为统一语义事件
  • Codex 作为第一个内置适配器,现有配置格式保持兼容
  • ReactionController、LiveStatusController 和 TerminalEventLedger 只消费规范化事件
  • 内部会话键改为 agent + sessionId,避免不同 Agent 的同名会话相互覆盖

后续接入新 Agent 时,只需要:

  1. 增加该 Agent 的事件采集/发布端
  2. 实现一个 AgentAdapter,将原始事件映射到统一协议
  3. 在组合入口注册适配器

宠物反应、实时状态和终止事件账本不需要针对新 Agent 修改。

兼容性

  • 不迁移现有 Codex 配置,避免扩大本次变更范围
  • 保留现有宠物资源和 Codex Hook 行为
  • 保留开发环境 devCsp,允许 Vite HMR WebSocket
  • 生产 CSP 保持不变

测试

  • npm test:9 个测试文件,61 个测试通过
  • npm run build:TypeScript 类型检查及 Vite 生产构建通过
  • cargo fmt --check:通过
  • cargo check --locked:通过
  • cargo test --locked:50 个测试通过
  • git diff --check:通过

新增回归覆盖包括:

  • 拖动只启动一次,同方向移动不重启动画
  • 有效水平反向才切换动画方向
  • Windows 结束事件必须匹配当前 dragId
  • 原生启动失败和 WebView 提前释放能够恢复
  • 拖动期间 Agent 状态更新不会重播拖动首帧
  • 第二个 Agent 可以通过适配器接入而不修改核心消费者
  • 不同 Agent 使用相同 sessionId 时状态与终止账本保持隔离

手动验证建议

Windows 开发环境下重点验证长时间拖动、缓慢左右反向、快速连续拖动、纵向移动,以及拖动过程中收到 Agent 事件后松开能否恢复最新宠物状态。

Summary by CodeRabbit

  • New Features
    • Added support for normalized events from multiple agent types, with agent-specific names, statuses, and session tracking.
    • Added cross-platform pet window dragging with improved native Windows behavior, completion handling, and position persistence.
    • Added centralized live-event updates across the main window, status display, and settings.
  • Bug Fixes
    • Prevented cross-agent session collisions and unnecessary animation restarts.
    • Improved recovery from failed, overlapping, or interrupted drag operations.
  • Tests
    • Expanded coverage for agent events, session isolation, status handling, and dragging behavior.

Use a dedicated drag state machine with native Windows lifecycle events, and route agent activity through a normalized adapter registry with cross-agent session isolation.
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 117c61f4-857b-45a6-b4d8-1b2832d4dfa8

📥 Commits

Reviewing files that changed from the base of the PR and between 8720759 and 423d011.

📒 Files selected for processing (3)
  • src-tauri/src/lib.rs
  • src-tauri/src/window_drag/unix.rs
  • src-tauri/src/window_drag/windows.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • src-tauri/src/window_drag/unix.rs
  • src-tauri/src/lib.rs
  • src-tauri/src/window_drag/windows.rs

📝 Walkthrough

Walkthrough

The PR adds multi-agent event contracts and Codex normalization, centralizes Tauri event publication, qualifies session identity by agent, and introduces cross-platform drag lifecycle handling with native Windows support.

Changes

Agent events and dragging

Layer / File(s) Summary
Agent contracts and adapter registry
src/agents/*, src/types.ts
Adds shared agent event types, Codex normalization, adapter registration, configuration flags, runtime signatures, and agent-qualified session keys.
Centralized Tauri event publication
src-tauri/src/agent_events.rs, src-tauri/src/hook_server.rs, src-tauri/src/lib.rs, src-tauri/tauri.conf.json
Adds shared event caching and publication to Tauri targets. Live-event retrieval uses the shared cache. Development CSP permits the local WebSocket connection.
Native and webview drag lifecycle
src/pet-drag-controller.ts, src/main.ts, src-tauri/src/window_drag*, src-tauri/src/lib.rs
Adds drag state management, native Windows completion handling, webview completion handling, overlap rejection, cancellation, click suppression, and position persistence.
Agent-aware status and reaction processing
src/status.ts, src/live-status.ts, src/reaction-controller.ts, src/terminal-event-ledger.ts, src/settings.ts, src/main.ts, src/*test.ts
Updates event consumers to normalize agent events, use agent-qualified session keys, render agent-specific labels, reset on runtime changes, and preserve drag animation state.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant AgentSource
  participant hook_server
  participant agent_events
  participant status
  participant LiveStatusController
  AgentSource->>hook_server: receive agent event
  hook_server->>agent_events: publish normalized payload
  agent_events->>status: emit agent-event
  status->>LiveStatusController: normalize and forward enabled event
  LiveStatusController->>LiveStatusController: update agent-qualified session state
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the two primary changes: stabilizing pet dragging and decoupling agent integrations.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
src-tauri/src/lib.rs (1)

524-525: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Add regression tests for the existing AgentEvent wire format. AgentEvent already applies #[serde(rename_all = "camelCase")]. Test both get_live_event and the agent-event payload.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/lib.rs` around lines 524 - 525, Add regression tests covering
the existing AgentEvent wire format: verify get_live_event serializes fields
using camelCase and verify the agent-event payload uses the same format. Reuse
the current AgentEvent fixtures or construction path and assert both event
retrieval and payload serialization without changing the production
serialization behavior.
src/pet-drag-controller.ts (1)

113-120: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Log the position persistence failure.

Line 119 discards every savePosition rejection. A repeated failure to persist the pet position then produces no signal. Add a console.warn in the catch handler, or pass an error callback through DragEffects.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pet-drag-controller.ts` around lines 113 - 120, Update the savePosition
rejection handling in PetDragController.finish to emit a console.warn containing
the persistence error instead of silently discarding it. Preserve the existing
asynchronous cleanup behavior and DragEffects interface unless using its error
callback is already established.
src-tauri/src/window_drag/windows.rs (1)

40-43: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Guard against a panic crossing the window procedure.

mark_entered and finish run inside a native window procedure. A panic that unwinds across the extern "system" boundary aborts the process. Both helpers handle lock errors, so the current risk is low. Wrapping the match body in std::panic::catch_unwind would make the handler resilient to future changes in the helpers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/window_drag/windows.rs` around lines 40 - 43, Wrap the native
window procedure’s match body, including the WM_NCDESTROY cleanup and calls to
mark_entered and finish, in std::panic::catch_unwind so panics cannot unwind
across the extern "system" boundary. Preserve the existing message handling and
cleanup behavior while containing any panic within the handler.
src/pet-drag-controller.test.ts (1)

90-99: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add coverage for reset() and the suppression boundary.

Two gaps remain. reset() is public and clears the drag animation, but no test calls it. The suppression test advances 181 ms, so it does not pin the boundary behavior at exactly CLICK_SUPPRESSION_MS.

Add a test that calls reset() during an active drag and asserts directions ends with null and stateKind() returns "idle". Add an assertion at exactly 180 ms.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pet-drag-controller.test.ts` around lines 90 - 99, Extend the drag
controller tests around the existing completed-drag suppression case to assert
shouldSuppressClick() remains true at exactly 180 ms, preserving the existing
post-boundary false assertion. Add coverage for the public reset() method during
an active drag, verifying directions ends with null and stateKind() returns
"idle".
src-tauri/src/window_drag.rs (1)

41-57: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Allow dead_code on non-Windows builds

When non-Windows non-test targets are checked with -D warnings, NativeDragRegistry methods have no production callers. Add #[cfg_attr(not(target_os = "windows"), allow(dead_code))] to the impl NativeDragRegistry block.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/window_drag.rs` around lines 41 - 57, Add
#[cfg_attr(not(target_os = "windows"), allow(dead_code))] to the impl
NativeDragRegistry block so its methods compile without dead-code warnings on
non-Windows non-test builds, while preserving existing behavior on Windows.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src-tauri/src/window_drag.rs`:
- Around line 32-58: The native exit path must recover when no enter signal
arrives. In src-tauri/src/window_drag.rs lines 32-58, update
NativeDragRegistry::finish to clear active unconditionally while returning the
id only when the session was entered, and adjust the test at lines 118-126
accordingly. In src/pet-drag-controller.ts lines 61-69, start a bounded fallback
timer when releaseRequested becomes true and completion mode is "native", and
invoke finish(id) when it expires.

In `@src/status.ts`:
- Around line 100-107: Update acceptEvent so the config and showsAgentLiveStatus
gate runs before computing or storing lastEventKey; only assign the
deduplication key and call controller.setAgentEvent after the event is eligible
for forwarding, allowing startup catch-up to process events received before
configuration loads.

---

Nitpick comments:
In `@src-tauri/src/lib.rs`:
- Around line 524-525: Add regression tests covering the existing AgentEvent
wire format: verify get_live_event serializes fields using camelCase and verify
the agent-event payload uses the same format. Reuse the current AgentEvent
fixtures or construction path and assert both event retrieval and payload
serialization without changing the production serialization behavior.

In `@src-tauri/src/window_drag.rs`:
- Around line 41-57: Add #[cfg_attr(not(target_os = "windows"),
allow(dead_code))] to the impl NativeDragRegistry block so its methods compile
without dead-code warnings on non-Windows non-test builds, while preserving
existing behavior on Windows.

In `@src-tauri/src/window_drag/windows.rs`:
- Around line 40-43: Wrap the native window procedure’s match body, including
the WM_NCDESTROY cleanup and calls to mark_entered and finish, in
std::panic::catch_unwind so panics cannot unwind across the extern "system"
boundary. Preserve the existing message handling and cleanup behavior while
containing any panic within the handler.

In `@src/pet-drag-controller.test.ts`:
- Around line 90-99: Extend the drag controller tests around the existing
completed-drag suppression case to assert shouldSuppressClick() remains true at
exactly 180 ms, preserving the existing post-boundary false assertion. Add
coverage for the public reset() method during an active drag, verifying
directions ends with null and stateKind() returns "idle".

In `@src/pet-drag-controller.ts`:
- Around line 113-120: Update the savePosition rejection handling in
PetDragController.finish to emit a console.warn containing the persistence error
instead of silently discarding it. Preserve the existing asynchronous cleanup
behavior and DragEffects interface unless using its error callback is already
established.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6cb283c0-d08c-4b85-a160-3665ec98b75c

📥 Commits

Reviewing files that changed from the base of the PR and between cf429bd and 06c6ce1.

📒 Files selected for processing (23)
  • src-tauri/src/agent_events.rs
  • src-tauri/src/hook_server.rs
  • src-tauri/src/lib.rs
  • src-tauri/src/window_drag.rs
  • src-tauri/src/window_drag/windows.rs
  • src-tauri/tauri.conf.json
  • src/agents/codex.ts
  • src/agents/index.ts
  • src/agents/registry.test.ts
  • src/agents/registry.ts
  • src/agents/types.ts
  • src/live-status.test.ts
  • src/live-status.ts
  • src/main.ts
  • src/pet-drag-controller.test.ts
  • src/pet-drag-controller.ts
  • src/reaction-controller.test.ts
  • src/reaction-controller.ts
  • src/settings.ts
  • src/status.ts
  • src/terminal-event-ledger.test.ts
  • src/terminal-event-ledger.ts
  • src/types.ts

Comment thread src-tauri/src/window_drag.rs Outdated
Comment on lines +32 to +58
impl NativeDragRegistry {
fn begin(&mut self, id: u64) -> Result<(), String> {
if self.active.is_some() {
return Err("已有窗口拖动正在进行".into());
}
self.active = Some(NativeDragSession { id, entered: false });
Ok(())
}

fn entered(&mut self) {
if let Some(active) = self.active.as_mut() {
active.entered = true;
}
}

fn cancel(&mut self, id: u64) {
if self.active.is_some_and(|active| active.id == id) {
self.active = None;
}
}

fn finish(&mut self) -> Option<u64> {
let active = self.active.filter(|active| active.entered)?;
self.active = None;
Some(active.id)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

A native drag with no completion signal strands both layers. The backend registry and the frontend controller both wait for the native completion path, and neither has a recovery route. If WM_ENTERSIZEMOVE does not arrive, WM_EXITSIZEMOVE produces no DragEnded, so the backend session stays occupied and the frontend state stays "active". Later drags then fail, and the pet keeps the drag animation.

  • src-tauri/src/window_drag.rs#L32-L58: make the exit path clear active unconditionally, and report the id only for an entered session. Update the test at lines 118-126 to match.
  • src/pet-drag-controller.ts#L61-L69: start a bounded timer when releaseRequested becomes true and the completion mode is "native". Call finish(id) when the timer expires.
📍 Affects 2 files
  • src-tauri/src/window_drag.rs#L32-L58 (this comment)
  • src/pet-drag-controller.ts#L61-L69
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/window_drag.rs` around lines 32 - 58, The native exit path must
recover when no enter signal arrives. In src-tauri/src/window_drag.rs lines
32-58, update NativeDragRegistry::finish to clear active unconditionally while
returning the id only when the session was entered, and adjust the test at lines
118-126 accordingly. In src/pet-drag-controller.ts lines 61-69, start a bounded
fallback timer when releaseRequested becomes true and completion mode is
"native", and invoke finish(id) when it expires.

Comment thread src/status.ts
Comment on lines +100 to 107
function acceptEvent(payload: RawAgentEvent): void {
const event = normalizeAgentEvent(payload);
if (!event) return;
const key = agentEventKey(event);
if (key === lastEventKey) return;
lastEventKey = key;
if (config?.codex.hooksEnabled && config.codex.showLiveStatus) controller.setAgentEvent(payload);
if (config && showsAgentLiveStatus(config, event.agent)) controller.setAgentEvent(event);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fix event loss when config is not yet loaded.

lastEventKey is set unconditionally, before the config and showsAgentLiveStatus gate. If an agent event arrives before config loads, the function records lastEventKey but never calls controller.setAgentEvent. The startup catch-up call to get_live_event later fetches the same event, but the dedup check on lastEventKey now discards it too. The event is lost permanently.

Move the config gate before the dedup key assignment, so the key is only recorded when the event is actually forwarded.

🐛 Proposed fix
 function acceptEvent(payload: RawAgentEvent): void {
   const event = normalizeAgentEvent(payload);
   if (!event) return;
-  const key = agentEventKey(event);
-  if (key === lastEventKey) return;
-  lastEventKey = key;
-  if (config && showsAgentLiveStatus(config, event.agent)) controller.setAgentEvent(event);
+  if (!config || !showsAgentLiveStatus(config, event.agent)) return;
+  const key = agentEventKey(event);
+  if (key === lastEventKey) return;
+  lastEventKey = key;
+  controller.setAgentEvent(event);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function acceptEvent(payload: RawAgentEvent): void {
const event = normalizeAgentEvent(payload);
if (!event) return;
const key = agentEventKey(event);
if (key === lastEventKey) return;
lastEventKey = key;
if (config?.codex.hooksEnabled && config.codex.showLiveStatus) controller.setAgentEvent(payload);
if (config && showsAgentLiveStatus(config, event.agent)) controller.setAgentEvent(event);
}
function acceptEvent(payload: RawAgentEvent): void {
const event = normalizeAgentEvent(payload);
if (!event) return;
if (!config || !showsAgentLiveStatus(config, event.agent)) return;
const key = agentEventKey(event);
if (key === lastEventKey) return;
lastEventKey = key;
controller.setAgentEvent(event);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/status.ts` around lines 100 - 107, Update acceptEvent so the config and
showsAgentLiveStatus gate runs before computing or storing lastEventKey; only
assign the deduplication key and call controller.setAgentEvent after the event
is eligible for forwarding, allowing startup catch-up to process events received
before configuration loads.

Select a complete Windows or Unix drag module at the facade and export the same API from each platform, avoiding cross-platform dead code under strict Clippy.
@LRainner
LRainner merged commit 4347640 into master Aug 1, 2026
8 checks passed
@LRainner
LRainner deleted the codex/fix-windows-drag-state branch August 1, 2026 13:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant