Action

Call Sheet MacOS

Posted by David Degner, Last update 7 days ago

UPDATES

7 days ago

July 2026 update: refreshed the macOS create action with the current EmailsToCallSheet script. Adds Gemini system-instruction prompting, PDF attachment text extraction, deterministic source-message links, native NSURLConnection requests, code-fence stripping, and updated ### call-sheet headings.

show all updates...

7 days ago

July 2026 update: refreshed the macOS create action with the current EmailsToCallSheet script. Adds Gemini system-instruction prompting, PDF attachment text extraction, deterministic source-message links, native NSURLConnection requests, code-fence stripping, and updated ### call-sheet headings.

7 months ago

Updated Icon

7 months ago

Improved the email filtering. Removed the Python requirement.

11 months ago

Updated after getting errors in Tahoe Beta.

Now includes support for highlighting multiple message conversations.

about 1 year ago

Now Gemini based and reconstructs the email thread through gemini instead of programatically.

over 1 year ago

Update for the latest Mail app. Now selects all emails in conversation based on subject.

over 1 year ago

Fixed email link finally

over 1 year ago

Fixed the return/linefeed error again

over 1 year ago

Replaced the shell script which appears to fix the beach-balling delay.

over 1 year ago

Updating the name

over 1 year ago

Fix text and MarkDown

over 1 year ago

Converted the output to Markdown

over 1 year ago

Fixed the Long Title bug through Return character handling

Changed the URL encoding to use AppleScript instead of Python for eventual complete removal of Python

Removed Temporary file cleanup to try and speed up the action

Changed OpenAI model to gpt-4o for speed

almost 2 years ago

Here’s an enumeration of the changes and improvements between V1 and V4 of the email thread processor script:

  1. Improved removeQuotedText function:

    • Added a more sophisticated quote detection system using patterns
    • Introduced a quoteHeaderPattern to identify and skip quote headers
    • Improved handling of different quote formats
  2. New helper functions:

    • Added a trim function to remove leading and trailing whitespace
    • Introduced a matchesPattern function for regex-like pattern matching
  3. Enhanced email processing:

    • Now includes the creation of message links for each email in the thread
    • Improved formatting of email details in the threadContent
  4. Cleanup functionality:

    • Added a cleanupTempFiles function to remove temporary files created during processing
  5. Error handling:

    • Improved error handling and user feedback throughout the script
  6. Prompt improvements:

    • Updated the prompt text with more detailed instructions
    • Added a requirement to include section headings even for empty sections
  7. File handling:

    • Improved temporary file naming convention (using “email_processor_” prefix)
    • Better management of file paths and cleanup
  8. Script structure:

    • Reorganized the script into more clearly defined functions
    • Added comments to improve code readability and maintainability
  9. Execution flow:

    • Added an execute function to encapsulate the main script logic
    • The script now runs the execute function automatically when launched
  10. Minor adjustments:

    • Updated variable names for clarity
    • Improved string concatenation and formatting throughout the script
  11. Compatibility:

    • Explicitly mentioned compatibility with modern MacOS and Python3

These improvements make the V4 script more robust, efficient, and user-friendly compared to the V1 version, with better handling of email threads and improved information extraction capabilities.

almost 2 years ago

