conductor-remote 1.117.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-D3_YBIAr.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 +10 -10
  132. package/dist-node/src/{transcript.js → transcript/parser.js} +4 -4
  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-ByTwVInL.css +0 -1
  169. package/dist/assets/index-LAm9X7wv.js +0 -65
  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,359 @@
1
+
2
+ on sidebarLinks()
3
+ -- The sidebar rows are AXLinks named "<repo> <title> +adds -dels". They live
4
+ -- two levels under the web area; collect them wherever they are at that depth.
5
+ --
6
+ -- `get` in front of each `whose` clause is load-bearing, and it is the whole
7
+ -- cost of this handler. Without it the clause stays a *reference*, so pulling
8
+ -- the elements out of it one at a time re-runs the filter against the live tree
9
+ -- for each one: measured on a 45-row sidebar, walking the references cost
10
+ -- 10.8s against 0.9s for the same query forced to a list once. That is paid by
11
+ -- every send the deep link doesn't answer and by every status change, which has
12
+ -- no path but this one.
13
+ set wa to my webArea()
14
+ tell application "System Events" to tell process "Conductor"
15
+ set out to {}
16
+ repeat with a in (UI elements of wa)
17
+ try
18
+ set out to out & (get UI elements of a whose role is "AXLink")
19
+ repeat with b in (UI elements of a)
20
+ set out to out & (get UI elements of b whose role is "AXLink")
21
+ end repeat
22
+ end try
23
+ end repeat
24
+ return out
25
+ end tell
26
+ end sidebarLinks
27
+
28
+ on findSidebarRowOnce()
29
+ set titles to my targetSidebarTitles()
30
+ if (count of titles) is 0 then return missing value
31
+ set repoName to system attribute "RELAY_WS_REPO"
32
+ set pair to my sidebarRowsAndNames()
33
+ set refs to item 1 of pair
34
+ set nms to item 2 of pair
35
+ set total to count of refs
36
+ if total is 0 or (count of nms) is not total then return missing value
37
+ repeat with candidate in titles
38
+ set needle to candidate as text
39
+ if needle is not "" then
40
+ set hits to {}
41
+ repeat with i from 1 to total
42
+ set rowName to my textOrEmpty(item i of nms)
43
+ if rowName contains needle then
44
+ if repoName is "" or rowName contains repoName then set end of hits to i
45
+ end if
46
+ end repeat
47
+ if (count of hits) is 1 then
48
+ set idx to item 1 of hits
49
+ set hit to item idx of refs
50
+ -- The references and the names are two reads, so a sidebar that
51
+ -- re-rendered between them (every commit rewrites a row's
52
+ -- "+adds -dels") would have shifted the index under us — and pressing
53
+ -- the neighbour sets the wrong workspace's status, which is the one
54
+ -- failure this whole file is built to refuse. Confirm the element
55
+ -- still carries the name it was picked for.
56
+ if (my axName(hit)) is (my textOrEmpty(item idx of nms)) then return hit
57
+ return missing value
58
+ end if
59
+ end if
60
+ end repeat
61
+ return missing value
62
+ end findSidebarRowOnce
63
+
64
+ on findSidebarRow()
65
+ -- The workspace's own sidebar row. Only rows that are actually rendered exist
66
+ -- in the AX tree (a collapsed section has none — expandSections is what opens
67
+ -- those), and the title follows Conductor's precedence, so we try each
68
+ -- candidate and require a *unique* hit — anything ambiguous returns nothing
69
+ -- rather than guessing at a neighbour. Two passes because the only thing that
70
+ -- fails the check inside is a sidebar that moved mid-read, and that is worth
71
+ -- one cheap retry.
72
+ set hit to my findSidebarRowOnce()
73
+ if hit is not missing value then return hit
74
+ return my findSidebarRowOnce()
75
+ end findSidebarRow
76
+
77
+ on textOrEmpty(v)
78
+ try
79
+ if v is missing value then return ""
80
+ return v as text
81
+ on error
82
+ return ""
83
+ end try
84
+ end textOrEmpty
85
+
86
+ on childRoles(el)
87
+ -- Every child's role in *one* Apple event. Every AX read here is a round trip
88
+ -- to another process, so asking element by element is what turned a sidebar
89
+ -- scan into 15 seconds — more, by itself, than this write's whole budget.
90
+ tell application "System Events" to tell process "Conductor"
91
+ try
92
+ return role of (UI elements of el)
93
+ on error
94
+ return {}
95
+ end try
96
+ end tell
97
+ end childRoles
98
+
99
+ on childRolesAndNames(el)
100
+ tell application "System Events" to tell process "Conductor"
101
+ try
102
+ return {role of (UI elements of el), name of (UI elements of el)}
103
+ on error
104
+ return {{}, {}}
105
+ end try
106
+ end tell
107
+ end childRolesAndNames
108
+
109
+ on looksLikeSidebarList(el)
110
+ -- The sidebar's workspace list is one flat run of section headers
111
+ -- (AXStaticText) and workspace rows (AXLink), with nothing else beside them.
112
+ -- That shape is the whole test, because the *transcript* hangs off the same
113
+ -- web area and holds AXLinks of its own — "the element with links in it"
114
+ -- would happily hand back a message bubble, and every press below would then
115
+ -- land in one.
116
+ set roleList to my childRoles(el)
117
+ if (count of roleList) is 0 then return false
118
+ set seenLink to false
119
+ repeat with r in roleList
120
+ set rt to my textOrEmpty(contents of r)
121
+ if rt is "AXLink" then
122
+ set seenLink to true
123
+ else if rt is not "AXStaticText" then
124
+ return false
125
+ end if
126
+ end repeat
127
+ return seenLink
128
+ end looksLikeSidebarList
129
+
130
+ on sidebarList()
131
+ -- Found by that shape rather than by an index, since the chrome above it
132
+ -- (project pickers, the resource-usage button) comes and goes. Two levels
133
+ -- deep, the same reach sidebarLinks() uses.
134
+ set wa to my webArea()
135
+ repeat with a in (my axKids(wa))
136
+ set nodeA to contents of a
137
+ if my looksLikeSidebarList(nodeA) then return nodeA
138
+ repeat with b in (my axKids(nodeA))
139
+ set nodeB to contents of b
140
+ if my looksLikeSidebarList(nodeB) then return nodeB
141
+ end repeat
142
+ end repeat
143
+ return missing value
144
+ end sidebarList
145
+
146
+ on sidebarRowsAndNames()
147
+ -- Every sidebar row paired with its name. Two Apple events off the list found
148
+ -- above, against one per row before.
149
+ set listEl to my sidebarList()
150
+ if listEl is not missing value then
151
+ tell application "System Events" to tell process "Conductor"
152
+ try
153
+ return {(UI elements of listEl whose role is "AXLink"), name of (UI elements of listEl whose role is "AXLink")}
154
+ end try
155
+ end tell
156
+ end if
157
+ -- No list matched the shape, so Conductor has reorganised its sidebar. Fall
158
+ -- back to the broad two-level sweep and one name read per row: slow, and the
159
+ -- reason the fast path exists, but it assumes nothing about the layout.
160
+ set refs to my sidebarLinks()
161
+ set nms to {}
162
+ repeat with entry in refs
163
+ set end of nms to my axName(contents of entry)
164
+ end repeat
165
+ return {refs, nms}
166
+ end sidebarRowsAndNames
167
+
168
+ on looksNumeric(t)
169
+ -- A folded section draws its row count where its rows would be. That number is
170
+ -- a sibling of the header, not a child of it, so it has to be told apart from a
171
+ -- header by hand — digits only, which no status or repo name is.
172
+ if t is "" then return false
173
+ repeat with c in (characters of t)
174
+ if "0123456789" does not contain (c as text) then return false
175
+ end repeat
176
+ return true
177
+ end looksNumeric
178
+
179
+ on collapsedSectionNames()
180
+ -- The sections someone has folded shut. A section's rows are *siblings* of
181
+ -- its header, so an expanded one is followed by an AXLink and a collapsed one
182
+ -- by its count, by the next header, or by nothing — and a collapsed section
183
+ -- renders no rows at all, which is why its workspaces are invisible to
184
+ -- Accessibility. Names, not element references: everything below re-finds the
185
+ -- header when it needs it, because the list re-renders under us (a status
186
+ -- change moves a row to another group) and a System Events reference is an
187
+ -- index into a tree that has already changed.
188
+ set listEl to my sidebarList()
189
+ if listEl is missing value then return {}
190
+ set pair to my childRolesAndNames(listEl)
191
+ set roleList to item 1 of pair
192
+ set nameList to item 2 of pair
193
+ set total to count of roleList
194
+ if total is 0 or (count of nameList) is not total then return {}
195
+ set out to {}
196
+ repeat with i from 1 to total
197
+ if (my textOrEmpty(item i of roleList)) is "AXStaticText" then
198
+ set nm to my textOrEmpty(item i of nameList)
199
+ if nm is not "" and not (my looksNumeric(nm)) then
200
+ set nextRole to ""
201
+ if i < total then set nextRole to my textOrEmpty(item (i + 1) of roleList)
202
+ if nextRole is not "AXLink" then set end of out to nm
203
+ end if
204
+ end if
205
+ end repeat
206
+ return out
207
+ end collapsedSectionNames
208
+
209
+ on sectionHeaderNamed(wanted)
210
+ -- Unique or nothing, like every other lookup here: two sections cannot share
211
+ -- a name, so an ambiguous hit means we are reading the wrong list.
212
+ set listEl to my sidebarList()
213
+ if listEl is missing value then return missing value
214
+ tell application "System Events" to tell process "Conductor"
215
+ try
216
+ set hits to (UI elements of listEl whose role is "AXStaticText" and name is wanted)
217
+ on error
218
+ return missing value
219
+ end try
220
+ end tell
221
+ if (count of hits) is 1 then return item 1 of hits
222
+ return missing value
223
+ end sectionHeaderNamed
224
+
225
+ on toggleSection(headerName)
226
+ set head to my sectionHeaderNamed(headerName)
227
+ if head is missing value then return false
228
+ tell application "System Events" to tell process "Conductor"
229
+ try
230
+ perform action "AXScrollToVisible" of head
231
+ end try
232
+ try
233
+ perform action "AXPress" of head
234
+ return true
235
+ on error
236
+ return false
237
+ end try
238
+ end tell
239
+ end toggleSection
240
+
241
+ on expandSections()
242
+ -- Open every folded section and report which ones we opened, so the caller can
243
+ -- put the sidebar back the way it found it. The header is the whole control —
244
+ -- it carries AXPress and toggles the group — so this needs no chevron, which
245
+ -- the webview does not expose anyway.
246
+ set opened to {}
247
+ repeat with nm in (my collapsedSectionNames())
248
+ if my toggleSection(nm as text) then set end of opened to (nm as text)
249
+ end repeat
250
+ if (count of opened) > 0 then delay 0.6
251
+ return opened
252
+ end expandSections
253
+
254
+ on restoreSections(names)
255
+ -- Fold back exactly what we opened. Best-effort on purpose: this runs after
256
+ -- the write has already landed, so a header that moved or vanished must not
257
+ -- turn a status change that worked into a reported failure.
258
+ repeat with nm in names
259
+ try
260
+ my toggleSection(nm as text)
261
+ end try
262
+ end repeat
263
+ end restoreSections
264
+
265
+ on focusViaSidebar()
266
+ -- Press that row: no keystrokes at all, so nothing can be swallowed by a
267
+ -- focused field or fire a palette *command*. Being wrong is survivable here —
268
+ -- assertWorkspace re-checks before we type — and a miss falls back to the palette.
269
+ set hit to my findSidebarRow()
270
+ if hit is missing value then return false
271
+ tell application "System Events" to tell process "Conductor"
272
+ perform action "AXPress" of hit
273
+ end tell
274
+ delay 0.9
275
+ return true
276
+ end focusViaSidebar
277
+
278
+ on focusViaPalette()
279
+ tell application "System Events"
280
+ key code 53
281
+ delay 0.25
282
+ keystroke "k" using {command down}
283
+ delay 0.7
284
+ keystroke (my targetField(2))
285
+ delay 0.9
286
+ key code 36
287
+ delay 1.3
288
+ end tell
289
+ end focusViaPalette
290
+
291
+ on paneAgrees()
292
+ -- Nothing in the open pane contradicts the target: a chat pane is there and its
293
+ -- header assertion passed (which is a no-op when there's no branch to check it
294
+ -- against). This is what confirms a sidebar press landed.
295
+ try
296
+ set strips to my tabGroups()
297
+ if (count of strips) is 0 then return false
298
+ my assertWorkspace(item 1 of strips)
299
+ return true
300
+ end try
301
+ return false
302
+ end paneAgrees
303
+
304
+ on atTargetWorkspace()
305
+ -- "Are we already there?" — the *strict* form of the question, so it needs a
306
+ -- branch to have been checked and not merely skipped. Worth asking first because
307
+ -- the pane header answers in ~0.5s where reading the sidebar's rows to press one
308
+ -- measured 5-7s, and ~1s since that scan went to bulk reads — still the larger
309
+ -- cost of the two: a phone sends to the same workspace over and over, and
310
+ -- Conductor stays where it was left. Safe as a shortcut because it *is* the
311
+ -- assertion the send makes before typing anyway.
312
+ if (system attribute "RELAY_WS_BRANCH") is "" then return false
313
+ return my paneAgrees()
314
+ end atTargetWorkspace
315
+
316
+ on openViaDeepLink()
317
+ -- Conductor's *own* workspace link, the one its sidebar row menu copies under
318
+ -- "Copy link" (Cmd+Shift+C): one URL carrying the workspace id and the chat id,
319
+ -- addressed the way the relay already reads them rather than by any label a
320
+ -- release can rename. It replaces the entire focus dance below - no sidebar
321
+ -- scan, no palette keystrokes, no title precedence to reproduce - and measures
322
+ -- ~0.6s against the 5-10s the row press costs on a 30-workspace sidebar.
323
+ --
324
+ -- Two things it fixes outright rather than merely speeding up. It navigates by
325
+ -- *id*, so a collapsed sidebar section (which hides the row from Accessibility
326
+ -- entirely) stops mattering; and it opens the chat tab explicitly, so a pane
327
+ -- left on a file view or a terminal - where the chat strip does not exist in
328
+ -- the AX tree at all, and every send failed with "couldn't identify the chat
329
+ -- tab strip" - comes back to the composer on its own.
330
+ --
331
+ -- Still fail-closed: the URL is fire-and-forget, so what counts as success is
332
+ -- the same pane assertion every other path here has to pass. A Conductor
333
+ -- without the route (before 0.71) ignores it silently, and a scheme macOS
334
+ -- can't resolve fails the "open" outright - both fall through to the sidebar.
335
+ set linkURL to system attribute "RELAY_WS_LINK"
336
+ if linkURL is "" then return false
337
+ try
338
+ do shell script "open " & quoted form of linkURL
339
+ on error
340
+ return false
341
+ end try
342
+ -- Bounded on purpose: this poll is also the cost of *not* having the route, and
343
+ -- the fallback below still has to run inside the same budget afterwards.
344
+ repeat with attempt from 1 to 10
345
+ delay 0.15
346
+ if my paneAgrees() then return true
347
+ end repeat
348
+ return false
349
+ end openViaDeepLink
350
+
351
+ on focusWorkspace()
352
+ if (my targetField(2)) is "" then return
353
+ if my openViaDeepLink() then return
354
+ if my atTargetWorkspace() then return
355
+ if my focusViaSidebar() then
356
+ if my paneAgrees() then return
357
+ end if
358
+ my focusViaPalette()
359
+ end focusWorkspace
@@ -0,0 +1,160 @@
1
+ import { MAC_LOCKED } from "../shared.js";
2
+ import { screenLocked } from "./guards.js";
3
+ import { CONDUCTOR_HANDLERS, osaError, SEND_ATTEMPT_MS } from "./runner.js";
4
+ import { focusQuery, withTargetEnvironment, workspaceLink } from "./targeting.js";
5
+ import { exec, uiTurn } from "./ui-lock.js";
6
+ /**
7
+ * Stop the answer a chat is streaming — the desktop app's stop button, reached
8
+ * through the same focus-and-assert path as a send and then Conductor's own
9
+ * "Cancel agent" shortcut (see `cancelAgent` in src/writes/applescript/lifecycle.applescript for why a
10
+ * keystroke rather than the button).
11
+ *
12
+ * The branch is required rather than optional. Everywhere else a workspace with
13
+ * no branch merely loses the pane assertion; here that assertion is the only thing
14
+ * standing between "stop this agent" and "throw away a different agent's turn", so
15
+ * a target that can't be checked is refused instead of aimed.
16
+ *
17
+ * Nothing is confirmed here: `sessions.status` leaving `working` is the receipt and
18
+ * src/http/routes/sessions.ts waits for it, the same way agent settings are confirmed against the DB
19
+ * rather than against the UI that was just driven.
20
+ */
21
+ export async function stopTurn(target) {
22
+ if (!target.workspace.branch) {
23
+ return { ok: false, strategy: 'applescript', error: 'workspace has no branch to focus' };
24
+ }
25
+ const script = `
26
+ ${CONDUCTOR_HANDLERS}
27
+
28
+ my activateConductor()
29
+ my focusWorkspace()
30
+ my selectChatTab()
31
+ my cancelAgent()
32
+ return "ok"`.trim();
33
+ try {
34
+ await withTargetEnvironment(target, targetEnvironment => uiTurn(() => exec('osascript', ['-e', script], {
35
+ env: { ...process.env, ...targetEnvironment },
36
+ timeout: SEND_ATTEMPT_MS
37
+ })));
38
+ return { ok: true, strategy: 'applescript' };
39
+ }
40
+ catch (err) {
41
+ return { ok: false, strategy: 'applescript', error: osaError(err) };
42
+ }
43
+ }
44
+ /**
45
+ * Hide one chat through Conductor's own Close tab action (Command-W).
46
+ *
47
+ * Like `stopTurn`, the branch is required because Command-W acts on whatever has
48
+ * keyboard focus. `selectChatTab` proves the target and `closeChatTab` moves focus
49
+ * back out of the terminal before pressing it. Closing is reversible in Conductor,
50
+ * but a running chat still gets its desktop confirmation: `closeRunning` is the
51
+ * caller's explicit answer to that prompt, never something this layer infers.
52
+ *
53
+ * The script only reports that it pressed the action. `sessions.is_hidden` is the
54
+ * receipt; src/http/routes/sessions.ts waits until `Reads.listSessions` no longer returns this id.
55
+ */
56
+ export async function closeChat(target, closeRunning) {
57
+ if (!target.workspace.branch) {
58
+ return { ok: false, strategy: 'applescript', error: 'workspace has no branch to focus' };
59
+ }
60
+ const script = `
61
+ ${CONDUCTOR_HANDLERS}
62
+
63
+ my activateConductor()
64
+ my focusWorkspace()
65
+ my selectChatTab()
66
+ my closeChatTab()
67
+ return "ok"`.trim();
68
+ try {
69
+ await withTargetEnvironment(target, targetEnvironment => uiTurn(() => exec('osascript', ['-e', script], {
70
+ env: {
71
+ ...process.env,
72
+ ...targetEnvironment,
73
+ RELAY_CLOSE_RUNNING: closeRunning ? '1' : ''
74
+ },
75
+ timeout: SEND_ATTEMPT_MS
76
+ })));
77
+ return { ok: true, strategy: 'applescript' };
78
+ }
79
+ catch (err) {
80
+ return { ok: false, strategy: 'applescript', error: osaError(err) };
81
+ }
82
+ }
83
+ /**
84
+ * Conductor's workspace route restores a hidden session before selecting it.
85
+ * Keep the UI lease through the DB receipt: another focus operation must not
86
+ * navigate away while Conductor's asynchronous route handler is unhiding it.
87
+ * The callback is deliberately exact-id and read-only, supplied by the server.
88
+ */
89
+ export async function restoreChat(workspaceId, sessionId, isVisible) {
90
+ return uiTurn(async () => {
91
+ if (isVisible())
92
+ return { ok: true, strategy: 'deep-link', alreadyOpen: true };
93
+ if ((await screenLocked()) === true) {
94
+ return { ok: false, strategy: 'deep-link', error: `${MAC_LOCKED}. Unlock it, then try again.` };
95
+ }
96
+ try {
97
+ await exec('open', [workspaceLink(workspaceId, sessionId)], { timeout: 5_000 });
98
+ for (let attempt = 0; attempt < 40; attempt++) {
99
+ await new Promise(resolve => setTimeout(resolve, 250));
100
+ if (isVisible())
101
+ return { ok: true, strategy: 'deep-link' };
102
+ }
103
+ return {
104
+ ok: false,
105
+ strategy: 'deep-link',
106
+ error: 'Conductor has not restored this tab yet. Try again, or update Conductor on your Mac.'
107
+ };
108
+ }
109
+ catch (error) {
110
+ return { ok: false, strategy: 'deep-link', error: osaError(error) };
111
+ }
112
+ });
113
+ }
114
+ /**
115
+ * Open a new chat in the target workspace — Conductor's "New chat, same files"
116
+ * (Cmd+T). Focuses the workspace first (its own link, see `workspaceLink`), then
117
+ * Cmd+T; the caller detects the freshly-created session id from the DB.
118
+ *
119
+ * The pane is asserted before the keystroke for the same reason a send asserts before
120
+ * typing: `focusWorkspace` confirms every route it takes except its last one, the
121
+ * palette, and Cmd+T against an unconfirmed pane opens a tab in someone else's
122
+ * workspace. Nothing catches that afterwards — the caller looks for the new session in
123
+ * *this* workspace's tab list, so a stray tab reads as "the id could not be read back"
124
+ * while sitting in a conversation nobody asked to change.
125
+ *
126
+ * Cmd+L ("Focus chat input") goes first for the reason `cancelAgent` does the same: a
127
+ * keystroke lands wherever focus is, and a focused terminal panel swallows this one.
128
+ * Measured live before the fix — the run reported success, `sessions` gained nothing,
129
+ * and `terminal_sessions` gained a row in this very workspace at the second the chord
130
+ * was sent. Both chords are Conductor's own, confirmed against its Cmd+/ dialog.
131
+ */
132
+ export async function newChat(workspace) {
133
+ if (!focusQuery(workspace))
134
+ return { ok: false, strategy: 'applescript', error: 'workspace has no branch to focus' };
135
+ const script = `
136
+ ${CONDUCTOR_HANDLERS}
137
+
138
+ my activateConductor()
139
+ my focusWorkspace()
140
+ set strips to my tabGroups()
141
+ if (count of strips) is 0 then error "couldn't find the chat pane to open a tab in"
142
+ my assertWorkspace(item 1 of strips)
143
+ tell application "System Events"
144
+ keystroke "l" using {command down}
145
+ delay 0.2
146
+ keystroke "t" using {command down}
147
+ end tell`.trim();
148
+ try {
149
+ // Shares the focus path with a send, so it needs the same ceiling: 15s was under
150
+ // the cost of activating a cold Conductor and finding the row on its own.
151
+ await withTargetEnvironment({ workspace, sessionId: null }, targetEnvironment => uiTurn(() => exec('osascript', ['-e', script], {
152
+ env: { ...process.env, ...targetEnvironment },
153
+ timeout: SEND_ATTEMPT_MS
154
+ })));
155
+ return { ok: true, strategy: 'applescript' };
156
+ }
157
+ catch (err) {
158
+ return { ok: false, strategy: 'applescript', error: osaError(err) };
159
+ }
160
+ }
@@ -0,0 +1,77 @@
1
+ import { isLockedError } from "../shared.js";
2
+ import { exec } from "./ui-lock.js";
3
+ /**
4
+ * Failures no amount of retrying will fix, so a caller that retries can stop at
5
+ * once instead of spending a whole budget to arrive at the same sentence.
6
+ *
7
+ * Matched on phrases this file writes itself — the first two from `refusalReason`,
8
+ * the rest from the target checks below — never on macOS's own wording, which we
9
+ * quote verbatim precisely because it drifts. The refusals are the ones a node
10
+ * upgrade causes by silently revoking Accessibility: they fail instantly and
11
+ * identically every time, so making the phone sit through a whole retry budget to
12
+ * be told a permission is missing is worse than being told at once. The rest are
13
+ * malformed targets — no session, no branch — which no attempt can turn into one.
14
+ */
15
+ const TERMINAL_ERRORS = [
16
+ 'not trusted for Accessibility',
17
+ 'blocked the relay from controlling the UI',
18
+ 'no session id to target',
19
+ 'workspace has no branch to focus'
20
+ ];
21
+ export function retryWontHelp(error) {
22
+ return error !== undefined && TERMINAL_ERRORS.some(phrase => error.includes(phrase));
23
+ }
24
+ /**
25
+ * A send that failed because the Mac's screen is locked. Deliberately *not* in
26
+ * `TERMINAL_ERRORS`: it isn't terminal (an unlock fixes it) but it also isn't a
27
+ * warm-up cost a retry loop should burn budget on — the caller parks the prompt
28
+ * instead (src/delivery/parked.ts) and the queue delivers it when the lock lifts. Matched
29
+ * on the phrase every lock refusal in src/writes/applescript/window.applescript starts with; the
30
+ * words are ours, so they can't drift under us the way macOS's can. The phrase
31
+ * itself lives in src/shared.ts because the phone matches it too — it is what
32
+ * puts the Screen Sharing link beside a refusal nobody else can clear.
33
+ */
34
+ export function lockBlocked(error) {
35
+ return isLockedError(error);
36
+ }
37
+ /**
38
+ * A run that ended with the prompt still sitting in Conductor's composer
39
+ * (`submitComposer`). The draft was never consumed, so this run wrote no row and
40
+ * the caller's confirm window has nothing to wait for — six seconds spent watching
41
+ * for something the run already proved didn't happen. Only the *waiting* is
42
+ * skipped: an earlier attempt's row can still be arriving, so the caller checks
43
+ * once before typing again.
44
+ *
45
+ * Matched on the phrase the send script writes itself, like `lockBlocked` above,
46
+ * so macOS wording can't drift under it.
47
+ */
48
+ export function sendNeverStarted(error) {
49
+ return (error ?? '').includes('still sitting in its composer');
50
+ }
51
+ /**
52
+ * Node's own read of the lock screen — the same CGSessionCopyCurrentDictionary
53
+ * probe `screenLocked()` in src/writes/applescript/window.applescript makes, minus the AppleScript
54
+ * wrapper, so the parked-prompt queue can poll it without spinning up a whole
55
+ * UI script (and without needing Accessibility at all). `null` means the probe
56
+ * itself failed — callers should treat that as "try the send and let it tell
57
+ * you", not as either lock state.
58
+ */
59
+ export async function screenLocked() {
60
+ // Keep in lockstep with `screenLocked()` in src/writes/applescript/window.applescript, traps and
61
+ // all: $.CFBridgingRelease segfaults under JXA, and without the bindFunction
62
+ // rebind deepUnwrap reads the CF dictionary as undefined — a silent "unlocked"
63
+ // on a locked Mac.
64
+ const jxa = "ObjC.import('CoreGraphics'); ObjC.bindFunction('CGSessionCopyCurrentDictionary', ['id', []]); const d = ObjC.deepUnwrap($.CGSessionCopyCurrentDictionary()) || {}; d.CGSSessionScreenIsLocked ? 'locked' : 'unlocked'";
65
+ try {
66
+ const { stdout } = await exec('osascript', ['-l', 'JavaScript', '-e', jxa], { timeout: 5_000 });
67
+ const out = stdout.trim();
68
+ if (out === 'locked')
69
+ return true;
70
+ if (out === 'unlocked')
71
+ return false;
72
+ return null;
73
+ }
74
+ catch {
75
+ return null;
76
+ }
77
+ }
@@ -0,0 +1,67 @@
1
+ import { WorkflowGatedCommandError } from "../orchestration/workflow/effect-runner.js";
2
+ import { readConductorAppleScript } from "./applescript/source.js";
3
+ /**
4
+ * How long one AppleScript run may take before it's killed.
5
+ *
6
+ * Sized from measurement, not taste. A send that *worked* measured 23.6s end to
7
+ * end on a 30-workspace sidebar — past the 20s ceiling this replaces, which is why
8
+ * ordinary sends were being killed mid-run and reported as "Conductor took too long
9
+ * to respond". The cost is Accessibility round trips, not waiting: activating a
10
+ * backgrounded Conductor and reading the pane cost ~10s cold, and finding the
11
+ * sidebar row to press another ~10s.
12
+ *
13
+ * `openViaDeepLink` took the row scan out of the common path — a whole send now
14
+ * measures ~4s, and focusing alone ~2s against the ~18s the same focus costs when
15
+ * the link is unavailable — so this budget is really the *fallback's*, kept at the
16
+ * size that fallback still needs. A ceiling costs nothing when a send is fast; only
17
+ * a doomed one waits it out, and the caller's own deadline is what bounds that.
18
+ */
19
+ export const SEND_ATTEMPT_MS = 28_000;
20
+ /**
21
+ * A restart's own ceiling. It is longer than a send's because it is mostly *waiting*:
22
+ * up to 4s for the quit to be honoured, then a cold launch, then `waitForWindow(60)`'s
23
+ * 15s for the first window — none of which can be hurried, and all of which the caller
24
+ * would rather wait through than be told "try again" about.
25
+ */
26
+ export const RESTART_ATTEMPT_MS = 45_000;
27
+ /**
28
+ * A run's own ceiling, taken off the caller's deadline at the moment it actually
29
+ * starts.
30
+ *
31
+ * Both halves matter. `uiTurn` above means a run can sit in the queue behind
32
+ * another write, so a duration computed when it was *requested* would let a queued
33
+ * run overshoot a deadline the caller is still holding a phone open on.
34
+ * `SEND_ATTEMPT_MS` then caps it, because a caller with a minute of budget still
35
+ * shouldn't spend all of it on one doomed run when a retry is the thing that works.
36
+ * The floor keeps a squeezed run honest instead of passing `timeout: 0`, which node
37
+ * reads as "no timeout at all".
38
+ */
39
+ export function runCeiling(deadline) {
40
+ return Math.max(5_000, Math.min(SEND_ATTEMPT_MS, deadline - Date.now()));
41
+ }
42
+ /**
43
+ * Every AppleScript handler the write path uses, assembled as one program from
44
+ * src/writes/applescript/. The source manifest also drives checking and copying.
45
+ *
46
+ * The source module resolves its sibling assets in both the checkout and the
47
+ * emitted package. Joining inserts nothing and trimming matches the original
48
+ * single-file loader, so every actuator appends to the same program as before.
49
+ * Read once at import, because a missing copy is a packaging bug and a relay that
50
+ * refuses to boot naming the missing assets is the loudest way to say so.
51
+ */
52
+ export const CONDUCTOR_HANDLERS = readConductorAppleScript().trimEnd();
53
+ /** osascript echoes the whole failing script back; keep just the reason for the phone. */
54
+ export function osaError(err, timeoutMsg = 'Conductor took too long to respond') {
55
+ const raw = err instanceof WorkflowGatedCommandError
56
+ ? [err.message, err.stderr].filter(Boolean).join('\n')
57
+ : err instanceof Error
58
+ ? err.message
59
+ : String(err);
60
+ // A timeout kill carries no execution error at all — its first line is
61
+ // "Command failed: osascript -e" plus the whole script, which is useless here.
62
+ if ((err instanceof WorkflowGatedCommandError && err.code === 'timed_out') ||
63
+ (err && typeof err === 'object' && 'killed' in err && err.killed)) {
64
+ return timeoutMsg;
65
+ }
66
+ return raw.match(/execution error: (.+?) \(-?\d+\)/)?.[1] ?? raw.split('\n')[0];
67
+ }
@@ -14,7 +14,7 @@ import path from 'node:path';
14
14
  * UI updates correctly because it's the real dispatch path. It is also the most
15
15
  * update-fragile surface (a private, versioned IPC), which is why it lives
16
16
  * behind the Actuator interface and falls back to AppleScript when the socket
17
- * can't be reached (see writes.ts).
17
+ * can't be reached (see src/writes/actuator.ts).
18
18
  *
19
19
  * Reverse-engineered from conductor-runtime (Conductor 0.76):
20
20
  * - socket: `$TMPDIR/conductor-sidecar-v2-<sidecarPid>.sock`