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,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
@@ -0,0 +1,155 @@
1
+
2
+ (* -- Instant Hotspot ----------------------------------------------------------
3
+ The funnel watchdog's last resort when this Mac has no route: press the row
4
+ for a personal hotspot in Control Center's Wi-Fi popover. networksetup can
5
+ only join a network that is broadcasting, and a personal hotspot usually
6
+ is not - the row in the Wi-Fi menu is fed by Continuity over Bluetooth, and
7
+ pressing it asks the phone to wake its hotspot, exactly like a human
8
+ clicking the Wi-Fi menu. Everything here targets process "ControlCenter",
9
+ not Conductor - same Accessibility grant, different process, which is why
10
+ the refusal mapping below skips refusalReason's -1712 branch (its words are
11
+ about a wedged Conductor).
12
+ Addressed by AXIdentifier throughout ("com.apple.menuextra.wifi",
13
+ "wifi-network-<name>", "wifi-header") - the items' names are missing value
14
+ and any visible text is localized, the identifiers are neither (measured
15
+ live on macOS 26.5, 2026-08). A "whose value of attribute ..." filter fails
16
+ wholesale (-1728) when any element lacks the attribute - several menu bar
17
+ items do - so every lookup here walks with a per-item try instead.
18
+ The SE-terms trap at the top of CLAUDE.md's applescript section bit this
19
+ code through a HANDLER PARAMETER: findAxId's first argument was once named
20
+ `container`, a System Events dictionary word, so inside the tell it
21
+ resolved as the term and the walk found nothing - through an error-eating
22
+ try, so a provably open popover read as "no popover" for a whole debugging
23
+ session while every direct read worked. osacompile accepts it, the handler
24
+ check can't see it. Keep every identifier in these handlers boring and
25
+ un-dictionary-like.
26
+ The menu bar item is a TOGGLE: the same press opens and closes the
27
+ popover, so nothing here presses it without first checking which state the
28
+ popover is in (wifiPopoverWindow), and an already-open popover aborts the
29
+ run - a human may be mid-gesture in it, and their next click kills an
30
+ automated run anyway (contention looks exactly like the status-row menu's:
31
+ fails while someone uses the Mac, clean when it is unattended). *)
32
+
33
+ on wifiMenuBarItem()
34
+ try
35
+ with timeout of 6 seconds
36
+ tell application "System Events" to tell process "ControlCenter"
37
+ repeat with mi in menu bar items of menu bar 1
38
+ try
39
+ if (value of attribute "AXIdentifier" of mi) is "com.apple.menuextra.wifi" then return contents of mi
40
+ end try
41
+ end repeat
42
+ end tell
43
+ end timeout
44
+ on error errText number errNum
45
+ if errNum is not -1712 then
46
+ set refusal to my refusalReason({-1, errNum, errText})
47
+ if refusal is not "" then error refusal
48
+ end if
49
+ error "reading the menu bar failed (" & errNum & "): " & errText
50
+ end try
51
+ error "the Wi-Fi control is not in the menu bar - turn on Show in Menu Bar for Wi-Fi in System Settings > Control Center"
52
+ end wifiMenuBarItem
53
+
54
+ on findAxId(parentEl, wantedId, depth)
55
+ -- Depth-capped even though the popover is small: the cap is what keeps a
56
+ -- macOS reshuffle of this tree from turning one lookup into a full crawl.
57
+ -- `parentEl` is deliberately not `container` - see the section comment.
58
+ tell application "System Events"
59
+ try
60
+ set kids to UI elements of parentEl
61
+ on error
62
+ return missing value
63
+ end try
64
+ end tell
65
+ repeat with el in kids
66
+ try
67
+ tell application "System Events"
68
+ if (value of attribute "AXIdentifier" of el) is wantedId then return contents of el
69
+ end tell
70
+ end try
71
+ if depth > 1 then
72
+ set hit to my findAxId(el, wantedId, depth - 1)
73
+ if hit is not missing value then return hit
74
+ end if
75
+ end repeat
76
+ return missing value
77
+ end findAxId
78
+
79
+ on wifiPopoverWindow()
80
+ -- The popover is whichever ControlCenter window carries the "wifi-header"
81
+ -- id. Existence is read fresh on every call: pressing a network row can
82
+ -- close the popover by itself, and the menu bar item is a toggle, so a
83
+ -- blind second press would reopen what just closed.
84
+ tell application "System Events" to tell process "ControlCenter"
85
+ try
86
+ set wins to windows
87
+ on error
88
+ return missing value
89
+ end try
90
+ end tell
91
+ repeat with w in wins
92
+ if my findAxId(w, "wifi-header", 3) is not missing value then return contents of w
93
+ end repeat
94
+ return missing value
95
+ end wifiPopoverWindow
96
+
97
+ on closeWifiPopover(wifiItem)
98
+ if my wifiPopoverWindow() is not missing value then
99
+ try
100
+ tell application "System Events" to perform action "AXPress" of wifiItem
101
+ end try
102
+ end if
103
+ end closeWifiPopover
104
+
105
+ on joinInstantHotspot()
106
+ -- The name arrives via a file, like RELAY_PROMPT_FILE: do shell script
107
+ -- output is decoded as UTF-8, so a name with non-ASCII letters survives
108
+ -- where a system attribute read may not. AppleScript's default nonliteral
109
+ -- text comparison then treats composed and decomposed accents as equal,
110
+ -- which matters because the AX identifier uses whatever normalization
111
+ -- macOS chose.
112
+ set hotspotName to do shell script "cat " & quoted form of (system attribute "RELAY_HOTSPOT_FILE")
113
+ if hotspotName is "" then error "no hotspot name provided"
114
+ -- The lock screen hides the session from Accessibility, so behind it this
115
+ -- press has nothing to land on. Named up front, in the same words the send
116
+ -- path uses, instead of surfacing later as a popover that never opened.
117
+ if my screenLocked() is "locked" then error "The Mac is locked - the Wi-Fi menu can't be pressed until it unlocks"
118
+ set wifiItem to my wifiMenuBarItem()
119
+ if my wifiPopoverWindow() is not missing value then error "the Wi-Fi popover is already open - someone may be using it, so nothing was pressed"
120
+ tell application "System Events" to perform action "AXPress" of wifiItem
121
+ -- The popover renders in its own time (measured 1-2s), and a human's click
122
+ -- anywhere on the Mac closes it, so poll rather than sleep once.
123
+ set pop to missing value
124
+ repeat 8 times
125
+ delay 0.5
126
+ set pop to my wifiPopoverWindow()
127
+ if pop is not missing value then exit repeat
128
+ end repeat
129
+ if pop is missing value then error "pressing the Wi-Fi control opened no popover - someone clicking on the Mac closes it the moment it opens (contention, not a fault); otherwise Control Center is not answering"
130
+ set rowEl to missing value
131
+ repeat 4 times
132
+ set rowEl to my findAxId(pop, "wifi-network-" & hotspotName, 6)
133
+ if rowEl is not missing value then exit repeat
134
+ delay 0.5
135
+ end repeat
136
+ if rowEl is missing value then
137
+ my closeWifiPopover(wifiItem)
138
+ error quote & hotspotName & quote & " is not in the Wi-Fi menu - the iPhone is out of Bluetooth range, has Bluetooth off, or Personal Hotspot is off in its Settings"
139
+ end if
140
+ -- The row is a toggle too: value 1 means already connected, and pressing
141
+ -- it then would DISCONNECT - the one outcome worse than doing nothing.
142
+ -- Only a read of exactly 1 skips the press; an unreadable value presses.
143
+ set rowValue to 0
144
+ try
145
+ tell application "System Events" to set rowValue to value of rowEl
146
+ end try
147
+ if rowValue is 1 then
148
+ my closeWifiPopover(wifiItem)
149
+ return "already-connected"
150
+ end if
151
+ tell application "System Events" to perform action "AXPress" of rowEl
152
+ delay 1
153
+ my closeWifiPopover(wifiItem)
154
+ return "pressed"
155
+ end joinInstantHotspot