Here’s an enumeration of the changes and improvements between V1 and V4 of the email thread processor script:

  1. Improved removeQuotedText function:

    • Added a more sophisticated quote detection system using patterns
    • Introduced a quoteHeaderPattern to identify and skip quote headers
    • Improved handling of different quote formats
  2. New helper functions:

    • Added a trim function to remove leading and trailing whitespace
    • Introduced a matchesPattern function for regex-like pattern matching
  3. Enhanced email processing:

    • Now includes the creation of message links for each email in the thread
    • Improved formatting of email details in the threadContent
  4. Cleanup functionality:

    • Added a cleanupTempFiles function to remove temporary files created during processing
  5. Error handling:

    • Improved error handling and user feedback throughout the script
  6. Prompt improvements:

    • Updated the prompt text with more detailed instructions
    • Added a requirement to include section headings even for empty sections
  7. File handling:

    • Improved temporary file naming convention (using “email_processor_” prefix)
    • Better management of file paths and cleanup
  8. Script structure:

    • Reorganized the script into more clearly defined functions
    • Added comments to improve code readability and maintainability
  9. Execution flow:

    • Added an execute function to encapsulate the main script logic
    • The script now runs the execute function automatically when launched
  10. Minor adjustments:

    • Updated variable names for clarity
    • Improved string concatenation and formatting throughout the script
  11. Compatibility:

    • Explicitly mentioned compatibility with modern MacOS and Python3

These improvements make the V4 script more robust, efficient, and user-friendly compared to the V1 version, with better handling of email threads and improved information extraction capabilities.

Call Sheet MacOS

Creates a new photography call sheet in Drafts from selected Mail.app messages using Google’s Gemini API.

Select one or more related client emails in Mail, run the action, and it will:

  • collect related thread messages by normalized subject
  • dedupe messages by Message-ID and sort chronologically
  • extract text from PDF attachments with PDFKit when a text layer is available
  • send the raw thread to Gemini with a structured production-assistant prompt
  • create a markdown call sheet plus a cleaned chronological email thread
  • append a ### SOURCE MESSAGES section with deterministic message:// links back to the original emails

Requires macOS, Mail.app, Drafts, and a Gemini API key stored in Keychain as Gemini_API_Key.

Part of the EmailsToCallSheet project:
https://github.com/ddegner/EmailsToCallSheet
https://www.daviddegner.com

