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.
- package/README.md +10 -8
- package/dist/assets/{PierrePatch-D3_YBIAr.js → PierrePatch-D4YbKD8Q.js} +1 -1
- package/dist/assets/index-BRzsSW8g.css +1 -0
- package/dist/assets/index-BaS6qXUW.js +65 -0
- package/dist/index.html +2 -2
- package/dist/push-sw.js +2 -2
- package/dist/sw.js +1 -1
- package/dist-node/scripts/nosleep-setup.js +2 -2
- package/dist-node/scripts/nosleep.js +3 -3
- package/dist-node/scripts/service/commands.js +32 -0
- package/dist-node/scripts/service/configuration.js +205 -0
- package/dist-node/scripts/service/environment.js +59 -0
- package/dist-node/scripts/service/flags.js +45 -0
- package/dist-node/scripts/service/installation.js +91 -0
- package/dist-node/scripts/service/launch-agent.js +134 -0
- package/dist-node/scripts/service/network.js +244 -0
- package/dist-node/scripts/service/presentation.js +101 -0
- package/dist-node/scripts/service/voice.js +113 -0
- package/dist-node/scripts/service.js +5 -980
- package/dist-node/src/{agent-config.js → agents/agent-config.js} +1 -1
- package/dist-node/src/{model-cache.js → agents/model-cache.js} +1 -1
- package/dist-node/src/{roles.js → agents/roles.js} +3 -3
- package/dist-node/src/db.js +1 -1
- package/dist-node/src/{parked.js → delivery/parked.js} +1 -1
- package/dist-node/src/{dev-server.js → dev-server/controller.js} +8 -291
- package/dist-node/src/dev-server/ports.js +63 -0
- package/dist-node/src/dev-server/processes.js +73 -0
- package/dist-node/src/dev-server/proxy.js +158 -0
- package/dist-node/src/{run-activity.js → dev-server/run-activity.js} +2 -2
- package/dist-node/src/dev-server/types.js +1 -0
- package/dist-node/src/{file-preview.js → files/file-preview.js} +1 -1
- package/dist-node/src/{change-stats.js → git/change-stats.js} +1 -1
- package/dist-node/src/{git.js → git/diff.js} +1 -1
- package/dist-node/src/{autoupdate.js → host/autoupdate.js} +1 -1
- package/dist-node/src/{funnel-watchdog.js → host/funnel-watchdog.js} +4 -4
- package/dist-node/src/{logbuf.js → host/logbuf.js} +1 -1
- package/dist-node/src/{nosleep-helper.js → host/nosleep-helper.js} +1 -1
- package/dist-node/src/{tailscale.js → host/tailscale.js} +1 -1
- package/dist-node/src/http/router-types.js +2 -0
- package/dist-node/src/http/router.js +78 -0
- package/dist-node/src/http/routes/create-workspace.js +90 -0
- package/dist-node/src/http/routes/files.js +53 -0
- package/dist-node/src/http/routes/prompts.js +363 -0
- package/dist-node/src/http/routes/sessions.js +279 -0
- package/dist-node/src/http/routes/state.js +186 -0
- package/dist-node/src/http/routes/system.js +220 -0
- package/dist-node/src/http/routes/voice.js +117 -0
- package/dist-node/src/http/routes/workflows.js +248 -0
- package/dist-node/src/http/routes/workspaces.js +267 -0
- package/dist-node/src/http/services/base.js +252 -0
- package/dist-node/src/http/services/delegations.js +301 -0
- package/dist-node/src/http/services/delivery.js +433 -0
- package/dist-node/src/http/services/files.js +199 -0
- package/dist-node/src/http/services/mcp.js +94 -0
- package/dist-node/src/http/services/responses.js +120 -0
- package/dist-node/src/http/services/voice.js +147 -0
- package/dist-node/src/http/services/workflow-probes.js +215 -0
- package/dist-node/src/http/services/workflow-state.js +195 -0
- package/dist-node/src/http/services/workflow.js +366 -0
- package/dist-node/src/http/services.js +35 -0
- package/dist-node/src/mcp/arguments.js +14 -0
- package/dist-node/src/mcp/dispatcher.js +56 -0
- package/dist-node/src/mcp/formatters.js +110 -0
- package/dist-node/src/mcp/protocol.js +7 -0
- package/dist-node/src/mcp/registry.js +66 -0
- package/dist-node/src/mcp/tools/agent-options.js +122 -0
- package/dist-node/src/mcp/tools/chat-actions.js +194 -0
- package/dist-node/src/mcp/tools/chats.js +194 -0
- package/dist-node/src/mcp/tools/delegation.js +135 -0
- package/dist-node/src/mcp/tools/system.js +135 -0
- package/dist-node/src/mcp/tools/voice.js +136 -0
- package/dist-node/src/mcp/tools/workspaces.js +259 -0
- package/dist-node/src/mcp/types.js +1 -0
- package/dist-node/src/mcp.js +6 -4
- package/dist-node/src/{notify.js → notifications/notify.js} +5 -5
- package/dist-node/src/{webpush.js → notifications/webpush.js} +1 -1
- package/dist-node/src/orchestration/delegation/codec.js +201 -0
- package/dist-node/src/{delegation-intake.js → orchestration/delegation/intake.js} +2 -2
- package/dist-node/src/{delegation-prompt.js → orchestration/delegation/prompt.js} +1 -1
- package/dist-node/src/orchestration/delegation/queue.js +255 -0
- package/dist-node/src/orchestration/delegation/store.js +111 -0
- package/dist-node/src/orchestration/delegation/types.js +2 -0
- package/dist-node/src/orchestration/persistence/capabilities.js +109 -0
- package/dist-node/src/orchestration/persistence/codecs.js +148 -0
- package/dist-node/src/orchestration/persistence/connection.js +205 -0
- package/dist-node/src/orchestration/persistence/db.js +101 -0
- package/dist-node/src/orchestration/persistence/effect-recovery.js +227 -0
- package/dist-node/src/orchestration/persistence/effect-transitions.js +157 -0
- package/dist-node/src/orchestration/persistence/effects.js +109 -0
- package/dist-node/src/orchestration/persistence/errors.js +39 -0
- package/dist-node/src/orchestration/persistence/events.js +29 -0
- package/dist-node/src/orchestration/persistence/idempotency.js +82 -0
- package/dist-node/src/orchestration/persistence/job-attempts.js +114 -0
- package/dist-node/src/orchestration/persistence/jobs.js +159 -0
- package/dist-node/src/orchestration/persistence/projections.js +190 -0
- package/dist-node/src/orchestration/persistence/quarantine.js +46 -0
- package/dist-node/src/orchestration/persistence/records.js +115 -0
- package/dist-node/src/orchestration/persistence/relays.js +78 -0
- package/dist-node/src/orchestration/persistence/runs.js +156 -0
- package/dist-node/src/{orchestration-schema.js → orchestration/persistence/schema.js} +1 -1
- package/dist-node/src/orchestration/persistence/types.js +1 -0
- package/dist-node/src/orchestration/persistence/ui-lease.js +197 -0
- package/dist-node/src/orchestration/persistence/values.js +114 -0
- package/dist-node/src/orchestration/workflow/commands.js +309 -0
- package/dist-node/src/orchestration/workflow/coordinator.js +28 -0
- package/dist-node/src/orchestration/workflow/effect-recovery.js +125 -0
- package/dist-node/src/orchestration/workflow/effects.js +307 -0
- package/dist-node/src/orchestration/workflow/errors.js +13 -0
- package/dist-node/src/orchestration/workflow/helpers.js +154 -0
- package/dist-node/src/orchestration/workflow/job-dispatch.js +312 -0
- package/dist-node/src/orchestration/workflow/job-results.js +280 -0
- package/dist-node/src/{workflow.js → orchestration/workflow/prompts.js} +2 -2
- package/dist-node/src/orchestration/workflow/root.js +293 -0
- package/dist-node/src/orchestration/workflow/start.js +99 -0
- package/dist-node/src/orchestration/workflow/state.js +105 -0
- package/dist-node/src/orchestration/workflow/types.js +1 -0
- package/dist-node/src/orchestration/workflow/wake.js +164 -0
- package/dist-node/src/prefs.js +1 -1
- package/dist-node/src/{background-tasks.js → reads/background-tasks.js} +1 -1
- package/dist-node/src/reads/messages.js +362 -0
- package/dist-node/src/reads/repository.js +87 -0
- package/dist-node/src/reads/sessions.js +165 -0
- package/dist-node/src/reads/types.js +1 -0
- package/dist-node/src/reads/workspaces.js +230 -0
- package/dist-node/src/reads/worktrees.js +45 -0
- package/dist-node/src/routes.js +5 -5
- package/dist-node/src/{search.js → search/coordinator.js} +4 -4
- package/dist-node/src/{search-worker.js → search/worker.js} +3 -3
- package/dist-node/src/server.js +14 -3930
- package/dist-node/src/settings.js +1 -1
- package/dist-node/src/shared.js +10 -10
- package/dist-node/src/{transcript.js → transcript/parser.js} +4 -4
- package/dist-node/src/{tool-usage-worker.js → usage/tool-usage-worker.js} +1 -1
- package/dist-node/src/{tool-usage.js → usage/tool-usage.js} +1 -1
- package/dist-node/src/voice/brief.js +22 -50
- package/dist-node/src/voice/context.js +1 -1
- package/dist-node/src/voice/dates.js +38 -0
- package/dist-node/src/voice/history.js +14 -5
- package/dist-node/src/voice/prompt.js +10 -9
- package/dist-node/src/voice/recall.js +115 -0
- package/dist-node/src/{speech.js → voice/speech.js} +1 -1
- package/dist-node/src/voice/tools.js +92 -3
- package/dist-node/src/voice/webrtc.js +1 -1
- package/dist-node/src/wire.js +2 -2
- package/dist-node/src/writes/actuator.js +124 -0
- package/dist-node/src/writes/agent-options.js +151 -0
- package/dist-node/src/writes/applescript/agent-options.applescript +334 -0
- package/dist-node/src/writes/applescript/chat-targeting.applescript +149 -0
- package/dist-node/src/writes/applescript/composer.applescript +195 -0
- package/dist-node/src/writes/applescript/hotspot.applescript +155 -0
- package/dist-node/src/writes/applescript/lifecycle.applescript +266 -0
- package/dist-node/src/writes/applescript/run-tasks.applescript +208 -0
- package/dist-node/src/writes/applescript/source.js +52 -0
- package/dist-node/src/writes/applescript/window.applescript +436 -0
- package/dist-node/src/writes/applescript/workspace-status.applescript +177 -0
- package/dist-node/src/writes/applescript/workspace-targeting.applescript +359 -0
- package/dist-node/src/writes/chats.js +160 -0
- package/dist-node/src/writes/guards.js +77 -0
- package/dist-node/src/writes/runner.js +67 -0
- package/dist-node/src/{sidecar.js → writes/sidecar.js} +1 -1
- package/dist-node/src/writes/system.js +81 -0
- package/dist-node/src/writes/targeting.js +121 -0
- package/dist-node/src/writes/types.js +1 -0
- package/dist-node/src/writes/ui-lock.js +229 -0
- package/dist-node/src/writes/workspaces.js +233 -0
- package/docs/voice-setup.md +34 -12
- package/package.json +2 -2
- package/dist/assets/index-ByTwVInL.css +0 -1
- package/dist/assets/index-LAm9X7wv.js +0 -65
- package/dist-node/src/conductor.applescript +0 -2279
- package/dist-node/src/delegations.js +0 -557
- package/dist-node/src/mcp-tools.js +0 -1319
- package/dist-node/src/orchestration-db.js +0 -2186
- package/dist-node/src/reads.js +0 -787
- package/dist-node/src/workflow-coordinator.js +0 -2119
- package/dist-node/src/writes.js +0 -1223
- /package/dist-node/src/{conductor-settings.js → agents/conductor-settings.js} +0 -0
- /package/dist-node/src/{firstprompt.js → delivery/firstprompt.js} +0 -0
- /package/dist-node/src/{sendonce.js → delivery/sendonce.js} +0 -0
- /package/dist-node/src/{preview-urls.js → dev-server/preview-urls.js} +0 -0
- /package/dist-node/src/{run-configs.js → dev-server/run-configs.js} +0 -0
- /package/dist-node/src/{attachments.js → files/attachments.js} +0 -0
- /package/dist-node/src/{icons.js → files/icons.js} +0 -0
- /package/dist-node/src/{staged-attachments.js → files/staged-attachments.js} +0 -0
- /package/dist-node/src/{fork-workspace.js → git/fork-workspace.js} +0 -0
- /package/dist-node/src/{merge.js → git/merge.js} +0 -0
- /package/dist-node/src/{pr.js → git/pr.js} +0 -0
- /package/dist-node/src/{nosleep.js → host/nosleep.js} +0 -0
- /package/dist-node/src/{relay-processes.js → host/relay-processes.js} +0 -0
- /package/dist-node/src/{ui-lease-watchdog.js → host/ui-lease-watchdog.js} +0 -0
- /package/dist-node/src/{wifi.js → host/wifi.js} +0 -0
- /package/dist-node/src/{orchestration-schema.generated.js → orchestration/persistence/schema.generated.js} +0 -0
- /package/dist-node/src/{workflow-effect-runner.js → orchestration/workflow/effect-runner.js} +0 -0
- /package/dist-node/src/{workflow-http.js → orchestration/workflow/http.js} +0 -0
- /package/dist-node/src/{workflow-machine.js → orchestration/workflow/machine.js} +0 -0
- /package/dist-node/src/{session-poller.js → reads/session-poller.js} +0 -0
- /package/dist-node/src/{context-breakdown.js → transcript/context-breakdown.js} +0 -0
- /package/dist-node/src/{chat-cursor.js → transcript/cursor.js} +0 -0
- /package/dist-node/src/{plan-usage.js → usage/plan-usage.js} +0 -0
- /package/dist-node/src/{tool-usage-service.js → usage/tool-usage-service.js} +0 -0
|
@@ -0,0 +1,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
|