From 6bef78675f89e412b2a42221a49be60440bb18cf Mon Sep 17 00:00:00 2001 From: Vellum Assistant Date: Mon, 23 Feb 2026 18:41:56 -0500 Subject: [PATCH] fix: tighten cross-app escape hatch + guard empty fuzzy match Replace overly broad regex patterns in taskExplicitlyRequestsCrossApp() with app-name-aware detection that requires two distinct known app names to be mentioned. The old patterns matched single-app actions like "switch to dark mode" or "move the file to trash". Guard fuzzy app matching against empty normalized input to prevent String.contains("") from matching the first running app arbitrarily when input like "---" normalizes to an empty string. Co-Authored-By: Claude Opus 4.6 --- assistant/src/daemon/computer-use-session.ts | 57 +++++++++++++++---- .../ComputerUse/ActionExecutor.swift | 4 +- 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/assistant/src/daemon/computer-use-session.ts b/assistant/src/daemon/computer-use-session.ts index 4af59c9c664..ae824a18147 100644 --- a/assistant/src/daemon/computer-use-session.ts +++ b/assistant/src/daemon/computer-use-session.ts @@ -248,27 +248,60 @@ export class ComputerUseSession { return value.toLowerCase().replace(/[^a-z0-9]/g, ''); } + /** + * Well-known app names used to detect cross-app intent in task text. + * Only needs to cover apps commonly referenced in cross-app workflows; + * the list does not need to be exhaustive. + */ + private static readonly KNOWN_APP_NAMES: ReadonlySet = new Set([ + 'chrome', 'google chrome', 'safari', 'firefox', 'arc', 'brave', 'edge', + 'slack', 'discord', 'zoom', 'teams', 'microsoft teams', + 'notion', 'obsidian', 'bear', 'notes', 'apple notes', + 'finder', 'terminal', 'iterm', 'iterm2', 'warp', + 'vscode', 'visual studio code', 'cursor', 'xcode', 'intellij', 'webstorm', + 'figma', 'sketch', 'photoshop', 'illustrator', + 'mail', 'outlook', 'gmail', 'thunderbird', + 'spotify', 'music', 'apple music', + 'messages', 'imessage', 'whatsapp', 'telegram', 'signal', + 'calendar', 'reminders', 'todoist', 'things', + 'pages', 'numbers', 'keynote', 'word', 'excel', 'powerpoint', + 'preview', 'acrobat', 'pdf expert', + 'vellum', 'vellum assistant', + 'linear', 'jira', 'github', 'gitlab', + 'postman', 'docker', 'tableplus', 'sequel pro', + 'system preferences', 'system settings', 'activity monitor', + ]); + /** * Returns true when the original user task text explicitly requests a * cross-app workflow (e.g. "copy from Chrome and paste into Vellum"). * Only the user's original task counts — model-generated reasoning * about switching apps does NOT qualify as an escape. + * + * Detection strategy: check whether the task text mentions at least two + * different known app names. This avoids false positives from generic + * phrases like "switch to dark mode" or "move the file to trash". */ private taskExplicitlyRequestsCrossApp(): boolean { if (!this.task) return false; const t = this.task.toLowerCase(); - // Matches patterns like "copy from X", "paste into Y", "switch to Z", - // "open X and Y", "drag from X to Y", "move … to ", etc. - const crossAppPatterns = [ - /\bcopy\s+from\s+\w+.*\bpaste\s+(in|into|to)\b/, - /\bswitch\s+to\s+\w+/, - /\bopen\s+\w+.*\band\s+(then\s+)?open\b/, - /\bdrag\s+from\s+\w+.*\bto\s+\w+/, - /\bmove\s+.*\bto\s+\w+/, - /\bfrom\s+\w+.*\b(into|to)\s+\w+/, - /\buse\s+\w+.*\band\s+\w+/, - ]; - return crossAppPatterns.some((p) => p.test(t)); + + // Collect distinct app names mentioned in the task text. + const mentionedApps = new Set(); + for (const appName of ComputerUseSession.KNOWN_APP_NAMES) { + // Word-boundary check: the app name must appear as a standalone word/phrase, + // not as a substring of another word. + const escaped = appName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + if (new RegExp(`\\b${escaped}\\b`).test(t)) { + // Normalize to a canonical form so e.g. "google chrome" and "chrome" + // are counted as the same app. + const canonical = ComputerUseSession.normalizeAppLabel(appName); + mentionedApps.add(canonical); + } + // Early exit once we confirm at least two distinct apps. + if (mentionedApps.size >= 2) return true; + } + return false; } /** diff --git a/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift b/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift index 3f5e3c7ade6..d103f1103c1 100644 --- a/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift +++ b/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift @@ -372,7 +372,9 @@ final class ActionExecutor: ActionExecuting { // 2. Normalized/fuzzy name matching against running apps let normalizedName = Self.normalizeAppName(name) - if let runningApp = workspace.runningApplications.first(where: { app in + // Guard: when the input normalizes to empty (e.g. "---"), String.contains("") + // returns true for any string, which would match the first running app arbitrarily. + if !normalizedName.isEmpty, let runningApp = workspace.runningApplications.first(where: { app in guard let localizedName = app.localizedName else { return false } let normalized = Self.normalizeAppName(localizedName) return normalized == normalizedName