Steps

  • runAppleScript (macOS only)

    use framework "Foundation"
    use framework "Quartz"
    use scripting additions
    -- =====================================================
    -- Drafts: Mail → Call Sheet (MacOS)
    -- =====================================================
    -- Notes for Drafts AppleScript actions (macOS):
    --  • Drafts calls `on execute(d)` automatically. The passed `d` is a read-only
    --    record, not a live draft; new drafts are created via `tell application "Drafts"`.
    --  • Networking uses NSURLConnection sendSynchronousRequest (works inside Drafts'
    --    host; NSURLSession's script-object delegate does not fire reliably there).
    --  • Messages are read once into records, then deduped and sorted with no Mail calls.
    --  • PDF attachments are extracted to text via PDFKit (text layer only, no OCR) and
    --    folded into each message body before the thread is sent to the model.
    --  • Message links are built deterministically (Markdown-safe) and appended after the
    --    model returns, never sent through the model (which would corrupt the Message-IDs).
    -- =====================================================
    -- *** USER SETTINGS ***
    property geminiAPIKeyName : "Gemini_API_Key" -- Keychain service name for the Gemini API key
    property geminiModel : "gemini-3.1-pro-preview" -- Primary model
    property draftsTags : {"callsheet"}
    property maxMessagesPerThread : 50 -- Cap to limit token/latency
    property maxPDFCharsPerAttachment : 20000 -- Cap extracted PDF text per attachment
    property showAlerts : true -- Set false to suppress display alerts when running from Drafts
    property requestTimeoutSecs : 180 -- Max seconds per Gemini API call
    -- System instruction: defines the model's role and output constraints.
    property systemInstruction : "You are a production assistant for photographer David Degner. You process raw email threads into structured call sheets.
    <rules>
    - Extract ONLY facts explicitly stated in the email thread. Never infer or fabricate details.
    - Omit conversational pleasantries, sign-offs, and email signatures.
    - Use markdown formatting only. Do not use HTML.
    - Do not wrap your output in code fences (no ```markdown or ``` delimiters).
    - If a section has no relevant information in the thread, include only the heading with no content below it.
    </rules>
    <output_format>
    Your response must contain exactly two sections separated by a line of 36 dashes (------------------------------------).
    The first line must be: # YYYYMMDD - {project-title}
    If the shoot date is unknown, use XXXXXXXXX in place of YYYYMMDD.
    Then include these markdown headings in order, each as ###:
    ### LOCATION
    The photography location or client address and start time.
    ### PROJECT DESCRIPTION
    Key objectives, scope, style, goals, or focus areas.  If a NYTimes assignment include the Assignment ID here.
    ### TEAM AND ROLES
    All mentioned team members, subjects, and their roles.
    ### CLIENT INFORMATION
    Client/company name, main contact (and role if mentioned), contact details (email, phone) without labels. Include agency name and contact if involved.
    ### PROJECT TIMELINE
    Labeled relevant dates: deadlines, shoot dates, delivery timelines.
    ### DELIVERABLES
    Required outputs (photos, videos) with quantity, format, and settings.
    ### BUDGET
    All mentions of budgets, costs, fees, pricing, estimates, quotes, rates, and monetary values.
    After the separator line, present the emails in correct chronological order with redundant quoted text and signatures removed.
    Format each message as:
    **From:** Sender Name, Date, Time
    Message content
    ---
    </output_format>"
    -- User prompt template: context-first, task-last (per Gemini best practices for long context)
    property userPromptIntro : "Process the following raw email thread into a call sheet and reconstructed thread per your instructions.
    <email_thread>"
    property userPromptOutro : "</email_thread>
    Produce the call sheet followed by the separator line and reconstructed thread now."
    -- =============================
    -- Utility helpers
    -- =============================
    on showAlert(t, m)
    	if showAlerts then
    		display alert t message m buttons {"OK"} default button "OK"
    	end if
    end showAlert
    on replace_chars(theText, searchString, replacementString)
    	set AppleScript's text item delimiters to searchString
    	set theItems to text items of theText
    	set AppleScript's text item delimiters to replacementString
    	set theText to theItems as string
    	set AppleScript's text item delimiters to ""
    	return theText
    end replace_chars
    on trim(someText)
    	set nsText to current application's NSString's stringWithString:someText
    	set trimmedText to nsText's stringByTrimmingCharactersInSet:(current application's NSCharacterSet's whitespaceAndNewlineCharacterSet())
    	return trimmedText as string
    end trim
    on lowerText(someText)
    	set nsText to current application's NSString's stringWithString:(someText as text)
    	return (nsText's lowercaseString()) as text
    end lowerText
    on stripCodeFences(someText)
    	-- Strip leading ```markdown or ``` and trailing ```
    	set t to my trim(someText)
    	if t begins with "```" then
    		-- Find end of first line (the opening fence)
    		set fenceEnd to offset of linefeed in t
    		if fenceEnd > 0 then
    			set t to text (fenceEnd + 1) thru -1 of t
    		end if
    	end if
    	if t ends with "```" then
    		set t to text 1 thru -4 of t
    	end if
    	return my trim(t)
    end stripCodeFences
    on normalizeSubject(s)
    	set t to my trim(s as text)
    	-- Strip leading bracket tags like [EXTERNAL], [EXT], [SECURE], etc. (repeatable)
    	repeat while (t begins with "[")
    		set closePos to offset of "]" in t
    		if closePos > 0 then
    			try
    				set t to my trim(text (closePos + 1) thru -1 of t)
    			on error
    				exit repeat
    			end try
    		else
    			exit repeat
    		end if
    	end repeat
    	-- Strip repeated prefixes (case-insensitive), allowing optional space after colon
    	repeat
    		set lc to my lowerText(t)
    		if lc begins with "re:" then
    			set t to my trim(text 4 thru -1 of t)
    		else if lc begins with "fw:" then
    			set t to my trim(text 4 thru -1 of t)
    		else if lc begins with "fwd:" then
    			set t to my trim(text 5 thru -1 of t)
    		else
    			exit repeat
    		end if
    	end repeat
    	return my trim(t)
    end normalizeSubject
    on getAPIKeyFromKeychain(keyName)
    	try
    		set apiKey to do shell script "security find-generic-password -w -s " & quoted form of keyName
    		return apiKey
    	on error
    		return missing value
    	end try
    end getAPIKeyFromKeychain
    -- Make a sender/date string safe to use as Markdown link text
    on sanitizeLinkLabel(someText)
    	set t to my replace_chars(someText, "[", "(")
    	set t to my replace_chars(t, "]", ")")
    	set t to my replace_chars(t, "<", "")
    	set t to my replace_chars(t, ">", "")
    	return t
    end sanitizeLinkLabel
    -- Build a Markdown-safe message:// URL from a raw Message-ID
    on messageURL(mid)
    	set nsID to current application's NSString's stringWithString:(mid as text)
    	set allowedChars to current application's NSCharacterSet's characterSetWithCharactersInString:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~@"
    	set encID to (nsID's stringByAddingPercentEncodingWithAllowedCharacters:allowedChars) as text
    	return "message://%3c" & encID & "%3e"
    end messageURL
    -- =============================
    -- PDF attachment extraction (PDFKit text layer; no OCR)
    -- =============================
    on pdfFileToText(posixPath)
    	set theURL to current application's NSURL's fileURLWithPath:posixPath
    	set pdfDoc to current application's PDFDocument's alloc()'s initWithURL:theURL
    	if pdfDoc is missing value then return "" -- unreadable or encrypted
    	set theString to pdfDoc's |string|()
    	if theString is missing value then return "" -- no embedded text layer (scanned image)
    	return (theString as text)
    end pdfFileToText
    on extractPDFText(theMessage)
    	set collectedText to ""
    	tell application "Mail"
    		set atts to mail attachments of theMessage
    	end tell
    	repeat with a in atts
    		tell application "Mail"
    			set aName to (name of a)
    			set isDown to (downloaded of a)
    			set aMime to ""
    			try
    				set aMime to (MIME type of a)
    			end try
    		end tell
    		set isPDF to (aMime is "application/pdf") or (aName ends with ".pdf") or (aName ends with ".PDF")
    		if isPDF then
    			try
    				if not isDown then error "attachment not downloaded"
    				set tmpPath to (POSIX path of (path to temporary items)) & "callsheet_" & (do shell script "uuidgen") & ".pdf"
    				tell application "Mail"
    					save a in (POSIX file tmpPath)
    				end tell
    				set t to my pdfFileToText(tmpPath)
    				do shell script "rm -f " & quoted form of tmpPath
    				set t to my trim(t)
    				if (count of t) > maxPDFCharsPerAttachment then
    					set t to (text 1 thru maxPDFCharsPerAttachment of t) & linefeed & "[...PDF text truncated...]"
    				end if
    				if t is not "" then
    					set collectedText to collectedText & "----- PDF ATTACHMENT: " & aName & " -----" & linefeed & t & linefeed & linefeed
    				end if
    			on error
    				-- not downloaded, encrypted, or unreadable: skip silently
    			end try
    		end if
    	end repeat
    	return collectedText
    end extractPDFText
    -- =============================
    -- Read messages once, dedupe by Message-ID in the same pass.
    -- Returns a list of records; no further Mail calls are needed after this.
    -- =============================
    on collectMessageData(messageList)
    	set recordList to {}
    	set seenIDs to {}
    	tell application "Mail"
    		repeat with m in messageList
    			set mid to message id of m
    			if seenIDs does not contain mid then
    				set end of seenIDs to mid
    				set theSender to sender of m
    				set theSubject to subject of m
    				set theDate to date received of m
    				set theBody to content of m
    				set pdfText to my extractPDFText(m)
    				if pdfText is not "" then set theBody to theBody & linefeed & linefeed & pdfText
    				set end of recordList to {msgID:mid, dateVal:theDate, theSender:theSender, theSubject:theSubject, theBody:theBody}
    			end if
    		end repeat
    	end tell
    	return recordList
    end collectMessageData
    -- Insertion sort, ascending by date. Pure AppleScript, no Apple events.
    -- Uses an explicit nested test (not `and`) so item j is never read when j < 1.
    on sortRecordsByDate(recordList)
    	set n to count of recordList
    	if n < 2 then return recordList
    	repeat with i from 2 to n
    		set curRec to item i of recordList
    		set curDate to dateVal of curRec
    		set j to i - 1
    		set placed to false
    		repeat while j ≥ 1 and (not placed)
    			if (dateVal of (item j of recordList)) > curDate then
    				set item (j + 1) of recordList to item j of recordList
    				set j to j - 1
    			else
    				set placed to true
    			end if
    		end repeat
    		set item (j + 1) of recordList to curRec
    	end repeat
    	return recordList
    end sortRecordsByDate
    -- =============================
    -- Gemini call via NSURLConnection (synchronous, sandbox-safe, no curl dependency)
    -- =============================
    on callGeminiAPI(apiKey, sysInstruction, promptText, modelName)
    	-- Build request JSON with Cocoa (safe escaping)
    	set dict to current application's NSMutableDictionary's dictionary()
    	-- System instruction (separate from user content)
    	set sysPartsArr to current application's NSMutableArray's array()
    	set sysPartDict to current application's NSMutableDictionary's dictionary()
    	sysPartDict's setObject:sysInstruction forKey:"text"
    	sysPartsArr's addObject:sysPartDict
    	set sysDict to current application's NSMutableDictionary's dictionary()
    	sysDict's setObject:sysPartsArr forKey:"parts"
    	dict's setObject:sysDict forKey:"system_instruction"
    	-- User content
    	set contentsArr to current application's NSMutableArray's array()
    	set partsArr to current application's NSMutableArray's array()
    	set partDict to current application's NSMutableDictionary's dictionary()
    	partDict's setObject:promptText forKey:"text"
    	partsArr's addObject:partDict
    	set contentDict to current application's NSMutableDictionary's dictionary()
    	contentDict's setObject:"user" forKey:"role"
    	contentDict's setObject:partsArr forKey:"parts"
    	contentsArr's addObject:contentDict
    	dict's setObject:contentsArr forKey:"contents"
    	-- Generation config
    	set genCfg to current application's NSMutableDictionary's dictionary()
    	genCfg's setObject:(current application's NSNumber's numberWithDouble:1.0) forKey:"temperature"
    	genCfg's setObject:(current application's NSNumber's numberWithInteger:16384) forKey:"maxOutputTokens"
    	dict's setObject:genCfg forKey:"generationConfig"
    	set jsonData to current application's NSJSONSerialization's dataWithJSONObject:dict options:0 |error|:(missing value)
    	-- Build NSURLRequest
    	set endpointStr to "https://generativelanguage.googleapis.com/v1beta/models/" & modelName & ":generateContent"
    	set reqURL to current application's NSURL's URLWithString:endpointStr
    	set urlRequest to current application's NSMutableURLRequest's requestWithURL:reqURL
    	urlRequest's setHTTPMethod:"POST"
    	urlRequest's setHTTPBody:jsonData
    	urlRequest's setValue:"application/json" forHTTPHeaderField:"Content-Type"
    	urlRequest's setValue:apiKey forHTTPHeaderField:"x-goog-api-key"
    	urlRequest's setTimeoutInterval:requestTimeoutSecs
    	-- Synchronous send via NSURLConnection (native macOS networking, works inside Drafts' host)
    	set {respData, urlResponse, respError} to current application's NSURLConnection's sendSynchronousRequest:urlRequest returningResponse:(reference) |error|:(reference)
    	if respError is not missing value then
    		set errDesc to (respError's localizedDescription()) as text
    		my showAlert("Network Error", "Failed to reach Gemini API:" & linefeed & errDesc)
    		return {success:false, body:""}
    	end if
    	if respData is missing value then
    		my showAlert("Network Error", "Gemini API returned no data.")
    		return {success:false, body:""}
    	end if
    	-- Parse response JSON
    	set respObj to current application's NSJSONSerialization's JSONObjectWithData:respData options:0 |error|:(missing value)
    	if respObj is missing value then
    		set rawResp to (current application's NSString's alloc()'s initWithData:respData encoding:(current application's NSUTF8StringEncoding)) as text
    		my showAlert("Gemini Error", "Could not parse API response as JSON." & linefeed & linefeed & "Raw response:" & linefeed & rawResp)
    		return {success:false, body:""}
    	end if
    	-- Check for API-level error object
    	set errObj to respObj's objectForKey:"error"
    	if errObj is not missing value then
    		set errMessage to (errObj's objectForKey:"message") as text
    		my showAlert("Gemini API Error", errMessage)
    		return {success:false, body:""}
    	end if
    	set candidates to respObj's objectForKey:"candidates"
    	if (candidates = missing value) or ((candidates's |count|()) = 0) then
    		set rawResp to (current application's NSString's alloc()'s initWithData:respData encoding:(current application's NSUTF8StringEncoding)) as text
    		my showAlert("Gemini Error", "Gemini returned no candidates." & linefeed & linefeed & "Response:" & linefeed & rawResp)
    		return {success:false, body:""}
    	end if
    	set firstCand to candidates's objectAtIndex:0
    	-- Check for content-filtering block (finishReason != "STOP")
    	set finishReason to firstCand's objectForKey:"finishReason"
    	if finishReason is not missing value then
    		set frText to finishReason as text
    		if frText is not "STOP" and frText is not "MAX_TOKENS" then
    			my showAlert("Gemini Blocked", "Response was blocked by content filter." & linefeed & "Reason: " & frText)
    			return {success:false, body:""}
    		end if
    	end if
    	set contentDict2 to firstCand's objectForKey:"content"
    	if contentDict2 is missing value then
    		my showAlert("Gemini Error", "Gemini candidate has no content object.")
    		return {success:false, body:""}
    	end if
    	set partsArray2 to contentDict2's objectForKey:"parts"
    	if (partsArray2 is missing value) or ((partsArray2's |count|()) = 0) then
    		my showAlert("Gemini Error", "Gemini returned no text parts.")
    		return {success:false, body:""}
    	end if
    	set outText to ""
    	repeat with i from 0 to ((partsArray2's |count|()) - 1)
    		set p to (partsArray2's objectAtIndex:i)
    		set t to p's objectForKey:"text"
    		if t is not missing value then set outText to outText & (t as text)
    	end repeat
    	-- Guard against valid JSON but empty actual text
    	if (my trim(outText)) is "" then
    		my showAlert("Gemini Error", "Gemini returned parts but no text content." & linefeed & linefeed & "This may indicate the model filtered the response or changed its output format.")
    		return {success:false, body:""}
    	end if
    	return {success:true, body:outText}
    end callGeminiAPI
    -- =============================
    -- Drafts Action Entry Point
    -- =============================
    on execute(d)
    	try
    		set allRelated to {}
    		set sel to {}
    		with timeout of 600 seconds
    			tell application "Mail"
    				if not (exists message viewer 1) then
    					my showAlert("No message viewer", "Open Mail and select one or more messages.")
    					return ""
    				end if
    				set sel to (selected messages of message viewer 1)
    				if sel is {} then
    					my showAlert("No email selected", "Please select an email (or multiple emails) in the viewer.")
    					return ""
    				end if
    				-- Collect related messages for each selection (subject-normalized)
    				repeat with baseMsg in sel
    					set subjRaw to subject of baseMsg
    					set subjCore to my normalizeSubject(subjRaw)
    					set matches to (messages of message viewer 1 whose subject contains subjCore)
    					repeat with m in matches
    						set end of allRelated to m
    					end repeat
    				end repeat
    			end tell
    		end timeout
    		-- Read each message once (deduped, with PDF text), then sort with no further Mail calls
    		set msgRecords to my collectMessageData(allRelated)
    		set msgRecords to my sortRecordsByDate(msgRecords)
    		-- Cap to the most recent N to keep prompts manageable
    		set totalCount to (count of msgRecords)
    		if totalCount > maxMessagesPerThread then
    			set startIndex to (totalCount - maxMessagesPerThread + 1)
    			set msgRecords to items startIndex thru totalCount of msgRecords
    		end if
    		-- Build the LLM thread and a deterministic, Markdown-safe source-link list, in one pass.
    		set threadContent to ""
    		set sourceLinks to ""
    		set recCount to (count of msgRecords)
    		repeat with k from 1 to recCount
    			set rec to item k of msgRecords
    			set theDate to dateVal of rec
    			set ds to (date string of theDate)
    			set ts to (time string of theDate)
    			set threadContent to threadContent & "From: " & (theSender of rec) & " / Subject: " & (theSubject of rec) & " / Date: " & ds & " " & ts & linefeed & (theBody of rec) & linefeed & linefeed & "---" & linefeed & linefeed
    			set lbl to my sanitizeLinkLabel((theSender of rec) & ", " & ds & " " & ts)
    			set sourceLinks to sourceLinks & "- [" & lbl & "](" & my messageURL(msgID of rec) & ")" & linefeed
    		end repeat
    		-- Validate we actually got email content
    		if (my trim(threadContent)) is "" then
    			my showAlert("Empty Thread", "No email content could be extracted from the selected messages.")
    			return ""
    		end if
    		-- Build the user prompt: context first, instruction last
    		set fullPrompt to userPromptIntro & linefeed & threadContent & linefeed & userPromptOutro
    		set geminiAPIKey to my getAPIKeyFromKeychain(geminiAPIKeyName)
    		if geminiAPIKey is missing value then
    			my showAlert("API Key Not Found", "Store your Gemini API Key in Keychain with the service name '" & geminiAPIKeyName & "'.")
    			return ""
    		end if
    		-- Single API call
    		set apiResult to my callGeminiAPI(geminiAPIKey, systemInstruction, fullPrompt, geminiModel)
    		if success of apiResult is false then return ""
    		set rawOutput to body of apiResult
    		-- Strip code fences if the model wrapped its output, normalize line endings
    		set cleanOutput to my stripCodeFences(rawOutput)
    		set fullContent to my replace_chars(cleanOutput, return, linefeed)
    		-- Append the deterministic source links (never routed through the model)
    		if (my trim(sourceLinks)) is not "" then
    			set fullContent to fullContent & linefeed & linefeed & "### SOURCE MESSAGES" & linefeed & linefeed & sourceLinks
    		end if
    		-- Create the draft directly via Drafts' AppleScript dictionary (no URL-length limit)
    		tell application "Drafts"
    			make new draft with properties {content:fullContent, flagged:false, tag list:draftsTags}
    		end tell
    		return ""
    	on error errMsg number errNum
    		my showAlert("Error", ("An error occurred: " & errMsg & " (" & errNum & ")"))
    		return ""
    	end try
    end execute
  • configure

    draftList
    noChange
    actionList
    noChange
    actionBar
    noChange
    tagEntry
    noChange
    loadActionGroup
    loadActionBarGroup
    loadWorkspace
    Call Sheets
    linksEnabled
    noChange
    pinningEnabled
    noChange

Options

  • After Success Default
    Notification Info
    Log Level Info
Items available in the Drafts Directory are uploaded by community members. Use appropriate caution reviewing downloaded items before use.