on tabGroups()
-- Level-order search, returning every tab group at the shallowest depth that
-- has one (the chat strip and the terminal strip are siblings). Bounded: the
-- pane sits ~5 levels down, and we must never descend into the transcript.
my requireWindow()
tell application "System Events" to tell process "Conductor"
set level to {window 1}
set depth to 0
repeat while (count of level) > 0 and depth < 8
set found to {}
set nextLevel to {}
repeat with entry in level
set node to contents of entry
try
set found to found & (get UI elements of node whose role is "AXTabGroup")
set nextLevel to nextLevel & (get UI elements of node)
end try
end repeat
if (count of found) > 0 then return found
set level to nextLevel
set depth to depth + 1
end repeat
end tell
return {}
end tabGroups
on stripRadios(tg)
-- Every radio button in a tab strip, whether it is a direct child or sits one
-- group deep, in tree order. Both shapes are live at once: the terminal strip's
-- tabs are direct children, while a chat tab is wrapped in its own AXGroup with
-- the file-view toggle beside it as a plain child. Reading only the direct ones
-- (and stopping as soon as any turned up) is what made a chat strip look like a
-- one-tab strip called "Close file view".
tell application "System Events" to tell process "Conductor"
set acc to {}
repeat with k in (UI elements of tg)
set node to contents of k
if (role of node) is "AXRadioButton" then
set end of acc to node
else
try
set acc to acc & (get UI elements of node whose role is "AXRadioButton")
end try
end if
end repeat
return acc
end tell
end stripRadios
on chatTabs(tg)
-- Of those, a chat tab is the one whose accessible name is its own close action,
-- "Close chat
". The strip's other radio button is "Close file view",
-- which is not a chat and must not shift the indices this file addresses tabs
-- by — off by one is how a prompt lands in the wrong conversation. Falls back to
-- every radio button when none carries the prefix, so a renamed label costs the
-- filter rather than emptying the strip.
set radios to my stripRadios(tg)
set chats to {}
repeat with entry in radios
set node to contents of entry
if (my axName(node)) starts with "Close chat" then set end of chats to node
end repeat
if (count of chats) > 0 then return chats
return radios
end chatTabs
on tabLabel(t)
tell application "System Events" to tell process "Conductor"
return (name of t) as text
end tell
end tabLabel
on pickChatStrip(strips, wantCount, wantTitle)
-- Score each candidate strip: a label match outweighs a tab-count match, and
-- a tie means we cannot tell the chat strip from the terminal strip.
set best to missing value
set bestTabs to {}
set bestScore to 0
set tied to false
repeat with entry in strips
set tg to contents of entry
set strip to my chatTabs(tg)
set score to 0
if wantCount > 0 and (count of strip) is wantCount then set score to score + 1
if wantTitle is not "" then
repeat with t in strip
if (my tabLabel(t)) contains wantTitle then
set score to score + 2
exit repeat
end if
end repeat
end if
if score > bestScore then
set bestScore to score
set best to tg
set bestTabs to strip
set tied to false
else if score is bestScore and score > 0 then
set tied to true
end if
end repeat
if bestScore is 0 then error "couldn't identify the chat tab strip"
if tied then error "can't tell which tab strip holds the target chat"
return {best, bestTabs}
end pickChatStrip
on lastPathSegment(s)
set saved to AppleScript's text item delimiters
set AppleScript's text item delimiters to "/"
set parts to text items of s
set AppleScript's text item delimiters to saved
return item -1 of parts
end lastPathSegment
on paneLabels(tg, wantRole)
-- Kept out of the caller's scope on purpose: inside a System Events tell,
-- ordinary-looking names (tabs, count) resolve as app terms instead of vars.
tell application "System Events" to tell process "Conductor"
set pane to value of attribute "AXParent" of tg
return (name of (UI elements of pane whose role is wantRole))
end tell
end paneLabels
on anyContains(haystack, needle)
repeat with entry in haystack
try
if (entry as text) contains needle then return true
end try
end repeat
return false
end anyContains
on assertWorkspace(tg)
-- The pane holding the chat strip also labels the open workspace: an
-- AXStaticText with the branch (sans owner prefix) and a repo popup button.
set wantBranch to system attribute "RELAY_WS_BRANCH"
if wantBranch is "" then return
set tail to my lastPathSegment(wantBranch)
if not (my anyContains(my paneLabels(tg, "AXStaticText"), tail)) then
error "the palette didn't land on " & tail
end if
set wantRepo to system attribute "RELAY_WS_REPO"
if wantRepo is not "" then
if not (my anyContains(my paneLabels(tg, "AXPopUpButton"), wantRepo)) then
error "the palette didn't land in " & wantRepo
end if
end if
end assertWorkspace