Fix cold startup and refresh pet experience
This commit is contained in:
1 parent
49c7fc0fc6
commit
feac498e1d
24 files changed
+183
-37
No files matched your search
@@ -14,6 +14,26 @@ Keep each user-visible change in its own bullet. Write the English text first, t
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed / 变更
|
||||
|
||||
- New installations now show pet-bubble sessions in one cross-Agent flat stack by default; grouping sessions by Agent remains available in Pet Configuration.
|
||||
|
||||
全新安装的宠物气泡现在默认将所有 Agent 会话放入同一个平铺堆栈;仍可在“宠物配置”中开启按 Agent 聚合。
|
||||
|
||||
- The English and Chinese READMEs now share three privacy-safe product screenshots covering the desktop-pet status experience, the five-area control center, and AI Pet Maker.
|
||||
|
||||
中英文 README 现在共用三张不含隐私信息的产品截图,分别展示桌宠状态体验、包含五个功能区域的控制中心,以及 AI 宠物制作。
|
||||
|
||||
### Fixed / 修复
|
||||
|
||||
- Fixed a macOS 26 crash when Metal completed a desktop-pet frame on its background completion queue while the callback was incorrectly isolated to the main actor.
|
||||
|
||||
修复 macOS 26 上 Metal 在后台完成队列结束桌宠帧渲染时,回调被错误隔离到主 Actor 而导致的闪退。
|
||||
|
||||
- Cold startup now joins the single PetCore bootstrap before handling the App's initial activation refresh, avoiding a transient offline state and duplicate recovery while a new installation validates and seeds its bundled pets.
|
||||
|
||||
冷启动处理 App 首次激活刷新前,现在会先汇入唯一的 PetCore 启动流程,避免全新安装校验并种入内置宠物期间短暂误报离线及重复触发恢复。
|
||||
|
||||
## [0.3.3] - 2026-08-10
|
||||
|
||||
### Changed / 变更
|
||||
|
||||
@@ -12,6 +12,10 @@ Agent Pet Companion is a native macOS desktop pet for people who work with one o
|
||||
|
||||
[Download the latest release](https://github.com/xjxtree/agent-pet-companion/releases) · macOS 14 or later · Apple silicon and Intel Macs
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/assets/screenshots/desktop-hero.png" width="1120" alt="Agent Pet Companion showing a desktop pet with live OpenCode, Pi Coding Agent, and Claude Code status bubbles">
|
||||
</p>
|
||||
|
||||
## What it does for you
|
||||
|
||||
- **Switch windows less often** — see Agent activity on your desktop without repeatedly opening a terminal or Agent app.
|
||||
@@ -21,6 +25,10 @@ Agent Pet Companion is a native macOS desktop pet for people who work with one o
|
||||
- **Make the pet yours** — use one of three bundled pets, import a `.petpack`, or create and edit your own pet with AI Pet Maker.
|
||||
- **Keep data centered on your Mac** — pets, settings, limited session summaries, and diagnostics remain local. The App does not read Agent credentials, tokens, cookies, or API keys.
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/assets/screenshots/control-center.png" width="1120" alt="Agent Pet Companion control center with its five navigation areas and the Pet Library">
|
||||
</p>
|
||||
|
||||
## Get started in three steps
|
||||
|
||||
1. Download and install the App from [GitHub Releases](https://github.com/xjxtree/agent-pet-companion/releases).
|
||||
@@ -47,6 +55,10 @@ Agent Connections is organized by Agent and session, so you do not need to confi
|
||||
|
||||
In **AI Pet Maker**, describe the appearance, style, and important traits you want to keep. The App uses locally available Codex capabilities to create a new pet or edit an existing one.
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/assets/screenshots/ai-pet-maker.png" width="1120" alt="AI Pet Maker creation workspace in Agent Pet Companion">
|
||||
</p>
|
||||
|
||||
- In-App creation supports low and standard resolution.
|
||||
- Creation progress is retained, and you can answer questions inside the App when a task needs your input.
|
||||
- You can also import compatible `.petpack` V3 packages, including high-resolution packages made by external workflows.
|
||||
|
||||
@@ -12,6 +12,10 @@ Agent Pet Companion 是一款 macOS 原生桌宠 App,适合同时使用一个
|
||||
|
||||
[下载最新版本](https://github.com/xjxtree/agent-pet-companion/releases) · 支持 macOS 14 及以上版本 · Apple 芯片与 Intel Mac
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/assets/screenshots/desktop-hero.png" width="1120" alt="Agent Pet Companion 桌宠与 OpenCode、Pi Coding Agent、Claude Code 实时状态气泡">
|
||||
</p>
|
||||
|
||||
## 它能帮你做什么
|
||||
|
||||
- **少一些来回切换**:在桌面上直接看到 Agent 的工作状态,不必反复打开终端或 Agent App。
|
||||
@@ -21,6 +25,10 @@ Agent Pet Companion 是一款 macOS 原生桌宠 App,适合同时使用一个
|
||||
- **拥有自己的桌宠**:使用三个内置宠物,导入 `.petpack`,或通过 AI 宠物制作创建和修改专属宠物。
|
||||
- **数据以本机为中心**:宠物、设置、有限的会话摘要和诊断信息保存在 Mac 上;App 不读取 Agent 的凭据、Token、Cookie 或 API Key。
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/assets/screenshots/control-center.png" width="1120" alt="包含五个导航区域和宠物库的 Agent Pet Companion 控制中心">
|
||||
</p>
|
||||
|
||||
## 三步开始使用
|
||||
|
||||
1. 从 [GitHub Releases](https://github.com/xjxtree/agent-pet-companion/releases) 下载并安装 App。
|
||||
@@ -47,6 +55,10 @@ Agent 连接按 Agent 和会话组织,不要求你逐个配置项目目录。A
|
||||
|
||||
在 **AI 宠物制作** 中描述外形、风格和你希望保留的特征,App 会通过本机可用的 Codex 能力创建新宠物,也可以基于现有宠物进行修改。
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/assets/screenshots/ai-pet-maker.png" width="1120" alt="Agent Pet Companion 的 AI 宠物制作工作区">
|
||||
</p>
|
||||
|
||||
- App 内支持低分辨率和标准分辨率制作。
|
||||
- 制作任务会保留进度;遇到需要确认的问题时,可以直接在 App 内回复。
|
||||
- 也可以导入符合 `.petpack` V3 规范的宠物包,包括由外部流程制作的高清宠物包。
|
||||
|
||||
@@ -1780,7 +1780,7 @@ final class AppStore: ObservableObject {
|
||||
workspace.openApplication(
|
||||
at: applicationURL,
|
||||
configuration: configuration
|
||||
) { application, error in
|
||||
) { @Sendable application, error in
|
||||
continuation.resume(returning:
|
||||
application != nil && error == nil
|
||||
? .openedAgentHost
|
||||
@@ -4047,6 +4047,14 @@ final class AppStore: ObservableObject {
|
||||
}
|
||||
|
||||
func refreshMakerAfterLifecycleEvent() async {
|
||||
// didBecomeActive is also delivered during the first launch, before a
|
||||
// clean-home PetCore has necessarily finished staging its runtime and
|
||||
// seeding bundled pets. Join that single bootstrap pipeline instead of
|
||||
// issuing an early snapshot request that briefly marks the service
|
||||
// offline and starts a duplicate recovery path.
|
||||
guard await runRuntimeBootstrapIfNeeded(startMode: .ensureRunning) else {
|
||||
return
|
||||
}
|
||||
_ = await refresh()
|
||||
await refreshGenerationHistory()
|
||||
guard let jobID = selectedGenerationHistoryJobID,
|
||||
|
||||
@@ -2492,7 +2492,7 @@ enum OverlayBubbleProjection {
|
||||
states: [ActiveAgentState],
|
||||
omittedCount: Int,
|
||||
dismissedSessionIDs: Set<String>,
|
||||
groupSessionsByAgent: Bool = true,
|
||||
groupSessionsByAgent: Bool = false,
|
||||
standaloneSessionOrder: [String] = [],
|
||||
standaloneStackExpanded: Bool = true,
|
||||
isExpanded: (AgentSource) -> Bool
|
||||
|
||||
@@ -1492,10 +1492,10 @@ final class PetMetalFrameRenderer: NSObject, MTKViewDelegate, PetRendererLifecyc
|
||||
// A successful command-buffer completion only means the GPU work
|
||||
// finished. Publish from the drawable's presented callback so pointer
|
||||
// geometry follows the frame that actually reached the display.
|
||||
// Keep this explicitly Sendable instead of relying on the SDK import.
|
||||
// The macOS 15 release SDK does not annotate this callback, so an
|
||||
// Keep both callbacks explicitly Sendable instead of relying on the
|
||||
// SDK imports. The macOS 15 release SDK does not annotate them, so an
|
||||
// unannotated closure inherits MainActor here and traps when
|
||||
// CoreAnimation invokes it on CAMetalLayerEventListenerQueue.
|
||||
// CoreAnimation or Metal invokes it on a completion queue.
|
||||
drawable.addPresentedHandler { @Sendable [weak self] presentedDrawable in
|
||||
self?.enqueueFramePresentationResolution(
|
||||
presentedDrawable.presentedTime > 0
|
||||
@@ -1506,7 +1506,7 @@ final class PetMetalFrameRenderer: NSObject, MTKViewDelegate, PetRendererLifecyc
|
||||
token: presentationToken
|
||||
)
|
||||
}
|
||||
commandBuffer.addCompletedHandler { [weak self] completedBuffer in
|
||||
commandBuffer.addCompletedHandler { @Sendable [weak self] completedBuffer in
|
||||
guard completedBuffer.status != .completed else { return }
|
||||
self?.enqueueFramePresentationResolution(
|
||||
.failed,
|
||||
|
||||
@@ -304,7 +304,10 @@ struct ReferenceImageDropZone: View {
|
||||
var accepted = false
|
||||
for provider in providers where provider.hasItemConformingToTypeIdentifier(UTType.fileURL.identifier) {
|
||||
accepted = true
|
||||
provider.loadItem(forTypeIdentifier: UTType.fileURL.identifier, options: nil) { item, _ in
|
||||
provider.loadItem(
|
||||
forTypeIdentifier: UTType.fileURL.identifier,
|
||||
options: nil
|
||||
) { @Sendable item, _ in
|
||||
guard let url = ReferenceImageDropItem.fileURL(from: item) else { return }
|
||||
Task { @MainActor in
|
||||
store.addReferenceImageURLs([url])
|
||||
|
||||
@@ -487,7 +487,7 @@ public struct BehaviorSettings: Codable, Equatable, Sendable {
|
||||
clickMenu: Bool = true,
|
||||
autoHide: Bool = false,
|
||||
sessionMessageTimeoutMinutes: Int = 15,
|
||||
groupSessionsByAgent: Bool = true,
|
||||
groupSessionsByAgent: Bool = false,
|
||||
sessionGroupDisplay: SessionGroupDisplay = .stacked,
|
||||
sources: [AgentSource: Bool] = Dictionary(uniqueKeysWithValues: AgentSource.allCases.map { ($0, true) }),
|
||||
events: [AgentEventKind: Bool] = Dictionary(uniqueKeysWithValues: AgentEventKind.allCases.map { ($0, true) })
|
||||
|
||||
@@ -11,6 +11,7 @@ struct BehaviorSettingsTests {
|
||||
#expect(AgentSource.allCases.allSatisfy {
|
||||
defaults.sources[$0] == true
|
||||
})
|
||||
#expect(!defaults.groupSessionsByAgent)
|
||||
|
||||
let legacy = try JSONDecoder().decode(
|
||||
BehaviorSettings.self,
|
||||
@@ -24,7 +25,8 @@ struct BehaviorSettingsTests {
|
||||
|
||||
@Test
|
||||
func patchContainsOnlyChangedScalarAndMapEntries() throws {
|
||||
let previous = BehaviorSettings()
|
||||
var previous = BehaviorSettings()
|
||||
previous.groupSessionsByAgent = true
|
||||
var next = previous
|
||||
next.autoHide = true
|
||||
next.interfaceLanguage = .simplifiedChinese
|
||||
@@ -71,7 +73,7 @@ struct BehaviorSettingsTests {
|
||||
|
||||
#expect(decoded.interfaceLanguage == .system)
|
||||
#expect(decoded.appearanceTheme == .system)
|
||||
#expect(decoded.groupSessionsByAgent)
|
||||
#expect(!decoded.groupSessionsByAgent)
|
||||
#expect(decoded.sessionGroupDisplay == .stacked)
|
||||
#expect(object["bubble_transparency"] == nil)
|
||||
}
|
||||
@@ -108,7 +110,7 @@ struct BehaviorSettingsTests {
|
||||
let behavior = BehaviorSettings(
|
||||
interfaceLanguage: .english,
|
||||
appearanceTheme: .light,
|
||||
groupSessionsByAgent: false,
|
||||
groupSessionsByAgent: true,
|
||||
sessionGroupDisplay: .expanded
|
||||
)
|
||||
let data = try JSONEncoder().encode(behavior)
|
||||
@@ -116,7 +118,7 @@ struct BehaviorSettingsTests {
|
||||
|
||||
#expect(decoded.interfaceLanguage == .english)
|
||||
#expect(decoded.appearanceTheme == .light)
|
||||
#expect(!decoded.groupSessionsByAgent)
|
||||
#expect(decoded.groupSessionsByAgent)
|
||||
#expect(decoded.sessionGroupDisplay == .expanded)
|
||||
#expect(SessionGroupDisplay.allCases.map(\.title) == ["堆叠", "展开"])
|
||||
}
|
||||
|
||||
@@ -86,7 +86,10 @@ struct AppStoreOverlaySnapshotTests {
|
||||
try store.applyStateSnapshot([
|
||||
"revision": "multi-source-overlay-1",
|
||||
"overlay_placement_revision": "0",
|
||||
"behavior": try jsonObject(BehaviorSettings(sessionGroupDisplay: .stacked)),
|
||||
"behavior": try jsonObject(BehaviorSettings(
|
||||
groupSessionsByAgent: true,
|
||||
sessionGroupDisplay: .stacked
|
||||
)),
|
||||
"behavior_revision": "1",
|
||||
"pets": [],
|
||||
"active_agent_sessions": try jsonArray(states),
|
||||
|
||||
@@ -231,7 +231,7 @@ struct BehaviorSettingsViewTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
func sidebarPreviewUsesTheDefaultAgentGroupAndFoldedMultiSessionPreference() throws {
|
||||
func sidebarPreviewUsesTheDefaultFlatFoldedSessionStack() throws {
|
||||
let presentation = SidebarConfigurationPreviewPresentation(
|
||||
behavior: BehaviorSettings(),
|
||||
localeIdentifier: "en"
|
||||
@@ -241,18 +241,20 @@ struct BehaviorSettingsViewTests {
|
||||
#expect(presentation.contents.count == 1)
|
||||
#expect(presentation.emptyReason == nil)
|
||||
#expect(content.source == .codex)
|
||||
#expect(!content.isStandaloneSessionCard)
|
||||
#expect(content.sessions.count == 2)
|
||||
#expect(content.isStandaloneSessionCard)
|
||||
#expect(content.sessions.count == 1)
|
||||
#expect(content.visibleSessions.count == 1)
|
||||
#expect(content.disclosureSessionCount == 2)
|
||||
#expect(content.isStacked)
|
||||
#expect(content.showsStackDecoration)
|
||||
#expect(content.sessions.map(\.eventType) == [.waiting, .failed])
|
||||
#expect(content.sessions.map(\.eventType) == [.waiting])
|
||||
#expect(presentation.petAction == .waiting)
|
||||
}
|
||||
|
||||
@Test
|
||||
func sidebarPreviewExpandsAgentSessionsAndPreservesTheirStateMessages() throws {
|
||||
var behavior = BehaviorSettings()
|
||||
behavior.groupSessionsByAgent = true
|
||||
behavior.sessionGroupDisplay = .expanded
|
||||
let presentation = SidebarConfigurationPreviewPresentation(
|
||||
behavior: behavior,
|
||||
@@ -349,8 +351,9 @@ struct BehaviorSettingsViewTests {
|
||||
let content = try #require(presentation.contents.first)
|
||||
|
||||
#expect(content.source == .pi)
|
||||
#expect(content.sessions.map(\.source) == [.pi, .pi])
|
||||
#expect(content.sessions.map(\.eventType) == [.done, .done])
|
||||
#expect(content.sessions.map(\.source) == [.pi])
|
||||
#expect(content.sessions.map(\.eventType) == [.done])
|
||||
#expect(content.disclosureSessionCount == 2)
|
||||
#expect(content.sessions.allSatisfy { $0.statusText == "Completed" })
|
||||
#expect(presentation.petAction == .done)
|
||||
}
|
||||
|
||||
@@ -183,6 +183,65 @@ struct InitialAppearanceBootstrapTests {
|
||||
await bootstrap.value
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Test
|
||||
func initialActivationRefreshJoinsBootstrapBeforeReadingSnapshot() async throws {
|
||||
let startupGate = InitialAppearanceReadyGate()
|
||||
let behavior = BehaviorSettings(appearanceTheme: .dark)
|
||||
let snapshot = try Self.stateSnapshotPayload(
|
||||
behavior: behavior,
|
||||
behaviorRevision: "1",
|
||||
pets: []
|
||||
)
|
||||
var snapshotRequests = 0
|
||||
var historyRequests = 0
|
||||
let store = AppStore(
|
||||
bootstrapHooks: AppStoreBootstrapHooks(
|
||||
ensureRunning: {
|
||||
await startupGate.waitForRelease()
|
||||
return .started
|
||||
},
|
||||
recover: { .started },
|
||||
fetchInitialBehavior: { _ in
|
||||
try Self.versionedBehaviorPayload(behavior, revision: "1")
|
||||
},
|
||||
refreshSnapshot: { store in
|
||||
snapshotRequests += 1
|
||||
try store.applyStateSnapshot(snapshot)
|
||||
},
|
||||
onReady: { _ in }
|
||||
),
|
||||
applicationAppearanceApplier: { _ in },
|
||||
overlayPresenter: { _, _ in },
|
||||
petCoreRequestOverride: { method, _, _ in
|
||||
guard method == "generation.history.list" else {
|
||||
throw PetCoreClientError.invalidResponse
|
||||
}
|
||||
historyRequests += 1
|
||||
return ["ok": true, "jobs": [], "truncated": false]
|
||||
},
|
||||
productConvergenceManifest: nil
|
||||
)
|
||||
|
||||
let lifecycleRefresh = Task { @MainActor in
|
||||
await store.refreshMakerAfterLifecycleEvent()
|
||||
}
|
||||
await startupGate.waitUntilStarted()
|
||||
await Task.yield()
|
||||
|
||||
#expect(snapshotRequests == 0)
|
||||
#expect(historyRequests == 0)
|
||||
#expect(store.petCoreOperationalState == .checking)
|
||||
|
||||
await startupGate.release()
|
||||
await lifecycleRefresh.value
|
||||
|
||||
#expect(snapshotRequests == 1)
|
||||
#expect(historyRequests == 1)
|
||||
#expect(store.hasLoadedStateSnapshot)
|
||||
#expect(store.petCoreOperationalState == .online)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Test
|
||||
func managedRuntimeUpgradeUsesTheBrandedBlockingStateFromBootstrapStart() async {
|
||||
|
||||
@@ -1697,6 +1697,7 @@ struct UIModelTests {
|
||||
@Test
|
||||
func agentGroupUsesTheConfiguredDefaultAndAllowsATemporaryOverride() {
|
||||
let store = makeStore()
|
||||
store.behavior.groupSessionsByAgent = true
|
||||
store.behavior.sessionGroupDisplay = .stacked
|
||||
#expect(!store.overlayAgentGroupIsExpanded(.codex))
|
||||
|
||||
@@ -1704,6 +1705,7 @@ struct UIModelTests {
|
||||
#expect(store.overlayAgentGroupIsExpanded(.codex))
|
||||
|
||||
let expandedStore = makeStore()
|
||||
expandedStore.behavior.groupSessionsByAgent = true
|
||||
expandedStore.behavior.sessionGroupDisplay = .expanded
|
||||
#expect(expandedStore.overlayAgentGroupIsExpanded(.opencode))
|
||||
}
|
||||
|
||||
@@ -700,7 +700,7 @@ impl Default for BehaviorSettings {
|
||||
click_menu: true,
|
||||
mouse_passthrough: true,
|
||||
auto_hide: false,
|
||||
group_sessions_by_agent: true,
|
||||
group_sessions_by_agent: false,
|
||||
session_group_display: SessionGroupDisplay::Stacked,
|
||||
session_message_timeout_minutes: DEFAULT_SESSION_MESSAGE_TIMEOUT_MINUTES,
|
||||
sources,
|
||||
|
||||
@@ -3873,7 +3873,7 @@ fn interface_language_patch_is_typed_and_persisted() {
|
||||
fn session_group_display_patch_is_typed_and_persisted() {
|
||||
let (_temp, state) = ready();
|
||||
let initial = snapshot(&state);
|
||||
assert_eq!(initial["behavior"]["group_sessions_by_agent"], true);
|
||||
assert_eq!(initial["behavior"]["group_sessions_by_agent"], false);
|
||||
assert_eq!(initial["behavior"]["session_group_display"], "stacked");
|
||||
|
||||
let revision = initial["behavior_revision"].as_str().unwrap();
|
||||
@@ -3881,16 +3881,16 @@ fn session_group_display_patch_is_typed_and_persisted() {
|
||||
&state,
|
||||
revision,
|
||||
json!({
|
||||
"group_sessions_by_agent": false,
|
||||
"group_sessions_by_agent": true,
|
||||
"session_group_display": "expanded"
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(updated["behavior"]["group_sessions_by_agent"], false);
|
||||
assert_eq!(updated["behavior"]["group_sessions_by_agent"], true);
|
||||
assert_eq!(updated["behavior"]["session_group_display"], "expanded");
|
||||
assert_eq!(
|
||||
snapshot(&state)["behavior"]["group_sessions_by_agent"],
|
||||
false
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot(&state)["behavior"]["session_group_display"],
|
||||
|
||||
@@ -1417,10 +1417,10 @@ fn agent_event_ingest_accepts_payload_json_alias() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn behavior_settings_default_serializes_system_language_and_stacked_session_group_display() {
|
||||
fn behavior_settings_default_serializes_flat_stacked_sessions() {
|
||||
let behavior = BehaviorSettings::default();
|
||||
assert_eq!(behavior.interface_language, InterfaceLanguage::System);
|
||||
assert!(behavior.group_sessions_by_agent);
|
||||
assert!(!behavior.group_sessions_by_agent);
|
||||
assert_eq!(behavior.session_group_display, SessionGroupDisplay::Stacked);
|
||||
assert_eq!(
|
||||
serde_json::to_value(&behavior).unwrap()["interface_language"],
|
||||
@@ -1459,7 +1459,7 @@ fn behavior_settings_decode_legacy_sparse_json_with_defaults() {
|
||||
petcore_types::AppearanceTheme::System
|
||||
);
|
||||
assert_eq!(decoded.interface_language, InterfaceLanguage::System);
|
||||
assert!(decoded.group_sessions_by_agent);
|
||||
assert!(!decoded.group_sessions_by_agent);
|
||||
assert!(decoded.status_bubble);
|
||||
assert!(decoded.click_menu);
|
||||
assert!(decoded.mouse_passthrough);
|
||||
@@ -1487,7 +1487,7 @@ fn behavior_settings_decode_legacy_sparse_json_with_defaults() {
|
||||
petcore_types::AppearanceTheme::System
|
||||
);
|
||||
assert_eq!(stored.interface_language, InterfaceLanguage::System);
|
||||
assert!(stored.group_sessions_by_agent);
|
||||
assert!(!stored.group_sessions_by_agent);
|
||||
assert!(stored.mouse_passthrough);
|
||||
assert_eq!(stored.session_group_display, SessionGroupDisplay::Stacked);
|
||||
assert_eq!(stored.sources.get(&AgentSource::Codex), Some(&false));
|
||||
|
||||
@@ -51,7 +51,7 @@ The exact schema and migrations are in [db.rs](../../crates/petcore/src/db.rs);
|
||||
|
||||
`onboarding_progress` uses `apc.onboarding-progress.v1` with `choose_pet`, `connect_agents`, `demo`, `completed`, and `skipped`. Missing data means `choose_pet` at revision `0`. Updates are compare-and-swap, move only forward or explicitly skip, and make `completed`/`skipped` terminal. Completion and enabling the pet commit together. Demo phases are App-local and never enter events or diagnostics.
|
||||
|
||||
`behavior` owns language, appearance, source/event enablement, timeouts, attention settings, bubble text scale, and `group_sessions_by_agent`. Agent-group disclosure defaults come from the setting; flat-tray disclosure, card slots, hover, and scrolling remain App-local. Transparent-area pointer passthrough is an App invariant, not a behavior preference. The legacy `mouse_passthrough` field remains serialized as `true` for mixed-version compatibility; persisted `false` values canonicalize to `true`, and new patches to that field are rejected.
|
||||
`behavior` owns language, appearance, source/event enablement, timeouts, attention settings, bubble text scale, and `group_sessions_by_agent`. New state defaults `group_sessions_by_agent` to `false`, producing the cross-Agent flat tray; an explicit stored choice remains authoritative. Agent-group disclosure defaults come from the setting; flat-tray disclosure, card slots, hover, and scrolling remain App-local. Transparent-area pointer passthrough is an App invariant, not a behavior preference. The legacy `mouse_passthrough` field remains serialized as `true` for mixed-version compatibility; persisted `false` values canonicalize to `true`, and new patches to that field are rejected.
|
||||
|
||||
`overlay_placement` stores the hard-clamped absolute center, display identity, and logical `display_width_pt` (100–300, default 112). Coordinates canonicalize to a shared 1/256 pt grid. A placement-specific decimal revision provides compare-and-swap authority. During upgrade, values from the previous 80–224 contract below the current minimum are normalized once to 100; all other malformed or out-of-contract values remain rejected. `overlay_placement_intent` carries only explicit reset/reposition instructions and clears atomically when the App acknowledges them. App-side drag revisions and retry journals are presentation state, not a second durable placement model.
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ The App and overlay share one UI process. PetCore is a separate per-user service
|
||||
- First run is a resumable three-scene root presentation. Pet choice and Agent setup reuse normal product operations; the demo remains View-local and never creates Agent events or diagnostics.
|
||||
- Connections and bubbles use `Agent → session`, never project directories as user-facing identity.
|
||||
- A session title, latest user message, current-turn Agent message, and normalized activity detail are separate bounded fields. PetCore supplies them; Swift does not reconstruct host payloads.
|
||||
- Bubbles can group sessions per Agent or show one cross-Agent card list. The flat list keeps stable slots while a turn alternates between thinking, planning, and tools; only a new activation or attention-state transition may move a card.
|
||||
- Bubbles default to one cross-Agent session-card list; users may opt into grouping sessions per Agent. The flat list keeps stable slots while a turn alternates between thinking, planning, and tools; only a new activation or attention-state transition may move a card.
|
||||
- Clicking the pet toggles the bubble. Only a concrete session row can open a validated exact-session or host-level destination.
|
||||
- On macOS 26, session bubbles use one untinted native Regular Liquid Glass surface plus a paired sub-point light/dark optical rim that preserves the rounded boundary across mixed bright and dark backdrops without adding a fill, tint, shadow, or second material. Compact floating controls may use Clear glass. Older systems use the system material fallback; accessibility settings can replace or strengthen the optical treatment.
|
||||
- Bundled pets are read-only defaults. Same-name pets coexist because identity is the manifest ID, not the display name.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 272 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 388 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
@@ -154,7 +154,10 @@ guard let mainWindow = resolvedMainWindow else {
|
||||
// the onboarding-specific scene progression is validated separately.
|
||||
var onboardingWasPresented = false
|
||||
var onboardingSkipAction: AXUIElement?
|
||||
for _ in 0..<80 {
|
||||
// Clean-home bootstrap validates and seeds the complete bundled inventory
|
||||
// before onboarding becomes authoritative. Do not mistake that bounded loading
|
||||
// state for a returning user whose onboarding was already completed.
|
||||
for _ in 0..<240 {
|
||||
let currentNodes = snapshotNodes(mainWindow)
|
||||
if currentNodes.contains(where: {
|
||||
$0.identifier == "onboarding.root"
|
||||
|
||||
@@ -92,7 +92,11 @@ ORIGINAL_BEHAVIOR_JSON="$(SNAPSHOT="$SNAPSHOT" python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
|
||||
print(json.dumps(json.loads(os.environ["SNAPSHOT"])["behavior"], ensure_ascii=False))
|
||||
behavior = dict(json.loads(os.environ["SNAPSHOT"])["behavior"])
|
||||
# The snapshot keeps this legacy read-only field for mixed-version clients, but
|
||||
# current behavior patches must omit it because pointer passthrough is invariant.
|
||||
behavior.pop("mouse_passthrough", None)
|
||||
print(json.dumps(behavior, ensure_ascii=False))
|
||||
PY
|
||||
)"
|
||||
ORIGINAL_PLACEMENT_JSON="$(SNAPSHOT="$SNAPSHOT" python3 - <<'PY'
|
||||
@@ -109,11 +113,12 @@ import os
|
||||
|
||||
data = json.loads(os.environ["SNAPSHOT"])
|
||||
behavior = dict(data["behavior"])
|
||||
behavior.pop("mouse_passthrough", None)
|
||||
behavior["enabled"] = True
|
||||
behavior["status_bubble"] = True
|
||||
behavior["click_menu"] = True
|
||||
behavior["mouse_passthrough"] = True
|
||||
behavior["auto_hide"] = False
|
||||
behavior["group_sessions_by_agent"] = True
|
||||
for key in ["codex", "claude_code", "pi", "opencode"]:
|
||||
behavior.setdefault("sources", {})[key] = True
|
||||
for key in ["start", "thinking", "plan", "tool", "waiting", "done", "failed"]:
|
||||
@@ -396,7 +401,7 @@ wait_for_overlay_ax() {
|
||||
local attempt
|
||||
local log_path="$TMP_DIR/overlay-ax-${expected_vertical_relation}.log"
|
||||
|
||||
for attempt in {1..40}; do
|
||||
for attempt in {1..120}; do
|
||||
if validate_overlay_ax "$expected_vertical_relation" >"$log_path" 2>&1; then
|
||||
cat "$log_path"
|
||||
return 0
|
||||
@@ -594,7 +599,7 @@ wait_for_single_compact_bubble() {
|
||||
local attempt
|
||||
local log_path="$TMP_DIR/overlay-ax-compact.log"
|
||||
|
||||
for attempt in {1..40}; do
|
||||
for attempt in {1..120}; do
|
||||
if validate_single_compact_bubble >"$log_path" 2>&1; then
|
||||
cat "$log_path"
|
||||
return 0
|
||||
@@ -628,6 +633,7 @@ import os
|
||||
|
||||
data = json.loads(os.environ["SNAPSHOT"])
|
||||
behavior = dict(data["behavior"])
|
||||
behavior.pop("mouse_passthrough", None)
|
||||
for key in ["codex", "claude_code", "pi", "opencode"]:
|
||||
behavior.setdefault("sources", {})[key] = key == "codex"
|
||||
print(json.dumps(behavior, ensure_ascii=False))
|
||||
|
||||
@@ -159,7 +159,10 @@ func agentWindows() -> [WindowInfo] {
|
||||
}
|
||||
|
||||
var windows: [WindowInfo] = []
|
||||
for _ in 0..<40 {
|
||||
// A genuinely empty home validates and seeds all three bundled petpacks before
|
||||
// the first overlay can exist. Keep this bound above that measured cold path;
|
||||
// later assertions still fail immediately when the App process exits.
|
||||
for _ in 0..<120 {
|
||||
windows = agentWindows()
|
||||
let floatingWindows = windows.filter { $0.layer != 0 }
|
||||
if expectsBubble {
|
||||
@@ -200,6 +203,14 @@ func isBubblePanel(_ window: WindowInfo) -> Bool {
|
||||
&& window.height <= 720
|
||||
}
|
||||
|
||||
func isControlPanel(_ window: WindowInfo) -> Bool {
|
||||
// The pet-side disclosure control is an independently attached 38 pt panel.
|
||||
// Keep the tolerance narrow so a larger unexpected input-owning window still
|
||||
// fails the desktop-blocking assertion below.
|
||||
abs(window.width - 38) <= 1
|
||||
&& abs(window.height - 38) <= 1
|
||||
}
|
||||
|
||||
let petPanels = floating.filter(isPetPanel)
|
||||
if petPanels.isEmpty {
|
||||
fputs("overlay runtime validation failed: no compact pet panel found\n", stderr)
|
||||
@@ -218,7 +229,9 @@ if expectsBubble && bubblePanels.isEmpty {
|
||||
exit(1)
|
||||
}
|
||||
|
||||
let unexpected = floating.filter { !isPetPanel($0) && !isBubblePanel($0) }
|
||||
let unexpected = floating.filter {
|
||||
!isPetPanel($0) && !isBubblePanel($0) && !isControlPanel($0)
|
||||
}
|
||||
if !unexpected.isEmpty {
|
||||
fputs("overlay runtime validation failed: unexpected floating window size may block desktop input:\n", stderr)
|
||||
for window in unexpected {
|
||||
|
||||
Reference in new issue
Block a user