diff --git a/assistant/src/daemon/handlers/misc.ts b/assistant/src/daemon/handlers/misc.ts index a086c80c17e..7dedba45b4c 100644 --- a/assistant/src/daemon/handlers/misc.ts +++ b/assistant/src/daemon/handlers/misc.ts @@ -20,6 +20,7 @@ import type { } from '../ipc-protocol.js'; import { log, wireEscalationHandler, renderHistoryContent, defineHandlers, type HandlerContext } from './shared.js'; import { handleCuSessionCreate } from './computer-use.js'; +import { detectQaIntent } from '../qa-intent.js'; // ─── Task submit handler ──────────────────────────────────────────────────── @@ -74,6 +75,7 @@ export async function handleTaskSubmit( if (interactionType === 'computer_use') { // Create CU session (reuse handleCuSessionCreate logic) const sessionId = uuid(); + const isQa = detectQaIntent(msg.task); const cuMsg: CuSessionCreate = { type: 'cu_session_create', sessionId, @@ -82,6 +84,7 @@ export async function handleTaskSubmit( screenHeight: msg.screenHeight, attachments: msg.attachments, interactionType: 'computer_use', + ...(isQa ? { qaMode: true } : {}), }; handleCuSessionCreate(cuMsg, socket, ctx); @@ -89,6 +92,7 @@ export async function handleTaskSubmit( type: 'task_routed', sessionId, interactionType: 'computer_use', + ...(isQa ? { qaMode: true } : {}), }); } else { // Create text QA session and immediately start processing diff --git a/assistant/src/daemon/handlers/shared.ts b/assistant/src/daemon/handlers/shared.ts index ef084d730c9..48808692f1c 100644 --- a/assistant/src/daemon/handlers/shared.ts +++ b/assistant/src/daemon/handlers/shared.ts @@ -9,6 +9,7 @@ import type { ClientMessage, CuSessionCreate, ServerMessage, SessionTransportMet import type { SecretPromptResult } from '../../permissions/secret-prompter.js'; import { getConfig } from '../../config/loader.js'; import type { DebouncerMap } from '../../util/debounce.js'; +import { detectQaIntent } from '../qa-intent.js'; const log = getLogger('handlers'); @@ -235,6 +236,7 @@ export function wireEscalationHandler( } const cuSessionId = uuid(); + const isQa = detectQaIntent(task); const cuMsg: CuSessionCreate = { type: 'cu_session_create', sessionId: cuSessionId, @@ -242,6 +244,8 @@ export function wireEscalationHandler( screenWidth, screenHeight, interactionType: 'computer_use', + reportToSessionId: sourceSessionId, + ...(isQa ? { qaMode: true } : {}), }; handleCuSessionCreate(cuMsg, currentSocket, ctx); @@ -251,6 +255,8 @@ export function wireEscalationHandler( interactionType: 'computer_use', task, escalatedFrom: sourceSessionId, + reportToSessionId: sourceSessionId, + ...(isQa ? { qaMode: true } : {}), }); return true; diff --git a/assistant/src/daemon/ipc-contract.ts b/assistant/src/daemon/ipc-contract.ts index 836e9c1c316..7cb87a23ab1 100644 --- a/assistant/src/daemon/ipc-contract.ts +++ b/assistant/src/daemon/ipc-contract.ts @@ -1545,6 +1545,10 @@ export interface TaskRouted { task?: string; /** Set when a text_qa session escalates to computer_use via computer_use_request_control. */ escalatedFrom?: string; + /** Whether this is a QA/test workflow session. */ + qaMode?: boolean; + /** The originating chat session ID for result injection. */ + reportToSessionId?: string; } export interface RideShotgunResult { diff --git a/assistant/src/daemon/qa-intent.ts b/assistant/src/daemon/qa-intent.ts new file mode 100644 index 00000000000..45114c8f6ed --- /dev/null +++ b/assistant/src/daemon/qa-intent.ts @@ -0,0 +1,21 @@ +/** + * Detect whether a user's task text indicates a QA/test workflow. + * Uses keyword/pattern matching for v1 — can be upgraded to semantic detection later. + */ +export function detectQaIntent(taskText: string): boolean { + const lower = taskText.toLowerCase().trim(); + + // Direct QA/test commands + if (/^(qa|test|verify|check)\b/.test(lower)) return true; + + // Natural language QA patterns + const qaPatterns = [ + /\b(run|do|perform|execute)\s+(a\s+)?(qa|test|check|verification)\b/, + /\b(test|qa|verify|check)\s+(this|the|that|my)\b/, + /\bhelp\s+me\s+(test|qa|verify|check)\b/, + /\b(can you|could you|please)\s+(test|qa|verify|check)\b/, + /\btesting\s+(the|this|that|my)\b/, + ]; + + return qaPatterns.some(p => p.test(lower)); +} diff --git a/clients/macos/vellum-assistant/ComputerUse/Session.swift b/clients/macos/vellum-assistant/ComputerUse/Session.swift index 72e80d2da07..b626805806b 100644 --- a/clients/macos/vellum-assistant/ComputerUse/Session.swift +++ b/clients/macos/vellum-assistant/ComputerUse/Session.swift @@ -995,8 +995,8 @@ final class ComputerUseSession: ObservableObject { summary = "Session cancelled by user" stepCount = currentStepNumber default: - status = "unknown" - summary = "Session ended in unexpected state" + status = "failed" + summary = "Session ended in unexpected state: \(state)" stepCount = currentStepNumber } @@ -1005,7 +1005,7 @@ final class ComputerUseSession: ObservableObject { if let recorder = screenRecorder, recorder.isRecording { do { let result = try await recorder.stopRecording() - let expiresAtEpoch = Int(Date().addingTimeInterval(7 * 24 * 3600).timeIntervalSince1970) + let expiresAtEpoch = Int(Date().addingTimeInterval(7 * 24 * 3600).timeIntervalSince1970 * 1000) recordingData = IPCCuSessionFinalizedRecording( localPath: result.fileURL.path, mimeType: result.mimeType, diff --git a/clients/shared/IPC/Generated/IPCContractGenerated.swift b/clients/shared/IPC/Generated/IPCContractGenerated.swift index 306c0f96bfd..d8e85b52cfd 100644 --- a/clients/shared/IPC/Generated/IPCContractGenerated.swift +++ b/clients/shared/IPC/Generated/IPCContractGenerated.swift @@ -1740,6 +1740,10 @@ public struct IPCTaskRouted: Codable, Sendable { public let task: String? /// Set when a text_qa session escalates to computer_use via computer_use_request_control. public let escalatedFrom: String? + /// Whether this is a QA/test workflow session. + public let qaMode: Bool? + /// The originating chat session ID for result injection. + public let reportToSessionId: String? } /// Server push — broadcast when a task run creates a conversation, so the client can show it as a chat thread.