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,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`
|