diff --git a/NucleicRemote/NucleicRemote/Models/RemoteStore.swift b/NucleicRemote/NucleicRemote/Models/RemoteStore.swift index 7d3c90d..659adfa 100644 --- a/NucleicRemote/NucleicRemote/Models/RemoteStore.swift +++ b/NucleicRemote/NucleicRemote/Models/RemoteStore.swift @@ -588,51 +588,45 @@ final class RemoteStore: ObservableObject { } /// Stream a short, believable canned turn for a session: assistant prose, a tool call + - /// result, usage, and a finish — flipping the summary running → awaiting-input. + /// result, usage, and a finish — flipping the summary running → awaiting-input. Each step + /// re-checks demo mode and cancellation so `exitDemo()` stops it cleanly. Runs entirely on the + /// main actor (the store is `@MainActor`, and a `Task` in an isolated method inherits it). private func demoRun(_ sessionID: SessionID, userText: String?) { if let userText, !userText.isEmpty { demoAppend(sessionID, .userText(TextChunk(messageID: UUID().uuidString, text: userText, isPartial: false))) } demoUpdateSession(sessionID) { $0.demoCopy(status: .running, disposition: .some(nil)) } + let toolID = UUID().uuidString let task = Task { [weak self] in - let toolID = UUID().uuidString - guard await self?.demoStep(sessionID, delayMs: 600, { - $0.demoAppend(sessionID, .assistantText(TextChunk( - messageID: UUID().uuidString, text: "On it — let me take a look.", isPartial: false))) - }) == true else { return } - guard await self?.demoStep(sessionID, delayMs: 700, { - $0.demoAppend(sessionID, .toolCallStarted(ToolCall( - toolCallID: toolID, name: "Bash", input: ["command": "npm test"]))) - }) == true else { return } - guard await self?.demoStep(sessionID, delayMs: 900, { - $0.demoAppend(sessionID, .toolResult(ToolResult( - toolCallID: toolID, content: "42 passing\n0 failing", isError: false))) - }) == true else { return } - guard await self?.demoStep(sessionID, delayMs: 700, { - $0.demoAppend(sessionID, .assistantText(TextChunk( - messageID: UUID().uuidString, - text: "All green — tests pass and the change is in place. Anything else?", - isPartial: false))) - $0.demoAppend(sessionID, .usage(Usage( - inputTokens: 12_400, outputTokens: 640, costUSD: 0.0088, contextInputTokens: 12_400))) - $0.demoAppend(sessionID, .runFinished(RunFinished(outcome: .completed, finalText: "Done."))) - }) == true else { return } - await self?.demoUpdateSession(sessionID) { + func pause(_ ms: Int) async -> Bool { + try? await Task.sleep(for: .milliseconds(ms)) + return !Task.isCancelled && (self?.demoMode ?? false) + } + guard let self else { return } + guard await pause(600) else { return } + self.demoAppend(sessionID, .assistantText(TextChunk( + messageID: UUID().uuidString, text: "On it — let me take a look.", isPartial: false))) + guard await pause(700) else { return } + self.demoAppend(sessionID, .toolCallStarted(ToolCall( + toolCallID: toolID, name: "Bash", input: ["command": "npm test"]))) + guard await pause(900) else { return } + self.demoAppend(sessionID, .toolResult(ToolResult( + toolCallID: toolID, content: "42 passing\n0 failing", isError: false))) + guard await pause(700) else { return } + self.demoAppend(sessionID, .assistantText(TextChunk( + messageID: UUID().uuidString, + text: "All green — tests pass and the change is in place. Anything else?", + isPartial: false))) + self.demoAppend(sessionID, .usage(Usage( + inputTokens: 12_400, outputTokens: 640, costUSD: 0.0088, contextInputTokens: 12_400))) + self.demoAppend(sessionID, .runFinished(RunFinished(outcome: .completed, finalText: "Done."))) + self.demoUpdateSession(sessionID) { $0.demoCopy(status: .awaitingInput, disposition: .some(.completed)) } } demoTasks.append(task) } - /// One step of a simulated run: sleep, then (if still in demo and not cancelled) apply a - /// mutation on the main actor. Returns false to abort the remaining steps. - private func demoStep(_ sessionID: SessionID, delayMs: UInt64, _ body: @MainActor (RemoteStore) -> Void) async -> Bool { - try? await Task.sleep(for: .milliseconds(Int(delayMs))) - guard !Task.isCancelled, demoMode else { return false } - body(self) - return true - } - private func demoStartChat(_ req: StartChatRequest) { let project = dashboard.projects.first { $0.id == req.projectID } let id = SessionID(rawValue: "demo-\(UUID().uuidString.prefix(8))")