修正開啟追視後 renderer 崩潰,追視改為只在待機時生效

摘要:
startAnim 對 look 會丟 TypeError 導致動畫迴圈整個死掉;另把追視改成只在 idle 時覆蓋。

根本原因:
1. 追視是單一靜態擺姿,TIMINGS 裡沒有 look 這個動作。但序列模型改寫後,
   tick 在動畫改變時無條件呼叫 startAnim(anim),傳入 "look" 時會走到
   cycleOf("look") → TIMINGS["look"].map → TypeError。
   例外從 requestAnimationFrame 的 callback 拋出,迴圈就此中止,
   整隻寵物凍結,也不再回應任何 hook 事件——就是使用者回報的「直接沒反應」。
2. 對照 Codex 時把 lookFrame 改成覆蓋所有狀態。但 Codex 的追視來源是 quick chat 的
   文字游標,只有使用者真的在打字時才存在;滑鼠卻是一直都在,照搬會讓工作中的
   running / waiting / review 動畫永遠被靜態擺姿蓋掉,寵物失去反映狀態的作用。

影響:
選單一勾「跟著游標看」,游標一動寵物就完全凍結,且不會恢復。

修法:
- startAnim 開頭對 look 早退,直接設成空的幀序列(靜態擺姿),
  不進入 cycleOf / baseSequence。draw 本來就單獨處理 look,tick 的推幀也已排除 look。
- resolveAnim 先取 baseState(),只有 base 為 idle 時才回傳 look;
  一併避免原本重複呼叫 baseState() 的副作用。
- README 的特色、行為對照表、與 Codex 的對照三處同步更新,
  對照表把「追視」拆成來源與優先序兩列並說明為何刻意不同。

不動的部分:
hover 跳躍維持進入時播 3 次、之後停住不再跳(使用者確認就是要這個行為)。
另已確認跳躍結束後會回到當下的基礎狀態而非一律 idle。

驗證:
- 單元測試(抽出實際程式碼執行):修正前 startAnim("look") 拋
  TypeError: Cannot read properties of undefined (reading 'map'),修正後回傳空序列;
  idle 6 幀、running 24 幀正常;resolveAnim 在追視開啟下
  idle→look、running→running、waiting→waiting、review→review,
  hover 跳躍優先於 look 與 running,跳躍結束回到 running 而非 idle,共 11 項全過。
- 實機開啟 followCursor 啟動:/state 持續回應且 anim 為 look(修正前此處即崩潰),
  log 無任何例外;灌入事件後 running / waiting 正確蓋過追視。
- 以 ELECTRON_RUN_AS_NODE 讀打包後 app.asar:版本 2.0.2、崩潰修正與追視優先序都在,
  hover 跳躍、矩形判定、右鍵選單未受影響;exe FileVersion 為 2.0.2。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-21 16:48:43 +08:00
co-authored by Claude Fable 5
parent 538b379dff
commit 7e2f689f26
4 changed files with 19 additions and 9 deletions
+4 -3
View File
@@ -32,7 +32,7 @@
- **完整實作 Codex V2 pet contract** — 8 欄 × 11 列、192×208 cell 的 spritesheet9 個標準動作列 + 16 個看向方向,**每一幀的時長都照 Codex 規格**,動起來跟在 Codex app 裡一模一樣。
- **行為邏輯逐項對照 Codex** — 狀態優先序、各狀態到期時間、動畫時序、hover 版面、初次問候、拖曳方向判定……都是從 Codex app 的 `app.asar` 讀出來的數值(見[與 Codex 的對照](#與-codex-的對照))。
- **16 方向追視(選配,預設關)** — 開啟後游標在哪她就看哪(000 = 正上方、順時針 22.5° 一格),追視時是靜態擺姿。Codex 的寵物**不看滑鼠**——它看的是 quick chat 的文字游標與 computer-use 的虛擬游標,所以預設關閉以求一致;喜歡的話在選單打開。
- **16 方向追視(選配,預設關)** — 開啟後、且目前是待機狀態時,游標在哪她就看哪(000 = 正上方、順時針 22.5° 一格),追視時是靜態擺姿Claude Code 在工作時以狀態動畫優先。Codex 的寵物**不看滑鼠**——它看的是 quick chat 的文字游標與 computer-use 的虛擬游標,所以預設關閉以求一致;喜歡的話在選單打開。
- **自動修正圖檔缺陷** — Codex 的 hatch 產生器有時會把整列 look 格子畫得比 idle 小(內附的小念第 10 列就小了 22%,游標一往下移人物就縮水)。以整列中位數偵測、用腳底當支點放大回正確身高;單格的姿勢差異不動。Codex 本身沒有做這件事。
- **與 Codex 同尺寸** — 100% 就是 Codex 桌面寵物的大小(126 × 137),高度用 Codex 的 `ceil(寬 × 208/192)` 算,兩邊並排看起來一樣大。
- **真正的桌面寵物** — 透明、無邊框、永遠置頂;透明區域**點穿**,只有壓在人物像素上滑鼠才會被吃掉,不擋你操作底下的視窗。
@@ -123,7 +123,7 @@ npm run autostart:install # (可選)開機自動啟動
| `SubagentStart` / `SubagentStop` | | `running` | |
| `PreCompact` | | `running` | 🗜️ 整理記憶中… |
| `SessionEnd` | | 移除該 session | 最後一個結束時 👋 掰掰 |
| (無事件) | 游標移動(追視開啟時) | 16 方向 `look`,覆蓋所有狀態 | |
| (無事件) | 游標移動(追視開啟時、且目前為 idle | 16 方向 `look` | |
| (無事件) | 拖曳 | 單次位移 ≥ 4 px 才決定 `running-left` / `running-right` | |
| (無事件) | 游標進入人物方框 | `jumping` × 3 → 回原狀態 | |
@@ -261,7 +261,8 @@ Claude Code ──hookstdin JSON)──▶ hook/claude-pet-hook.js ──PO
| `jumping` | 定義在 `respondToHover ? "jumping" : state`,但整個 app 沒人開啟那個 prop | **選擇打開**:游標進入人物方框就播 3 次 |
| hover 版面 | 40 px 內進入、56 px 外退出;人物縮 0.9、下移 14 px、上方 24 px 控制鈕、200 ms | 未採用——控制鈕的功能就是選單,右鍵已經有了 |
| hover 判定 | 用矩形(`pet-area-hover-changed` 由 rect 算) | 相同(逐像素會因每格輪廓不同而抖動) |
| 追視 | quick chat 文字游標/computer-use 游標,死區 1 px,覆蓋所有狀態 | 沒有對應來源;改為**選配**看滑鼠,預設關 |
| 追視來源 | quick chat 文字游標/computer-use 游標 | 沒有對應來源;改為**選配**看滑鼠,預設關 |
| 追視優先序 | `lookFrame` 覆蓋所有狀態 | 只在 idle 時追視。Codex 的文字游標只有你在打字時才存在,滑鼠卻是一直都在,照搬會讓工作中的動畫永遠被靜態擺姿蓋掉 |
| 拖曳方向 | 單次取樣水平位移 ≥ 4 px | 相同 |
| 拖曳放開 | 彈簧回彈(spring 220 / damping 26 | 無(直接停在放開處) |
| 點擊 | 開 quick chat | 揮手(沒有對應功能) |
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "claude-pet",
"version": "2.0.1",
"version": "2.0.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-pet",
"version": "2.0.1",
"version": "2.0.2",
"devDependencies": {
"electron": "^43.4.1",
"electron-builder": "^26.0.12"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claude-pet",
"version": "2.0.1",
"version": "2.0.2",
"description": "Codex Pets 相容的 Claude Code 桌面寵物(spriteVersionNumber 29 個動作列 + 16 方向追視)",
"main": "main.js",
"private": true,
+12 -3
View File
@@ -103,6 +103,12 @@ function baseSequence(anim) {
const reducedMotionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
function startAnim(anim, now) {
// 追視是單一靜態擺姿,沒有幀序列(TIMINGS 裡沒有 look)。
// 少了這道判斷會走進 cycleOf("look") 而丟 TypeErrorrequestAnimationFrame 迴圈整個死掉。
if (anim === "look") {
player = { anim, frames: [], loopStart: null, step: 0, stepStart: now };
return;
}
let seq;
if (reducedMotionQuery.matches) {
// 與 Codex 相同:系統關閉動畫時只顯示該狀態的第一格
@@ -287,9 +293,12 @@ function resolveAnim() {
// 一次性動作(點擊揮手、初次問候)。Codex 的 lookFrame 會蓋過這些,但它的追視來源不是滑鼠;
// 滑鼠永遠在畫面上,若照搬會讓點擊揮手永遠看不到,所以這裡讓手勢優先。
if (overlay) return overlay.anim;
// CodexlookFrame 一旦有值就覆蓋所有狀態,靜態擺姿
if (followCursor && petInfo?.version === 2 && look.idx !== null) return "look";
return baseState();
const base = baseState();
// Codex 的 lookFrame 會覆蓋所有狀態,但它的追視來源是 quick chat 的文字游標,
// 只有你真的在那裡打字時才存在。滑鼠則是一直都在,照搬會讓工作中的動畫永遠被靜態擺姿蓋掉,
// 寵物就失去反映 Claude Code 狀態的作用,所以只在 idle 時追視。
if (base === "idle" && followCursor && petInfo?.version === 2 && look.idx !== null) return "look";
return base;
}
function projectName(cwd) {