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.
Files changed (200) hide show
  1. package/README.md +10 -8
  2. package/dist/assets/{PierrePatch-BPp5Cf-p.js → PierrePatch-D4YbKD8Q.js} +1 -1
  3. package/dist/assets/index-BRzsSW8g.css +1 -0
  4. package/dist/assets/index-BaS6qXUW.js +65 -0
  5. package/dist/index.html +2 -2
  6. package/dist/push-sw.js +2 -2
  7. package/dist/sw.js +1 -1
  8. package/dist-node/scripts/nosleep-setup.js +2 -2
  9. package/dist-node/scripts/nosleep.js +3 -3
  10. package/dist-node/scripts/service/commands.js +32 -0
  11. package/dist-node/scripts/service/configuration.js +205 -0
  12. package/dist-node/scripts/service/environment.js +59 -0
  13. package/dist-node/scripts/service/flags.js +45 -0
  14. package/dist-node/scripts/service/installation.js +91 -0
  15. package/dist-node/scripts/service/launch-agent.js +134 -0
  16. package/dist-node/scripts/service/network.js +244 -0
  17. package/dist-node/scripts/service/presentation.js +101 -0
  18. package/dist-node/scripts/service/voice.js +113 -0
  19. package/dist-node/scripts/service.js +5 -980
  20. package/dist-node/src/{agent-config.js → agents/agent-config.js} +1 -1
  21. package/dist-node/src/{model-cache.js → agents/model-cache.js} +1 -1
  22. package/dist-node/src/{roles.js → agents/roles.js} +3 -3
  23. package/dist-node/src/db.js +1 -1
  24. package/dist-node/src/{parked.js → delivery/parked.js} +1 -1
  25. package/dist-node/src/{dev-server.js → dev-server/controller.js} +8 -291
  26. package/dist-node/src/dev-server/ports.js +63 -0
  27. package/dist-node/src/dev-server/processes.js +73 -0
  28. package/dist-node/src/dev-server/proxy.js +158 -0
  29. package/dist-node/src/{run-activity.js → dev-server/run-activity.js} +2 -2
  30. package/dist-node/src/dev-server/types.js +1 -0
  31. package/dist-node/src/{file-preview.js → files/file-preview.js} +1 -1
  32. package/dist-node/src/{change-stats.js → git/change-stats.js} +1 -1
  33. package/dist-node/src/{git.js → git/diff.js} +1 -1
  34. package/dist-node/src/{autoupdate.js → host/autoupdate.js} +1 -1
  35. package/dist-node/src/{funnel-watchdog.js → host/funnel-watchdog.js} +4 -4
  36. package/dist-node/src/{logbuf.js → host/logbuf.js} +1 -1
  37. package/dist-node/src/{nosleep-helper.js → host/nosleep-helper.js} +1 -1
  38. package/dist-node/src/{tailscale.js → host/tailscale.js} +1 -1
  39. package/dist-node/src/http/router-types.js +2 -0
  40. package/dist-node/src/http/router.js +78 -0
  41. package/dist-node/src/http/routes/create-workspace.js +90 -0
  42. package/dist-node/src/http/routes/files.js +53 -0
  43. package/dist-node/src/http/routes/prompts.js +363 -0
  44. package/dist-node/src/http/routes/sessions.js +279 -0
  45. package/dist-node/src/http/routes/state.js +186 -0
  46. package/dist-node/src/http/routes/system.js +220 -0
  47. package/dist-node/src/http/routes/voice.js +117 -0
  48. package/dist-node/src/http/routes/workflows.js +248 -0
  49. package/dist-node/src/http/routes/workspaces.js +267 -0
  50. package/dist-node/src/http/services/base.js +252 -0
  51. package/dist-node/src/http/services/delegations.js +301 -0
  52. package/dist-node/src/http/services/delivery.js +433 -0
  53. package/dist-node/src/http/services/files.js +199 -0
  54. package/dist-node/src/http/services/mcp.js +94 -0
  55. package/dist-node/src/http/services/responses.js +120 -0
  56. package/dist-node/src/http/services/voice.js +147 -0
  57. package/dist-node/src/http/services/workflow-probes.js +215 -0
  58. package/dist-node/src/http/services/workflow-state.js +195 -0
  59. package/dist-node/src/http/services/workflow.js +366 -0
  60. package/dist-node/src/http/services.js +35 -0
  61. package/dist-node/src/mcp/arguments.js +14 -0
  62. package/dist-node/src/mcp/dispatcher.js +56 -0
  63. package/dist-node/src/mcp/formatters.js +110 -0
  64. package/dist-node/src/mcp/protocol.js +7 -0
  65. package/dist-node/src/mcp/registry.js +66 -0
  66. package/dist-node/src/mcp/tools/agent-options.js +122 -0
  67. package/dist-node/src/mcp/tools/chat-actions.js +194 -0
  68. package/dist-node/src/mcp/tools/chats.js +194 -0
  69. package/dist-node/src/mcp/tools/delegation.js +135 -0
  70. package/dist-node/src/mcp/tools/system.js +135 -0
  71. package/dist-node/src/mcp/tools/voice.js +136 -0
  72. package/dist-node/src/mcp/tools/workspaces.js +259 -0
  73. package/dist-node/src/mcp/types.js +1 -0
  74. package/dist-node/src/mcp.js +6 -4
  75. package/dist-node/src/{notify.js → notifications/notify.js} +5 -5
  76. package/dist-node/src/{webpush.js → notifications/webpush.js} +1 -1
  77. package/dist-node/src/orchestration/delegation/codec.js +201 -0
  78. package/dist-node/src/{delegation-intake.js → orchestration/delegation/intake.js} +2 -2
  79. package/dist-node/src/{delegation-prompt.js → orchestration/delegation/prompt.js} +1 -1
  80. package/dist-node/src/orchestration/delegation/queue.js +255 -0
  81. package/dist-node/src/orchestration/delegation/store.js +111 -0
  82. package/dist-node/src/orchestration/delegation/types.js +2 -0
  83. package/dist-node/src/orchestration/persistence/capabilities.js +109 -0
  84. package/dist-node/src/orchestration/persistence/codecs.js +148 -0
  85. package/dist-node/src/orchestration/persistence/connection.js +205 -0
  86. package/dist-node/src/orchestration/persistence/db.js +101 -0
  87. package/dist-node/src/orchestration/persistence/effect-recovery.js +227 -0
  88. package/dist-node/src/orchestration/persistence/effect-transitions.js +157 -0
  89. package/dist-node/src/orchestration/persistence/effects.js +109 -0
  90. package/dist-node/src/orchestration/persistence/errors.js +39 -0
  91. package/dist-node/src/orchestration/persistence/events.js +29 -0
  92. package/dist-node/src/orchestration/persistence/idempotency.js +82 -0
  93. package/dist-node/src/orchestration/persistence/job-attempts.js +114 -0
  94. package/dist-node/src/orchestration/persistence/jobs.js +159 -0
  95. package/dist-node/src/orchestration/persistence/projections.js +190 -0
  96. package/dist-node/src/orchestration/persistence/quarantine.js +46 -0
  97. package/dist-node/src/orchestration/persistence/records.js +115 -0
  98. package/dist-node/src/orchestration/persistence/relays.js +78 -0
  99. package/dist-node/src/orchestration/persistence/runs.js +156 -0
  100. package/dist-node/src/{orchestration-schema.js → orchestration/persistence/schema.js} +1 -1
  101. package/dist-node/src/orchestration/persistence/types.js +1 -0
  102. package/dist-node/src/orchestration/persistence/ui-lease.js +197 -0
  103. package/dist-node/src/orchestration/persistence/values.js +114 -0
  104. package/dist-node/src/orchestration/workflow/commands.js +309 -0
  105. package/dist-node/src/orchestration/workflow/coordinator.js +28 -0
  106. package/dist-node/src/orchestration/workflow/effect-recovery.js +125 -0
  107. package/dist-node/src/orchestration/workflow/effects.js +307 -0
  108. package/dist-node/src/orchestration/workflow/errors.js +13 -0
  109. package/dist-node/src/orchestration/workflow/helpers.js +154 -0
  110. package/dist-node/src/orchestration/workflow/job-dispatch.js +312 -0
  111. package/dist-node/src/orchestration/workflow/job-results.js +280 -0
  112. package/dist-node/src/{workflow.js → orchestration/workflow/prompts.js} +2 -2
  113. package/dist-node/src/orchestration/workflow/root.js +293 -0
  114. package/dist-node/src/orchestration/workflow/start.js +99 -0
  115. package/dist-node/src/orchestration/workflow/state.js +105 -0
  116. package/dist-node/src/orchestration/workflow/types.js +1 -0
  117. package/dist-node/src/orchestration/workflow/wake.js +164 -0
  118. package/dist-node/src/prefs.js +1 -1
  119. package/dist-node/src/{background-tasks.js → reads/background-tasks.js} +1 -1
  120. package/dist-node/src/reads/messages.js +362 -0
  121. package/dist-node/src/reads/repository.js +87 -0
  122. package/dist-node/src/reads/sessions.js +165 -0
  123. package/dist-node/src/reads/types.js +1 -0
  124. package/dist-node/src/reads/workspaces.js +230 -0
  125. package/dist-node/src/reads/worktrees.js +45 -0
  126. package/dist-node/src/routes.js +5 -5
  127. package/dist-node/src/{search.js → search/coordinator.js} +4 -4
  128. package/dist-node/src/{search-worker.js → search/worker.js} +3 -3
  129. package/dist-node/src/server.js +14 -3930
  130. package/dist-node/src/settings.js +1 -1
  131. package/dist-node/src/shared.js +130 -9
  132. package/dist-node/src/{transcript.js → transcript/parser.js} +4 -124
  133. package/dist-node/src/{tool-usage-worker.js → usage/tool-usage-worker.js} +1 -1
  134. package/dist-node/src/{tool-usage.js → usage/tool-usage.js} +1 -1
  135. package/dist-node/src/voice/brief.js +22 -50
  136. package/dist-node/src/voice/context.js +1 -1
  137. package/dist-node/src/voice/dates.js +38 -0
  138. package/dist-node/src/voice/history.js +14 -5
  139. package/dist-node/src/voice/prompt.js +10 -9
  140. package/dist-node/src/voice/recall.js +115 -0
  141. package/dist-node/src/{speech.js → voice/speech.js} +1 -1
  142. package/dist-node/src/voice/tools.js +92 -3
  143. package/dist-node/src/voice/webrtc.js +1 -1
  144. package/dist-node/src/wire.js +2 -2
  145. package/dist-node/src/writes/actuator.js +124 -0
  146. package/dist-node/src/writes/agent-options.js +151 -0
  147. package/dist-node/src/writes/applescript/agent-options.applescript +334 -0
  148. package/dist-node/src/writes/applescript/chat-targeting.applescript +149 -0
  149. package/dist-node/src/writes/applescript/composer.applescript +195 -0
  150. package/dist-node/src/writes/applescript/hotspot.applescript +155 -0
  151. package/dist-node/src/writes/applescript/lifecycle.applescript +266 -0
  152. package/dist-node/src/writes/applescript/run-tasks.applescript +208 -0
  153. package/dist-node/src/writes/applescript/source.js +52 -0
  154. package/dist-node/src/writes/applescript/window.applescript +436 -0
  155. package/dist-node/src/writes/applescript/workspace-status.applescript +177 -0
  156. package/dist-node/src/writes/applescript/workspace-targeting.applescript +359 -0
  157. package/dist-node/src/writes/chats.js +160 -0
  158. package/dist-node/src/writes/guards.js +77 -0
  159. package/dist-node/src/writes/runner.js +67 -0
  160. package/dist-node/src/{sidecar.js → writes/sidecar.js} +1 -1
  161. package/dist-node/src/writes/system.js +81 -0
  162. package/dist-node/src/writes/targeting.js +121 -0
  163. package/dist-node/src/writes/types.js +1 -0
  164. package/dist-node/src/writes/ui-lock.js +229 -0
  165. package/dist-node/src/writes/workspaces.js +233 -0
  166. package/docs/voice-setup.md +34 -12
  167. package/package.json +2 -2
  168. package/dist/assets/index-DNLdIfZ2.css +0 -1
  169. package/dist/assets/index-QJR7tn2I.js +0 -61
  170. package/dist-node/src/conductor.applescript +0 -2279
  171. package/dist-node/src/delegations.js +0 -557
  172. package/dist-node/src/mcp-tools.js +0 -1319
  173. package/dist-node/src/orchestration-db.js +0 -2186
  174. package/dist-node/src/reads.js +0 -787
  175. package/dist-node/src/workflow-coordinator.js +0 -2119
  176. package/dist-node/src/writes.js +0 -1223
  177. /package/dist-node/src/{conductor-settings.js → agents/conductor-settings.js} +0 -0
  178. /package/dist-node/src/{firstprompt.js → delivery/firstprompt.js} +0 -0
  179. /package/dist-node/src/{sendonce.js → delivery/sendonce.js} +0 -0
  180. /package/dist-node/src/{preview-urls.js → dev-server/preview-urls.js} +0 -0
  181. /package/dist-node/src/{run-configs.js → dev-server/run-configs.js} +0 -0
  182. /package/dist-node/src/{attachments.js → files/attachments.js} +0 -0
  183. /package/dist-node/src/{icons.js → files/icons.js} +0 -0
  184. /package/dist-node/src/{staged-attachments.js → files/staged-attachments.js} +0 -0
  185. /package/dist-node/src/{fork-workspace.js → git/fork-workspace.js} +0 -0
  186. /package/dist-node/src/{merge.js → git/merge.js} +0 -0
  187. /package/dist-node/src/{pr.js → git/pr.js} +0 -0
  188. /package/dist-node/src/{nosleep.js → host/nosleep.js} +0 -0
  189. /package/dist-node/src/{relay-processes.js → host/relay-processes.js} +0 -0
  190. /package/dist-node/src/{ui-lease-watchdog.js → host/ui-lease-watchdog.js} +0 -0
  191. /package/dist-node/src/{wifi.js → host/wifi.js} +0 -0
  192. /package/dist-node/src/{orchestration-schema.generated.js → orchestration/persistence/schema.generated.js} +0 -0
  193. /package/dist-node/src/{workflow-effect-runner.js → orchestration/workflow/effect-runner.js} +0 -0
  194. /package/dist-node/src/{workflow-http.js → orchestration/workflow/http.js} +0 -0
  195. /package/dist-node/src/{workflow-machine.js → orchestration/workflow/machine.js} +0 -0
  196. /package/dist-node/src/{session-poller.js → reads/session-poller.js} +0 -0
  197. /package/dist-node/src/{context-breakdown.js → transcript/context-breakdown.js} +0 -0
  198. /package/dist-node/src/{chat-cursor.js → transcript/cursor.js} +0 -0
  199. /package/dist-node/src/{plan-usage.js → usage/plan-usage.js} +0 -0
  200. /package/dist-node/src/{tool-usage-service.js → usage/tool-usage-service.js} +0 -0
