Cursor: Apply local changes for cloud agent

This commit is contained in:
Rocky
2026-08-27 18:01:46 +08:00
parent 39002336c0
commit 42e6252f01
148 changed files with 120105 additions and 8122 deletions
@@ -70,6 +70,10 @@ public struct AISessionState: Equatable, Sendable {
public private(set) var conversationID: UUID?
public private(set) var activeUtteranceID: UUID?
public private(set) var answer: AIAnswer?
/// Three reply choices retained until the user taps one whole card.
public private(set) var replyVariants: [AIReplyVariant]
/// Stable selection hook for future local feedback recording.
public private(set) var selectedReplyVariant: AIReplyVariant?
/// Live LLM draft while `phase == .generating`. Cleared on final/cancel.
public private(set) var draftAnswerText: String?
public private(set) var transcript: String
@@ -82,6 +86,8 @@ public struct AISessionState: Equatable, Sendable {
conversationID: UUID? = nil,
activeUtteranceID: UUID? = nil,
answer: AIAnswer? = nil,
replyVariants: [AIReplyVariant] = [],
selectedReplyVariant: AIReplyVariant? = nil,
draftAnswerText: String? = nil,
transcript: String = "",
errorMessage: String? = nil
@@ -90,6 +96,8 @@ public struct AISessionState: Equatable, Sendable {
self.conversationID = conversationID
self.activeUtteranceID = activeUtteranceID
self.answer = answer
self.replyVariants = replyVariants
self.selectedReplyVariant = selectedReplyVariant
self.draftAnswerText = draftAnswerText
self.transcript = transcript
self.errorMessage = errorMessage
@@ -110,6 +118,13 @@ public struct AISessionState: Equatable, Sendable {
phase == .ready && answer?.deliveryState == .ready
}
public var canSelectReplyVariant: Bool {
phase == .ready
&& answer == nil
&& selectedReplyVariant == nil
&& replyVariants.count == AIReplyVariant.Kind.allCases.count
}
public var canSend: Bool {
phase == .awaitingSend && answer?.deliveryState == .awaitingSend
}
@@ -172,12 +187,49 @@ public struct AISessionState: Equatable, Sendable {
public mutating func receiveAnswer(_ text: String, utteranceID: UUID) {
guard isActive, activeUtteranceID == utteranceID else { return }
answer = AIAnswer(text: text)
replyVariants = []
selectedReplyVariant = nil
draftAnswerText = nil
phase = .ready
activeUtteranceID = nil
errorMessage = nil
}
public mutating func receiveReplyVariants(
_ variants: [AIReplyVariant],
utteranceID: UUID
) {
guard isActive, activeUtteranceID == utteranceID else { return }
let kinds = Set(variants.map(\.kind))
guard variants.count == AIReplyVariant.Kind.allCases.count,
kinds == Set(AIReplyVariant.Kind.allCases) else {
return
}
answer = nil
replyVariants = AIReplyVariant.Kind.allCases.compactMap { kind in
variants.first { $0.kind == kind }
}
selectedReplyVariant = nil
draftAnswerText = nil
phase = .ready
activeUtteranceID = nil
errorMessage = nil
}
/// Converts the tapped choice into the existing insertion model. Keeping
/// the selected variant exposes kind/emotion for later feedback recording.
@discardableResult
public mutating func selectReplyVariant(id: UUID) -> AIAnswer? {
guard canSelectReplyVariant,
let variant = replyVariants.first(where: { $0.id == id }) else {
return nil
}
let selectedAnswer = AIAnswer(id: variant.id, text: variant.text)
selectedReplyVariant = variant
answer = selectedAnswer
return selectedAnswer
}
public mutating func markAnswerInserted(offersSend: Bool) {
guard canInsert else { return }
answer?.markInserted(offersSend: offersSend)
@@ -195,6 +247,8 @@ public struct AISessionState: Equatable, Sendable {
public mutating func discardReadyAnswer() {
guard phase == .ready else { return }
answer = nil
replyVariants = []
selectedReplyVariant = nil
activeUtteranceID = nil
draftAnswerText = nil
transcript = ""
@@ -233,6 +287,9 @@ public struct AISessionState: Equatable, Sendable {
}
private var restingPhase: Phase {
if !replyVariants.isEmpty, selectedReplyVariant == nil {
return .ready
}
guard let answer else { return .idle }
switch answer.deliveryState {
case .ready: