[{"data":1,"prerenderedAt":1547},["ShallowReactive",2],{"blog-post-en-015_context-aware-dictation-email-detection":3,"blog-translations-015_context-aware-dictation-email-detection":1536},{"id":4,"title":5,"body":6,"date":1521,"description":1522,"draft":1523,"extension":1524,"image":1525,"meta":1526,"navigation":223,"path":1527,"publishDate":1521,"readingTime":127,"seo":1528,"stem":1529,"tags":1530,"translationKey":1534,"__hash__":1535},"blog_en/blog/en/015_context-aware-dictation-email-detection.md","Your macOS App Should Know What the User Is Doing",{"type":7,"value":8,"toc":1515},"minimark",[9,13,16,21,24,30,147,150,155,398,405,408,412,415,428,431,434,496,503,567,574,577,693,696,700,712,717,724,835,844,849,852,1058,1061,1066,1069,1258,1261,1363,1366,1470,1474,1477,1487,1493,1503,1506,1511],[10,11,12],"p",{},"There is a moment in every dictation app where the user pauses and thinks: \"Wait, should I switch modes?\" They are writing an email but the app doesn't know that. It transcribes their words literally, no greeting, no sign-off, no formatting. Just a wall of text they'll have to manually edit into something that looks like an email.",[10,14,15],{},"I decided to kill that moment. Yakki should detect that you're composing an email and act accordingly: capture the recipients, subject, thread context, and use all of that to format your dictation properly. No mode switching. No menus. You hold the key, you speak, and the text comes out formatted like an email because the app already knows that's what you're writing.",[17,18,20],"h2",{"id":19},"the-two-layer-detection-strategy","The Two-Layer Detection Strategy",[10,22,23],{},"Email detection has a fundamental split: native apps and browser-based email. Apple Mail has a bundle identifier you can match. Gmail in Chrome does not. So I built two detection layers that run in sequence.",[10,25,26],{},[27,28,29],"strong",{},"Layer 1: Bundle ID matching for native apps.",[31,32,37],"pre",{"className":33,"code":34,"language":35,"meta":36,"style":36},"language-swift shiki shiki-themes github-dark github-dark","private static let emailAppBundleIds: Set\u003CString> = [\n    \"com.apple.mail\",\n    \"com.microsoft.Outlook\",\n    \"com.readdle.smartemail-Mac\",  // Spark\n    \"com.superhuman.electron\",\n    \"com.freron.MailMate\",\n    \"com.postbox-inc.postbox\",\n    \"com.mimestream.Mimestream\",\n]\n","swift","",[38,39,40,78,88,96,109,117,125,133,141],"code",{"__ignoreMap":36},[41,42,45,49,52,55,59,63,66,69,72,75],"span",{"class":43,"line":44},"line",1,[41,46,48],{"class":47},"sOPea","private",[41,50,51],{"class":47}," static",[41,53,54],{"class":47}," let",[41,56,58],{"class":57},"suv1-"," emailAppBundleIds: ",[41,60,62],{"class":61},"s8ozJ","Set",[41,64,65],{"class":57},"\u003C",[41,67,68],{"class":61},"String",[41,70,71],{"class":57},"> ",[41,73,74],{"class":47},"=",[41,76,77],{"class":57}," [\n",[41,79,81,85],{"class":43,"line":80},2,[41,82,84],{"class":83},"s4wv1","    \"com.apple.mail\"",[41,86,87],{"class":57},",\n",[41,89,91,94],{"class":43,"line":90},3,[41,92,93],{"class":83},"    \"com.microsoft.Outlook\"",[41,95,87],{"class":57},[41,97,99,102,105],{"class":43,"line":98},4,[41,100,101],{"class":83},"    \"com.readdle.smartemail-Mac\"",[41,103,104],{"class":57},",  ",[41,106,108],{"class":107},"sJ8bj","// Spark\n",[41,110,112,115],{"class":43,"line":111},5,[41,113,114],{"class":83},"    \"com.superhuman.electron\"",[41,116,87],{"class":57},[41,118,120,123],{"class":43,"line":119},6,[41,121,122],{"class":83},"    \"com.freron.MailMate\"",[41,124,87],{"class":57},[41,126,128,131],{"class":43,"line":127},7,[41,129,130],{"class":83},"    \"com.postbox-inc.postbox\"",[41,132,87],{"class":57},[41,134,136,139],{"class":43,"line":135},8,[41,137,138],{"class":83},"    \"com.mimestream.Mimestream\"",[41,140,87],{"class":57},[41,142,144],{"class":43,"line":143},9,[41,145,146],{"class":57},"]\n",[10,148,149],{},"Fast and definitive. If the frontmost app has one of these bundle IDs, you're in an email context. No ambiguity, no false positives.",[10,151,152],{},[27,153,154],{},"Layer 2: Window title pattern matching for browser-based email.",[31,156,158],{"className":33,"code":157,"language":35,"meta":36,"style":36},"private static let emailWindowPatterns: [String] = [\n    \"Gmail\", \"Outlook\", \"Yahoo Mail\",\n    \"Proton Mail\", \"ProtonMail\",\n    // ... compose window indicators\n]\n\nvar isEmailContext: Bool {\n    // Check native email apps by bundle ID\n    if Self.emailAppBundleIds.contains(bundleIdentifier) {\n        return true\n    }\n\n    // Check browser with email-related window title\n    let category = ApplicationCategory.category(for: bundleIdentifier)\n    if category == .browser, let title = windowTitle {\n        return Self.emailWindowPatterns.contains { pattern in\n            title.localizedCaseInsensitiveContains(pattern)\n        }\n    }\n\n    return false\n}\n",[38,159,160,180,198,210,215,219,225,239,244,261,270,276,281,287,313,337,355,367,373,378,383,392],{"__ignoreMap":36},[41,161,162,164,166,168,171,173,176,178],{"class":43,"line":44},[41,163,48],{"class":47},[41,165,51],{"class":47},[41,167,54],{"class":47},[41,169,170],{"class":57}," emailWindowPatterns: [",[41,172,68],{"class":61},[41,174,175],{"class":57},"] ",[41,177,74],{"class":47},[41,179,77],{"class":57},[41,181,182,185,188,191,193,196],{"class":43,"line":80},[41,183,184],{"class":83},"    \"Gmail\"",[41,186,187],{"class":57},", ",[41,189,190],{"class":83},"\"Outlook\"",[41,192,187],{"class":57},[41,194,195],{"class":83},"\"Yahoo Mail\"",[41,197,87],{"class":57},[41,199,200,203,205,208],{"class":43,"line":90},[41,201,202],{"class":83},"    \"Proton Mail\"",[41,204,187],{"class":57},[41,206,207],{"class":83},"\"ProtonMail\"",[41,209,87],{"class":57},[41,211,212],{"class":43,"line":98},[41,213,214],{"class":107},"    // ... compose window indicators\n",[41,216,217],{"class":43,"line":111},[41,218,146],{"class":57},[41,220,221],{"class":43,"line":119},[41,222,224],{"emptyLinePlaceholder":223},true,"\n",[41,226,227,230,233,236],{"class":43,"line":127},[41,228,229],{"class":47},"var",[41,231,232],{"class":57}," isEmailContext: ",[41,234,235],{"class":61},"Bool",[41,237,238],{"class":57}," {\n",[41,240,241],{"class":43,"line":135},[41,242,243],{"class":107},"    // Check native email apps by bundle ID\n",[41,245,246,249,252,255,258],{"class":43,"line":143},[41,247,248],{"class":47},"    if",[41,250,251],{"class":61}," Self",[41,253,254],{"class":57},".emailAppBundleIds.",[41,256,257],{"class":61},"contains",[41,259,260],{"class":57},"(bundleIdentifier) {\n",[41,262,264,267],{"class":43,"line":263},10,[41,265,266],{"class":47},"        return",[41,268,269],{"class":61}," true\n",[41,271,273],{"class":43,"line":272},11,[41,274,275],{"class":57},"    }\n",[41,277,279],{"class":43,"line":278},12,[41,280,224],{"emptyLinePlaceholder":223},[41,282,284],{"class":43,"line":283},13,[41,285,286],{"class":107},"    // Check browser with email-related window title\n",[41,288,290,293,296,298,301,304,307,310],{"class":43,"line":289},14,[41,291,292],{"class":47},"    let",[41,294,295],{"class":57}," category ",[41,297,74],{"class":47},[41,299,300],{"class":57}," ApplicationCategory.",[41,302,303],{"class":61},"category",[41,305,306],{"class":57},"(",[41,308,309],{"class":61},"for",[41,311,312],{"class":57},": bundleIdentifier)\n",[41,314,316,318,320,323,326,329,332,334],{"class":43,"line":315},15,[41,317,248],{"class":47},[41,319,295],{"class":57},[41,321,322],{"class":47},"==",[41,324,325],{"class":57}," .browser, ",[41,327,328],{"class":47},"let",[41,330,331],{"class":57}," title ",[41,333,74],{"class":47},[41,335,336],{"class":57}," windowTitle {\n",[41,338,340,342,344,347,349,352],{"class":43,"line":339},16,[41,341,266],{"class":47},[41,343,251],{"class":61},[41,345,346],{"class":57},".emailWindowPatterns.",[41,348,257],{"class":61},[41,350,351],{"class":57}," { pattern ",[41,353,354],{"class":47},"in\n",[41,356,358,361,364],{"class":43,"line":357},17,[41,359,360],{"class":57},"            title.",[41,362,363],{"class":61},"localizedCaseInsensitiveContains",[41,365,366],{"class":57},"(pattern)\n",[41,368,370],{"class":43,"line":369},18,[41,371,372],{"class":57},"        }\n",[41,374,376],{"class":43,"line":375},19,[41,377,275],{"class":57},[41,379,381],{"class":43,"line":380},20,[41,382,224],{"emptyLinePlaceholder":223},[41,384,386,389],{"class":43,"line":385},21,[41,387,388],{"class":47},"    return",[41,390,391],{"class":61}," false\n",[41,393,395],{"class":43,"line":394},22,[41,396,397],{"class":57},"}\n",[10,399,400,401,404],{},"The key insight: only check window titles when the frontmost app is a browser. This prevents false positives from, say, a text editor with a file called \"Gmail notes.md\" open. The ",[38,402,403],{},"ApplicationCategory"," system classifies apps by bundle ID into categories (browser, editor, terminal, etc.), and only falls through to title matching for browsers.",[10,406,407],{},"This two-layer approach catches every native email client and every major browser-based email service without any user configuration.",[17,409,411],{"id":410},"the-timing-problem","The Timing Problem",[10,413,414],{},"The whole illusion breaks if the user feels a delay. Context detection has a race condition built into its requirements. Three things must happen in the same instant:",[416,417,418,422,425],"ol",{},[419,420,421],"li",{},"Know which app was in the foreground (before Yakki's window appears)",[419,423,424],{},"Capture the window screenshot (before the app state changes)",[419,426,427],{},"Start recording audio (without delay)",[10,429,430],{},"These compete. The screenshot needs the app to be visible. The audio needs to start immediately. And step 1 has to happen before macOS shifts focus to Yakki.",[10,432,433],{},"The solution: capture the app reference synchronously on hotkey press, then run everything else in parallel.",[31,435,437],{"className":33,"code":436,"language":35,"meta":36,"style":36},"func hotkeyPressed() {\n    // CONTEXT AWARE: Store reference to the current frontmost app\n    // BEFORE Yakki takes focus. This must happen immediately.\n    if appState.contextConfig.isEnabled {\n        ContextIdentificationManager.shared.storePreviousApplication()\n    }\n\n    // ... start recording, UI updates, etc.\n}\n",[38,438,439,451,456,461,468,479,483,487,492],{"__ignoreMap":36},[41,440,441,444,448],{"class":43,"line":44},[41,442,443],{"class":47},"func",[41,445,447],{"class":446},"sFR8T"," hotkeyPressed",[41,449,450],{"class":57},"() {\n",[41,452,453],{"class":43,"line":80},[41,454,455],{"class":107},"    // CONTEXT AWARE: Store reference to the current frontmost app\n",[41,457,458],{"class":43,"line":90},[41,459,460],{"class":107},"    // BEFORE Yakki takes focus. This must happen immediately.\n",[41,462,463,465],{"class":43,"line":98},[41,464,248],{"class":47},[41,466,467],{"class":57}," appState.contextConfig.isEnabled {\n",[41,469,470,473,476],{"class":43,"line":111},[41,471,472],{"class":57},"        ContextIdentificationManager.shared.",[41,474,475],{"class":61},"storePreviousApplication",[41,477,478],{"class":57},"()\n",[41,480,481],{"class":43,"line":119},[41,482,275],{"class":57},[41,484,485],{"class":43,"line":127},[41,486,224],{"emptyLinePlaceholder":223},[41,488,489],{"class":43,"line":135},[41,490,491],{"class":107},"    // ... start recording, UI updates, etc.\n",[41,493,494],{"class":43,"line":143},[41,495,397],{"class":57},[10,497,498,499,502],{},"The call is ",[38,500,501],{},"nonisolated"," for a reason:",[31,504,506],{"className":33,"code":505,"language":35,"meta":36,"style":36},"nonisolated func storePreviousApplication() {\n    let app = NSWorkspace.shared.frontmostApplication\n    Task { @MainActor in\n        self.previousApplication = app\n    }\n}\n",[38,507,508,520,532,546,559,563],{"__ignoreMap":36},[41,509,510,512,515,518],{"class":43,"line":44},[41,511,501],{"class":47},[41,513,514],{"class":47}," func",[41,516,517],{"class":446}," storePreviousApplication",[41,519,450],{"class":57},[41,521,522,524,527,529],{"class":43,"line":80},[41,523,292],{"class":47},[41,525,526],{"class":57}," app ",[41,528,74],{"class":47},[41,530,531],{"class":57}," NSWorkspace.shared.frontmostApplication\n",[41,533,534,537,540,543],{"class":43,"line":90},[41,535,536],{"class":61},"    Task",[41,538,539],{"class":57}," { ",[41,541,542],{"class":47},"@MainActor",[41,544,545],{"class":47}," in\n",[41,547,548,551,554,556],{"class":43,"line":98},[41,549,550],{"class":61},"        self",[41,552,553],{"class":57},".previousApplication ",[41,555,74],{"class":47},[41,557,558],{"class":57}," app\n",[41,560,561],{"class":43,"line":111},[41,562,275],{"class":57},[41,564,565],{"class":43,"line":119},[41,566,397],{"class":57},[10,568,569,570,573],{},"The reference is captured on the calling thread. Microseconds, not milliseconds. The ",[38,571,572],{},"Task"," ships it to MainActor storage asynchronously. By the time the audio engine starts, the context manager already knows which app was active.",[10,575,576],{},"Meanwhile, the full context capture runs concurrently with recording:",[31,578,580],{"className":33,"code":579,"language":35,"meta":36,"style":36},"// CONTEXT AWARE: Capture context in parallel with audio capture starting\nif appState.contextConfig.isEnabled {\n    Task {\n        let context = await ContextIdentificationManager.shared.captureContext(\n            config: appState.contextConfig\n        )\n        if let ctx = context, ctx.isEmailContext {\n            let emailContext = await ContextIdentificationManager.shared\n                .captureEmailContextIfNeeded(config: appState.contextConfig)\n        }\n    }\n}\n",[38,581,582,587,594,600,622,630,635,650,665,681,685,689],{"__ignoreMap":36},[41,583,584],{"class":43,"line":44},[41,585,586],{"class":107},"// CONTEXT AWARE: Capture context in parallel with audio capture starting\n",[41,588,589,592],{"class":43,"line":80},[41,590,591],{"class":47},"if",[41,593,467],{"class":57},[41,595,596,598],{"class":43,"line":90},[41,597,536],{"class":61},[41,599,238],{"class":57},[41,601,602,605,608,610,613,616,619],{"class":43,"line":98},[41,603,604],{"class":47},"        let",[41,606,607],{"class":57}," context ",[41,609,74],{"class":47},[41,611,612],{"class":47}," await",[41,614,615],{"class":57}," ContextIdentificationManager.shared.",[41,617,618],{"class":61},"captureContext",[41,620,621],{"class":57},"(\n",[41,623,624,627],{"class":43,"line":111},[41,625,626],{"class":61},"            config",[41,628,629],{"class":57},": appState.contextConfig\n",[41,631,632],{"class":43,"line":119},[41,633,634],{"class":57},"        )\n",[41,636,637,640,642,645,647],{"class":43,"line":127},[41,638,639],{"class":47},"        if",[41,641,54],{"class":47},[41,643,644],{"class":57}," ctx ",[41,646,74],{"class":47},[41,648,649],{"class":57}," context, ctx.isEmailContext {\n",[41,651,652,655,658,660,662],{"class":43,"line":135},[41,653,654],{"class":47},"            let",[41,656,657],{"class":57}," emailContext ",[41,659,74],{"class":47},[41,661,612],{"class":47},[41,663,664],{"class":57}," ContextIdentificationManager.shared\n",[41,666,667,670,673,675,678],{"class":43,"line":143},[41,668,669],{"class":57},"                .",[41,671,672],{"class":61},"captureEmailContextIfNeeded",[41,674,306],{"class":57},[41,676,677],{"class":61},"config",[41,679,680],{"class":57},": appState.contextConfig)\n",[41,682,683],{"class":43,"line":263},[41,684,372],{"class":57},[41,686,687],{"class":43,"line":272},[41,688,275],{"class":57},[41,690,691],{"class":43,"line":278},[41,692,397],{"class":57},[10,694,695],{},"Hold the key. Start speaking. While your voice becomes text, the context manager is capturing a screenshot, running OCR, parsing recipients. By the time you release the key, both are done.",[17,697,699],{"id":698},"the-ocr-pipeline","The OCR Pipeline",[10,701,702,703,707,708,711],{},"Knowing you're in an email app is half the problem. The app also needs to know ",[704,705,706],"em",{},"who"," you're writing to, ",[704,709,710],{},"what"," the subject is, and whether this is a reply or a fresh thread. That's the difference between generic formatting and formatting that reads like your assistant already saw the conversation.",[10,713,714],{},[27,715,716],{},"Stage 1: Screenshot capture.",[10,718,719,720,723],{},"The challenge is finding the right window. Apps like Apple Mail open a separate compose window, and I need to capture that one, not the inbox. The focused window's ",[38,721,722],{},"CGWindowID"," comes through the Accessibility API:",[31,725,727],{"className":33,"code":726,"language":35,"meta":36,"style":36},"let appElement = AXUIElementCreateApplication(app.processIdentifier)\nvar windowRef: AnyObject?\nlet windowResult = AXUIElementCopyAttributeValue(\n    appElement, kAXFocusedWindowAttribute as CFString, &windowRef\n)\n\nvar focusedWID: CGWindowID = 0\nlet axErr = _AXUIElementGetWindow(windowRef as! AXUIElement, &focusedWID)\n",[38,728,729,744,757,771,788,793,797,809],{"__ignoreMap":36},[41,730,731,733,736,738,741],{"class":43,"line":44},[41,732,328],{"class":47},[41,734,735],{"class":57}," appElement ",[41,737,74],{"class":47},[41,739,740],{"class":61}," AXUIElementCreateApplication",[41,742,743],{"class":57},"(app.processIdentifier)\n",[41,745,746,748,751,754],{"class":43,"line":80},[41,747,229],{"class":47},[41,749,750],{"class":57}," windowRef: ",[41,752,753],{"class":61},"AnyObject",[41,755,756],{"class":47},"?\n",[41,758,759,761,764,766,769],{"class":43,"line":90},[41,760,328],{"class":47},[41,762,763],{"class":57}," windowResult ",[41,765,74],{"class":47},[41,767,768],{"class":61}," AXUIElementCopyAttributeValue",[41,770,621],{"class":57},[41,772,773,776,779,782,785],{"class":43,"line":98},[41,774,775],{"class":57},"    appElement, kAXFocusedWindowAttribute ",[41,777,778],{"class":47},"as",[41,780,781],{"class":57}," CFString, ",[41,783,784],{"class":47},"&",[41,786,787],{"class":57},"windowRef\n",[41,789,790],{"class":43,"line":111},[41,791,792],{"class":57},")\n",[41,794,795],{"class":43,"line":119},[41,796,224],{"emptyLinePlaceholder":223},[41,798,799,801,804,806],{"class":43,"line":127},[41,800,229],{"class":47},[41,802,803],{"class":57}," focusedWID: CGWindowID ",[41,805,74],{"class":47},[41,807,808],{"class":61}," 0\n",[41,810,811,813,816,818,821,824,827,830,832],{"class":43,"line":135},[41,812,328],{"class":47},[41,814,815],{"class":57}," axErr ",[41,817,74],{"class":47},[41,819,820],{"class":61}," _AXUIElementGetWindow",[41,822,823],{"class":57},"(windowRef ",[41,825,826],{"class":47},"as!",[41,828,829],{"class":57}," AXUIElement, ",[41,831,784],{"class":47},[41,833,834],{"class":57},"focusedWID)\n",[10,836,837,840,841,843],{},[38,838,839],{},"_AXUIElementGetWindow"," is a private HIServices function, stable since macOS 10.5, used by every accessibility tool on the platform. It gives us the exact ",[38,842,722],{}," for the focused window, even when the app has multiple windows across multiple screens.",[10,845,846],{},[27,847,848],{},"Stage 2: Vision OCR.",[10,850,851],{},"A screenshot is just pixels. To get structured data, the pixels have to become text:",[31,853,855],{"className":33,"code":854,"language":35,"meta":36,"style":36},"func extractContext(from image: CGImage) async -> EmailContext {\n    let (ocrText, confidence) = await performOCR(on: image)\n\n    let recipients = extractRecipients(from: text)\n    let subject = extractSubject(from: text)\n    let (isReply, isForward, threadContext) = extractThreadInfo(from: text)\n    let signatureStyle = detectSignatureStyle(from: text)\n\n    return EmailContext(\n        recipients: recipients,\n        subject: subject,\n        threadContext: threadContext,\n        signatureStyle: signatureStyle,\n        isReply: isReply,\n        isForward: isForward,\n        ocrConfidence: confidence\n    )\n}\n",[38,856,857,881,903,907,926,944,962,980,984,993,1001,1009,1017,1025,1033,1041,1049,1054],{"__ignoreMap":36},[41,858,859,861,864,866,869,872,875,878],{"class":43,"line":44},[41,860,443],{"class":47},[41,862,863],{"class":446}," extractContext",[41,865,306],{"class":57},[41,867,868],{"class":446},"from",[41,870,871],{"class":57}," image: CGImage) ",[41,873,874],{"class":47},"async",[41,876,877],{"class":47}," ->",[41,879,880],{"class":57}," EmailContext {\n",[41,882,883,885,888,890,892,895,897,900],{"class":43,"line":80},[41,884,292],{"class":47},[41,886,887],{"class":57}," (ocrText, confidence) ",[41,889,74],{"class":47},[41,891,612],{"class":47},[41,893,894],{"class":61}," performOCR",[41,896,306],{"class":57},[41,898,899],{"class":61},"on",[41,901,902],{"class":57},": image)\n",[41,904,905],{"class":43,"line":90},[41,906,224],{"emptyLinePlaceholder":223},[41,908,909,911,914,916,919,921,923],{"class":43,"line":98},[41,910,292],{"class":47},[41,912,913],{"class":57}," recipients ",[41,915,74],{"class":47},[41,917,918],{"class":61}," extractRecipients",[41,920,306],{"class":57},[41,922,868],{"class":61},[41,924,925],{"class":57},": text)\n",[41,927,928,930,933,935,938,940,942],{"class":43,"line":111},[41,929,292],{"class":47},[41,931,932],{"class":57}," subject ",[41,934,74],{"class":47},[41,936,937],{"class":61}," extractSubject",[41,939,306],{"class":57},[41,941,868],{"class":61},[41,943,925],{"class":57},[41,945,946,948,951,953,956,958,960],{"class":43,"line":119},[41,947,292],{"class":47},[41,949,950],{"class":57}," (isReply, isForward, threadContext) ",[41,952,74],{"class":47},[41,954,955],{"class":61}," extractThreadInfo",[41,957,306],{"class":57},[41,959,868],{"class":61},[41,961,925],{"class":57},[41,963,964,966,969,971,974,976,978],{"class":43,"line":127},[41,965,292],{"class":47},[41,967,968],{"class":57}," signatureStyle ",[41,970,74],{"class":47},[41,972,973],{"class":61}," detectSignatureStyle",[41,975,306],{"class":57},[41,977,868],{"class":61},[41,979,925],{"class":57},[41,981,982],{"class":43,"line":135},[41,983,224],{"emptyLinePlaceholder":223},[41,985,986,988,991],{"class":43,"line":143},[41,987,388],{"class":47},[41,989,990],{"class":61}," EmailContext",[41,992,621],{"class":57},[41,994,995,998],{"class":43,"line":263},[41,996,997],{"class":61},"        recipients",[41,999,1000],{"class":57},": recipients,\n",[41,1002,1003,1006],{"class":43,"line":272},[41,1004,1005],{"class":61},"        subject",[41,1007,1008],{"class":57},": subject,\n",[41,1010,1011,1014],{"class":43,"line":278},[41,1012,1013],{"class":61},"        threadContext",[41,1015,1016],{"class":57},": threadContext,\n",[41,1018,1019,1022],{"class":43,"line":283},[41,1020,1021],{"class":61},"        signatureStyle",[41,1023,1024],{"class":57},": signatureStyle,\n",[41,1026,1027,1030],{"class":43,"line":289},[41,1028,1029],{"class":61},"        isReply",[41,1031,1032],{"class":57},": isReply,\n",[41,1034,1035,1038],{"class":43,"line":315},[41,1036,1037],{"class":61},"        isForward",[41,1039,1040],{"class":57},": isForward,\n",[41,1042,1043,1046],{"class":43,"line":339},[41,1044,1045],{"class":61},"        ocrConfidence",[41,1047,1048],{"class":57},": confidence\n",[41,1050,1051],{"class":43,"line":357},[41,1052,1053],{"class":57},"    )\n",[41,1055,1056],{"class":43,"line":369},[41,1057,397],{"class":57},[10,1059,1060],{},"Vision runs entirely on-device. No network calls, no privacy concerns. The full OCR pass typically completes in 40-80ms depending on window size.",[10,1062,1063],{},[27,1064,1065],{},"Stage 3: Pattern matching to extract fields.",[10,1067,1068],{},"Email fields aren't just \"To:\" and \"Subject:\". They are \"Para:\", \"À:\", \"An:\", \"Asunto:\", \"Objet:\", \"Betreff:\" depending on the user's locale:",[31,1070,1072],{"className":33,"code":1071,"language":35,"meta":36,"style":36},"private let toPatterns = [\"To:\", \"Para:\", \"À:\", \"An:\", \"A:\"]\nprivate let ccPatterns = [\"Cc:\", \"CC:\", \"Copia:\", \"Copie:\", \"Kopie:\"]\nprivate let subjectPatterns = [\"Subject:\", \"Asunto:\", \"Objet:\", \"Betreff:\", \"Oggetto:\", \"件名:\"]\n\nprivate let replyPatterns = [\n    \"On .+ wrote:\",        // English\n    \"El .+ escribió:\",     // Spanish\n    \"Le .+ a écrit:\",      // French\n    \"Am .+ schrieb:\",      // German\n]\n",[38,1073,1074,1113,1151,1194,1198,1211,1222,1233,1244,1254],{"__ignoreMap":36},[41,1075,1076,1078,1080,1083,1085,1088,1091,1093,1096,1098,1101,1103,1106,1108,1111],{"class":43,"line":44},[41,1077,48],{"class":47},[41,1079,54],{"class":47},[41,1081,1082],{"class":57}," toPatterns ",[41,1084,74],{"class":47},[41,1086,1087],{"class":57}," [",[41,1089,1090],{"class":83},"\"To:\"",[41,1092,187],{"class":57},[41,1094,1095],{"class":83},"\"Para:\"",[41,1097,187],{"class":57},[41,1099,1100],{"class":83},"\"À:\"",[41,1102,187],{"class":57},[41,1104,1105],{"class":83},"\"An:\"",[41,1107,187],{"class":57},[41,1109,1110],{"class":83},"\"A:\"",[41,1112,146],{"class":57},[41,1114,1115,1117,1119,1122,1124,1126,1129,1131,1134,1136,1139,1141,1144,1146,1149],{"class":43,"line":80},[41,1116,48],{"class":47},[41,1118,54],{"class":47},[41,1120,1121],{"class":57}," ccPatterns ",[41,1123,74],{"class":47},[41,1125,1087],{"class":57},[41,1127,1128],{"class":83},"\"Cc:\"",[41,1130,187],{"class":57},[41,1132,1133],{"class":83},"\"CC:\"",[41,1135,187],{"class":57},[41,1137,1138],{"class":83},"\"Copia:\"",[41,1140,187],{"class":57},[41,1142,1143],{"class":83},"\"Copie:\"",[41,1145,187],{"class":57},[41,1147,1148],{"class":83},"\"Kopie:\"",[41,1150,146],{"class":57},[41,1152,1153,1155,1157,1160,1162,1164,1167,1169,1172,1174,1177,1179,1182,1184,1187,1189,1192],{"class":43,"line":90},[41,1154,48],{"class":47},[41,1156,54],{"class":47},[41,1158,1159],{"class":57}," subjectPatterns ",[41,1161,74],{"class":47},[41,1163,1087],{"class":57},[41,1165,1166],{"class":83},"\"Subject:\"",[41,1168,187],{"class":57},[41,1170,1171],{"class":83},"\"Asunto:\"",[41,1173,187],{"class":57},[41,1175,1176],{"class":83},"\"Objet:\"",[41,1178,187],{"class":57},[41,1180,1181],{"class":83},"\"Betreff:\"",[41,1183,187],{"class":57},[41,1185,1186],{"class":83},"\"Oggetto:\"",[41,1188,187],{"class":57},[41,1190,1191],{"class":83},"\"件名:\"",[41,1193,146],{"class":57},[41,1195,1196],{"class":43,"line":98},[41,1197,224],{"emptyLinePlaceholder":223},[41,1199,1200,1202,1204,1207,1209],{"class":43,"line":111},[41,1201,48],{"class":47},[41,1203,54],{"class":47},[41,1205,1206],{"class":57}," replyPatterns ",[41,1208,74],{"class":47},[41,1210,77],{"class":57},[41,1212,1213,1216,1219],{"class":43,"line":119},[41,1214,1215],{"class":83},"    \"On .+ wrote:\"",[41,1217,1218],{"class":57},",        ",[41,1220,1221],{"class":107},"// English\n",[41,1223,1224,1227,1230],{"class":43,"line":127},[41,1225,1226],{"class":83},"    \"El .+ escribió:\"",[41,1228,1229],{"class":57},",     ",[41,1231,1232],{"class":107},"// Spanish\n",[41,1234,1235,1238,1241],{"class":43,"line":135},[41,1236,1237],{"class":83},"    \"Le .+ a écrit:\"",[41,1239,1240],{"class":57},",      ",[41,1242,1243],{"class":107},"// French\n",[41,1245,1246,1249,1251],{"class":43,"line":143},[41,1247,1248],{"class":83},"    \"Am .+ schrieb:\"",[41,1250,1240],{"class":57},[41,1252,1253],{"class":107},"// German\n",[41,1255,1256],{"class":43,"line":263},[41,1257,146],{"class":57},[10,1259,1260],{},"The extractor also classifies signature style (formal or casual) so the LLM can match the user's tone:",[31,1262,1264],{"className":33,"code":1263,"language":35,"meta":36,"style":36},"private let formalSignaturePatterns = [\n    \"Best regards\", \"Kind regards\", \"Sincerely\",\n    \"Cordialmente\", \"Atentamente\",\n]\nprivate let casualSignaturePatterns = [\n    \"Thanks\", \"Thank you\", \"Cheers\",\n    \"Gracias\", \"Merci\", \"Danke\",\n]\n",[38,1265,1266,1279,1296,1308,1312,1325,1342,1359],{"__ignoreMap":36},[41,1267,1268,1270,1272,1275,1277],{"class":43,"line":44},[41,1269,48],{"class":47},[41,1271,54],{"class":47},[41,1273,1274],{"class":57}," formalSignaturePatterns ",[41,1276,74],{"class":47},[41,1278,77],{"class":57},[41,1280,1281,1284,1286,1289,1291,1294],{"class":43,"line":80},[41,1282,1283],{"class":83},"    \"Best regards\"",[41,1285,187],{"class":57},[41,1287,1288],{"class":83},"\"Kind regards\"",[41,1290,187],{"class":57},[41,1292,1293],{"class":83},"\"Sincerely\"",[41,1295,87],{"class":57},[41,1297,1298,1301,1303,1306],{"class":43,"line":90},[41,1299,1300],{"class":83},"    \"Cordialmente\"",[41,1302,187],{"class":57},[41,1304,1305],{"class":83},"\"Atentamente\"",[41,1307,87],{"class":57},[41,1309,1310],{"class":43,"line":98},[41,1311,146],{"class":57},[41,1313,1314,1316,1318,1321,1323],{"class":43,"line":111},[41,1315,48],{"class":47},[41,1317,54],{"class":47},[41,1319,1320],{"class":57}," casualSignaturePatterns ",[41,1322,74],{"class":47},[41,1324,77],{"class":57},[41,1326,1327,1330,1332,1335,1337,1340],{"class":43,"line":119},[41,1328,1329],{"class":83},"    \"Thanks\"",[41,1331,187],{"class":57},[41,1333,1334],{"class":83},"\"Thank you\"",[41,1336,187],{"class":57},[41,1338,1339],{"class":83},"\"Cheers\"",[41,1341,87],{"class":57},[41,1343,1344,1347,1349,1352,1354,1357],{"class":43,"line":127},[41,1345,1346],{"class":83},"    \"Gracias\"",[41,1348,187],{"class":57},[41,1350,1351],{"class":83},"\"Merci\"",[41,1353,187],{"class":57},[41,1355,1356],{"class":83},"\"Danke\"",[41,1358,87],{"class":57},[41,1360,1361],{"class":43,"line":135},[41,1362,146],{"class":57},[10,1364,1365],{},"All of this collapses into one struct:",[31,1367,1369],{"className":33,"code":1368,"language":35,"meta":36,"style":36},"struct EmailContext {\n    var recipients: [EmailRecipient]\n    var subject: String?\n    var threadContext: String?\n    var existingBodyText: String?\n    var signatureStyle: SignatureStyle?\n    var isReply: Bool\n    var isForward: Bool\n    var captureLatency: TimeInterval\n    var ocrConfidence: Float\n}\n",[38,1370,1371,1380,1388,1399,1410,1421,1430,1440,1449,1456,1466],{"__ignoreMap":36},[41,1372,1373,1376,1378],{"class":43,"line":44},[41,1374,1375],{"class":47},"struct",[41,1377,990],{"class":446},[41,1379,238],{"class":57},[41,1381,1382,1385],{"class":43,"line":80},[41,1383,1384],{"class":47},"    var",[41,1386,1387],{"class":57}," recipients: [EmailRecipient]\n",[41,1389,1390,1392,1395,1397],{"class":43,"line":90},[41,1391,1384],{"class":47},[41,1393,1394],{"class":57}," subject: ",[41,1396,68],{"class":61},[41,1398,756],{"class":47},[41,1400,1401,1403,1406,1408],{"class":43,"line":98},[41,1402,1384],{"class":47},[41,1404,1405],{"class":57}," threadContext: ",[41,1407,68],{"class":61},[41,1409,756],{"class":47},[41,1411,1412,1414,1417,1419],{"class":43,"line":111},[41,1413,1384],{"class":47},[41,1415,1416],{"class":57}," existingBodyText: ",[41,1418,68],{"class":61},[41,1420,756],{"class":47},[41,1422,1423,1425,1428],{"class":43,"line":119},[41,1424,1384],{"class":47},[41,1426,1427],{"class":57}," signatureStyle: SignatureStyle",[41,1429,756],{"class":47},[41,1431,1432,1434,1437],{"class":43,"line":127},[41,1433,1384],{"class":47},[41,1435,1436],{"class":57}," isReply: ",[41,1438,1439],{"class":61},"Bool\n",[41,1441,1442,1444,1447],{"class":43,"line":135},[41,1443,1384],{"class":47},[41,1445,1446],{"class":57}," isForward: ",[41,1448,1439],{"class":61},[41,1450,1451,1453],{"class":43,"line":143},[41,1452,1384],{"class":47},[41,1454,1455],{"class":57}," captureLatency: TimeInterval\n",[41,1457,1458,1460,1463],{"class":43,"line":263},[41,1459,1384],{"class":47},[41,1461,1462],{"class":57}," ocrConfidence: ",[41,1464,1465],{"class":61},"Float\n",[41,1467,1468],{"class":43,"line":272},[41,1469,397],{"class":57},[17,1471,1473],{"id":1472},"what-this-requires","What This Requires",[10,1475,1476],{},"Building this kind of intelligence requires getting three things right.",[10,1478,1479,1482,1483,1486],{},[27,1480,1481],{},"Detection must be fast and definitive."," Bundle IDs never lie. Window titles are accurate 99% of the time. Accessibility attributes give you the focused window with certainty. If you're building any kind of context-aware macOS app, start with ",[38,1484,1485],{},"NSWorkspace.shared.frontmostApplication"," and a bundle ID lookup table, basic application awareness in 20 lines of code. Accessibility APIs and OCR come after. The multilingual patterns for field extraction are 30 lines of code and the difference between a feature that works for everyone and a feature that works for English speakers.",[10,1488,1489,1492],{},[27,1490,1491],{},"Timing must be invisible."," Capture the app reference synchronously on the hotkey press. Run everything else (screenshots, OCR, pattern matching) in a parallel Task. The entire pipeline runs in under 100ms, imperceptible to the user, especially since it runs concurrently with audio recording. The user should never feel the system thinking. The moment they do, the illusion is broken.",[10,1494,1495,1498,1499,1502],{},[27,1496,1497],{},"Privacy must be architectural."," Some apps should never be observed: password managers, banking apps, terminals with sensitive output. I maintain a ",[38,1500,1501],{},"ContextBlocklist"," that short-circuits detection before any screenshot is taken. Privacy isn't a feature. It's a constraint that shapes every other decision in the system.",[1504,1505],"hr",{},[10,1507,1508],{},[704,1509,1510],{},"This is part of an ongoing series about building Yakki, a macOS dictation app. The context-aware system described here is the foundation for the email formatting mode, which uses LLM-based post-processing to turn raw dictation into properly formatted emails.",[1512,1513,1514],"style",{},"html pre.shiki code .sOPea, html code.shiki .sOPea{--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sFR8T, html code.shiki .sFR8T{--shiki-default:#B392F0;--shiki-dark:#B392F0}",{"title":36,"searchDepth":90,"depth":90,"links":1516},[1517,1518,1519,1520],{"id":19,"depth":80,"text":20},{"id":410,"depth":80,"text":411},{"id":698,"depth":80,"text":699},{"id":1472,"depth":80,"text":1473},"2026-03-31","How to build invisible application awareness in under 100ms. A deep dive into detecting email contexts on macOS using bundle IDs, window title matching, Accessibility APIs, and Vision OCR—all running on-device.",false,"md",null,{},"/blog/en/015_context-aware-dictation-email-detection",{"title":5,"description":1522},"blog/en/015_context-aware-dictation-email-detection",[1531,35,1532,1533],"engineering","macos","context-awareness","context-aware-dictation-email-detection","f_cadSXyFO7bPQX_dt2hq45aRKceCf9jmi8FUq6bVq8",[1537,1542],{"code":1538,"name":1539,"title":1540,"path":1541},"es","Español","Tu app de macOS debería saber qué está haciendo el usuario","/es/blog/015_context-aware-dictation-email-detection",{"code":1543,"name":1544,"title":1545,"path":1546},"fr","Français","Votre app macOS devrait savoir ce que fait l'utilisateur","/fr/blog/015_context-aware-dictation-email-detection",1775207564258]