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.