conductor-remote 1.116.0 → 1.118.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -8
- package/dist/assets/{PierrePatch-BPp5Cf-p.js → PierrePatch-D4YbKD8Q.js} +1 -1
- package/dist/assets/index-BRzsSW8g.css +1 -0
- package/dist/assets/index-BaS6qXUW.js +65 -0
- package/dist/index.html +2 -2
- package/dist/push-sw.js +2 -2
- package/dist/sw.js +1 -1
- package/dist-node/scripts/nosleep-setup.js +2 -2
- package/dist-node/scripts/nosleep.js +3 -3
- package/dist-node/scripts/service/commands.js +32 -0
- package/dist-node/scripts/service/configuration.js +205 -0
- package/dist-node/scripts/service/environment.js +59 -0
- package/dist-node/scripts/service/flags.js +45 -0
- package/dist-node/scripts/service/installation.js +91 -0
- package/dist-node/scripts/service/launch-agent.js +134 -0
- package/dist-node/scripts/service/network.js +244 -0
- package/dist-node/scripts/service/presentation.js +101 -0
- package/dist-node/scripts/service/voice.js +113 -0
- package/dist-node/scripts/service.js +5 -980
- package/dist-node/src/{agent-config.js → agents/agent-config.js} +1 -1
- package/dist-node/src/{model-cache.js → agents/model-cache.js} +1 -1
- package/dist-node/src/{roles.js → agents/roles.js} +3 -3
- package/dist-node/src/db.js +1 -1
- package/dist-node/src/{parked.js → delivery/parked.js} +1 -1
- package/dist-node/src/{dev-server.js → dev-server/controller.js} +8 -291
- package/dist-node/src/dev-server/ports.js +63 -0
- package/dist-node/src/dev-server/processes.js +73 -0
- package/dist-node/src/dev-server/proxy.js +158 -0
- package/dist-node/src/{run-activity.js → dev-server/run-activity.js} +2 -2
- package/dist-node/src/dev-server/types.js +1 -0
- package/dist-node/src/{file-preview.js → files/file-preview.js} +1 -1
- package/dist-node/src/{change-stats.js → git/change-stats.js} +1 -1
- package/dist-node/src/{git.js → git/diff.js} +1 -1
- package/dist-node/src/{autoupdate.js → host/autoupdate.js} +1 -1
- package/dist-node/src/{funnel-watchdog.js → host/funnel-watchdog.js} +4 -4
- package/dist-node/src/{logbuf.js → host/logbuf.js} +1 -1
- package/dist-node/src/{nosleep-helper.js → host/nosleep-helper.js} +1 -1
- package/dist-node/src/{tailscale.js → host/tailscale.js} +1 -1
- package/dist-node/src/http/router-types.js +2 -0
- package/dist-node/src/http/router.js +78 -0
- package/dist-node/src/http/routes/create-workspace.js +90 -0
- package/dist-node/src/http/routes/files.js +53 -0
- package/dist-node/src/http/routes/prompts.js +363 -0
- package/dist-node/src/http/routes/sessions.js +279 -0
- package/dist-node/src/http/routes/state.js +186 -0
- package/dist-node/src/http/routes/system.js +220 -0
- package/dist-node/src/http/routes/voice.js +117 -0
- package/dist-node/src/http/routes/workflows.js +248 -0
- package/dist-node/src/http/routes/workspaces.js +267 -0
- package/dist-node/src/http/services/base.js +252 -0
- package/dist-node/src/http/services/delegations.js +301 -0
- package/dist-node/src/http/services/delivery.js +433 -0
- package/dist-node/src/http/services/files.js +199 -0
- package/dist-node/src/http/services/mcp.js +94 -0
- package/dist-node/src/http/services/responses.js +120 -0
- package/dist-node/src/http/services/voice.js +147 -0
- package/dist-node/src/http/services/workflow-probes.js +215 -0
- package/dist-node/src/http/services/workflow-state.js +195 -0
- package/dist-node/src/http/services/workflow.js +366 -0
- package/dist-node/src/http/services.js +35 -0
- package/dist-node/src/mcp/arguments.js +14 -0
- package/dist-node/src/mcp/dispatcher.js +56 -0
- package/dist-node/src/mcp/formatters.js +110 -0
- package/dist-node/src/mcp/protocol.js +7 -0
- package/dist-node/src/mcp/registry.js +66 -0
- package/dist-node/src/mcp/tools/agent-options.js +122 -0
- package/dist-node/src/mcp/tools/chat-actions.js +194 -0
- package/dist-node/src/mcp/tools/chats.js +194 -0
- package/dist-node/src/mcp/tools/delegation.js +135 -0
- package/dist-node/src/mcp/tools/system.js +135 -0
- package/dist-node/src/mcp/tools/voice.js +136 -0
- package/dist-node/src/mcp/tools/workspaces.js +259 -0
- package/dist-node/src/mcp/types.js +1 -0
- package/dist-node/src/mcp.js +6 -4
- package/dist-node/src/{notify.js → notifications/notify.js} +5 -5
- package/dist-node/src/{webpush.js → notifications/webpush.js} +1 -1
- package/dist-node/src/orchestration/delegation/codec.js +201 -0
- package/dist-node/src/{delegation-intake.js → orchestration/delegation/intake.js} +2 -2
- package/dist-node/src/{delegation-prompt.js → orchestration/delegation/prompt.js} +1 -1
- package/dist-node/src/orchestration/delegation/queue.js +255 -0
- package/dist-node/src/orchestration/delegation/store.js +111 -0
- package/dist-node/src/orchestration/delegation/types.js +2 -0
- package/dist-node/src/orchestration/persistence/capabilities.js +109 -0
- package/dist-node/src/orchestration/persistence/codecs.js +148 -0
- package/dist-node/src/orchestration/persistence/connection.js +205 -0
- package/dist-node/src/orchestration/persistence/db.js +101 -0
- package/dist-node/src/orchestration/persistence/effect-recovery.js +227 -0
- package/dist-node/src/orchestration/persistence/effect-transitions.js +157 -0
- package/dist-node/src/orchestration/persistence/effects.js +109 -0
- package/dist-node/src/orchestration/persistence/errors.js +39 -0
- package/dist-node/src/orchestration/persistence/events.js +29 -0
- package/dist-node/src/orchestration/persistence/idempotency.js +82 -0
- package/dist-node/src/orchestration/persistence/job-attempts.js +114 -0
- package/dist-node/src/orchestration/persistence/jobs.js +159 -0
- package/dist-node/src/orchestration/persistence/projections.js +190 -0
- package/dist-node/src/orchestration/persistence/quarantine.js +46 -0
- package/dist-node/src/orchestration/persistence/records.js +115 -0
- package/dist-node/src/orchestration/persistence/relays.js +78 -0
- package/dist-node/src/orchestration/persistence/runs.js +156 -0
- package/dist-node/src/{orchestration-schema.js → orchestration/persistence/schema.js} +1 -1
- package/dist-node/src/orchestration/persistence/types.js +1 -0
- package/dist-node/src/orchestration/persistence/ui-lease.js +197 -0
- package/dist-node/src/orchestration/persistence/values.js +114 -0
- package/dist-node/src/orchestration/workflow/commands.js +309 -0
- package/dist-node/src/orchestration/workflow/coordinator.js +28 -0
- package/dist-node/src/orchestration/workflow/effect-recovery.js +125 -0
- package/dist-node/src/orchestration/workflow/effects.js +307 -0
- package/dist-node/src/orchestration/workflow/errors.js +13 -0
- package/dist-node/src/orchestration/workflow/helpers.js +154 -0
- package/dist-node/src/orchestration/workflow/job-dispatch.js +312 -0
- package/dist-node/src/orchestration/workflow/job-results.js +280 -0
- package/dist-node/src/{workflow.js → orchestration/workflow/prompts.js} +2 -2
- package/dist-node/src/orchestration/workflow/root.js +293 -0
- package/dist-node/src/orchestration/workflow/start.js +99 -0
- package/dist-node/src/orchestration/workflow/state.js +105 -0
- package/dist-node/src/orchestration/workflow/types.js +1 -0
- package/dist-node/src/orchestration/workflow/wake.js +164 -0
- package/dist-node/src/prefs.js +1 -1
- package/dist-node/src/{background-tasks.js → reads/background-tasks.js} +1 -1
- package/dist-node/src/reads/messages.js +362 -0
- package/dist-node/src/reads/repository.js +87 -0
- package/dist-node/src/reads/sessions.js +165 -0
- package/dist-node/src/reads/types.js +1 -0
- package/dist-node/src/reads/workspaces.js +230 -0
- package/dist-node/src/reads/worktrees.js +45 -0
- package/dist-node/src/routes.js +5 -5
- package/dist-node/src/{search.js → search/coordinator.js} +4 -4
- package/dist-node/src/{search-worker.js → search/worker.js} +3 -3
- package/dist-node/src/server.js +14 -3930
- package/dist-node/src/settings.js +1 -1
- package/dist-node/src/shared.js +130 -9
- package/dist-node/src/{transcript.js → transcript/parser.js} +4 -124
- package/dist-node/src/{tool-usage-worker.js → usage/tool-usage-worker.js} +1 -1
- package/dist-node/src/{tool-usage.js → usage/tool-usage.js} +1 -1
- package/dist-node/src/voice/brief.js +22 -50
- package/dist-node/src/voice/context.js +1 -1
- package/dist-node/src/voice/dates.js +38 -0
- package/dist-node/src/voice/history.js +14 -5
- package/dist-node/src/voice/prompt.js +10 -9
- package/dist-node/src/voice/recall.js +115 -0
- package/dist-node/src/{speech.js → voice/speech.js} +1 -1
- package/dist-node/src/voice/tools.js +92 -3
- package/dist-node/src/voice/webrtc.js +1 -1
- package/dist-node/src/wire.js +2 -2
- package/dist-node/src/writes/actuator.js +124 -0
- package/dist-node/src/writes/agent-options.js +151 -0
- package/dist-node/src/writes/applescript/agent-options.applescript +334 -0
- package/dist-node/src/writes/applescript/chat-targeting.applescript +149 -0
- package/dist-node/src/writes/applescript/composer.applescript +195 -0
- package/dist-node/src/writes/applescript/hotspot.applescript +155 -0
- package/dist-node/src/writes/applescript/lifecycle.applescript +266 -0
- package/dist-node/src/writes/applescript/run-tasks.applescript +208 -0
- package/dist-node/src/writes/applescript/source.js +52 -0
- package/dist-node/src/writes/applescript/window.applescript +436 -0
- package/dist-node/src/writes/applescript/workspace-status.applescript +177 -0
- package/dist-node/src/writes/applescript/workspace-targeting.applescript +359 -0
- package/dist-node/src/writes/chats.js +160 -0
- package/dist-node/src/writes/guards.js +77 -0
- package/dist-node/src/writes/runner.js +67 -0
- package/dist-node/src/{sidecar.js → writes/sidecar.js} +1 -1
- package/dist-node/src/writes/system.js +81 -0
- package/dist-node/src/writes/targeting.js +121 -0
- package/dist-node/src/writes/types.js +1 -0
- package/dist-node/src/writes/ui-lock.js +229 -0
- package/dist-node/src/writes/workspaces.js +233 -0
- package/docs/voice-setup.md +34 -12
- package/package.json +2 -2
- package/dist/assets/index-DNLdIfZ2.css +0 -1
- package/dist/assets/index-QJR7tn2I.js +0 -61
- package/dist-node/src/conductor.applescript +0 -2279
- package/dist-node/src/delegations.js +0 -557
- package/dist-node/src/mcp-tools.js +0 -1319
- package/dist-node/src/orchestration-db.js +0 -2186
- package/dist-node/src/reads.js +0 -787
- package/dist-node/src/workflow-coordinator.js +0 -2119
- package/dist-node/src/writes.js +0 -1223
- /package/dist-node/src/{conductor-settings.js → agents/conductor-settings.js} +0 -0
- /package/dist-node/src/{firstprompt.js → delivery/firstprompt.js} +0 -0
- /package/dist-node/src/{sendonce.js → delivery/sendonce.js} +0 -0
- /package/dist-node/src/{preview-urls.js → dev-server/preview-urls.js} +0 -0
- /package/dist-node/src/{run-configs.js → dev-server/run-configs.js} +0 -0
- /package/dist-node/src/{attachments.js → files/attachments.js} +0 -0
- /package/dist-node/src/{icons.js → files/icons.js} +0 -0
- /package/dist-node/src/{staged-attachments.js → files/staged-attachments.js} +0 -0
- /package/dist-node/src/{fork-workspace.js → git/fork-workspace.js} +0 -0
- /package/dist-node/src/{merge.js → git/merge.js} +0 -0
- /package/dist-node/src/{pr.js → git/pr.js} +0 -0
- /package/dist-node/src/{nosleep.js → host/nosleep.js} +0 -0
- /package/dist-node/src/{relay-processes.js → host/relay-processes.js} +0 -0
- /package/dist-node/src/{ui-lease-watchdog.js → host/ui-lease-watchdog.js} +0 -0
- /package/dist-node/src/{wifi.js → host/wifi.js} +0 -0
- /package/dist-node/src/{orchestration-schema.generated.js → orchestration/persistence/schema.generated.js} +0 -0
- /package/dist-node/src/{workflow-effect-runner.js → orchestration/workflow/effect-runner.js} +0 -0
- /package/dist-node/src/{workflow-http.js → orchestration/workflow/http.js} +0 -0
- /package/dist-node/src/{workflow-machine.js → orchestration/workflow/machine.js} +0 -0
- /package/dist-node/src/{session-poller.js → reads/session-poller.js} +0 -0
- /package/dist-node/src/{context-breakdown.js → transcript/context-breakdown.js} +0 -0
- /package/dist-node/src/{chat-cursor.js → transcript/cursor.js} +0 -0
- /package/dist-node/src/{plan-usage.js → usage/plan-usage.js} +0 -0
- /package/dist-node/src/{tool-usage-service.js → usage/tool-usage-service.js} +0 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
(*
|
|
2
|
+
Everything the relay drives Conductor's UI with, in one place.
|
|
3
|
+
|
|
4
|
+
Loaded verbatim by src/writes.ts, which appends a few lines naming the
|
|
5
|
+
handlers to run and hands the result to `osascript`. It lives in its own file
|
|
6
|
+
rather than a TypeScript template literal for three reasons: an editor can
|
|
7
|
+
highlight it, `yarn verify` can compile-check it (scripts/check-applescript.ts,
|
|
8
|
+
the repository validator for this language), and a backtick in a comment
|
|
9
|
+
no longer terminates the string it used to sit inside — so this sentence,
|
|
10
|
+
which contains `one`, is now harmless.
|
|
11
|
+
|
|
12
|
+
The target rides in on the environment (RELAY_WS_*, RELAY_TAB_*, RELAY_SET_*,
|
|
13
|
+
RELAY_PROMPT_FILE) rather than being interpolated, which keeps AppleScript
|
|
14
|
+
string escaping out of the picture entirely. The three matching labels use a
|
|
15
|
+
UTF-8 temporary file because `system attribute` decodes text as MacRoman.
|
|
16
|
+
|
|
17
|
+
Two traps to respect when editing:
|
|
18
|
+
|
|
19
|
+
- Inside a `tell application "System Events"` block, ordinary-looking variable
|
|
20
|
+
names resolve as System Events terms. `tabs` and `groups` become "every
|
|
21
|
+
tab/group of the process", so a loop over your own list quietly iterates the
|
|
22
|
+
wrong thing and the handler returns nothing. Keep logic outside the tell and
|
|
23
|
+
reach in through one-line helpers, or name the variables `strip` and `pane`.
|
|
24
|
+
- Every read is rooted at `window 1`, and Conductor keeps running with its
|
|
25
|
+
windows closed. Go through `requireWindow` / `webArea` so a window that
|
|
26
|
+
closed mid-run says so in words.
|
|
27
|
+
|
|
28
|
+
How the chat targeting works, and why each step is here:
|
|
29
|
+
|
|
30
|
+
`openViaDeepLink` normally selects the chat already, by id. The handlers below
|
|
31
|
+
are what confirm that, and what still does the work for the fallback focus
|
|
32
|
+
paths, which only reach the right workspace. A workspace holds several chats
|
|
33
|
+
and Conductor keeps typing in whichever tab is already active, so a send
|
|
34
|
+
addressed to a non-active chat would land in the wrong agent.
|
|
35
|
+
|
|
36
|
+
Conductor's webview exposes its whole tree through macOS Accessibility. The
|
|
37
|
+
chat strip is an AXTabGroup whose AXRadioButtons are the tabs (AXValue marks
|
|
38
|
+
the selected one, AXPress switches to it and does *not* close the chat). The
|
|
39
|
+
strip's order matches reads.listSessions (created_at ASC), so the caller
|
|
40
|
+
addresses a tab by 1-based index and we cross-check the label.
|
|
41
|
+
|
|
42
|
+
Three traps that targeting has to survive:
|
|
43
|
+
|
|
44
|
+
- The tabs are not the strip's direct children. Each sits one AXGroup deep and
|
|
45
|
+
is named for its own close action ("Close chat <title>"), while a "Close file
|
|
46
|
+
view" radio button, which is not a chat, *is* a direct child. Reading the
|
|
47
|
+
direct children and stopping there made every chat strip look like a one-tab
|
|
48
|
+
strip called "Close file view", which then lost the scoring below to the
|
|
49
|
+
terminal panel and failed every send with "chat tab 1 not found". So
|
|
50
|
+
`stripRadios` reads both levels and `chatTabs` keeps the "Close chat" ones.
|
|
51
|
+
- The terminal panel is an AXTabGroup too (radio buttons named Setup / Run /
|
|
52
|
+
Terminal 1), a sibling of the chat strip in the same pane. Picking "the first
|
|
53
|
+
tab group" would sometimes press a terminal tab, so candidates are *scored*
|
|
54
|
+
on tab count plus label and a tie refuses to act.
|
|
55
|
+
- The palette can land on the wrong workspace (a loose query matches a command;
|
|
56
|
+
a deleted branch opens a modal). The pane header carries the branch and repo,
|
|
57
|
+
so we read them back and bail if they disagree.
|
|
58
|
+
|
|
59
|
+
Target is read from RELAY_TAB_{INDEX,COUNT,TITLE} and RELAY_WS_{BRANCH,REPO};
|
|
60
|
+
index 0 disables the tab step. Every failure path errors out so the caller
|
|
61
|
+
aborts *before* typing — landing in the wrong chat is worse than not sending.
|
|
62
|
+
*)
|
|
63
|
+
|
|
64
|
+
on splitLines(s)
|
|
65
|
+
-- `do shell script` converts the file's LF separators to AppleScript returns.
|
|
66
|
+
-- `paragraphs` handles that form and keeps empty fields between separators.
|
|
67
|
+
return paragraphs of s
|
|
68
|
+
end splitLines
|
|
69
|
+
|
|
70
|
+
-- The target file's first two lines are tab title and palette query. The
|
|
71
|
+
-- remaining lines are sidebar-title candidates. The path is ASCII; the file
|
|
72
|
+
-- contents reach AppleScript through `do shell script` as UTF-8.
|
|
73
|
+
on targetField(fieldIndex)
|
|
74
|
+
set fields to my splitLines(do shell script "cat " & quoted form of (system attribute "RELAY_TARGET_FILE"))
|
|
75
|
+
if fieldIndex > (count of fields) then return ""
|
|
76
|
+
return item fieldIndex of fields
|
|
77
|
+
end targetField
|
|
78
|
+
|
|
79
|
+
on targetSidebarTitles()
|
|
80
|
+
set fields to my splitLines(do shell script "cat " & quoted form of (system attribute "RELAY_TARGET_FILE"))
|
|
81
|
+
if (count of fields) < 3 then return {}
|
|
82
|
+
set titles to {}
|
|
83
|
+
repeat with fieldIndex from 3 to (count of fields)
|
|
84
|
+
set end of titles to item fieldIndex of fields
|
|
85
|
+
end repeat
|
|
86
|
+
return titles
|
|
87
|
+
end targetSidebarTitles
|
|
88
|
+
|
|
89
|
+
on windowProbe()
|
|
90
|
+
-- One read, three outcomes, none of them guessed: {count, errNum, errText}.
|
|
91
|
+
-- count is -1 when macOS refused the read at all. Everything that wants to
|
|
92
|
+
-- know about windows goes through here, because "returned 0" and "refused to
|
|
93
|
+
-- answer" are different facts and only this handler still has both.
|
|
94
|
+
-- The timeout is load-bearing: a wedged Conductor (seen live after a restart
|
|
95
|
+
-- collided with a relaunch) answers deep links but hangs every AX read, and
|
|
96
|
+
-- without a cap this call blocks until the node-side ceiling kills the whole
|
|
97
|
+
-- run as "took too long" — 28s of silence instead of a named cause. 6s turns
|
|
98
|
+
-- that into error -1712, which refusalReason maps to words.
|
|
99
|
+
try
|
|
100
|
+
with timeout of 6 seconds
|
|
101
|
+
tell application "System Events" to tell process "Conductor"
|
|
102
|
+
set winCount to (count of windows)
|
|
103
|
+
end tell
|
|
104
|
+
end timeout
|
|
105
|
+
return {winCount, 0, ""}
|
|
106
|
+
on error errText number errNum
|
|
107
|
+
return {-1, errNum, errText}
|
|
108
|
+
end try
|
|
109
|
+
end windowProbe
|
|
110
|
+
|
|
111
|
+
on hasWindow()
|
|
112
|
+
-- Also false when the process is gone or macOS refused: callers that only
|
|
113
|
+
-- need a boolean keep getting one. windowFailure() is what tells them why.
|
|
114
|
+
return (item 1 of my windowProbe()) > 0
|
|
115
|
+
end hasWindow
|
|
116
|
+
|
|
117
|
+
on refusalReason(probe)
|
|
118
|
+
-- macOS's own words, matched on the error it actually returned rather than on
|
|
119
|
+
-- a capability flag we hope is accurate. "UI elements enabled" was the earlier
|
|
120
|
+
-- attempt at this and is not trustworthy — it can report true for a process
|
|
121
|
+
-- that is not itself trusted, and then the refusal reappears downstream as
|
|
122
|
+
-- "no open window". These strings are the two grants the write path needs.
|
|
123
|
+
set errNum to item 2 of probe
|
|
124
|
+
set errText to item 3 of probe
|
|
125
|
+
if errText contains "assistive access" then return "the relay is not trusted for Accessibility - grant it to the node binary running the relay in System Settings > Privacy & Security > Accessibility, then restart the relay (upgrading node silently revokes it). macOS said: " & errText
|
|
126
|
+
if errNum is -1743 or errText contains "Not authorized" then return "macOS blocked the relay from controlling the UI - grant Automation permission to the node binary running the relay in System Settings > Privacy & Security > Automation. macOS said: " & errText
|
|
127
|
+
if errNum is -1712 then return "Conductor stopped answering UI reads (the Accessibility query timed out) - the app looks wedged even though it may still process deep links. Force-quit Conductor on your Mac and open it again; if it repeats with a healthy Conductor, System Events is stuck - run: killall 'System Events'"
|
|
128
|
+
return ""
|
|
129
|
+
end refusalReason
|
|
130
|
+
|
|
131
|
+
on conductorRunning()
|
|
132
|
+
try
|
|
133
|
+
tell application "System Events" to return (exists process "Conductor")
|
|
134
|
+
on error
|
|
135
|
+
return false
|
|
136
|
+
end try
|
|
137
|
+
end conductorRunning
|
|
138
|
+
|
|
139
|
+
on joinList(lst, sep)
|
|
140
|
+
set saved to AppleScript's text item delimiters
|
|
141
|
+
set AppleScript's text item delimiters to sep
|
|
142
|
+
set out to lst as text
|
|
143
|
+
set AppleScript's text item delimiters to saved
|
|
144
|
+
return out
|
|
145
|
+
end joinList
|
|
146
|
+
|
|
147
|
+
on processWindowCounts()
|
|
148
|
+
-- Every process whose name looks like Conductor, with its window count. If the
|
|
149
|
+
-- app is visibly on screen while "process Conductor" reports zero, the windows
|
|
150
|
+
-- belong to a process we are not addressing — and that only shows up here.
|
|
151
|
+
set out to {}
|
|
152
|
+
try
|
|
153
|
+
tell application "System Events"
|
|
154
|
+
repeat with proc in (every process whose name contains "Conductor")
|
|
155
|
+
set end of out to ((name of proc) & "=" & (count of windows of proc))
|
|
156
|
+
end repeat
|
|
157
|
+
end tell
|
|
158
|
+
end try
|
|
159
|
+
return out
|
|
160
|
+
end processWindowCounts
|
|
161
|
+
|
|
162
|
+
on menuTitles()
|
|
163
|
+
try
|
|
164
|
+
tell application "System Events" to tell process "Conductor"
|
|
165
|
+
return name of every menu bar item of menu bar 1
|
|
166
|
+
end tell
|
|
167
|
+
on error
|
|
168
|
+
return {}
|
|
169
|
+
end try
|
|
170
|
+
end menuTitles
|
|
171
|
+
|
|
172
|
+
on serverWindowCount()
|
|
173
|
+
-- Second opinion from the window server, because the Accessibility tree only
|
|
174
|
+
-- shows windows on the *current* Space: a full-screen Conductor (which owns its
|
|
175
|
+
-- own Space) or a Mac sitting at the lock screen reads as "no window" from AX
|
|
176
|
+
-- while the window very much exists — and acting on the AX answer alone once
|
|
177
|
+
-- quit a Conductor whose window the user had left open all along.
|
|
178
|
+
-- CGWindowListCopyWindowInfo(kCGWindowListOptionAll) sees every Space; owner
|
|
179
|
+
-- name and layer are readable without the Screen Recording grant (only window
|
|
180
|
+
-- *titles* are gated). Layer 0 keeps it to real windows. -1 = the probe itself
|
|
181
|
+
-- failed, which callers must treat as "no opinion", never as "no window".
|
|
182
|
+
-- The bindFunction is the load-bearing part, twice over: as shipped this read
|
|
183
|
+
-- "$.CFBridgingRelease(...)", which is an *inline*, not a symbol, and calling it
|
|
184
|
+
-- through JXA segfaults the osascript child (measured: exit 139) — so this probe
|
|
185
|
+
-- returned -1 on every call and the veto it feeds never once fired. And without
|
|
186
|
+
-- the rebind, JXA types the CF return as a bare pointer that deepUnwrap answers
|
|
187
|
+
-- with undefined — same wrong verdict, silently. Rebinding the function to
|
|
188
|
+
-- return 'id' makes the array real (verified live: 12 windows, behind a lock).
|
|
189
|
+
set jxa to "ObjC.import('CoreGraphics'); ObjC.bindFunction('CGWindowListCopyWindowInfo', ['id', ['unsigned int', 'unsigned int']]); const w = ObjC.deepUnwrap($.CGWindowListCopyWindowInfo(0, 0)) || []; w.filter(x => x.kCGWindowOwnerName === 'Conductor' && x.kCGWindowLayer === 0).length"
|
|
190
|
+
try
|
|
191
|
+
return (do shell script "osascript -l JavaScript -e " & quoted form of jxa) as integer
|
|
192
|
+
on error
|
|
193
|
+
return -1
|
|
194
|
+
end try
|
|
195
|
+
end serverWindowCount
|
|
196
|
+
|
|
197
|
+
on screenLocked()
|
|
198
|
+
-- "locked" / "unlocked" / "unknown". CGSessionCopyCurrentDictionary carries
|
|
199
|
+
-- CGSSessionScreenIsLocked = 1 while the lock screen is up and omits the key
|
|
200
|
+
-- entirely when it is not — same stdlib JXA channel as serverWindowCount.
|
|
201
|
+
-- This matters because the lock screen is the product's home ground: sends
|
|
202
|
+
-- come from a phone precisely when nobody is at the Mac, so "is the lock up"
|
|
203
|
+
-- is the difference between a state a restart can fix and one it provably
|
|
204
|
+
-- makes worse (see the restart gate in activateConductor).
|
|
205
|
+
-- Same two traps as serverWindowCount: $.CFBridgingRelease segfaults under JXA,
|
|
206
|
+
-- and without the rebind deepUnwrap reads the CF dictionary as undefined — which
|
|
207
|
+
-- the "|| {}" then turned into a confident "unlocked" on a locked Mac. The fixed
|
|
208
|
+
-- probe was verified against a genuinely locked screen (CGSSessionScreenIsLocked
|
|
209
|
+
-- true, every session key present).
|
|
210
|
+
set jxa to "ObjC.import('CoreGraphics'); ObjC.bindFunction('CGSessionCopyCurrentDictionary', ['id', []]); const d = ObjC.deepUnwrap($.CGSessionCopyCurrentDictionary()) || {}; d.CGSSessionScreenIsLocked ? 'locked' : 'unlocked'"
|
|
211
|
+
try
|
|
212
|
+
return do shell script "osascript -l JavaScript -e " & quoted form of jxa
|
|
213
|
+
on error
|
|
214
|
+
return "unknown"
|
|
215
|
+
end try
|
|
216
|
+
end screenLocked
|
|
217
|
+
|
|
218
|
+
on windowEvidence()
|
|
219
|
+
-- Zero windows is a dead end without knowing what the AX tree *does* show, and
|
|
220
|
+
-- guessing the next thing to press has already cost a round trip (there is no
|
|
221
|
+
-- "New Window" item — Conductor is single-window). So the error carries the
|
|
222
|
+
-- evidence instead: who macOS thinks Conductor is, what its menus offer, how
|
|
223
|
+
-- many windows the window server counts across all Spaces (AX only sees the
|
|
224
|
+
-- current one), and whether the lock screen is up.
|
|
225
|
+
set procs to my processWindowCounts()
|
|
226
|
+
set titles to my menuTitles()
|
|
227
|
+
set ev to " [window server: " & my serverWindowCount() & "; screen: " & my screenLocked() & "]"
|
|
228
|
+
if (count of procs) > 0 then set ev to ev & " [processes: " & my joinList(procs, ", ") & "]"
|
|
229
|
+
if (count of titles) > 0 then set ev to ev & " [menus: " & my joinList(titles, ", ") & "]"
|
|
230
|
+
return ev
|
|
231
|
+
end windowEvidence
|
|
232
|
+
|
|
233
|
+
on windowFailure()
|
|
234
|
+
-- "" when a window is there; otherwise why not, in words the phone can act on.
|
|
235
|
+
-- ASCII on purpose: these reach the phone, and osascript decodes -e by the
|
|
236
|
+
-- caller's locale, which a LaunchAgent doesn't set. The last branch carries
|
|
237
|
+
-- the raw error number and text on purpose — an unrecognised refusal must
|
|
238
|
+
-- arrive as itself, not as the most plausible-sounding of the known causes.
|
|
239
|
+
set probe to my windowProbe()
|
|
240
|
+
set winCount to item 1 of probe
|
|
241
|
+
if winCount > 0 then return ""
|
|
242
|
+
if winCount is 0 then return "Conductor is running but exposes no window - reopen and a Dock click both drew nothing. Open or restart it on your Mac and try again." & my windowEvidence()
|
|
243
|
+
set refusal to my refusalReason(probe)
|
|
244
|
+
if refusal is not "" then return refusal
|
|
245
|
+
if not (my conductorRunning()) then return "Conductor is not running - the relay started it, but it did not come up in time. Try again in a few seconds."
|
|
246
|
+
return "couldn't read Conductor's windows (" & (item 2 of probe) & "): " & (item 3 of probe)
|
|
247
|
+
end windowFailure
|
|
248
|
+
|
|
249
|
+
on requireWindow()
|
|
250
|
+
set reason to my windowFailure()
|
|
251
|
+
if reason is not "" then error reason
|
|
252
|
+
end requireWindow
|
|
253
|
+
|
|
254
|
+
on dockClick()
|
|
255
|
+
-- Last resort when the "reopen" AppleEvent draws nothing: click the app's Dock
|
|
256
|
+
-- tile. Same intent, different delivery — the Dock raises it through the app's
|
|
257
|
+
-- own event loop rather than as an Apple event we send, which is what an app
|
|
258
|
+
-- that never wired up reopen can still answer. (There is no "New Window" menu
|
|
259
|
+
-- item to press: Conductor is single-window and single-instance.)
|
|
260
|
+
try
|
|
261
|
+
tell application "System Events" to tell application process "Dock"
|
|
262
|
+
click UI element "Conductor" of list 1
|
|
263
|
+
end tell
|
|
264
|
+
return true
|
|
265
|
+
on error
|
|
266
|
+
return false
|
|
267
|
+
end try
|
|
268
|
+
end dockClick
|
|
269
|
+
|
|
270
|
+
on restartConductor()
|
|
271
|
+
-- The last lever: Conductor is single-window and single-instance, so when a
|
|
272
|
+
-- running app answers neither reopen nor a Dock click there is nothing left to
|
|
273
|
+
-- press. Deliberately fire-and-forget — a cold launch is 5-10s on its own,
|
|
274
|
+
-- past what the phone's budget leaves once the send's own delays are counted —
|
|
275
|
+
-- so this returns as soon as the relaunch is under way and the caller tells the
|
|
276
|
+
-- phone to send again into a warm app. Gated by RELAY_ALLOW_RESTART, which the
|
|
277
|
+
-- relay only sets when the DB says no session is mid-turn.
|
|
278
|
+
try
|
|
279
|
+
tell application "Conductor" to quit
|
|
280
|
+
on error errText
|
|
281
|
+
return "couldn't quit Conductor to restart it: " & errText
|
|
282
|
+
end try
|
|
283
|
+
repeat with attempt from 1 to 16
|
|
284
|
+
if not (my conductorRunning()) then exit repeat
|
|
285
|
+
delay 0.25
|
|
286
|
+
end repeat
|
|
287
|
+
if my conductorRunning() then return "Conductor ignored the quit, so the relay left it alone - quit it on your Mac and try again."
|
|
288
|
+
try
|
|
289
|
+
tell application "Conductor" to activate
|
|
290
|
+
on error errText
|
|
291
|
+
return "quit Conductor but couldn't start it again: " & errText
|
|
292
|
+
end try
|
|
293
|
+
return ""
|
|
294
|
+
end restartConductor
|
|
295
|
+
|
|
296
|
+
on restartApp()
|
|
297
|
+
-- The restart the phone asks for (POST /api/conductor/restart), as opposed to
|
|
298
|
+
-- the one activateConductor falls back to on its own. Same lever, different
|
|
299
|
+
-- entry, and the second entry is the whole point: that fallback fires only when
|
|
300
|
+
-- a *running* Conductor draws no window, so it is unreachable for the failure
|
|
301
|
+
-- this exists for — a Conductor whose window, sidebar and composer are all fine
|
|
302
|
+
-- while its agent runtime has stopped producing output. Measured live
|
|
303
|
+
-- (2026-09-02): prompts kept landing as `session_messages` rows for hours after
|
|
304
|
+
-- the last agent frame, so every window probe here would have said "healthy".
|
|
305
|
+
--
|
|
306
|
+
-- RELAY_ALLOW_RESTART is deliberately NOT consulted. That flag exists so the
|
|
307
|
+
-- fallback never destroys work the caller didn't ask about; here the caller is
|
|
308
|
+
-- a person who pressed a button, and server.ts has already counted the working
|
|
309
|
+
-- chats from the DB and refused without `stopAgents`.
|
|
310
|
+
set refusal to my refusalReason(my windowProbe())
|
|
311
|
+
if refusal is not "" then error refusal
|
|
312
|
+
-- Behind the lock screen a relaunch comes up windowless, and once came up
|
|
313
|
+
-- wedged (answering deep links, hanging every AX read) — the same reason the
|
|
314
|
+
-- fallback refuses there. The sentence starts with the phrase both sides match
|
|
315
|
+
-- on (MAC_LOCKED in src/shared.ts) so the phone draws its unlock link.
|
|
316
|
+
if my screenLocked() is "locked" then error "The Mac is locked - relaunching Conductor behind the lock screen is what leaves it wedged, so the relay won't. Unlock the Mac and try again." & my windowEvidence()
|
|
317
|
+
set restartError to my restartConductor()
|
|
318
|
+
if restartError is not "" then error restartError & my windowEvidence()
|
|
319
|
+
-- The fallback returns the moment the relaunch starts, because it is spending a
|
|
320
|
+
-- *send's* budget and a cold launch doesn't fit beside one. This run has nothing
|
|
321
|
+
-- else to do with its budget, so it waits for the window and the phone hears
|
|
322
|
+
-- what actually happened instead of "try again": ~5-10s for a cold launch here.
|
|
323
|
+
if not my waitForWindow(60) then error "Conductor was relaunched but hasn't drawn a window yet. Give it a few seconds, then try again." & my windowEvidence()
|
|
324
|
+
return "ok"
|
|
325
|
+
end restartApp
|
|
326
|
+
|
|
327
|
+
on waitForWindow(attempts)
|
|
328
|
+
-- "reopen" is the dock-click Apple event, and for most apps that is what
|
|
329
|
+
-- recreates a closed window ("activate" on its own does not). Nudge a few times
|
|
330
|
+
-- across the wait: a cold launch can reach "process exists" long before it
|
|
331
|
+
-- draws anything. If reopen has drawn nothing by ~3s the app isn't answering
|
|
332
|
+
-- that event, so escalate to a real Dock click rather than repeating what has
|
|
333
|
+
-- already failed twice.
|
|
334
|
+
repeat with attempt from 1 to attempts
|
|
335
|
+
if my hasWindow() then return true
|
|
336
|
+
if attempt is 2 or attempt is 8 or attempt is 20 then
|
|
337
|
+
try
|
|
338
|
+
tell application "Conductor" to reopen
|
|
339
|
+
tell application "Conductor" to activate
|
|
340
|
+
end try
|
|
341
|
+
end if
|
|
342
|
+
if attempt is 12 or attempt is 28 then my dockClick()
|
|
343
|
+
delay 0.25
|
|
344
|
+
end repeat
|
|
345
|
+
return false
|
|
346
|
+
end waitForWindow
|
|
347
|
+
|
|
348
|
+
on activateConductor()
|
|
349
|
+
-- Conductor keeps running with every window closed (standard macOS: the red
|
|
350
|
+
-- button doesn't quit), and it may not be running at all. Both used to surface
|
|
351
|
+
-- as "Can't get window 1 of process Conductor. Invalid index." from whichever
|
|
352
|
+
-- handler happened to run first. So: launch-or-front it ("activate" starts it
|
|
353
|
+
-- when it isn't running), wait for a real window, then say what actually went
|
|
354
|
+
-- wrong in words the phone can act on.
|
|
355
|
+
-- Permissions first: a refusal reads as "no process, no window" from every
|
|
356
|
+
-- probe below, and waiting out the patience budget to say the wrong thing
|
|
357
|
+
-- helps nobody. A refusal is the one failure launching can't fix.
|
|
358
|
+
set firstProbe to my windowProbe()
|
|
359
|
+
set refusal to my refusalReason(firstProbe)
|
|
360
|
+
if refusal is not "" then error refusal
|
|
361
|
+
-- The lock ahead of everything else: the lock screen hides the whole session
|
|
362
|
+
-- from Accessibility, so a locked Mac reads "no window" from every probe below
|
|
363
|
+
-- and the ladder spends the patience budget, the Dock click and the evidence
|
|
364
|
+
-- sweeps discovering what one read already knew — measured live (2026-08-14),
|
|
365
|
+
-- the attempt died at the node-side ceiling as "took too long" and the phone
|
|
366
|
+
-- never heard the word "locked". Asking here also stops "activate" from
|
|
367
|
+
-- launching Conductor behind the lock screen, which is where the windowless
|
|
368
|
+
-- launches that wedged it came from. A probe that already shows a window
|
|
369
|
+
-- proves the session is unlocked (AX can't see into a locked one), so the
|
|
370
|
+
-- happy path pays nothing. The error stays retryable on purpose: the caller's
|
|
371
|
+
-- retry loop polls this cheaply until its deadline, so an unlock mid-budget
|
|
372
|
+
-- lands the send by itself — that is the observed recovery, not a theory.
|
|
373
|
+
if (item 1 of firstProbe) < 1 and my screenLocked() is "locked" then
|
|
374
|
+
error "The Mac is locked - the lock screen hides Conductor from the relay, so nothing can be sent or pressed. Unlock the Mac and try again." & my windowEvidence()
|
|
375
|
+
end if
|
|
376
|
+
try
|
|
377
|
+
tell application "Conductor" to activate
|
|
378
|
+
on error errText
|
|
379
|
+
error "couldn't bring Conductor to the front: " & errText
|
|
380
|
+
end try
|
|
381
|
+
delay 0.4
|
|
382
|
+
-- Patience sized to what we're waiting for. An app that already shows a window
|
|
383
|
+
-- returns from waitForWindow immediately, so the short budget only ever times a
|
|
384
|
+
-- redraw (~4s is plenty). Zero windows gets the cold-launch budget (~9s) even
|
|
385
|
+
-- when the process exists: the restart below is fire-and-forget, so the *next*
|
|
386
|
+
-- attempt arrives while Conductor is still drawing its first window — "process
|
|
387
|
+
-- exists, no window yet" — and a 4s wait there quits it again mid-launch,
|
|
388
|
+
-- which is how one restart becomes a loop that never lets it come up. ~9s
|
|
389
|
+
-- still fits the 28s script budget once the send's own delays are counted.
|
|
390
|
+
set patience to 16
|
|
391
|
+
if (item 1 of firstProbe) < 1 then set patience to 36
|
|
392
|
+
if my waitForWindow(patience) then return
|
|
393
|
+
-- Running, readable, and still zero windows: reopen and the Dock click have
|
|
394
|
+
-- both drawn nothing, so a restart is the only thing left. "Open it on your
|
|
395
|
+
-- Mac" is not advice a phone can act on — that is the whole point of this app.
|
|
396
|
+
if (system attribute "RELAY_ALLOW_RESTART") is "1" and (item 1 of my windowProbe()) is 0 then
|
|
397
|
+
-- Evidence before the restart tears the state down: which processes match
|
|
398
|
+
-- Conductor and who owns a window is the one fact that separates "genuinely
|
|
399
|
+
-- windowless" (a locked or unattended Mac) from "we're addressing the wrong
|
|
400
|
+
-- process" — and after the relaunch it would describe the new process, not
|
|
401
|
+
-- the state that failed. windowFailure() already carries it; this branch
|
|
402
|
+
-- used to drop it, which made a repeating no-window failure undebuggable
|
|
403
|
+
-- from the log alone.
|
|
404
|
+
set ev to my windowEvidence()
|
|
405
|
+
set lockState to my screenLocked()
|
|
406
|
+
-- AX saying "no window" is not the same as there being no window: full
|
|
407
|
+
-- screen on another Space and the lock screen both hide a live window from
|
|
408
|
+
-- AX, and restarting then destroys a window the user left open (measured
|
|
409
|
+
-- live: exactly that loop wedged Conductor for a whole evening). Only the
|
|
410
|
+
-- window server can tell the difference, so a window it CAN see blocks the
|
|
411
|
+
-- restart — this is a human's problem, said in words, not the relay's.
|
|
412
|
+
if my serverWindowCount() > 0 then
|
|
413
|
+
if lockState is "locked" then error "The Mac is locked - Conductor's window is open behind the lock screen, where the relay can't reach it. Unlock the Mac and try again." & ev
|
|
414
|
+
error "Conductor's window exists but is not reachable - it is probably full screen on another Space. Take Conductor out of full screen, then send again." & ev
|
|
415
|
+
end if
|
|
416
|
+
-- No window anywhere and the lock screen up: don't restart. Every relaunch
|
|
417
|
+
-- fired behind the lock screen in the live incident came up windowless, and
|
|
418
|
+
-- the last one came up wedged — answering deep links, hanging every AX
|
|
419
|
+
-- read. A restart here is not a lever, it is how the wreckage happened.
|
|
420
|
+
if lockState is "locked" then error "The Mac is locked and Conductor has no window. Relaunching behind the lock screen is what leaves it wedged, so the relay won't - unlock the Mac and try again." & ev
|
|
421
|
+
set restartError to my restartConductor()
|
|
422
|
+
if restartError is not "" then error restartError & ev
|
|
423
|
+
error "Conductor was running with no window and ignored both reopen and a Dock click, so the relay restarted it. Give it a few seconds and send again." & ev
|
|
424
|
+
end if
|
|
425
|
+
my requireWindow()
|
|
426
|
+
end activateConductor
|
|
427
|
+
|
|
428
|
+
on webArea()
|
|
429
|
+
-- The webview root: the window, then four nested containers. Guarded, or a
|
|
430
|
+
-- window that closed mid-run reappears as an "Invalid index" from a caller
|
|
431
|
+
-- that has nothing to do with windows.
|
|
432
|
+
my requireWindow()
|
|
433
|
+
tell application "System Events" to tell process "Conductor"
|
|
434
|
+
return UI element 1 of UI element 1 of UI element 1 of UI element 1 of window 1
|
|
435
|
+
end tell
|
|
436
|
+
end webArea
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
|
|
2
|
+
on axRole(el)
|
|
3
|
+
tell application "System Events" to tell process "Conductor"
|
|
4
|
+
try
|
|
5
|
+
return role of el
|
|
6
|
+
on error
|
|
7
|
+
return ""
|
|
8
|
+
end try
|
|
9
|
+
end tell
|
|
10
|
+
end axRole
|
|
11
|
+
|
|
12
|
+
on axKids(el)
|
|
13
|
+
tell application "System Events" to tell process "Conductor"
|
|
14
|
+
try
|
|
15
|
+
return UI elements of el
|
|
16
|
+
on error
|
|
17
|
+
return {}
|
|
18
|
+
end try
|
|
19
|
+
end tell
|
|
20
|
+
end axKids
|
|
21
|
+
|
|
22
|
+
on menusUnder(root, maxDepth)
|
|
23
|
+
-- Every AXMenu at or below root, breadth-first and bounded. The status
|
|
24
|
+
-- submenu opens *nested inside* the row menu rather than beside it, so a
|
|
25
|
+
-- search that stops at the first hit (the way setModel scans the web area's
|
|
26
|
+
-- direct children) never sees it.
|
|
27
|
+
set level to {root}
|
|
28
|
+
set found to {}
|
|
29
|
+
set depth to 0
|
|
30
|
+
repeat while (count of level) > 0 and depth < maxDepth
|
|
31
|
+
set nextLevel to {}
|
|
32
|
+
repeat with entry in level
|
|
33
|
+
set node to contents of entry
|
|
34
|
+
if (my axRole(node)) is "AXMenu" then set end of found to node
|
|
35
|
+
set nextLevel to nextLevel & (my axKids(node))
|
|
36
|
+
end repeat
|
|
37
|
+
set level to nextLevel
|
|
38
|
+
set depth to depth + 1
|
|
39
|
+
end repeat
|
|
40
|
+
return found
|
|
41
|
+
end menusUnder
|
|
42
|
+
|
|
43
|
+
on axName(el)
|
|
44
|
+
-- Guarded: a menu re-renders under us, and reading the name of an element that
|
|
45
|
+
-- has since gone throws an object-specifier error from wherever we happened to be.
|
|
46
|
+
tell application "System Events" to tell process "Conductor"
|
|
47
|
+
try
|
|
48
|
+
set n to name of el
|
|
49
|
+
if n is missing value then return ""
|
|
50
|
+
return n as text
|
|
51
|
+
on error
|
|
52
|
+
return ""
|
|
53
|
+
end try
|
|
54
|
+
end tell
|
|
55
|
+
end axName
|
|
56
|
+
|
|
57
|
+
on menuItemNamed(m, wanted)
|
|
58
|
+
repeat with mi in (my axKids(m))
|
|
59
|
+
set node to contents of mi
|
|
60
|
+
if (my axRole(node)) is "AXMenuItem" and (my axName(node)) is wanted then return node
|
|
61
|
+
end repeat
|
|
62
|
+
return missing value
|
|
63
|
+
end menuItemNamed
|
|
64
|
+
|
|
65
|
+
on waitForMenuWith(root, maxDepth, itemName, attempts)
|
|
66
|
+
-- Identify the menu by an item it *contains*, never as "the first AXMenu on
|
|
67
|
+
-- screen": a model picker left open, or a menu from the run before, is also an
|
|
68
|
+
-- AXMenu, and picking it produced "no Set status item" on a menu that had never
|
|
69
|
+
-- been the workspace's. A menu is drawn by the webview, not by us, so how long
|
|
70
|
+
-- it takes varies with what Conductor is busy doing — poll rather than guess
|
|
71
|
+
-- one delay; the common case costs a single sweep.
|
|
72
|
+
repeat with i from 1 to attempts
|
|
73
|
+
repeat with entry in my menusUnder(root, maxDepth)
|
|
74
|
+
set node to contents of entry
|
|
75
|
+
if (my menuItemNamed(node, itemName)) is not missing value then return node
|
|
76
|
+
end repeat
|
|
77
|
+
delay 0.5
|
|
78
|
+
end repeat
|
|
79
|
+
return missing value
|
|
80
|
+
end waitForMenuWith
|
|
81
|
+
|
|
82
|
+
on dismissMenus()
|
|
83
|
+
-- Two escapes: one for the submenu, one for the row menu. Leaving either open
|
|
84
|
+
-- would swallow the next run's keystrokes.
|
|
85
|
+
tell application "System Events"
|
|
86
|
+
key code 53
|
|
87
|
+
delay 0.25
|
|
88
|
+
key code 53
|
|
89
|
+
end tell
|
|
90
|
+
end dismissMenus
|
|
91
|
+
|
|
92
|
+
on pressStatusMenu(rowEl, wanted)
|
|
93
|
+
-- Scroll it into view first. A row that exists in the AX tree but sits outside
|
|
94
|
+
-- the sidebar's visible strip accepts AXShowMenu and draws nothing — which is
|
|
95
|
+
-- exactly what happens right after a status change moves it to another group.
|
|
96
|
+
tell application "System Events" to tell process "Conductor"
|
|
97
|
+
try
|
|
98
|
+
perform action "AXScrollToVisible" of rowEl
|
|
99
|
+
end try
|
|
100
|
+
end tell
|
|
101
|
+
delay 0.4
|
|
102
|
+
-- Clear anything already open (a picker, or a menu a previous run left behind)
|
|
103
|
+
-- so the sweep below can only match the one this right-click draws.
|
|
104
|
+
tell application "System Events" to key code 53
|
|
105
|
+
delay 0.3
|
|
106
|
+
tell application "System Events" to tell process "Conductor"
|
|
107
|
+
perform action "AXShowMenu" of rowEl
|
|
108
|
+
end tell
|
|
109
|
+
-- Depth 4, not the whole tree: the row menu is a portal near the top of the web
|
|
110
|
+
-- area, and the *transcript* hangs off that same root — a deep sweep walks every
|
|
111
|
+
-- message bubble and costs more than the rest of this write put together (it
|
|
112
|
+
-- grows with the conversation, so it degrades as you use the app).
|
|
113
|
+
set rowMenu to my waitForMenuWith(my webArea(), 4, "Set status", 6)
|
|
114
|
+
if rowMenu is missing value then
|
|
115
|
+
my dismissMenus()
|
|
116
|
+
error "the workspace's menu didn't open — or it no longer offers Set status"
|
|
117
|
+
end if
|
|
118
|
+
set statusItem to my menuItemNamed(rowMenu, "Set status")
|
|
119
|
+
tell application "System Events" to tell process "Conductor"
|
|
120
|
+
perform action "AXPress" of statusItem
|
|
121
|
+
end tell
|
|
122
|
+
-- The submenu is an AXMenu nested a few levels *inside* the row menu (Conductor
|
|
123
|
+
-- expands it in place rather than opening a sibling), so search from that menu —
|
|
124
|
+
-- searching the web area again would re-walk the world. Two things make this a
|
|
125
|
+
-- poll rather than a read: the row-menu handle goes stale across the expansion
|
|
126
|
+
-- (it reports no children instead of erroring), so it is re-found each pass; and
|
|
127
|
+
-- the submenu's *items* arrive a beat after the submenu itself, so the thing we
|
|
128
|
+
-- wait for is the wanted status, not the container that will hold it.
|
|
129
|
+
set subMenu to missing value
|
|
130
|
+
repeat with attempt from 1 to 8
|
|
131
|
+
set liveMenu to my waitForMenuWith(my webArea(), 4, "Set status", 1)
|
|
132
|
+
if liveMenu is not missing value then
|
|
133
|
+
set subMenu to my waitForMenuWith(liveMenu, 5, wanted, 1)
|
|
134
|
+
end if
|
|
135
|
+
if subMenu is not missing value then exit repeat
|
|
136
|
+
delay 0.4
|
|
137
|
+
end repeat
|
|
138
|
+
if subMenu is missing value then
|
|
139
|
+
my dismissMenus()
|
|
140
|
+
error "Conductor never offered a status called " & wanted
|
|
141
|
+
end if
|
|
142
|
+
set choice to my menuItemNamed(subMenu, wanted)
|
|
143
|
+
tell application "System Events" to tell process "Conductor"
|
|
144
|
+
perform action "AXPress" of choice
|
|
145
|
+
end tell
|
|
146
|
+
delay 0.6
|
|
147
|
+
end pressStatusMenu
|
|
148
|
+
|
|
149
|
+
on setWorkspaceStatus()
|
|
150
|
+
-- Conductor has no menu-bar or palette command for this, so the only lever is
|
|
151
|
+
-- the sidebar row's own context menu (Mark as unread / Pin / Set status /
|
|
152
|
+
-- Rename / Copy link / Archive). Right-clicking the row needs no focus change,
|
|
153
|
+
-- so unlike a send this never disturbs which workspace is on screen.
|
|
154
|
+
set wanted to system attribute "RELAY_SET_STATUS"
|
|
155
|
+
if wanted is "" then error "no status requested"
|
|
156
|
+
set theRow to my findSidebarRow()
|
|
157
|
+
set opened to {}
|
|
158
|
+
if theRow is missing value then
|
|
159
|
+
-- A folded section renders no rows, so the row may simply not be in the tree
|
|
160
|
+
-- yet. There is no palette command to fall back to, so open the sections
|
|
161
|
+
-- ourselves and look again — then fold them back, since the one thing this
|
|
162
|
+
-- write promises is that the sidebar is where you left it.
|
|
163
|
+
set opened to my expandSections()
|
|
164
|
+
if (count of opened) > 0 then set theRow to my findSidebarRow()
|
|
165
|
+
end if
|
|
166
|
+
if theRow is missing value then
|
|
167
|
+
my restoreSections(opened)
|
|
168
|
+
error "couldn't find this workspace in the sidebar"
|
|
169
|
+
end if
|
|
170
|
+
try
|
|
171
|
+
my pressStatusMenu(theRow, wanted)
|
|
172
|
+
on error errMsg
|
|
173
|
+
my restoreSections(opened)
|
|
174
|
+
error errMsg
|
|
175
|
+
end try
|
|
176
|
+
my restoreSections(opened)
|
|
177
|
+
end setWorkspaceStatus
|