@@ -0,0 +1,334 @@
1
+
2
+ on composerControls()
3
+ set strips to my tabGroups()
4
+ if (count of strips) is 0 then error "couldn't find the composer"
5
+ tell application "System Events" to tell process "Conductor"
6
+ set pane to value of attribute "AXParent" of (item 1 of strips)
7
+ set composerBox to item 1 of (UI elements of pane whose name is "composer")
8
+ set out to {}
9
+ repeat with e in (UI elements of composerBox)
10
+ set end of out to contents of e
11
+ repeat with e2 in (UI elements of e)
12
+ set end of out to contents of e2
13
+ end repeat
14
+ end repeat
15
+ return out
16
+ end tell
17
+ end composerControls
18
+
19
+ on controlNamed(wanted)
20
+ repeat with entry in my composerControls()
21
+ set c to contents of entry
22
+ if my tabLabel(c) is wanted then return c
23
+ end repeat
24
+ return missing value
25
+ end controlNamed
26
+
27
+ on effortButton()
28
+ -- The effort control is a button whose *label is its current value*, so it is
29
+ -- identified by that label rather than a stable name. Claude calls its top
30
+ -- level Ultracode while Codex calls the corresponding level Ultra. Codex's
31
+ -- None state is the exception: its button has no accessible name at all, so
32
+ -- accept one (and only one) unnamed button *after* the proven model picker.
33
+ -- The main composer also contains unnamed attachment/stop buttons before that
34
+ -- picker, and pressing one of those would be worse than failing.
35
+ set levels to {"Light", "Low", "Medium", "High", "Extra high", "Max", "Ultracode", "Ultra"}
36
+ set sawModel to false
37
+ set unnamed to {}
38
+ repeat with entry in my composerControls()
39
+ set c to contents of entry
40
+ if (my axRole(c)) is "AXPopUpButton" and (my axName(c)) contains "Change agent" then
41
+ set sawModel to true
42
+ else if sawModel and (my axRole(c)) is "AXButton" then
43
+ set label to my axName(c)
44
+ if label is in levels then return c
45
+ if label is "" then set end of unnamed to c
46
+ end if
47
+ end repeat
48
+ if (count of unnamed) is 1 then return item 1 of unnamed
49
+ return missing value
50
+ end effortButton
51
+
52
+ on setEffort(wanted)
53
+ -- Claude owns a six-value ring ending in Ultracode; Codex adds None and ends
54
+ -- in Ultra. Step around either ring once and confirm the label landed.
55
+ set btn to my effortButton()
56
+ if btn is missing value then error "couldn't find the effort control"
57
+ repeat 8 times
58
+ set currentLabel to my axName(btn)
59
+ if currentLabel is wanted or (wanted is "__UNNAMED_EFFORT__" and currentLabel is "") then return
60
+ tell application "System Events" to tell process "Conductor"
61
+ perform action "AXPress" of btn
62
+ end tell
63
+ delay 0.35
64
+ set btn to my effortButton()
65
+ if btn is missing value then error "the effort control vanished mid-cycle"
66
+ end repeat
67
+ error "couldn't set effort to " & wanted
68
+ end setEffort
69
+
70
+ on setPlan(wanted)
71
+ set box to my controlNamed("Plan")
72
+ if box is missing value then error "couldn't find the Plan toggle"
73
+ tell application "System Events" to tell process "Conductor"
74
+ set current to ((value of box) as text)
75
+ if (wanted is "1" and current is "0") or (wanted is "0" and current is not "0") then
76
+ perform action "AXPress" of box
77
+ delay 0.4
78
+ if ((value of box) as text) is current then error "the Plan toggle didn't change"
79
+ end if
80
+ end tell
81
+ end setPlan
82
+
83
+ on pressFast()
84
+ -- Fast has no AX state to read (its label is always "Fast"), so the caller
85
+ -- decides whether a press is needed and re-checks the DB afterwards.
86
+ set btn to my controlNamed("Fast")
87
+ if btn is missing value then error "this model has no Fast toggle"
88
+ tell application "System Events" to tell process "Conductor"
89
+ perform action "AXPress" of btn
90
+ end tell
91
+ delay 0.4
92
+ end pressFast
93
+
94
+ on firstLine(s)
95
+ set saved to AppleScript's text item delimiters
96
+ set AppleScript's text item delimiters to linefeed
97
+ set parts to text items of s
98
+ set AppleScript's text item delimiters to saved
99
+ return item 1 of parts
100
+ end firstLine
101
+
102
+ on pickerShowsModel(popup, wanted)
103
+ set pickerLabel to my tabLabel(popup)
104
+ if pickerLabel contains ("(" & wanted & ")") then return true
105
+ -- The menu can label a newly released model "Opus 5 NEW", while the
106
+ -- composer deliberately drops that temporary badge and says "Opus 5".
107
+ -- Treat that exact display form as the same choice; the surrounding
108
+ -- parentheses keep "Opus 5" distinct from a longer model name.
109
+ if wanted ends with " NEW" then
110
+ set baseName to text 1 thru -5 of wanted
111
+ if pickerLabel contains ("(" & baseName & ")") then return true
112
+ end if
113
+ return false
114
+ end pickerShowsModel
115
+
116
+ on modelMenuItem(wanted)
117
+ -- Resolve one model from the picker that is already open. The menu can append
118
+ -- `NEW` to a freshly released model while the relay deliberately exposes the
119
+ -- stable name without it, so the same unique-prefix rule as setModel applies.
120
+ set chosen to missing value
121
+ set loose to {}
122
+ set wa to my webArea()
123
+ tell application "System Events" to tell process "Conductor"
124
+ repeat with m in (get UI elements of wa whose role is "AXMenu")
125
+ repeat with mi in (get UI elements of m whose role is "AXMenuItem")
126
+ set label to my firstLine(my tabLabel(mi))
127
+ if label is wanted then
128
+ set chosen to contents of mi
129
+ else if label starts with wanted then
130
+ set end of loose to contents of mi
131
+ end if
132
+ end repeat
133
+ end repeat
134
+ end tell
135
+ if chosen is missing value and (count of loose) is 1 then set chosen to item 1 of loose
136
+ if chosen is missing value then
137
+ if (count of loose) > 1 then error "several models match " & wanted
138
+ error "no model named " & wanted
139
+ end if
140
+ return chosen
141
+ end modelMenuItem
142
+
143
+ on modelDefaultButton(menuItem)
144
+ -- Every non-default row has a child star button named "Set <model> as
145
+ -- default and select". The starred row has no button at all. Read the role and
146
+ -- label together so an unrelated control a future picker grows cannot be
147
+ -- mistaken for a global preference write.
148
+ repeat with childNode in my axKids(menuItem)
149
+ set childEl to contents of childNode
150
+ if (my axRole(childEl)) is "AXButton" and (my axName(childEl)) contains " as default" then return childEl
151
+ end repeat
152
+ return missing value
153
+ end modelDefaultButton
154
+
155
+ on modelMenuItems()
156
+ set found to {}
157
+ set wa to my webArea()
158
+ tell application "System Events" to tell process "Conductor"
159
+ repeat with m in (get UI elements of wa whose role is "AXMenu")
160
+ set found to found & (get UI elements of m whose role is "AXMenuItem")
161
+ end repeat
162
+ end tell
163
+ return found
164
+ end modelMenuItems
165
+
166
+ on defaultModelInOpenPicker()
167
+ -- The selected star is exposed by *removing* the star button from that row.
168
+ -- Require exactly one such row. If managed settings disable every star, or a
169
+ -- Conductor release changes this tree, failing closed is safer than reporting
170
+ -- that whichever model was requested became the user's global default.
171
+ set defaults to {}
172
+ repeat with mi in my modelMenuItems()
173
+ set menuItem to contents of mi
174
+ if (my modelDefaultButton(menuItem)) is missing value then set end of defaults to my firstLine(my tabLabel(menuItem))
175
+ end repeat
176
+ if (count of defaults) is not 1 then error "couldn't identify Conductor's default model"
177
+ return item 1 of defaults
178
+ end defaultModelInOpenPicker
179
+
180
+ on modelPickerButton()
181
+ set popup to missing value
182
+ repeat with entry in my composerControls()
183
+ set c to contents of entry
184
+ if my tabLabel(c) contains "Change agent" then set popup to c
185
+ end repeat
186
+ if popup is missing value then error "couldn't find the model picker"
187
+ return popup
188
+ end modelPickerButton
189
+
190
+ on openModelPicker()
191
+ set popup to my modelPickerButton()
192
+ tell application "System Events" to tell process "Conductor"
193
+ perform action "AXPress" of popup
194
+ end tell
195
+ delay 1.0
196
+ end openModelPicker
197
+
198
+ on setDefaultModel(wanted)
199
+ -- Conductor's star is one combined action: its accessible name is "Set … as
200
+ -- default and select". Mirror that rather than pretending this only changes a
201
+ -- preference — the target chat switches model too, and the picker label is
202
+ -- read back below as the receipt for that half.
203
+ set initialPopup to my modelPickerButton()
204
+ set alreadySelected to my pickerShowsModel(initialPopup, wanted)
205
+ tell application "System Events" to tell process "Conductor"
206
+ perform action "AXPress" of initialPopup
207
+ end tell
208
+ delay 1.0
209
+ try
210
+ set chosen to my modelMenuItem(wanted)
211
+ set chosenLabel to my firstLine(my tabLabel(chosen))
212
+ set currentDefault to my defaultModelInOpenPicker()
213
+ if currentDefault is chosenLabel then
214
+ if alreadySelected then
215
+ tell application "System Events" to key code 53
216
+ return chosenLabel
217
+ end if
218
+ -- The star is already selected, but this chat is not. Its row is now
219
+ -- the remaining half of Conductor's combined action.
220
+ tell application "System Events" to tell process "Conductor"
221
+ perform action "AXPress" of chosen
222
+ end tell
223
+ delay 0.8
224
+ set selectedPopup to my modelPickerButton()
225
+ if not my pickerShowsModel(selectedPopup, chosenLabel) then error "the model didn't switch to " & chosenLabel
226
+ return chosenLabel
227
+ end if
228
+ set starButton to my modelDefaultButton(chosen)
229
+ if starButton is missing value then error "the default-model star is missing for " & chosenLabel
230
+ tell application "System Events" to tell process "Conductor"
231
+ perform action "AXPress" of starButton
232
+ end tell
233
+ delay 0.8
234
+ set selectedPopup to my modelPickerButton()
235
+ if not my pickerShowsModel(selectedPopup, chosenLabel) then error "the model didn't switch to " & chosenLabel
236
+ -- Re-open and read the star back. Pressing is fire-and-forget; the only safe
237
+ -- success is the requested row becoming the unique one with no star button.
238
+ my openModelPicker()
239
+ set verified to my modelMenuItem(wanted)
240
+ set verifiedLabel to my firstLine(my tabLabel(verified))
241
+ set currentDefault to my defaultModelInOpenPicker()
242
+ tell application "System Events" to key code 53
243
+ if currentDefault is not verifiedLabel then error "Conductor didn't mark " & verifiedLabel & " as the default model"
244
+ return verifiedLabel
245
+ on error errText number errNum
246
+ tell application "System Events" to key code 53
247
+ error errText number errNum
248
+ end try
249
+ end setDefaultModel
250
+
251
+ on setModel(wanted)
252
+ set popup to missing value
253
+ repeat with entry in my composerControls()
254
+ set c to contents of entry
255
+ if my tabLabel(c) contains "Change agent" then set popup to c
256
+ end repeat
257
+ if popup is missing value then error "couldn't find the model picker"
258
+ if my pickerShowsModel(popup, wanted) then return
259
+ tell application "System Events" to tell process "Conductor"
260
+ perform action "AXPress" of popup
261
+ end tell
262
+ delay 1.0
263
+ -- The relay removes a temporary `NEW` badge before it sends this label back,
264
+ -- so a prefix match resolves that menu item. It is accepted only when unique:
265
+ -- "Sonnet 4.6" would
266
+ -- otherwise also match "Sonnet 4.6 1M"), which must fail rather than guess.
267
+ set chosen to missing value
268
+ set loose to {}
269
+ set wa to my webArea()
270
+ tell application "System Events" to tell process "Conductor"
271
+ repeat with m in (get UI elements of wa whose role is "AXMenu")
272
+ repeat with mi in (get UI elements of m whose role is "AXMenuItem")
273
+ set label to my firstLine(my tabLabel(mi))
274
+ if label is wanted then
275
+ set chosen to contents of mi
276
+ else if label starts with wanted then
277
+ set end of loose to contents of mi
278
+ end if
279
+ end repeat
280
+ end repeat
281
+ end tell
282
+ if chosen is missing value and (count of loose) is 1 then set chosen to item 1 of loose
283
+ if chosen is missing value then
284
+ tell application "System Events" to key code 53
285
+ if (count of loose) > 1 then error "several models match " & wanted
286
+ error "no model named " & wanted
287
+ end if
288
+ set chosenLabel to my firstLine(my tabLabel(chosen))
289
+ tell application "System Events" to tell process "Conductor"
290
+ perform action "AXPress" of chosen
291
+ end tell
292
+ delay 0.8
293
+ set popup to missing value
294
+ repeat with entry in my composerControls()
295
+ set c to contents of entry
296
+ if my tabLabel(c) contains "Change agent" then set popup to c
297
+ end repeat
298
+ if popup is missing value then error "the model picker vanished"
299
+ if not my pickerShowsModel(popup, chosenLabel) then error "the model didn't switch to " & chosenLabel
300
+ end setModel
301
+
302
+ on listModels()
303
+ -- Enumerate the picker rather than hard-coding a model list that would rot on
304
+ -- every Conductor release. The first tagged line carries the unique starred
305
+ -- row, followed by the ordinary labels. Opens the menu, reads, closes it again.
306
+ my openModelPicker()
307
+ try
308
+ set labels to {}
309
+ repeat with mi in my modelMenuItems()
310
+ set end of labels to my firstLine(my tabLabel(mi))
311
+ end repeat
312
+ set defaultLabel to my defaultModelInOpenPicker()
313
+ tell application "System Events" to key code 53
314
+ set saved to AppleScript's text item delimiters
315
+ set AppleScript's text item delimiters to linefeed
316
+ set joined to labels as text
317
+ set AppleScript's text item delimiters to saved
318
+ return "__CONDUCTOR_DEFAULT_MODEL__" & tab & defaultLabel & linefeed & joined
319
+ on error errText number errNum
320
+ tell application "System Events" to key code 53
321
+ error errText number errNum
322
+ end try
323
+ end listModels
324
+
325
+ on applyAgentOptions()
326
+ set wantEffort to system attribute "RELAY_SET_EFFORT"
327
+ set wantPlan to system attribute "RELAY_SET_PLAN"
328
+ set wantFast to system attribute "RELAY_SET_FAST"
329
+ set wantModel to system attribute "RELAY_SET_MODEL"
330
+ if wantModel is not "" then my setModel(wantModel)
331
+ if wantEffort is not "" then my setEffort(wantEffort)
332
+ if wantPlan is not "" then my setPlan(wantPlan)
333
+ if wantFast is "1" then my pressFast()
334
+ end applyAgentOptions
@@ -0,0 +1,149 @@
1
+
2
+ on tabGroups()
3
+ -- Level-order search, returning every tab group at the shallowest depth that
4
+ -- has one (the chat strip and the terminal strip are siblings). Bounded: the
5
+ -- pane sits ~5 levels down, and we must never descend into the transcript.
6
+ my requireWindow()
7
+ tell application "System Events" to tell process "Conductor"
8
+ set level to {window 1}
9
+ set depth to 0
10
+ repeat while (count of level) > 0 and depth < 8
11
+ set found to {}
12
+ set nextLevel to {}
13
+ repeat with entry in level
14
+ set node to contents of entry
15
+ try
16
+ set found to found & (get UI elements of node whose role is "AXTabGroup")
17
+ set nextLevel to nextLevel & (get UI elements of node)
18
+ end try
19
+ end repeat
20
+ if (count of found) > 0 then return found
21
+ set level to nextLevel
22
+ set depth to depth + 1
23
+ end repeat
24
+ end tell
25
+ return {}
26
+ end tabGroups
27
+
28
+ on stripRadios(tg)
29
+ -- Every radio button in a tab strip, whether it is a direct child or sits one
30
+ -- group deep, in tree order. Both shapes are live at once: the terminal strip's
31
+ -- tabs are direct children, while a chat tab is wrapped in its own AXGroup with
32
+ -- the file-view toggle beside it as a plain child. Reading only the direct ones
33
+ -- (and stopping as soon as any turned up) is what made a chat strip look like a
34
+ -- one-tab strip called "Close file view".
35
+ tell application "System Events" to tell process "Conductor"
36
+ set acc to {}
37
+ repeat with k in (UI elements of tg)
38
+ set node to contents of k
39
+ if (role of node) is "AXRadioButton" then
40
+ set end of acc to node
41
+ else
42
+ try
43
+ set acc to acc & (get UI elements of node whose role is "AXRadioButton")
44
+ end try
45
+ end if
46
+ end repeat
47
+ return acc
48
+ end tell
49
+ end stripRadios
50
+
51
+ on chatTabs(tg)
52
+ -- Of those, a chat tab is the one whose accessible name is its own close action,
53
+ -- "Close chat <title>". The strip's other radio button is "Close file view",
54
+ -- which is not a chat and must not shift the indices this file addresses tabs
55
+ -- by — off by one is how a prompt lands in the wrong conversation. Falls back to
56
+ -- every radio button when none carries the prefix, so a renamed label costs the
57
+ -- filter rather than emptying the strip.
58
+ set radios to my stripRadios(tg)
59
+ set chats to {}
60
+ repeat with entry in radios
61
+ set node to contents of entry
62
+ if (my axName(node)) starts with "Close chat" then set end of chats to node
63
+ end repeat
64
+ if (count of chats) > 0 then return chats
65
+ return radios
66
+ end chatTabs
67
+
68
+ on tabLabel(t)
69
+ tell application "System Events" to tell process "Conductor"
70
+ return (name of t) as text
71
+ end tell
72
+ end tabLabel
73
+
74
+ on pickChatStrip(strips, wantCount, wantTitle)
75
+ -- Score each candidate strip: a label match outweighs a tab-count match, and
76
+ -- a tie means we cannot tell the chat strip from the terminal strip.
77
+ set best to missing value
78
+ set bestTabs to {}
79
+ set bestScore to 0
80
+ set tied to false
81
+ repeat with entry in strips
82
+ set tg to contents of entry
83
+ set strip to my chatTabs(tg)
84
+ set score to 0
85
+ if wantCount > 0 and (count of strip) is wantCount then set score to score + 1
86
+ if wantTitle is not "" then
87
+ repeat with t in strip
88
+ if (my tabLabel(t)) contains wantTitle then
89
+ set score to score + 2
90
+ exit repeat
91
+ end if
92
+ end repeat
93
+ end if
94
+ if score > bestScore then
95
+ set bestScore to score
96
+ set best to tg
97
+ set bestTabs to strip
98
+ set tied to false
99
+ else if score is bestScore and score > 0 then
100
+ set tied to true
101
+ end if
102
+ end repeat
103
+ if bestScore is 0 then error "couldn't identify the chat tab strip"
104
+ if tied then error "can't tell which tab strip holds the target chat"
105
+ return {best, bestTabs}
106
+ end pickChatStrip
107
+
108
+ on lastPathSegment(s)
109
+ set saved to AppleScript's text item delimiters
110
+ set AppleScript's text item delimiters to "/"
111
+ set parts to text items of s
112
+ set AppleScript's text item delimiters to saved
113
+ return item -1 of parts
114
+ end lastPathSegment
115
+
116
+ on paneLabels(tg, wantRole)
117
+ -- Kept out of the caller's scope on purpose: inside a System Events tell,
118
+ -- ordinary-looking names (tabs, count) resolve as app terms instead of vars.
119
+ tell application "System Events" to tell process "Conductor"
120
+ set pane to value of attribute "AXParent" of tg
121
+ return (name of (UI elements of pane whose role is wantRole))
122
+ end tell
123
+ end paneLabels
124
+
125
+ on anyContains(haystack, needle)
126
+ repeat with entry in haystack
127
+ try
128
+ if (entry as text) contains needle then return true
129
+ end try
130
+ end repeat
131
+ return false
132
+ end anyContains
133
+
134
+ on assertWorkspace(tg)
135
+ -- The pane holding the chat strip also labels the open workspace: an
136
+ -- AXStaticText with the branch (sans owner prefix) and a repo popup button.
137
+ set wantBranch to system attribute "RELAY_WS_BRANCH"
138
+ if wantBranch is "" then return
139
+ set tail to my lastPathSegment(wantBranch)
140
+ if not (my anyContains(my paneLabels(tg, "AXStaticText"), tail)) then
141
+ error "the palette didn't land on " & tail
142
+ end if
143
+ set wantRepo to system attribute "RELAY_WS_REPO"
144
+ if wantRepo is not "" then
145
+ if not (my anyContains(my paneLabels(tg, "AXPopUpButton"), wantRepo)) then
146
+ error "the palette didn't land in " & wantRepo
147
+ end if
148
+ end if
149
+ end assertWorkspace
@@ -0,0 +1,195 @@
1
+
2
+ on normalizeNewlines(s)
3
+ -- "do shell script" hands back CR-delimited text; the composer reads back LF.
4
+ -- Without this the verification below never matches a multi-line prompt.
5
+ set saved to AppleScript's text item delimiters
6
+ set AppleScript's text item delimiters to return
7
+ set parts to text items of s
8
+ set AppleScript's text item delimiters to linefeed
9
+ set joined to parts as text
10
+ set AppleScript's text item delimiters to saved
11
+ return joined
12
+ end normalizeNewlines
13
+
14
+ on composerField()
15
+ -- The composer's AXTextArea: an AXGroup named "composer" in the chat pane, one
16
+ -- text area inside it. Resolved once and handed to both the fill and the send
17
+ -- confirm below, because reaching it means walking the pane from the tab strip
18
+ -- and that walk measured ~0.5s on a live window. Returns missing value when the
19
+ -- composer isn't there at all, which is the caller's cue to fall back to pasting.
20
+ set strips to my tabGroups()
21
+ if (count of strips) is 0 then return missing value
22
+ try
23
+ tell application "System Events" to tell process "Conductor"
24
+ set pane to value of attribute "AXParent" of (item 1 of strips)
25
+ set composerBox to item 1 of (UI elements of pane whose name is "composer")
26
+ return item 1 of (UI elements of composerBox whose role is "AXTextArea")
27
+ end tell
28
+ end try
29
+ return missing value
30
+ end composerField
31
+
32
+ on composerTextOf(textBox)
33
+ -- What the box holds now, or missing value when it cannot be read at all. A pane
34
+ -- that re-rendered leaves the old reference pointing at a dead element, and
35
+ -- "can't read it" must never be reported as "still holding the prompt": that is
36
+ -- the one state that presses Enter again, and it would do so at the moment the
37
+ -- box may already be empty.
38
+ try
39
+ tell application "System Events" to tell process "Conductor"
40
+ return (value of textBox) as text
41
+ end tell
42
+ end try
43
+ return missing value
44
+ end composerTextOf
45
+
46
+ on fillComposer(textBox, promptText)
47
+ -- Write the prompt straight into the composer's AXTextArea instead of
48
+ -- stashing the clipboard, pressing Cmd+L and pasting. AXFocused and AXValue
49
+ -- are both settable, so this needs no keystrokes and no clipboard hijack.
50
+ -- Returns false (→ caller falls back to pasting) if anything looks off, but
51
+ -- *clears whatever it wrote first*: leaving half a prompt behind would make
52
+ -- the fallback paste append to it and send a garbled prompt.
53
+ if promptText is "" then return false
54
+ if textBox is missing value then return false
55
+ try
56
+ tell application "System Events" to tell process "Conductor"
57
+ set value of attribute "AXFocused" of textBox to true
58
+ set value of textBox to promptText
59
+ delay 0.25
60
+ if ((value of textBox) as text) does not contain promptText then
61
+ set value of textBox to ""
62
+ return false
63
+ end if
64
+ end tell
65
+ on error
66
+ try
67
+ my clearComposer(textBox)
68
+ end try
69
+ return false
70
+ end try
71
+ return true
72
+ end fillComposer
73
+
74
+ on clearComposer(textBox)
75
+ if textBox is missing value then return
76
+ tell application "System Events" to tell process "Conductor"
77
+ set value of textBox to ""
78
+ end tell
79
+ end clearComposer
80
+
81
+ on pressSend(queueIt)
82
+ -- Return sends, Command-Return queues the prompt behind the running answer.
83
+ -- The choice is made out here because inside a System Events tell an ordinary
84
+ -- parameter name can resolve as one of its own terms.
85
+ if queueIt then
86
+ tell application "System Events" to key code 36 using {command down}
87
+ else
88
+ tell application "System Events" to key code 36
89
+ end if
90
+ end pressSend
91
+
92
+ on submitComposer(textBox, promptText, queueIt)
93
+ -- Press Enter, then read the composer back. Conductor consumes the draft when it
94
+ -- accepts a prompt, so a box still holding the text is a keystroke that did
95
+ -- nothing — and that read is the only receipt a run can collect about its own
96
+ -- send.
97
+ --
98
+ -- Why it earns its place: the caller's other receipt is the transcript row, which
99
+ -- costs a 6s watch (CONFIRM_WINDOW_MS in server.ts) and then a whole second run
100
+ -- through activate, focus, tab and fill. Measured from this Mac's own relay logs,
101
+ -- a send whose single fault was a first Enter Conductor ignored took ~10s from the
102
+ -- prompt appearing in the composer to it being sent, with the text sitting on
103
+ -- screen the whole way. Exactly one user row was written, so the keystroke was
104
+ -- lost rather than delayed.
105
+ --
106
+ -- Pressing again is safe *because* of the read, not in spite of it: the prompt is
107
+ -- still in the box, so nothing was consumed, so there is nothing to duplicate.
108
+ -- The one state that would break that is a Conductor which queues on Command-Return
109
+ -- and *keeps* the draft, since the box would then read as unsent after a send that
110
+ -- worked. Unverified against the live app, and the only path that could reach it is
111
+ -- Command-Return in the phone's composer (Composer.tsx, a hardware keyboard); a tap
112
+ -- always sends. Worth checking before anything else starts queueing.
113
+ -- A Conductor too busy to have handled the first Enter is also too busy to answer
114
+ -- an AX read, which blocks here rather than handing back stale text.
115
+ --
116
+ -- **Two presses, not three, and the second one is on probation.** The first
117
+ -- occurrence in the wild (2026-09-01 16:59:57) had the box survive all three, and
118
+ -- no send has ever been recorded as rescued by a later press — so the value here
119
+ -- is the *detection*, which lets the caller stop watching for a row that cannot
120
+ -- come (`sendNeverStarted` in writes.ts) rather than spending the 6s window on it.
121
+ -- Every press past the first that doesn't work is pure delay added to a send that
122
+ -- is already failing, which is why the budget shrank from 3×1.2s to 2×0.8s.
123
+ --
124
+ -- Returns the press that cleared the box, or 0 for a box that never cleared. The
125
+ -- count is the point: a rescued send is otherwise indistinguishable from one that
126
+ -- worked first time, so the failure this handler exists for would stop appearing
127
+ -- in the log the day it stopped costing 10s, and nobody could tell whether it was
128
+ -- fixed or merely hidden.
129
+ if promptText is "" then
130
+ my pressSend(queueIt)
131
+ return 1
132
+ end if
133
+ repeat with pressCount from 1 to 2
134
+ my pressSend(queueIt)
135
+ if textBox is missing value then return pressCount
136
+ repeat with tick from 1 to 4
137
+ delay 0.2
138
+ set seen to my composerTextOf(textBox)
139
+ if seen is missing value then return pressCount
140
+ if seen does not contain promptText then return pressCount
141
+ end repeat
142
+ end repeat
143
+ return 0
144
+ end submitComposer
145
+
146
+ on pasteComposer()
147
+ -- Fallback for when the composer isn't reachable: Cmd+L focuses it (after the
148
+ -- palette, focus sits on a button, not the text box), then paste.
149
+ tell application "System Events"
150
+ set the clipboard to (do shell script "cat" & " " & quoted form of (system attribute "RELAY_PROMPT_FILE"))
151
+ keystroke "l" using {command down}
152
+ delay 0.3
153
+ keystroke "v" using {command down}
154
+ delay 0.15
155
+ end tell
156
+ end pasteComposer
157
+
158
+ on selectChatTab()
159
+ set wantIndex to (system attribute "RELAY_TAB_INDEX") as integer
160
+ if wantIndex is 0 then return
161
+ set wantCount to (system attribute "RELAY_TAB_COUNT") as integer
162
+ set wantTitle to my targetField(1)
163
+ set strips to my tabGroups()
164
+ if (count of strips) is 0 then
165
+ -- A lone chat has no ambiguity to resolve; more than one and we must not guess.
166
+ if wantCount <= 1 then return
167
+ error "couldn't find the chat tab strip"
168
+ end if
169
+ -- Assert the workspace first: every strip lives in the same pane, so this
170
+ -- reports "wrong workspace" rather than a confusing "no chat strip".
171
+ my assertWorkspace(item 1 of strips)
172
+ set picked to my pickChatStrip(strips, wantCount, wantTitle)
173
+ set tabs to item 2 of picked
174
+ tell application "System Events" to tell process "Conductor"
175
+ set target to missing value
176
+ if wantIndex <= (count of tabs) then
177
+ set candidate to contents of (item wantIndex of tabs)
178
+ if wantTitle is "" or (name of candidate) contains wantTitle then set target to candidate
179
+ end if
180
+ if target is missing value and wantTitle is not "" then
181
+ repeat with t in tabs
182
+ if (name of t) contains wantTitle then
183
+ if target is not missing value then error "several chat tabs match " & wantTitle
184
+ set target to contents of t
185
+ end if
186
+ end repeat
187
+ end if
188
+ if target is missing value then error "chat tab " & wantIndex & " not found"
189
+ if (value of target) is not true then
190
+ perform action "AXPress" of target
191
+ delay 0.5
192
+ end if
193
+ if (value of target) is not true then error "couldn't switch to the target chat tab"
194
+ end tell
195
+ end selectChatTab