feat: implement iOS state management (AppState + ManifestStore)#111
feat: implement iOS state management (AppState + ManifestStore)#1112witstudios wants to merge 3 commits intomainfrom
Conversation
Observable state layer for the iOS app: - AppState: manages connections list, active connection, REST/WS lifecycle - ManifestStore: manifest cache with full refresh and incremental WS updates - UserDefaults-backed connection persistence - Auto-connect to last-used server on launch - Connection switching (disconnect current, connect to new) - Error state management with user-facing messages Closes #82
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughTwo new observable state management classes introduced for the iOS app: AppState manages the connection lifecycle, WebSocket lifecycle, and connection persistence with auto-connect capability, while ManifestStore provides centralized manifest caching with full refresh and incremental update support. Total additions: 378 lines. Changes
Sequence Diagram(s)sequenceDiagram
participant User as User
participant AppState as AppState
participant PPGClient as PPGClient (REST)
participant ManifestStore as ManifestStore
participant WebSocketManager as WebSocketManager
participant Server as Server
User->>AppState: connect(to:)
activate AppState
AppState->>PPGClient: Establish REST connection
activate PPGClient
PPGClient-->>AppState: REST ready
deactivate PPGClient
AppState->>ManifestStore: refresh()
activate ManifestStore
ManifestStore->>PPGClient: Fetch manifest
activate PPGClient
PPGClient-->>ManifestStore: Manifest data
deactivate PPGClient
ManifestStore-->>AppState: Manifest cached
deactivate ManifestStore
AppState->>WebSocketManager: start(for:)
activate WebSocketManager
WebSocketManager->>Server: Open WebSocket
activate Server
Server-->>WebSocketManager: Connection established
deactivate Server
WebSocketManager-->>AppState: WS ready
deactivate WebSocketManager
AppState-->>User: Connected
deactivate AppState
Server->>WebSocketManager: WebSocket event (agent/worktree update)
activate WebSocketManager
WebSocketManager->>AppState: handleWebSocketEvent(_:)
activate AppState
AppState->>ManifestStore: updateAgentStatus() or updateWorktreeStatus()
activate ManifestStore
ManifestStore-->>AppState: State updated
deactivate ManifestStore
AppState-->>WebSocketManager: Event processed
deactivate AppState
deactivate WebSocketManager
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Security: - Strip tokens from UserDefaults, store via TokenStorage (Keychain) - Add PersistedConnection projection for token-free serialization - Clean up orphaned Keychain tokens on duplicate replacement and removal Thread safety: - Mark AppState and ManifestStore @mainactor at class level - Remove redundant per-method @mainactor annotations Concurrency: - Guard against concurrent connect() calls with isConnecting check - Make updateConnection() async to await reconnect instead of fire-and-forget UX: - Stop clearing errorMessage in disconnect() so errors remain visible
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
ios/PPGMobile/PPGMobile/State/AppState.swift (1)
105-125: UsedeferforisConnectingreset.This makes the connection-state cleanup future-proof if new early returns are added.
♻️ Proposed refactor
isConnecting = true errorMessage = nil + defer { isConnecting = false } @@ do { try await client.testConnection() } catch { - isConnecting = false errorMessage = "Cannot reach server: \(error.localizedDescription)" return } @@ - isConnecting = false🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ios/PPGMobile/PPGMobile/State/AppState.swift` around lines 105 - 125, Replace the manual resets of isConnecting with a defer-based cleanup: set isConnecting = true at the start of the connection routine (the method containing isConnecting, errorMessage, await client.configure(...), try await client.testConnection(), etc.), immediately add defer { isConnecting = false } so the flag is always cleared on function exit (including early returns), and remove the explicit isConnecting = false lines after the catch and at the end; keep setting errorMessage before returning (e.g., inside the catch) so the defer still runs after the return. Ensure this change is applied around the function that calls client.configure, client.testConnection, startWebSocket(for:), manifestStore.refresh(), and sets activeConnection/UserDefaults (the method referencing isConnecting and activeConnection).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@ios/PPGMobile/PPGMobile/State/AppState.swift`:
- Around line 192-213: startWebSocket's closures can run after stopWebSocket and
mutate state for an old session; fix by gating callbacks to the active session:
when creating ws in startWebSocket assign a unique session token/identifier (or
capture the WebSocketManager instance) and capture that token in the
onStateChange and onEvent closures, then inside those closures check that the
captured token still matches the current webSocket/session (e.g., compare
against self?.webSocket or a stored sessionId) before updating webSocketState or
calling handleWebSocketEvent; keep stopWebSocket setting webSocket = nil (or
clearing sessionId) so stale callbacks fail the gate.
- Around line 1-46: Add an explicit import Observation at the top of the file
alongside import Foundation so the `@Observable` attribute on AppState is
resolved; update the imports section (before the AppState declaration and any
use of `@Observable`) to include "import Observation".
In `@ios/PPGMobile/PPGMobile/State/ManifestStore.swift`:
- Around line 1-12: The file uses the `@Observable` macro on the ManifestStore
class but only imports Foundation, causing a compile error; add an import for
the Observation framework at the top of the file so the `@Observable` macro
resolves (i.e., add "import Observation" above the ManifestStore declaration to
let the `@Observable` annotation compile correctly).
---
Nitpick comments:
In `@ios/PPGMobile/PPGMobile/State/AppState.swift`:
- Around line 105-125: Replace the manual resets of isConnecting with a
defer-based cleanup: set isConnecting = true at the start of the connection
routine (the method containing isConnecting, errorMessage, await
client.configure(...), try await client.testConnection(), etc.), immediately add
defer { isConnecting = false } so the flag is always cleared on function exit
(including early returns), and remove the explicit isConnecting = false lines
after the catch and at the end; keep setting errorMessage before returning
(e.g., inside the catch) so the defer still runs after the return. Ensure this
change is applied around the function that calls client.configure,
client.testConnection, startWebSocket(for:), manifestStore.refresh(), and sets
activeConnection/UserDefaults (the method referencing isConnecting and
activeConnection).
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
ios/PPGMobile/PPGMobile/State/AppState.swiftios/PPGMobile/PPGMobile/State/ManifestStore.swift
| import Foundation | ||
|
|
||
| // MARK: - UserDefaults Keys | ||
|
|
||
| private enum DefaultsKey { | ||
| static let savedConnections = "ppg_saved_connections" | ||
| static let lastConnectionId = "ppg_last_connection_id" | ||
| } | ||
|
|
||
| /// Codable projection of ServerConnection without the token. | ||
| /// Tokens are stored separately in Keychain via TokenStorage. | ||
| private struct PersistedConnection: Codable { | ||
| let id: UUID | ||
| var host: String | ||
| var port: Int | ||
| var caCertificate: String? | ||
|
|
||
| init(from connection: ServerConnection) { | ||
| self.id = connection.id | ||
| self.host = connection.host | ||
| self.port = connection.port | ||
| self.caCertificate = connection.caCertificate | ||
| } | ||
|
|
||
| func toServerConnection(token: String) -> ServerConnection { | ||
| ServerConnection( | ||
| id: id, | ||
| host: host, | ||
| port: port, | ||
| caCertificate: caCertificate, | ||
| token: token | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| // MARK: - AppState | ||
|
|
||
| /// Root application state managing server connections and the REST/WS lifecycle. | ||
| /// | ||
| /// `AppState` is the single entry point for connection management. It persists | ||
| /// connection metadata to `UserDefaults` and tokens to Keychain via `TokenStorage`. | ||
| /// Auto-connects to the last-used server on launch and coordinates `PPGClient` | ||
| /// (REST) and `WebSocketManager` (WS) through `ManifestStore`. | ||
| @MainActor | ||
| @Observable | ||
| final class AppState { |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, check the actual AppState.swift file to see current imports and `@Observable` usage
find . -name "AppState.swift" -type f | head -5Repository: 2witstudios/ppg-cli
Length of output: 109
🏁 Script executed:
# Search for `@Observable` usage across the codebase to see import patterns
rg "@Observable" --type swift -B 2 | head -50Repository: 2witstudios/ppg-cli
Length of output: 539
🌐 Web query:
Swift Observation framework @observable macro requires import statement iOS
💡 Result:
@Observable lives in the Observation module, so the explicit import is:
import ObservationHowever, in many iOS app files you won’t need to add it because SwiftUI (and often Foundation) already pulls it in for you—e.g. if you import SwiftUI, @Observable typically resolves without import Observation. [1], [2]
Use import Observation when you’re in a non-SwiftUI / non-Foundation context (e.g., a pure Swift package target or a file that doesn’t import those modules) and the compiler can’t find @Observable. [2]
🏁 Script executed:
# Check the actual full imports in AppState.swift
head -50 ios/PPGMobile/PPGMobile/State/AppState.swiftRepository: 2witstudios/ppg-cli
Length of output: 1507
🏁 Script executed:
# Check ManifestStore.swift imports to see the pattern for `@Observable` usage
head -20 ios/PPGMobile/PPGMobile/State/ManifestStore.swiftRepository: 2witstudios/ppg-cli
Length of output: 691
🏁 Script executed:
# Search for any Observation imports in the codebase
rg "import Observation" --type swiftRepository: 2witstudios/ppg-cli
Length of output: 45
🏁 Script executed:
# Check if SwiftUI is imported anywhere that might affect module resolution
rg "import SwiftUI" --type swift | head -20Repository: 2witstudios/ppg-cli
Length of output: 45
🏁 Script executed:
# Check the project's Swift/iOS deployment target to understand the context
find . -name "*.pbxproj" -o -name "project.yml" -o -name "Package.swift" | head -5Repository: 2witstudios/ppg-cli
Length of output: 106
🏁 Script executed:
# Look for any Info.plist or build configuration that might show Swift version/iOS target
find . -name "Info.plist" -o -name "*.xcconfig" | head -10Repository: 2witstudios/ppg-cli
Length of output: 91
🏁 Script executed:
# Check if there are any other files importing Observation in different ways
rg "Observation" --type swift -iRepository: 2witstudios/ppg-cli
Length of output: 45
🏁 Script executed:
# Check project deployment target and Swift version in pbxproj
cat "./PPG CLI/PPG CLI.xcodeproj/project.pbxproj" | grep -i "IPHONEOS_DEPLOYMENT_TARGET\|SWIFT_VERSION" | head -10Repository: 2witstudios/ppg-cli
Length of output: 162
🏁 Script executed:
# Check if any swift files compile or build configuration details
cat "./PPG CLI/PPG CLI/Info.plist" | head -30Repository: 2witstudios/ppg-cli
Length of output: 563
🏁 Script executed:
# Look for any build phase or configuration that might show minimum deployment
rg "IPHONEOS_DEPLOYMENT_TARGET|swift" "./PPG CLI/PPG CLI.xcodeproj" | head -20Repository: 2witstudios/ppg-cli
Length of output: 465
🏁 Script executed:
# Find iOS deployment target in the pbxproj file
grep -i "IPHONEOS_DEPLOYMENT_TARGET" "./PPG CLI/PPG CLI.xcodeproj/project.pbxproj" | head -5Repository: 2witstudios/ppg-cli
Length of output: 45
🏁 Script executed:
# Check if there's a Podfile or any CocoaPods configuration
find . -name "Podfile" -o -name "Podfile.lock" | head -5Repository: 2witstudios/ppg-cli
Length of output: 45
🏁 Script executed:
# Verify that neither AppState.swift nor ManifestStore.swift import SwiftUI
rg "import SwiftUI" ios/PPGMobile/PPGMobile/State/Repository: 2witstudios/ppg-cli
Length of output: 45
🏁 Script executed:
# Check the actual structure - determine if this is iOS or macOS focused
ls -la ios/ PPG*/ 2>/dev/null | head -20Repository: 2witstudios/ppg-cli
Length of output: 546
🏁 Script executed:
# Look at the actual project structure to understand the context
find . -maxdepth 3 -name "*.swift" -path "*/PPGMobile/*" | wc -l
find . -maxdepth 3 -name "*.swift" -path "*/PPG CLI/*" | wc -lRepository: 2witstudios/ppg-cli
Length of output: 67
🏁 Script executed:
# Check what the main project targets are
grep -E "productName|productType|name.*=" "./PPG CLI/PPG CLI.xcodeproj/project.pbxproj" | head -20Repository: 2witstudios/ppg-cli
Length of output: 467
🏁 Script executed:
# Verify if Observation is actually available or if there's a deployment target mismatch
grep -i "IPHONEOS\|MACOSX\|DEPLOYMENT" "./PPG CLI/PPG CLI.xcodeproj/project.pbxproj" | grep -v ".pbxproj" | head -20Repository: 2witstudios/ppg-cli
Length of output: 692
🏁 Script executed:
# Check if there are separate iOS targets or if PPGMobile is standalone
find ios/PPGMobile -name "*.pbxproj" -o -name "*.xcodeproj"Repository: 2witstudios/ppg-cli
Length of output: 45
🏁 Script executed:
# Look for any build configurations or workspace files for iOS
find ios/ -name "*.xcodeproj" -o -name "*.xcworkspace"Repository: 2witstudios/ppg-cli
Length of output: 45
🏁 Script executed:
# Verify Swift Observation availability in macOS 26.1 and check if Foundation re-exports it
rg "Foundation.*Observation|Observation.*Foundation" --type swiftRepository: 2witstudios/ppg-cli
Length of output: 45
🏁 Script executed:
# Check if the iOS/macOS project actually builds or if there's a test/CI configuration
find . -name "*.yml" -o -name ".github" -type d | head -10Repository: 2witstudios/ppg-cli
Length of output: 159
🏁 Script executed:
# Look for any Xcode build logs or CI configuration that might show if this compiles
find . -name "*.log" -o -name "*.yml" -o -name ".github*" 2>/dev/null | head -10Repository: 2witstudios/ppg-cli
Length of output: 159
🏁 Script executed:
# Definitively check: does Foundation.framework re-export Observation in current versions?
# Look for any documentation or comments about this in the codebase
rg "Observable|Observation" --type swift -B 2 -A 2Repository: 2witstudios/ppg-cli
Length of output: 782
🏁 Script executed:
# Check if PPGMobile is built as part of CI or if it's just source code without active builds
cat ./.github/workflows/ci.ymlRepository: 2witstudios/ppg-cli
Length of output: 519
🏁 Script executed:
# Check for any references to PPGMobile in build/test scripts
rg "PPGMobile" ./.githubRepository: 2witstudios/ppg-cli
Length of output: 45
🏁 Script executed:
# Determine the actual minimum iOS deployment target for PPGMobile if it has one
find ios/PPGMobile -name "*.swift" -type f | head -1 | xargs head -100 | rg "IPHONEOS"Repository: 2witstudios/ppg-cli
Length of output: 45
🏁 Script executed:
# Check if there are any package manifests in the iOS directory
find ios/ -name "*.swift" -type f -exec grep -l "Package\|target\|dependencies" {} \; | head -5Repository: 2witstudios/ppg-cli
Length of output: 45
Add explicit import Observation to use @Observable.
Line 45 uses @Observable but the file only imports Foundation. While Foundation may implicitly re-export Observation in some contexts, the explicit import is required and best practice. Without it, the code will fail to compile in contexts where Observation is not available.
Fix
import Foundation
+import Observation📝 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.
| import Foundation | |
| // MARK: - UserDefaults Keys | |
| private enum DefaultsKey { | |
| static let savedConnections = "ppg_saved_connections" | |
| static let lastConnectionId = "ppg_last_connection_id" | |
| } | |
| /// Codable projection of ServerConnection without the token. | |
| /// Tokens are stored separately in Keychain via TokenStorage. | |
| private struct PersistedConnection: Codable { | |
| let id: UUID | |
| var host: String | |
| var port: Int | |
| var caCertificate: String? | |
| init(from connection: ServerConnection) { | |
| self.id = connection.id | |
| self.host = connection.host | |
| self.port = connection.port | |
| self.caCertificate = connection.caCertificate | |
| } | |
| func toServerConnection(token: String) -> ServerConnection { | |
| ServerConnection( | |
| id: id, | |
| host: host, | |
| port: port, | |
| caCertificate: caCertificate, | |
| token: token | |
| ) | |
| } | |
| } | |
| // MARK: - AppState | |
| /// Root application state managing server connections and the REST/WS lifecycle. | |
| /// | |
| /// `AppState` is the single entry point for connection management. It persists | |
| /// connection metadata to `UserDefaults` and tokens to Keychain via `TokenStorage`. | |
| /// Auto-connects to the last-used server on launch and coordinates `PPGClient` | |
| /// (REST) and `WebSocketManager` (WS) through `ManifestStore`. | |
| @MainActor | |
| @Observable | |
| final class AppState { | |
| import Foundation | |
| import Observation | |
| // MARK: - UserDefaults Keys | |
| private enum DefaultsKey { | |
| static let savedConnections = "ppg_saved_connections" | |
| static let lastConnectionId = "ppg_last_connection_id" | |
| } | |
| /// Codable projection of ServerConnection without the token. | |
| /// Tokens are stored separately in Keychain via TokenStorage. | |
| private struct PersistedConnection: Codable { | |
| let id: UUID | |
| var host: String | |
| var port: Int | |
| var caCertificate: String? | |
| init(from connection: ServerConnection) { | |
| self.id = connection.id | |
| self.host = connection.host | |
| self.port = connection.port | |
| self.caCertificate = connection.caCertificate | |
| } | |
| func toServerConnection(token: String) -> ServerConnection { | |
| ServerConnection( | |
| id: id, | |
| host: host, | |
| port: port, | |
| caCertificate: caCertificate, | |
| token: token | |
| ) | |
| } | |
| } | |
| // MARK: - AppState | |
| /// Root application state managing server connections and the REST/WS lifecycle. | |
| /// | |
| /// `AppState` is the single entry point for connection management. It persists | |
| /// connection metadata to `UserDefaults` and tokens to Keychain via `TokenStorage`. | |
| /// Auto-connects to the last-used server on launch and coordinates `PPGClient` | |
| /// (REST) and `WebSocketManager` (WS) through `ManifestStore`. | |
| `@MainActor` | |
| `@Observable` | |
| final class AppState { |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@ios/PPGMobile/PPGMobile/State/AppState.swift` around lines 1 - 46, Add an
explicit import Observation at the top of the file alongside import Foundation
so the `@Observable` attribute on AppState is resolved; update the imports section
(before the AppState declaration and any use of `@Observable`) to include "import
Observation".
| private func startWebSocket(for connection: ServerConnection) { | ||
| stopWebSocket() | ||
|
|
||
| let ws = WebSocketManager(url: connection.webSocketURL) | ||
| ws.onStateChange = { [weak self] state in | ||
| Task { @MainActor in | ||
| self?.webSocketState = state | ||
| } | ||
| } | ||
| ws.onEvent = { [weak self] event in | ||
| Task { @MainActor in | ||
| self?.handleWebSocketEvent(event) | ||
| } | ||
| } | ||
| webSocket = ws | ||
| ws.connect() | ||
| } | ||
|
|
||
| private func stopWebSocket() { | ||
| webSocket?.disconnect() | ||
| webSocket = nil | ||
| } |
There was a problem hiding this comment.
Gate WebSocket callbacks to the active socket session.
Old socket callbacks can still fire after stopWebSocket() during connection switches, and currently they can mutate state for the wrong active connection.
🛡️ Proposed fix (session token gating)
let client = PPGClient()
let manifestStore: ManifestStore
private var webSocket: WebSocketManager?
+ private var webSocketSession = UUID()
@@
private func startWebSocket(for connection: ServerConnection) {
stopWebSocket()
+ let session = UUID()
+ webSocketSession = session
let ws = WebSocketManager(url: connection.webSocketURL)
ws.onStateChange = { [weak self] state in
Task { `@MainActor` in
- self?.webSocketState = state
+ guard let self, self.webSocketSession == session else { return }
+ self.webSocketState = state
}
}
ws.onEvent = { [weak self] event in
Task { `@MainActor` in
- self?.handleWebSocketEvent(event)
+ guard let self, self.webSocketSession == session else { return }
+ self.handleWebSocketEvent(event)
}
}
webSocket = ws
ws.connect()
}
private func stopWebSocket() {
+ webSocketSession = UUID()
webSocket?.disconnect()
webSocket = nil
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@ios/PPGMobile/PPGMobile/State/AppState.swift` around lines 192 - 213,
startWebSocket's closures can run after stopWebSocket and mutate state for an
old session; fix by gating callbacks to the active session: when creating ws in
startWebSocket assign a unique session token/identifier (or capture the
WebSocketManager instance) and capture that token in the onStateChange and
onEvent closures, then inside those closures check that the captured token still
matches the current webSocket/session (e.g., compare against self?.webSocket or
a stored sessionId) before updating webSocketState or calling
handleWebSocketEvent; keep stopWebSocket setting webSocket = nil (or clearing
sessionId) so stale callbacks fail the gate.
| import Foundation | ||
|
|
||
| // MARK: - ManifestStore | ||
|
|
||
| /// Caches the ppg manifest and applies incremental WebSocket updates. | ||
| /// | ||
| /// `ManifestStore` owns the manifest data and provides read access to views. | ||
| /// It is updated either by a full REST fetch or by individual WebSocket events | ||
| /// (agent/worktree status changes) to keep the UI responsive without polling. | ||
| @MainActor | ||
| @Observable | ||
| final class ManifestStore { |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify Swift files that use `@Observable` but don't import Observation
python3 <<'PY'
from pathlib import Path
import re
print("=== Swift files using `@Observable` without Observation import ===")
for p in sorted(Path(".").rglob("*.swift")):
text = p.read_text(encoding="utf-8", errors="ignore")
if "@Observable" in text and not re.search(r'^\s*import\s+Observation\b', text, re.MULTILINE):
print(p)
PYRepository: 2witstudios/ppg-cli
Length of output: 222
🏁 Script executed:
# Check the specific file mentioned in the review
echo "=== Content of ios/PPGMobile/PPGMobile/State/ManifestStore.swift ==="
head -20 ios/PPGMobile/PPGMobile/State/ManifestStore.swiftRepository: 2witstudios/ppg-cli
Length of output: 760
🏁 Script executed:
# Check Swift version requirements in project configuration
echo "=== Project configuration files ==="
find ios -name "*.pbxproj" -o -name "Package.swift" -o -name ".swift-version" | head -5Repository: 2witstudios/ppg-cli
Length of output: 98
Add Observation import for @Observable usage.
@Observable is used on line 11, but this file currently imports only Foundation. This causes a compilation error since the Observation framework must be explicitly imported for the @Observable macro to resolve.
🔧 Proposed fix
import Foundation
+import Observation📝 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.
| import Foundation | |
| // MARK: - ManifestStore | |
| /// Caches the ppg manifest and applies incremental WebSocket updates. | |
| /// | |
| /// `ManifestStore` owns the manifest data and provides read access to views. | |
| /// It is updated either by a full REST fetch or by individual WebSocket events | |
| /// (agent/worktree status changes) to keep the UI responsive without polling. | |
| @MainActor | |
| @Observable | |
| final class ManifestStore { | |
| import Foundation | |
| import Observation | |
| // MARK: - ManifestStore | |
| /// Caches the ppg manifest and applies incremental WebSocket updates. | |
| /// | |
| /// `ManifestStore` owns the manifest data and provides read access to views. | |
| /// It is updated either by a full REST fetch or by individual WebSocket events | |
| /// (agent/worktree status changes) to keep the UI responsive without polling. | |
| `@MainActor` | |
| `@Observable` | |
| final class ManifestStore { |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@ios/PPGMobile/PPGMobile/State/ManifestStore.swift` around lines 1 - 12, The
file uses the `@Observable` macro on the ManifestStore class but only imports
Foundation, causing a compile error; add an import for the Observation framework
at the top of the file so the `@Observable` macro resolves (i.e., add "import
Observation" above the ManifestStore declaration to let the `@Observable`
annotation compile correctly).
Summary
AppState.swift—@Observableroot state managing server connections, REST/WS lifecycle, UserDefaults persistence, and auto-connect on launchManifestStore.swift— Separate manifest cache with full REST refresh and incremental WebSocket updates (agent/worktree status changes)Dependencies
Test plan
AppStateloads/saves connections from UserDefaults correctlyconnect()tests reachability before activating connectiondisconnect()tears down WebSocket and clears manifestManifestStore.refresh()fetches and caches manifest via RESTCloses #82
Summary by CodeRabbit