Skip to content

feat: implement iOS state management (AppState + ManifestStore)#111

Open
2witstudios wants to merge 3 commits intomainfrom
ppg/issue-82-ios-state
Open

feat: implement iOS state management (AppState + ManifestStore)#111
2witstudios wants to merge 3 commits intomainfrom
ppg/issue-82-ios-state

Conversation

@2witstudios
Copy link
Owner

@2witstudios 2witstudios commented Feb 27, 2026

Summary

  • AppState.swift@Observable root state managing server connections, REST/WS lifecycle, UserDefaults persistence, and auto-connect on launch
  • ManifestStore.swift — Separate manifest cache with full REST refresh and incremental WebSocket updates (agent/worktree status changes)
  • Connection CRUD with duplicate detection, switching (disconnect → connect), and last-used persistence

Dependencies

Test plan

  • Verify AppState loads/saves connections from UserDefaults correctly
  • Verify auto-connect picks up last-used connection on launch
  • Verify connect() tests reachability before activating connection
  • Verify disconnect() tears down WebSocket and clears manifest
  • Verify ManifestStore.refresh() fetches and caches manifest via REST
  • Verify incremental agent/worktree status updates mutate cached manifest correctly
  • Verify connection switching disconnects current before connecting to new
  • Verify error state surfaces user-facing messages on connection failure

Closes #82

Summary by CodeRabbit

  • New Features
    • Added automatic reconnection to the last-used server on app launch
    • Enhanced server connection management with persistent storage across app sessions
    • Real-time manifest caching with incremental updates and synchronization
    • Improved error handling and state tracking for server operations

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
@chatgpt-codex-connector
Copy link

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai
Copy link

coderabbitai bot commented Feb 27, 2026

Warning

Rate limit exceeded

@2witstudios has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 9 minutes and 37 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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.

📥 Commits

Reviewing files that changed from the base of the PR and between dd9f355 and 74e8924.

📒 Files selected for processing (3)
  • ios/PPGMobile/PPGMobile/State/AppState.swift
  • ios/PPGMobile/PPGMobile/State/ManifestStore.swift
  • src/commands/spawn.test.ts
📝 Walkthrough

Walkthrough

Two 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

Cohort / File(s) Summary
AppState
ios/PPGMobile/PPGMobile/State/AppState.swift
New @Observable class managing server connections, REST/WS lifecycle, connection persistence via UserDefaults and Keychain, auto-connect on launch, and user-facing error state. Coordinates manifest refresh via injected ManifestStore and WebSocketManager.
ManifestStore
ios/PPGMobile/PPGMobile/State/ManifestStore.swift
New @MainActor @Observable`` class providing centralized manifest caching with full refresh via REST, incremental granular updates (agent/worktree status), and convenience accessors for sorted worktrees, all agents, and agent count distribution.

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 In burrows of state, we organize the way,
Connections persist from yesterday's play,
Manifests cached, incremental and bright,
WebSockets dance through the iOS night!
AppState and Store, hand-in-paw, take their flight! 🚀

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The PR description covers key components, dependencies, test plan, and relates to issue #82, but does not follow the template structure with 'What/Why/How' sections. Restructure the description to follow the template with 'What', 'Why', 'How', and 'Checklist' sections for consistency with repository standards.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change: implementing iOS state management with AppState and ManifestStore components.
Linked Issues check ✅ Passed The PR implements all requirements from issue #82: AppState and ManifestStore as @Observable classes, connection lifecycle management, UserDefaults persistence, auto-connect, connection switching, and error handling.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the linked issue #82 requirements: the two new state management files (AppState.swift and ManifestStore.swift) with no extraneous modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ppg/issue-82-ios-state

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.

❤️ Share

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

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
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
ios/PPGMobile/PPGMobile/State/AppState.swift (1)

105-125: Use defer for isConnecting reset.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 34deb69 and dd9f355.

📒 Files selected for processing (2)
  • ios/PPGMobile/PPGMobile/State/AppState.swift
  • ios/PPGMobile/PPGMobile/State/ManifestStore.swift

Comment on lines +1 to +46
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 {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 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 -5

Repository: 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 -50

Repository: 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 Observation

However, 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.swift

Repository: 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.swift

Repository: 2witstudios/ppg-cli

Length of output: 691


🏁 Script executed:

# Search for any Observation imports in the codebase
rg "import Observation" --type swift

Repository: 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 -20

Repository: 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 -5

Repository: 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 -10

Repository: 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 -i

Repository: 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 -10

Repository: 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 -30

Repository: 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 -20

Repository: 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 -5

Repository: 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 -5

Repository: 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 -20

Repository: 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 -l

Repository: 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 -20

Repository: 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 -20

Repository: 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 swift

Repository: 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 -10

Repository: 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 -10

Repository: 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 2

Repository: 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.yml

Repository: 2witstudios/ppg-cli

Length of output: 519


🏁 Script executed:

# Check for any references to PPGMobile in build/test scripts
rg "PPGMobile" ./.github

Repository: 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 -5

Repository: 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.

Suggested change
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".

Comment on lines +192 to +213
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
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +1 to +12
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 {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 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)
PY

Repository: 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.swift

Repository: 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 -5

Repository: 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.

Suggested change
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).

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.

Implement iOS state management (AppState + ManifestStore)

1 participant