From 947b5dcb3f56c89fc69ad3a7a40ab9093ed5eb5c Mon Sep 17 00:00:00 2001 From: Dustin Hilgaertner Date: Fri, 1 May 2026 23:20:24 -0500 Subject: [PATCH] Stop completed reviews from restarting on app relaunch Review sessions used to re-`cat` their `.crow-review-prompt.md` into Claude on every app launch because `launchClaude` had no way to tell first-launch from a restart. Persist `Session.reviewPromptDispatched` and gate the prompt branch on it: once the prompt fires, subsequent launches fall through to `claude --continue`. Existing persisted sessions decode the missing field as `true` so the bug does not re-trigger after upgrade. Closes #224 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Sources/CrowCore/Models/Session.swift | 14 ++++++++++++-- Sources/Crow/App/SessionService.swift | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/Packages/CrowCore/Sources/CrowCore/Models/Session.swift b/Packages/CrowCore/Sources/CrowCore/Models/Session.swift index 25e34cb..dde6a44 100644 --- a/Packages/CrowCore/Sources/CrowCore/Models/Session.swift +++ b/Packages/CrowCore/Sources/CrowCore/Models/Session.swift @@ -12,6 +12,10 @@ public struct Session: Identifiable, Codable, Sendable { public var provider: Provider? public var createdAt: Date public var updatedAt: Date + // Whether a review-kind session has had its initial `/crow-review-pr` + // prompt dispatched. Gates the launchClaude prompt-vs-`--continue` + // branch so completed reviews don't restart on app relaunch. + public var reviewPromptDispatched: Bool public init( id: UUID = UUID(), @@ -23,7 +27,8 @@ public struct Session: Identifiable, Codable, Sendable { ticketNumber: Int? = nil, provider: Provider? = nil, createdAt: Date = Date(), - updatedAt: Date = Date() + updatedAt: Date = Date(), + reviewPromptDispatched: Bool = false ) { self.id = id self.name = name @@ -35,9 +40,13 @@ public struct Session: Identifiable, Codable, Sendable { self.provider = provider self.createdAt = createdAt self.updatedAt = updatedAt + self.reviewPromptDispatched = reviewPromptDispatched } - // Backward-compatible decoding: default `kind` to `.work` when missing from older persisted data. + // Backward-compatible decoding: default `kind` to `.work` when missing + // from older persisted data. `reviewPromptDispatched` defaults to `true` + // when missing so existing review sessions don't re-trigger their prompt + // on the first launch after upgrade (CROW-224). public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(UUID.self, forKey: .id) @@ -50,5 +59,6 @@ public struct Session: Identifiable, Codable, Sendable { provider = try container.decodeIfPresent(Provider.self, forKey: .provider) createdAt = try container.decode(Date.self, forKey: .createdAt) updatedAt = try container.decode(Date.self, forKey: .updatedAt) + reviewPromptDispatched = try container.decodeIfPresent(Bool.self, forKey: .reviewPromptDispatched) ?? true } } diff --git a/Sources/Crow/App/SessionService.swift b/Sources/Crow/App/SessionService.swift index 438b619..345df73 100644 --- a/Sources/Crow/App/SessionService.swift +++ b/Sources/Crow/App/SessionService.swift @@ -350,12 +350,19 @@ final class SessionService { let routedTerminal: SessionTerminal? = sessionID.flatMap { sid in appState.terminals[sid]?.first(where: { $0.id == terminalID }) } + // Review-kind sessions dispatch their `/crow-review-pr` prompt on first + // launch only — on subsequent app restarts, fall through to + // `claude --continue` so the existing conversation resumes instead of + // re-running the entire review (CROW-224). + var reviewPromptJustDispatched = false let claudeText: String = { if let sessionID, let session = appState.sessions.first(where: { $0.id == sessionID }), session.kind == .review, + !session.reviewPromptDispatched, let worktree = appState.primaryWorktree(for: sessionID) { let promptPath = (worktree.worktreePath as NSString).appendingPathComponent(".crow-review-prompt.md") + reviewPromptJustDispatched = true return "\(envPrefix)\(claudePath)\(rcArgs) \"$(cat \(promptPath))\"\n" } else { return "\(envPrefix)\(claudePath)\(rcArgs) --continue\n" @@ -371,6 +378,17 @@ final class SessionService { if rcEnabled { appState.remoteControlActiveTerminals.insert(terminalID) } + + if reviewPromptJustDispatched, let sessionID { + if let idx = appState.sessions.firstIndex(where: { $0.id == sessionID }) { + appState.sessions[idx].reviewPromptDispatched = true + } + store.mutate { data in + if let idx = data.sessions.firstIndex(where: { $0.id == sessionID }) { + data.sessions[idx].reviewPromptDispatched = true + } + } + } } /// Discard a failed terminal surface and re-attempt creation.