import Foundation import Observation /// One line in the transcript. The renderer draws these and nothing else, so /// everything the bridge reports has to become one of them or be dropped on /// purpose. struct Entry: Identifiable, Equatable { enum Kind: Equatable { case you case ac case edit case note case bad } let id = UUID() var kind: Kind var text: String } /// What the status pill says, and in which colour. enum Health: Equatable { case idle case working case live case failed } struct ModelChoice: Identifiable { let id: String let title: String let premium: Bool } struct ProviderChoice: Identifiable { let id: String let available: Bool let notice: String } struct ProviderApproval: Identifiable { let id: String let title: String let detail: String let canAccept: Bool let alwaysLabel: String? let alwaysScope: String? } struct SessionNotice: Identifiable { let id = UUID() let text: String let action: String? let working: Bool } struct InspectedRevision { let version: Int; let source: String } struct SourceRevision: Identifiable { let id: Int let reason: String let at: String } struct SessionSummary: Identifiable { let id: String let title: String let medium: String let route: String let updatedAt: String } /// The whole of what SwiftUI observes. It holds no logic about turns, tools or /// publishing — those live in `aesel/phone/session.mjs`, shared with the /// desktop. This is the projection of that session's event stream into /// something a view can draw. @Observable final class Session { static func command(for text: String) -> String? { let normalized = text.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() let aliases = ["logout":"logout", "log out":"logout", "signout":"logout", "sign out":"logout", "login":"login", "log in":"login", "signin":"login", "sign in":"login", "settings":"settings", "help":"help", "stop":"stop", "publish":"publish", "open":"open", "new":"new", "home":"home"] if normalized.hasPrefix("/") { let name = String(normalized.dropFirst()) return name == "buy" ? "buy" : aliases[name] } return aliases[normalized] } static let draftPreviewURL = embeddedPreviewURL(URL(string: "https://aesthetic.computer/wipe?noauth=true")!) static func embeddedPreviewURL(_ url: URL) -> URL { guard var target = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return url } if target.host == "prompt.ac" { target.host = "aesthetic.computer" } let flags = ["nogap", "nolabel", "autoreload"] var query = (target.queryItems ?? []).filter { !flags.contains($0.name) } query += flags.map { URLQueryItem(name: $0, value: "true") } target.queryItems = query return target.url ?? url } var notices: [String: SessionNotice] = [:] var entries: [Entry] = [] var history: [SessionSummary] = [] var medium = "piece" var model = "" var modelChoices: [ModelChoice] = [] var provider = "ac" var providers: [ProviderChoice] = [] var accountDeletionBusy = false var accountDeleted = false var accountDeletionDate: Date? var accountDeletionMailed = false var accountDeletionSummary = "" var accountNotice = "" var mcpAutoAllow = true var supportsApprovalPolicy = false var approval: ProviderApproval? var hostOperationID: String? var providerNotice: String { providers.first { $0.id == provider }?.notice ?? "" } var accountReady: Bool { signedIn && !handle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } var accessNotice = "An AC account and @handle are required." var providerReady: Bool { provider == "ac" ? signedIn : providers.contains { $0.id == provider && $0.available } } var canStartTurn: Bool { accountReady && !viewingHistory && providerReady && !busy && hostOperationID == nil && fatal == nil } var reportedModel = "" var currentThreadID = "" var currentSessionID: String { currentThreadID } var status: String = "starting" var health: Health = .idle var route: String = "" var previewURL: URL? = Session.draftPreviewURL var shareURL: URL? var source = "" var composer = "" var revisions: [SourceRevision] = [] var inspectedRevision: InspectedRevision? var currentRevision = 0 var selectedRevision: Int? var historicalSource = "" var historicalEntries: [Entry] = [] var historicalTranscriptAvailable = true var viewingHistory: Bool { selectedRevision != nil } var displayedRevision: Int { selectedRevision ?? currentRevision } var displayedSource: String { viewingHistory ? historicalSource : source } var displayedEntries: [Entry] { viewingHistory ? historicalEntries : entries } var pieceVersion: Int { currentRevision } var publishedRevision: Int? var autoPublish = true var exportJSON: String? var showSignIn = false var signInLoading = false var signInError: String? var busy = false var signedIn = false var handle = "" var handleColors: [String] = [] var braincells: Double? var braincellDollars: Double? var freeDollars: Double? var purchasedDollars: Double? var creditsStatus = "Sign in to view braincells" /// Set when the host page itself could not start — a missing dev server is /// the usual cause, and a blank screen is a bad way to say so. var fatal: String? /// The `
  • ` currently receiving streamed deltas. Held as an index because /// `Entry` is a value type and SwiftUI needs the array mutated in place. var streamingCode = "" private var codeArguments = "" private var codeItemID = "" private var streamingIndex: Int? // Decode only the source string of an incomplete write_piece JSON object. // A split escape waits for the next token; draft code is never executed. static func partialSource(_ arguments: String) -> String? { guard let start = arguments.range(of: #"(? Health { switch kind { case "working": return .working case "live": return .live case "failed": return .failed default: return .idle } } }