# Hopet 详细功能文档 v0.1 > 版本:0.1 (初版) > 最后更新:2026-04-24 > 状态:Draft — 与 [architecture.md](./architecture.md) 同步交付 --- ## 1. 用户画像与使用场景 ### 1.1 核心用户 - **AI Pair Programmer**:长时间与 Claude Code / Codex 配合工作的开发者,常开多个终端多个会话。 - **Side-by-Side 工作者**:在 IDE 或浏览器工作时,AI 会话窗口被遮盖,希望不切换窗口就能知道 AI 状态。 - **Ritual 偏好者**:享受桌面陪伴物的开发者,把工具装饰化作为工作仪式感的一部分。 ### 1.2 典型场景 | 场景 | 现状痛点 | Hopet 解决方式 | | --- | --- | --- | | AI 正在跑一个长任务,用户去看文档 | 不时切回终端检查进度 | 宠物一直"回复中"动画;完成时播放"完成"动画并可弹通知 | | AI 问用户 AskUserQuestion | 用户没注意到终端已停等待 | 宠物切到"询问"专属动画 + 刘海条文字提醒 + (可选)通知 | | 需要权限确认(Bash/Edit) | 长任务里夹杂多次权限弹窗,容易错过 | hook-backed CLI 会话切到"权限请求"动画并展示决策卡;Codex VSCode / Cursor 插件审批仍用插件自己的 UI | | 多个会话并发(`cc-1` / `cc-2` / codex) | 哪个在跑、哪个卡住不清楚 | 每个会话独立宠物,位置互不重叠 | --- ## 2. 功能总览 ### 2.1 功能矩阵 | 模块 | v0.1 | v0.2 | v0.3 | | --- | --- | --- | --- | | 状态感知动画(Claude Code,idle / responding / thinking / tool-use / permission-prompt / ask-user / completed) | ✅ 含 ask-user(通过 AskUserQuestion tool 路由) | ✅ | ✅ | | 状态感知动画(Codex CLI 0.129+,无 ask-user 和 error-interrupted) | ✅ 6 hook 完整生命周期(`~/.codex/hooks.json`) | ✅ | ✅ | | 状态感知动画(Codex VSCode / Cursor 插件) | ✅ 只读本地 rollout,会话 / 回复 / 工具 / 完成状态;不接管插件权限审批 | ✅ | ✅ | | `error-interrupted` 状态有事件源 | ⛔ 枚举值保留,但 `PostToolUseFailure` 太常态已停用;见 [hooks-and-priority.md §1.1 注 2](./hooks-and-priority.md#11-实际订阅的-claude-code-hook) | 视未来真"会话级错误"事件出现而定 | TBD | | 刘海屏 Dynamic Notch | ✅ 三态:collapsed / expanded / fullBubble | ✅ | ✅ | | 顶部悬浮条降级(无刘海机型,`notch.fallbackBarEnabled`) | ✅ | ✅ | ✅ | | 桌面宠物(**全局唯一**,聚合所有 AI 工具、所有 session) | ✅ | ✅ | ✅ | | 会话气泡(**竖栈贴宠物头顶 + ScrollView 滚动**,每气泡 = 1 个活跃 session) | ✅ 显示 cwd 末层 / 标题 / 最近回复摘要 / 状态徽章 / 状态时长 | ✅ | ✅ | | 状态聚合(多 session → 单宠物按优先级) | ✅ 详见 [hooks-and-priority.md](./hooks-and-priority.md) | ✅ | ✅ | | Subagent 同步类 hook 重路由到主 session | ✅ `EventRouter` 通过 `transcript_path` 找主 session,子 agent 不创建气泡 | ✅ | ✅ | | 点击宠物本体 → 启动新会话 | ⛔ 已明确不做(详见 [architecture.md §12.5](./architecture.md#125-关于在气泡里自由输入消息v01-不做的功能)) | TBD | TBD | | 气泡上 PermissionRequest 决策(Allow / Deny / Ask) | ✅ hook socket 同步回包,跨所有宿主 | ✅ | ✅ | | AskUserQuestion 自动展开答题(选项 + 自定义文本 + multiSelect) | ✅ hook 回包 `updatedInput.answers`,跨所有宿主 | ✅ | ✅ | | ExitPlanMode plan-approval 卡片(plan markdown + 继续规划反馈) | ✅ | ✅ | ✅ | | **气泡里自由打字注入消息**(在 idle / 任意状态 session 上) | ⛔ macOS 无干净通用注入路径,详见 [architecture.md §12.5](./architecture.md#125-关于在气泡里自由输入消息v01-不做的功能) | 评估 PTY wrapper / IDE 扩展 | TBD | | 偏好面板 8 Tab(Overview / Themes / Appearance / Bindings / Hooks / Behavior / Notifs / About) | ✅ 全部实现 | ✅ | ✅ | | 主题系统 — 内置 Hopi 主题 | ✅ | ✅ | ✅ | | 主题系统 — 用户自定义主题(8 个 GIF + manifest,文件夹 / `.zip` 自动扫描) | ✅ | ✅ | ✅ | | 主题系统 — `.hopettheme` zip 分发 | ⛔ | ⛔ | ✅ 含 zip slip 防护 | | 主题切换(全局单一) | ✅ `HopetConfig.activeThemeId` | ✅ | ✅ | | Hook 安装向导 + Doctor | ✅ | ✅ | ✅ | | Listener 软静音 toggle(不动 hook 文件,运行时丢事件) | ✅ | ✅ | ✅ | | 通知中心横幅 | ⚠️ NotificationsTab 仅有 Toggle 占位,未真正注册 UserNotifications | ✅ 联通 | ✅ + 主题声音 | | Onboarding 向导 + 权限引导 | ⛔ | ✅ | ✅ | | 快捷键录制 / 全局快捷键 | ⛔ | TBD | ✅ | | `hopet` CLI | ⛔ | ✅ | ✅ | | 自动更新(Sparkle + EdDSA) | ⛔ | ✅ | ✅ | > 标注约定:`✅` = 已交付;`⚠️` = 实现中 / 受限;`⛔` = 本期明确不交付(已与 architecture.md §1.2 / §13 对齐)。 ### 2.2 非功能需求 - **启动时间** ≤ 1.5s 到宠物可见 - **单宠物闲置 CPU** ≤ 1%(Apple Silicon M1) - **单宠物闲置内存** ≤ 80 MB - **动画帧率**:60 fps;低电量模式自动降到 30 fps - **IPC 端到端延迟** ≤ 50ms(hook 脚本 exec 到动画切换) --- ## 3. 核心功能详述 ### 3.1 状态感知与动画映射 八种状态的完整映射(以内置 "Hopi" 小海豹主题为例)。**优先级**列用于多 session 聚合到单宠物时决定显示哪个状态(数字小=优先级高,详见 [hooks-and-priority.md §2](./hooks-and-priority.md#2-petstate-优先级)): | 优先级 | PetState | 触发事件 | 宠物动画(Hopi) | 刘海条文案 | 通知 | v0.1 | | --- | --- | --- | --- | --- | --- | --- | | **P0** | `ask-user` | Claude `PreToolUse` hook(`tool_name == AskUserQuestion`,fire-and-forget 让 UI 提前展示)+ `PermissionRequest` hook(同一调用,带 `requestId`,挂起等用户作答) | 歪头 + 双鳍捧问号牌 | `❓ Waiting for your answer` | ⛔ 横幅占位 | ✅ + 该 session 气泡自动展开为对话气泡 | | **P1** | `permission-prompt` | Claude `PermissionRequest` hook(Notification 兼容回退已删除,避免双发) | 警觉抬身瞪眼 + 红色感叹号闪烁 | `⚠️ Permission needed` | ⛔ 横幅占位 | ✅ | | **P2** | `error-interrupted` | **当前无事件源**——`PostToolUseFailure` 在 Claude 上太常态(`grep` / `head` 等命令的非零退出),改为只触发 `cancelPending` 不切状态。枚举值保留 | 瘫软成一块麻薯 + 小闪电 | `Interrupted` | 可配置 | ⛔(枚举可用,但不会被触发)| | **P3** | `tool-use` | Claude `PreToolUse` hook(`tool_name != AskUserQuestion`),Codex 同 | 戴圆眼镜翻书 / 工具 icon 漂浮在鳍旁 | `Running tool…` | — | ✅ | | **P4** | `thinking` | responding 持续 ≥ 8 s(`ThinkingTimer` 主动判定,无对应 hook) | 前鳍托腮,头顶省略号气泡闪烁 | `Thinking deeply…` | — | ✅ | | **P5** | `responding` | Claude `UserPromptSubmit` hook,Codex 同 | 两只前鳍交替拍小键盘 | `Responding…` | — | ✅ | | **P6** | `completed` | Claude `Stop` hook,Codex 同 | 开心拍鳍 + 小跳(非循环,1 s) | `Done ✓` | 可配置 | ✅ | | **P7** | `idle` | 无活跃 session / completed 后 2 s(`CompletedDecayTimer`) | 趴坐眨眼,身体随呼吸起伏,偶尔轻拍短尾鳍 | `Idle` | — | ✅ | **聚合规则**:宠物展示的是所有活跃 session(跨所有 AI 工具)中**优先级最高**的那个状态(权威定义见 [hooks-and-priority.md §2](./hooks-and-priority.md#2-petstate-优先级))。Hopet 全局只有一只宠物,不再随 session 数量或工具数量增加。**leader session 的气泡边框会高亮**,让用户一眼看出当下宠物状态来自哪个 session。 动画切换由 SwiftUI 视图层 `withAnimation` 直接承担,没有独立的 `AnimationController`。 **优先级**:同一时刻若有多个候选状态,按 `ask-user > permission-prompt > error-interrupted > tool-use > thinking > responding > completed > idle` 的优先级选择,刘海条始终只显示最高优先级的那一条。 ### 3.2 刘海屏 Dynamic Notch #### 3.2.1 三态设计 ```mermaid stateDiagram-v2 [*] --> collapsed collapsed --> expanded: hover 显示 ↓ 后点击 / 高优先级状态 expanded --> collapsed: 点击 ↑ / completed 摘要 3s 超时 / pending 消失 ``` 各态的视觉呈现: | 态 | 尺寸 | 内容 | 触发 | | --- | --- | --- | --- | | **collapsed** | 贴近物理刘海宽度的黑色区域(有刘海机型约 160–220 pt,无刘海降级 184 pt),高度 = 顶部刘海保留区 + 26 pt 状态条 | 底部状态条显示小色点 + 最高优先级状态文案(`Idle` / `Thinking…` / `Responding…` 等);hover 时右侧显示 ↓ | 默认态 | | **expanded** | 最大宽度 560 pt,高度按内容包裹且不超过屏幕高度 1/4;completed 摘要独立停留 3s 后收起(不受 completed→idle 2s 降级影响) | 顶部预留 ↑ 收起控制区,正文从其下方开始;权限 / AskUser / 完成摘要卡片;手动展开时显示当前会话详情(状态 / cwd / 最近提问 / 最近回复,耗时每秒刷新);不再显示独立关闭按钮 | 点击 collapsed 的 ↓ 或 出现 permission-prompt / ask-user / completed | | **fullBubble** | 视气泡内容自适应 | 把活跃气泡内容直接嵌进刘海下方(实验态,仅 `NotchView.swift` 内含) | 内部用 | #### 3.2.2 吸附与动效 - 定位:使用 `NSScreen.safeAreaInsets` 与 `auxiliaryTopLeftArea / auxiliaryTopRightArea`(macOS 14+)推导主屏刘海 gap;有刘海屏从屏幕顶端开始渲染纯黑区域,高度覆盖顶部安全区并在底部追加 26 pt 状态条,状态文案只放在底部状态条。无刘海降级条吸附菜单栏下沿。 - 伸展动效:窗口顶部锚定,expanded 与 collapsed 共用同一个屏幕中心点,宽度从中点向两侧撑开到最大 560,高度从上往下按内容展开;`spring(response: 0.44, damping: 0.92)`,窗口 frame 使用 0.42s ease-in-out。 - Level:`.statusBar + 1`,确保菜单栏不会盖住刘海下沿补黑区域;窗口宽度限制在刘海中心 gap,跨所有 Space、不进入 Mission Control。 - 多屏:优先在带刘海的内建屏幕渲染刘海条;没有刘海屏时才使用主屏幕降级条。外接显示器上宠物本体正常显示。 #### 3.2.3 降级策略(无刘海机型) - 无 `safeAreaInsets.top > 0` 的 Mac → 启用 `FallbackTopBarWindow`: - 屏幕顶部中央悬浮一条 184×26 的黑色下沿 - 与刘海条具备相同的三态能力 - 默认关闭,可在偏好中开启(避免遮挡菜单栏) - 用户亦可在偏好里强制切换为"仅宠物,不要顶部条"。 --- ### 3.3 桌面宠物本体 Hopet **全局只有一只**宠物,所有 AI 工具(Claude / Codex / 未来其它)的所有活跃 session 共用之。宠物状态 = 所有活跃 session 中最高优先级的那个 session 状态(聚合规则见 [hooks-and-priority.md §2-3](./hooks-and-priority.md#2-petstate-优先级))。 #### 3.3.1 窗口行为 - `NSPanel` + `.nonactivatingPanel` + `.fullSizeContentView`:点击宠物不抢走当前 App 焦点 - `collectionBehavior = [.canJoinAllSpaces, .stationary, .ignoresCycle]`:跨所有 Space、不出现在 `⌘Tab` - `level = .floating + 1`:浮于普通窗口之上,但低于系统弹窗 - 背景完全透明;宠物本体与气泡分别响应点击(HitTestProxy 按 alpha > 0.1 判定) #### 3.3.2 位置与拖拽 - 启动时从 `config.json` 读取 `lastPosition`;首次启动宠物默认在主屏右下 - 长按 0.2s 进入拖拽态;松开吸附到最近的屏幕边缘(可关闭吸附) - 拖动宠物时**会话气泡跟随移动**(保持环绕几何) - 拖出屏幕时自动 clamp 回可见区 #### 3.3.3 无活跃 session 时 宠物保持显示为 idle(作为"看一眼当前状态"的窗口),可在偏好里设为"无 session 时隐藏"。 #### 3.3.4 交互反馈 | 操作 | 反馈 | | --- | --- | | 鼠标悬停 | 视图层尚未做 tooltip(计划项) | | 左键单击宠物本体 | v0.1 无操作(曾用于"新开 session",已移除) | | 右键点击宠物 | 视图层尚未做 context menu(计划项;菜单栏图标作为兜底入口) | | 长按(≥ 0.2 s) | 进入拖拽(窗口位置跟随,气泡 ScrollView 同步移动) | | **左键单击气泡** | 不做"展开为大卡片"——气泡的展开态完全由 `Session.pendingKind`(Permission / AskUserQuestion / ExitPlanMode)自动驱动,无 pending 时是固定的默认卡片(标题 / cwd / 最近回复 / 状态徽章 / 状态时长) | | 气泡右上角 ✕ | 手动 dismiss 这条会话气泡(真活会话被误关时下一次状态事件会冷启重建) | --- ### 3.4 输入入口 v0.1 只有两个用户输入入口,都建立在 **Claude 主动开口**(hook 同步等待)的前提上。**不**提供"在 idle 气泡上打字给 Claude",也**不**提供"点击宠物本体启动新会话"——后者本质是同一类问题(详见 [architecture.md §12.5](./architecture.md#125-关于在气泡里自由输入消息v01-不做的功能))。 | 触发 | 含义 | 走的路径 | | --- | --- | --- | | **PermissionRequest 自动展开**(Claude 主动) | 回答工具调用是否允许 | §3.4.1 | | **AskUserQuestion 自动展开**(Claude 主动) | 回答 Claude 的提问 | §3.4.2 | 启动新会话的方式:用户照常在自己的终端 / Cursor / VS Code / IDE 内嵌终端里打 `claude` / `codex`,Hopet 通过 hook 自动感知,新气泡随 SessionStart 事件出现。 Codex VSCode / Cursor 插件主会话不触发 `~/.codex/hooks.json`;Hopet 通过本地 rollout watcher 只读感知其状态。该路径没有同步审批回包,因此插件弹出的权限审批不展示 Hopet 决策卡。 #### 3.4.1 PermissionRequest 自动展开 当 Claude 触发 `PermissionRequest` hook(如要执行 Bash/Edit 等需要权限的工具): 1. hopet-emit 通过 socket 把请求转给 Hopet,**Claude 进程被挂起等响应**(30s 超时) 2. 该 session 的气泡**自动从环绕态展开**为 360×160 的决策卡,显示工具名 + 命令/路径预览 3. 用户点 **Allow** / **Deny** / **Handoff** 4. 决策通过同一条挂起的 socket 回写:`{ "hookSpecificOutput": { "hookEventName": "PermissionRequest", "decision": { "behavior": "allow"|"deny" } } }` 5. Claude 拿到决策继续工具调用循环;如选"交给终端"则回 `{}`,Claude 自走它的 TUI 弹窗 跨 iTerm / Apple Terminal / VS Code / Cursor 内嵌终端 / Ghostty / Warp 等所有宿主工作——这条路是协议级的,跟终端注入路径无关。 Codex VSCode / Cursor 插件自己的审批弹窗不走这条 hook socket;Hopet 不接管其 Allow / Deny / Handoff,审批期间最多展示普通工具执行状态。 #### 3.4.2 AskUserQuestion 自动展开 `AskUserQuestion` 是 Claude Code 的内置工具,每次调用走标准 `PermissionRequest` hook(Hopet 通过 `tool_name=="AskUserQuestion"` 在该 hook 上分流)。 1. hopet-emit 把 `tool_input.questions` 透传给 Hopet 2. 气泡**自动展开**为 360×220 的答题卡:标题/序号 + 提问 + 选项按钮(来自 `tool_input.options`) + 自定义文本框 3. 多问题时分页填写,每题必填一项再下一页 4. 最后一页提交时一次性回包: ```json { "hookSpecificOutput": { "hookEventName": "PermissionRequest", "decision": { "behavior": "allow", "updatedInput": { "questions": [...原 questions], "answers": { "问题文案 A": "回答 A", "问题文案 B": "回答 B" } } } } } ``` 5. Claude 用 `updatedInput` 重跑 AskUserQuestion 工具,工具识别 `answers` 字段直接把它当结果返回 宠物动画同步切到 `ask-user` 态。**这条路同样跨所有宿主工作**——不依赖 PTY 注入或终端自动化。 #### 3.4.3 关于"自由打字给 Claude"和"点击宠物启动新会话" 两个看起来理应有的功能,v0.1 都不做: - **气泡里自由打字给 Claude**:macOS 没有可靠的反向 stdin 注入路径(详见 [architecture.md §12.5](./architecture.md#125-关于在气泡里自由输入消息v01-不做的功能)) - **点击宠物本体启动新会话**:本质是同一类问题——即使弹一个目录选择器 + 输入框,命令也只能复制到剪贴板让用户自己粘贴,Hopet 没有持有这个新 session 的 stdin。这种"看似引导实则脱节"的体验已从 v0.1 移除 两类都不打算用 PTY wrapper / IDE 扩展暴力实现——前者要求改启动方式,后者要求装额外组件,都偏离了 Hopet "感知层 + 协议层"的定位。 要给 Claude 发新消息:照常在自己的终端 / Cursor / VS Code / IDE 内嵌终端里打字。Hopet 只负责通过 hook 把状态变化映射成桌面动画,不替代输入 UI。 --- ### 3.5 会话气泡(Session Bubbles) 气泡**竖栈贴宠物头顶 + ScrollView 滚动**,每个气泡 = 一个活跃 session(跨 AI 工具)。气泡是宠物身份信息的最小载体,提供 cwd / 标题 / 最近回复摘要 / 状态徽章 / 状态时长等关键元信息。 > 旧设计是围绕宠物环绕排布,问题是宠物靠近屏幕边缘时气泡越界、Leader 弧线视觉指向无法 hit-test、6+ 气泡时视觉拥挤。竖栈方案让气泡顺序按 `startedAt` 倒序明确(最新在最上)、可滚动、可承载 plan-approval / askUser 这类高大卡片。 #### 3.5.1 默认显示内容 默认卡片(无 pending 时)按行展示: ``` ┌────────────────────────────────────────┐ │ Title [×] │ ← 会话标题(≤ 40 字符);无标题时不渲染此行(避免和下行 cwd 重复) │ 📁 Hopet · Responding · running 3s │ ← cwd 最后一层 / 状态徽章 / stateDurationPhrase │ "Looking at PetStageView.swift…" │ ← lastAssistantMessage(Stop hook 抽取的本轮回复开头,≤ 120 字符) └────────────────────────────────────────┘ ``` `stateDurationPhrase` 在 running 态显示 `running 5m`,在终态(idle / completed / errorInterrupted)显示 `5m ago`。 气泡边框颜色 = 该 session 当前状态色(leader 加粗,其它常规)。 #### 3.5.2 视觉规格 | 元素 | 估算值 | | --- | --- | | 默认卡片高度 | 76 pt | | Permission 决策卡 | 260 pt | | Plan-approval 卡(ExitPlanMode) | 430 pt | | AskUserQuestion 答题卡 | 390 pt | | 旧 fire-and-forget 问询卡(legacyQuestion) | 120 pt | | 单可见默认卡数(超出滚动) | 5 | | 气泡间距 | 6 pt | | 气泡-宠物间距 | 6 pt | | 字体 | 系统等宽(monospaced),按 preferences.md §11.4 | | 描边 | 硬黑 1.5 pt(普通)/ 2.5 pt(leader) | | 阴影 | 块状偏移(不模糊),与气泡 PixelChrome 同语言 | 布局算法见 [architecture.md §12.4](./architecture.md#124-会话气泡布局算法)。 #### 3.5.3 Leader 高亮 宠物的聚合状态由"最高优先级 session"驱动(详见 [hooks-and-priority.md §3](./hooks-and-priority.md#3-聚合算法))。`PetInstance.drivenBySessionId` 对应的气泡: - 描边加粗 - 描边色 = 当前宠物状态色 用户因此能一眼看出"哪个 session 是当下宠物状态的来源"。 #### 3.5.4 展开行为(pendingKind 驱动) 气泡的形态完全由 `Session.pendingKind` 决定,无需用户点击: | pendingKind | 形态 | 触发 | | --- | --- | --- | | `nil` | 默认卡片 | 无挂起的同步类 hook | | `permission` | Permission 决策卡(Allow / Deny / Ask) | `PermissionRequest` hook(详见 [§3.4.1](#341-permissionrequest-自动展开)) | | `planApproval` | Plan-approval 卡(plan markdown + 继续规划反馈) | `PermissionRequest` hook 且 `tool_name == ExitPlanMode` | | `askUser` | AskUserQuestion 答题卡(选项按钮 + 自定义文本 + multiSelect) | `PermissionRequest` hook 且 `tool_name == AskUserQuestion`(详见 [§3.4.2](#342-askuserquestion-自动展开)) | | `legacyQuestion` | 旧 fire-and-forget 问询卡(仅展示问句,无答题入口) | `PreToolUse` 路径上 `tool_name == AskUserQuestion`(让 UI 提前进入答题态;真正的同步答题靠 `PermissionRequest` 路径) | 每个气泡右上角有 ✕ 用于手动 dismiss。 #### 3.5.5 屏幕边缘适应 宠物窗口高度由 `PetWindow.stageSize.height` 限定;气泡 ScrollView 视口上限按窗口可用空间裁剪。宠物靠近屏幕底时 `clamp` 回可见区,气泡仍然贴宠物头顶。 #### 3.5.6 气泡数量 无硬上限——超出可见区即滚动。实践中很少同时跑超过 5 个活跃 session,多余的靠 ScrollView 兜底。 --- ### 3.6 偏好面板 自绘 `PixelTabBar` 顶部分段(不用系统 `TabView`),整体跑在 `PixelGridBackground` 上,与宠物气泡 / 刘海条同语言。详细像素风设计见 [preferences.md §11](./preferences.md)。 Tab 顺序:**Overview · Themes · Appearance · Bindings · Hooks · Behavior · Notifs · About**。 #### 3.6.1 Overview - 全局宠物卡片:状态 glyph + 活跃 session 数 + Locate 按钮(让宠物闪烁定位) - Display 快捷卡片:`Show notch bar` 写入 `UserDefaults notch.enabled`,实时显示 / 隐藏刘海条;`Show pet` 写入 `UserDefaults pet.visible`,实时显示 / 隐藏宠物窗口 - Sessions 列表:每个 session 一行(工具名 / 标题 / state / badgeLabel / 用时 / `×` 删除按钮) - 空状态提示 #### 3.6.2 Themes - 已安装主题列表,每条 `PixelCard` 展示 56×56 预览首帧 + 名称 + 描述 + Apply / Delete 按钮 - 顶部 "Import Theme…" 按钮:弹出 sheet - 主题名输入框 + 8 个 `PixelDropSlot`(按 `PetState.allCases` 排列) - 或拖入文件夹 / `.zip`,`UserThemeImporter.DirectoryScan` 自动按文件名匹配 PetState 并报告缺失 / 重复 / 不识别的文件 - 用户主题显示 `[user]` 角标;内置 `hopi.default` 不显示 Delete 按钮 #### 3.6.3 Appearance - 三选一 `PixelSegmentedControl`:Light / Dark / System - 实时生效(`NSApp.appearance = NSAppearance(named: ...)`),偏好同步写入 `HopetConfig.appearance` #### 3.6.4 Bindings(全局主题) - 单个 `PixelCard`:全局主题 `Picker(.menu)`,写入 `HopetConfig.activeThemeId` - v0.x 起宠物全局唯一,主题也只有一个全局值——表格形态的"按 AI 绑定"已废弃;如未来引入按 cwd / session 切主题会单独建模 #### 3.6.5 Hooks - 每个识别的 AI 工具一张 `PixelCard`:工具名 + `Listening on/off` 状态 + `PixelToggle`(软静音) - Toggle 不动 hook 文件——启动时已无条件落盘,off 只让 EventRouter 静默丢事件并清掉无 pending 的气泡 - 底部 `PixelCard`:Doctor "Run" 按钮 + monospaced ScrollView 显示诊断输出 #### 3.6.6 Behavior 四块 `PixelCard`(General / Notch / Terminal / Diagnostics)。Notch 区的 `Show notch bar` 与 Overview 的同名开关共享 `UserDefaults notch.enabled`,实时控制刘海条可见性;`Show top bar on non-notch displays` 写入 `UserDefaults notch.fallbackBarEnabled`,只影响无物理刘海屏幕上的降级顶条。其它 Behavior 项仍是偏好 UI 骨架,未全部接入运行时。 #### 3.6.7 Notifications 两块 `PixelCard`(Banners 分类 Toggle / Sound 占位说明)。当前只有 UI 占位,未真正注册 UserNotifications(v0.2 才会联通)。 #### 3.6.8 About 居中 `PixelCard`:项目标题 + 版本 + 一句话描述 + feedback Link。 --- ### 3.7 主题系统 #### 3.7.1 主题加载流程 ```mermaid flowchart TD Start["App 启动"] --> Builtin["DefaultTheme.hopi 硬编码构造"] Builtin --> Scan["扫描 ~/.hopet/themes/*/manifest.json"] Scan --> Decode{"manifest.json 解码"} Decode -->|GIF 主题| BuildGIF["按 PetState.rawValue 收集 8 个 GIF
构造 FrameAnimation.gifFile(url:)"] Decode -->|Codex pet| BuildCodex["保留 spritesheet.png / webp
按固定映射构造图集动画"] Decode -->|失败| Skip["跳过并 warn"] BuildGIF --> Store["ThemeStore 持有列表"] BuildCodex --> Store Store --> Active["activeThemeId 取自 HopetConfig,
缺失时降级 hopi.default"] ``` GIF 渲染走 `GIFAnimationView`:`ImageIO` 解码所有帧,保留 GIF 内嵌可变帧延迟,`TimelineView` 按 `(elapsed % totalDuration)` 二分定位当前帧;帧图缓存 key = `(URL.path, mtime)`,删除 / 重导入主题后 mtime 变化自动失效。Codex pet 直接解码 v1 的 1536×1872 或当前 v2 的 1536×2288 PNG / WebP 图集,并从前 9 个标准动作行裁帧,不生成 GIF 中间文件。 #### 3.7.2 导入用户主题 详见 [preferences.md §5.3](./preferences.md)。入口:ThemesTab 的 "Import Theme…" 按钮。三种填法: - **8 槽手填**:为每个 `PetState` 拖入或选择一个 GIF - **文件夹 / .zip 自动扫描**:拖入一整个目录或 `.zip`,`UserThemeImporter.DirectoryScan` 按文件名(忽略大小写、忽略 `-` / `_` / 空格)匹配 PetState - **Codex pet 包**:选择含 `pet.json` 与 `spritesheet.png` 或 `spritesheet.webp` 的文件夹或 `.zip`;保留原始图集,不转码 GIF 校验: 1. UTI 必须是 `public.gif`(避免改名 `.gif` 绕过) 2. `CGImageSourceCreateWithURL` 必须成功且帧数 ≥ 1 3. 任一校验失败 → 删除半成品目录、报错红字、保留 sheet 供修正 Codex pet 校验:`pet.json.spritesheetPath` 必须为 `spritesheet.png` 或 `spritesheet.webp`;v1 包为 1536×1872(8×9),v2 包以 `spriteVersionNumber: 2` 标识且为 1536×2288(8×11),每格均为 192×208。两代均要求 `id` / `displayName` / `description`;`kind` 仅为旧 v1 的可选字段。导入器逐格检测 alpha,只接受从左至右连续的可见帧;空帧后重新出现内容或 v2 的帧数不符合契约都会拒绝,避免播放空白帧。允许根目录或一层子目录中恰好一个包。 > v0.1 不支持 `.hopettheme` zip 分发(含 zip slip 防护,留 v0.3+)。当前 zip 仅作为"一次性导入容器"用,导入完成立刻解到 `~/.hopet/themes//` 并丢弃 staging。 #### 3.7.3 自定义主题简要指南 1. 准备 8 个 GIF,按 `PetState.rawValue` 命名:`idle.gif` / `responding.gif` / `thinking.gif` / `tool-use.gif` / `permission-prompt.gif` / `ask-user.gif` / `completed.gif` / `error-interrupted.gif` 2. 把它们放进一个文件夹或打成 `.zip` 3. 在 ThemesTab 点 Import Theme…,填主题名,拖入文件夹 / zip 4. 点 Apply 即生效 **帧规范**: - 透明背景 GIF;每个 state 建议 8–24 帧;loop 自然衔接 - 8 个文件**必须齐全**——缺任一帧整个主题非法 **Codex pet 状态映射**:Hopet 的会话状态比 Codex 图集的动作行更强调“此刻需要什么”。默认映射固定如下,状态色、气泡和权限交互仍由 Hopet 自己渲染: | Hopet 状态 | Codex 图集行 | 设计理由 | | --- | --- | --- | | `idle` | `idle` | 无活跃会话 | | `thinking` | `review` | 审阅/斟酌的视觉最贴合深度思考 | | `responding` | `running` | 正在连续推进主任务 | | `tool-use` | `run right` | 工具执行采用明确的前进动作 | | `permission-prompt` | `waiting` | 宠物等待用户决定 | | `ask-user` | `waving` | 主动招呼用户回答 | | `completed` | `jumping` | 完成后的正向反馈 | | `error-interrupted` | `failed` | 失败/中断反馈 | `run left` 是 Codex 的朝向动画,不代表独立会话状态。Hopet 的宠物不随 session 在桌面上移动,所以不映射它。 --- ## 4. 内置主题清单(v0.1) ### 4.1 Hopi — 圆滚滚的小海豹 - `id`:`hopi.default` - 风格:像素 + 轻微抗锯齿,Cute 向;主体是一只身体呈水滴形、胖乎乎、短尾鳍的小海豹 - 动画总量:8 种状态 × 平均 12 帧 ≈ 96 PNG - 单只宠物纹理总体积 ≤ 3 MB(打包后) - 色调:主色 `#B8C7D4`(海豹灰蓝),腹白 `#F5F7FA`,描边 `#2A3A4A`,强调色 `#F4A258`(用于眼睛高光与完成动画) - 动画设计要点: - `idle`:趴坐姿,身体随呼吸轻微起伏,每 3s 眨一次大眼睛,偶尔轻拍一下短尾鳍 - `responding`:两只前鳍交替拍打虚拟键盘,1s 一个循环,脑袋轻微左右摆 - `thinking`:用一只前鳍抵着下巴做托腮状,头顶问号/省略号气泡忽明忽暗 - `tool-use`:戴上小圆眼镜,鳍边漂浮扳手/书本等工具图标 - `permission-prompt`:警觉抬身、瞪大眼睛,头顶红色感叹号闪烁 - `ask-user`:歪头,两只前鳍合捧起一枚黄色问号牌 - `completed`:开心拍鳍 + 原地小跳一下,头顶撒出小星星(非循环 1s) - `error-interrupted`:整只软塌塌摊成一块麻薯,上方飘着小黑电雷 ### 4.2 设计文件组织 - 源文件:Figma(交付时随主题包附 `.fig` 链接 README) - 导出:每帧 PNG + `manifest.json` 由 `scripts/build-theme.sh` 自动生成 --- ## 5. Hook 安装与卸载流程 ### 5.1 首次安装(Claude Code) 面板"Hooks → Claude Code → 安装"时: 1. 检测 `~/.claude/settings.json` 是否存在,不存在则创建空 `{}` 2. 备份为 `~/.claude/settings.json.hopet.bak`(带时间戳) 3. Merge 架构文档 8.4 所列的 hook 条目;遇到已有同名 hook 则 **append**(保留用户既有 hook) 4. 将 `hopet-emit` 从 bundle 资源拷贝到 `~/.hopet/bin/hopet-emit` 并 `chmod +x` 5. 调用 `HookDoctor`: - settings.json 语法正确 - `~/.hopet/bin/hopet-emit` 存在且可执行 - socket 路径能被触达(App 正在运行) 6. UI 显示 Healthy 勾 ### 5.2 卸载 - 反向移除 Hopet 添加的条目,保留用户其它 hook - 可选择恢复到最近一次 `.hopet.bak` - 保留 `~/.hopet/bin/hopet-emit` 以便随时重装;彻底卸载选项删除整个 `~/.hopet/bin/` ### 5.3 升级 - App 升级时如 `hopet-emit` 版本低于 bundle 版本,自动替换二进制(不改 settings.json) ### 5.4 故障排查(HookDoctor 输出示例) ``` ✓ ~/.claude/settings.json 存在且合法 ✓ 已发现 7 个 Hopet 注入的 hook 条目 ✗ ~/.hopet/run/hopetd.sock 不存在 → 请确认 Hopet.app 正在运行 ✓ ~/.hopet/bin/hopet-emit 可执行 (mode 0755, 1.4 MB) ``` --- ## 6. 偏好设置项一览 实际落盘 schema 在 `Sources/Hopet/Core/HopetConfig.swift`,与 [preferences.md §4.3](./preferences.md) 一致。`~/.hopet/config.json` 当前只承载这些键: | 键 | 类型 | 默认 | 说明 | | --- | --- | --- | --- | | `version` | Int | 1 | schema 版本号;未知版本整体降级到默认值并 warn | | `appearance` | Enum | `system` | `light` / `dark` / `system`;切换实时生效(`NSApp.appearance`) | | `activeThemeId` | String | `hopi.default` | 当前主题 id;启动时若指向不存在主题(用户删目录后)降级到 `hopi.default` 并写回 | | `listeners.claudeCode` | Bool | `true` | Claude Code listener 软静音;off 时 EventRouter 静默丢事件、SceneRouter 清掉无 pending 的气泡,hook 文件不动 | | `listeners.codex` | Bool | `true` | Codex CLI listener 软静音;与上同义 | 以下 UI 可见性偏好刻意保留在 `UserDefaults`,不进入 `~/.hopet/config.json`: | 键 | 类型 | 默认 | 说明 | | --- | --- | --- | --- | | `pet.visible` | Bool | `true` | Overview / 菜单栏 Toggle 的单一真相源;`SceneRouter` 实时 show/hide 宠物窗口 | | `notch.enabled` | Bool | `true` | Overview / Behavior Notch 的单一真相源;`SceneRouter` 实时 show/hide 刘海条 | | `notch.fallbackBarEnabled` | Bool | `false` | 无物理刘海显示器的降级顶条开关;只有 `notch.enabled = true` 时才可能显示,切换后由 `SceneRouter` 立即重算 | 其它过去文档列出的 `general.launchAtLogin` / `pet.maxConcurrent` / `bubble.preferredTerminal` / `notifications.*` / `advanced.logLevel` 等键,BehaviorTab / NotificationsTab 当前只是 UI 占位骨架,**未全部联通到运行时行为,也未写入 config.json**。这些项会在 v0.2 真正落地时补入 HopetConfig schema。 --- ## 7. 快捷键 > v0.1 未实现快捷键录制与全局快捷键注册——以下表格是规划态。当前的兜底入口是菜单栏图标(左键展开菜单:显示宠物 / 打开偏好 / 退出)。 | 快捷键 | 作用 | 范围 | v0.1 状态 | | --- | --- | --- | --- | | `⌘⇧H` | 显示/隐藏宠物 | 全局 | ⛔ | | `⌘,` | 打开偏好面板 | App active 时 | ⛔ | | `⌘W` | 关闭当前窗口 | App active 时 | ⛔ | | `Esc` | 关闭气泡展开 / 取消拖拽 | 气泡 active | ⛔ | | `↩` | 提交 Permission 决策 / AskUserQuestion 答题 | 对应卡片 active | ⛔(按钮可达) | ### 7.1 录制与冲突处理 - **录制 UI**:Behavior Tab 提供快捷键录制器(基于 `KeyboardShortcuts` 库或自实现),用户可自定义两个全局快捷键 - **保留快捷键禁止**:禁止录制系统级保留键(`⌘Q` / `⌘Tab` / `⌘Space` 等),录制时实时反馈"已被系统占用" - **注册失败处理**:调用 `RegisterEventHotKey` 失败(通常因被其它 App 抢占)时: 1. 菜单栏图标显示 warning 小角标 2. Behavior Tab 该快捷键行高亮黄色 + 提示文案 "已被其它 App 占用,建议改键" 3. 提供「重新录制」「禁用此快捷键」两个按钮 4. 不重试注册(避免循环),直到用户手动操作 - **冲突可观测**:偏好底部"诊断"按钮的输出包含两个全局快捷键的当前注册状态 - **关闭**:每个全局快捷键都可独立关闭,关闭后菜单栏入口与 App 内的"打开偏好"按钮仍然可达 ### 7.2 终极兜底 无论全局快捷键状态如何,菜单栏图标始终是 100% 可达入口:左键单击展开菜单("快速询问…" / "显示宠物" / "偏好…" / "退出")。 --- ## 8. 权限与首次引导 > v0.1 未实现 Onboarding 向导,以下是 v0.2 的规划态;当前首次启动直接进入主界面,用户需自行打开偏好面板的 Hooks Tab 完成安装。 ### 8.1 Onboarding 页面(v0.2 规划) 流程为 5 步向导: 1. **欢迎** — 短动画演示一次完整状态流 2. **选择默认 AI 工具** — Claude Code / Codex(可多选) 3. **安装 Hooks** — 一键安装,失败给出手动指引 4. **授予权限**(按需请求):通知(必选推荐) 5. **完成** — 显示第一只宠物,触发一次 `completed` 动画作为 welcome ### 8.2 权限缺失时的降级 - Permission / AskUserQuestion 答题不依赖任何 macOS 权限(hook 通道是协议级) - 通知中心横幅 v0.1 未真正注册(NotificationsTab 仅有 Toggle 占位) - 任一关键权限缺失时的菜单栏小红点提示属于 v0.2 规划项 --- ## 9. 错误与降级处理 ### 9.1 运行时错误 | 错误场景 | 行为 | | --- | --- | | Socket 创建失败(地址被占) | 自动 unlink 残留 socket 重试 3 次;仍失败则菜单栏红点 + 日志 | | Hook 事件 JSON 解析失败 | 记录 warn 日志,丢弃该事件,不影响其它事件 | | 主题加载失败 | 回退到内置 `hopi.default`;弹窗提示用户 | | 帧图缺失 | 该状态动画用静态首帧兜底 | | 多显示器变化(外接拔插) | 0.5s debounce 后重新计算刘海 rect 与宠物位置 | | 深色/浅色模式切换 | 刘海条与面板自动跟随;宠物主题可自行声明 `variantDark` | | 系统省电模式 | 动画降到 30fps;刘海条动效弱化 | | Accessibility 权限被撤销 | 下次使用 A 模式时降级并提示 | | App 升级后所有权限被系统遗忘 | 未签名分发的固有限制(TCC 按签名绑定);首次启动检测到权限全失时,显示一次性的再次引导页,一键跳转至对应系统设置 | ### 9.2 用户可见错误 - 偏好面板 Behavior Tab 底部有"诊断"按钮,一键执行 HookDoctor、socket 自检、权限自检、主题校验,结果可复制到剪贴板便于 issue 提交 - `~/.hopet/logs/hopet.log` 始终保留最近 3 天日志,可在 Advanced 中打开日志目录 ### 9.3 崩溃恢复 - App 崩溃后重启:从 `state/sessions.json` 恢复最近 10 分钟内的会话,显示为 `idle` 灰度;后续事件触发时自动"醒来" - 超过 10 分钟则视为过期不恢复 --- ## 10. 后续版本功能规划 v0.2 / v0.3 完整范围由 [architecture.md §13](./architecture.md#13-里程碑路线) 维护,本节不重复列举。从用户视角的关键期待: - **v0.2**:外部启动的 session 也能原位输入(Accessibility 路径);首批第三方主题导入;Codex 细粒度状态;自动更新与声音反馈 - **v0.3**:主题作者工具链与文档;可自定义"状态 → 动画"映射;多显示器;iCloud 同步偏好 ### 长期愿景(v1.0+) - 主题商店(需要后端) - 跨机器"宠物形象"跟随(扫码配对) - 与 IDE 插件联动(Cursor / VS Code),展示 AI 侧边栏状态 - 社区协作:多只宠物之间的互动(例如一只完成时把任务交给另一只) --- ## 文档关联 - 技术实现细节请见 [architecture.md](./architecture.md) - 协议/schema/脚本样例在 architecture.md 第 8、9 章附录