conductor-remote 1.27.3 → 1.28.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/dist/index.html
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
<title>Conductor Remote</title>
|
|
14
14
|
<!-- Runs before the module bundle so it can catch a stale shell that fails to boot. -->
|
|
15
15
|
<script src="/self-heal.js"></script>
|
|
16
|
-
<script type="module" crossorigin src="/assets/index-
|
|
16
|
+
<script type="module" crossorigin src="/assets/index-ChC5JHf_.js"></script>
|
|
17
17
|
<link rel="stylesheet" crossorigin href="/assets/index-DnE5ulTu.css">
|
|
18
18
|
<link rel="manifest" href="/manifest.webmanifest"></head>
|
|
19
19
|
<body>
|
package/dist/sw.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
if(!self.define){let e,i={};const n=(n,s)=>(n=new URL(n+".js",s).href,i[n]||new Promise(i=>{if("document"in self){const e=document.createElement("script");e.src=n,e.onload=i,document.head.appendChild(e)}else e=n,importScripts(n),i()}).then(()=>{let e=i[n];if(!e)throw new Error(`Module ${n} didn’t register its module`);return e}));self.define=(s,r)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(i[o])return;let
|
|
1
|
+
if(!self.define){let e,i={};const n=(n,s)=>(n=new URL(n+".js",s).href,i[n]||new Promise(i=>{if("document"in self){const e=document.createElement("script");e.src=n,e.onload=i,document.head.appendChild(e)}else e=n,importScripts(n),i()}).then(()=>{let e=i[n];if(!e)throw new Error(`Module ${n} didn’t register its module`);return e}));self.define=(s,r)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(i[o])return;let l={};const c=e=>n(e,o),t={module:{uri:o},exports:l,require:c};i[o]=Promise.all(s.map(e=>t[e]||c(e))).then(e=>(r(...e),l))}}define(["./workbox-9c191d2f"],function(e){"use strict";self.addEventListener("message",e=>{e.data&&"SKIP_WAITING"===e.data.type&&self.skipWaiting()}),e.clientsClaim(),e.precacheAndRoute([{url:"self-heal.js",revision:"49bd63adb25a09341f8d2610e8bd3c76"},{url:"index.html",revision:"a23ebd6c333ee14357fb2a1cd7ab440f"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-DnE5ulTu.css",revision:null},{url:"assets/index-ChC5JHf_.js",revision:null},{url:"apple-touch-icon.png",revision:"2b9301416b880d45d4bb655f2600d1f2"},{url:"icon-192.png",revision:"c5e01ac58768627e18ee7b8b6a9239ef"},{url:"icon-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon-maskable-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon.svg",revision:"c1aee186821798733dd477e69a0ef243"},{url:"manifest.webmanifest",revision:"cf88fbc5755108a7fe0616fa160a8a15"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("/index.html"),{denylist:[/^\/api\//]}))});
|
package/dist-node/src/server.js
CHANGED
|
@@ -12,11 +12,17 @@ import { mergePr } from "./merge.js";
|
|
|
12
12
|
import { attachPrStatus } from "./pr.js";
|
|
13
13
|
import { Reads } from "./reads.js";
|
|
14
14
|
import { driftWarningLines, tailscaleBin } from "./tailscale.js";
|
|
15
|
-
import { createWorkspace, describeActuator, EFFORT_LABELS, listAgentModels, newChat, pickActuator, setAgentOptions } from "./writes.js";
|
|
15
|
+
import { createWorkspace, describeActuator, EFFORT_LABELS, listAgentModels, newChat, pickActuator, setAgentOptions, setRestartGuard } from "./writes.js";
|
|
16
16
|
const cfg = loadConfig();
|
|
17
17
|
const db = new ConductorDb(cfg.dbPath);
|
|
18
18
|
const reads = new Reads(db, cfg.workspacesRoot);
|
|
19
19
|
const actuator = pickActuator(cfg.writeStrategy);
|
|
20
|
+
// A windowless Conductor that ignores reopen *and* a Dock click can only be fixed
|
|
21
|
+
// by restarting it — and quitting takes any agent mid-turn down with it. So the
|
|
22
|
+
// write path may only do that while nothing is working, which is a DB fact, not
|
|
23
|
+
// something AppleScript can see. Read fresh each time: a session can start between
|
|
24
|
+
// the phone opening the app and the send landing.
|
|
25
|
+
setRestartGuard(() => !reads.listWorkspaces().some(w => w.session_status === 'working'));
|
|
20
26
|
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
21
27
|
/**
|
|
22
28
|
* Confirm a prompt actually reached the session by watching the transcript for a
|
package/dist-node/src/writes.js
CHANGED
|
@@ -67,32 +67,39 @@ on splitLines(s)
|
|
|
67
67
|
return parts
|
|
68
68
|
end splitLines
|
|
69
69
|
|
|
70
|
-
on
|
|
71
|
-
--
|
|
70
|
+
on windowProbe()
|
|
71
|
+
-- One read, three outcomes, none of them guessed: {count, errNum, errText}.
|
|
72
|
+
-- count is -1 when macOS refused the read at all. Everything that wants to
|
|
73
|
+
-- know about windows goes through here, because "returned 0" and "refused to
|
|
74
|
+
-- answer" are different facts and only this handler still has both.
|
|
72
75
|
try
|
|
73
76
|
tell application "System Events" to tell process "Conductor"
|
|
74
|
-
|
|
77
|
+
set winCount to (count of windows)
|
|
75
78
|
end tell
|
|
76
|
-
|
|
77
|
-
|
|
79
|
+
return {winCount, 0, ""}
|
|
80
|
+
on error errText number errNum
|
|
81
|
+
return {-1, errNum, errText}
|
|
78
82
|
end try
|
|
83
|
+
end windowProbe
|
|
84
|
+
|
|
85
|
+
on hasWindow()
|
|
86
|
+
-- Also false when the process is gone or macOS refused: callers that only
|
|
87
|
+
-- need a boolean keep getting one. windowFailure() is what tells them why.
|
|
88
|
+
return (item 1 of my windowProbe()) > 0
|
|
79
89
|
end hasWindow
|
|
80
90
|
|
|
81
|
-
on
|
|
82
|
-
--
|
|
83
|
-
--
|
|
84
|
-
--
|
|
85
|
-
--
|
|
86
|
-
--
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return "macOS blocked the relay from controlling the UI (" & errNum & ") - grant Automation permission to the node binary running the relay in System Settings > Privacy & Security > Automation. " & errText
|
|
92
|
-
end try
|
|
93
|
-
if not axTrusted then return "the relay is not trusted for Accessibility - grant it to the node binary running the relay in System Settings > Privacy & Security > Accessibility, then restart the relay (upgrading node silently revokes it)"
|
|
91
|
+
on refusalReason(probe)
|
|
92
|
+
-- macOS's own words, matched on the error it actually returned rather than on
|
|
93
|
+
-- a capability flag we hope is accurate. "UI elements enabled" was the earlier
|
|
94
|
+
-- attempt at this and is not trustworthy — it can report true for a process
|
|
95
|
+
-- that is not itself trusted, and then the refusal reappears downstream as
|
|
96
|
+
-- "no open window". These strings are the two grants the write path needs.
|
|
97
|
+
set errNum to item 2 of probe
|
|
98
|
+
set errText to item 3 of probe
|
|
99
|
+
if errText contains "assistive access" then return "the relay is not trusted for Accessibility - grant it to the node binary running the relay in System Settings > Privacy & Security > Accessibility, then restart the relay (upgrading node silently revokes it). macOS said: " & errText
|
|
100
|
+
if errNum is -1743 or errText contains "Not authorized" then return "macOS blocked the relay from controlling the UI - grant Automation permission to the node binary running the relay in System Settings > Privacy & Security > Automation. macOS said: " & errText
|
|
94
101
|
return ""
|
|
95
|
-
end
|
|
102
|
+
end refusalReason
|
|
96
103
|
|
|
97
104
|
on conductorRunning()
|
|
98
105
|
try
|
|
@@ -102,20 +109,122 @@ on conductorRunning()
|
|
|
102
109
|
end try
|
|
103
110
|
end conductorRunning
|
|
104
111
|
|
|
105
|
-
on
|
|
112
|
+
on joinList(lst, sep)
|
|
113
|
+
set saved to AppleScript's text item delimiters
|
|
114
|
+
set AppleScript's text item delimiters to sep
|
|
115
|
+
set out to lst as text
|
|
116
|
+
set AppleScript's text item delimiters to saved
|
|
117
|
+
return out
|
|
118
|
+
end joinList
|
|
119
|
+
|
|
120
|
+
on processWindowCounts()
|
|
121
|
+
-- Every process whose name looks like Conductor, with its window count. If the
|
|
122
|
+
-- app is visibly on screen while "process Conductor" reports zero, the windows
|
|
123
|
+
-- belong to a process we are not addressing — and that only shows up here.
|
|
124
|
+
set out to {}
|
|
125
|
+
try
|
|
126
|
+
tell application "System Events"
|
|
127
|
+
repeat with proc in (every process whose name contains "Conductor")
|
|
128
|
+
set end of out to ((name of proc) & "=" & (count of windows of proc))
|
|
129
|
+
end repeat
|
|
130
|
+
end tell
|
|
131
|
+
end try
|
|
132
|
+
return out
|
|
133
|
+
end processWindowCounts
|
|
134
|
+
|
|
135
|
+
on menuTitles()
|
|
136
|
+
try
|
|
137
|
+
tell application "System Events" to tell process "Conductor"
|
|
138
|
+
return name of every menu bar item of menu bar 1
|
|
139
|
+
end tell
|
|
140
|
+
on error
|
|
141
|
+
return {}
|
|
142
|
+
end try
|
|
143
|
+
end menuTitles
|
|
144
|
+
|
|
145
|
+
on windowEvidence()
|
|
146
|
+
-- Zero windows is a dead end without knowing what the AX tree *does* show, and
|
|
147
|
+
-- guessing the next thing to press has already cost a round trip (there is no
|
|
148
|
+
-- "New Window" item — Conductor is single-window). So the error carries the
|
|
149
|
+
-- evidence instead: who macOS thinks Conductor is, and what its menus offer.
|
|
150
|
+
set procs to my processWindowCounts()
|
|
151
|
+
set titles to my menuTitles()
|
|
152
|
+
set ev to ""
|
|
153
|
+
if (count of procs) > 0 then set ev to ev & " [processes: " & my joinList(procs, ", ") & "]"
|
|
154
|
+
if (count of titles) > 0 then set ev to ev & " [menus: " & my joinList(titles, ", ") & "]"
|
|
155
|
+
return ev
|
|
156
|
+
end windowEvidence
|
|
157
|
+
|
|
158
|
+
on windowFailure()
|
|
159
|
+
-- "" when a window is there; otherwise why not, in words the phone can act on.
|
|
106
160
|
-- ASCII on purpose: these reach the phone, and osascript decodes -e by the
|
|
107
|
-
-- caller's locale, which a LaunchAgent doesn't set.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
161
|
+
-- caller's locale, which a LaunchAgent doesn't set. The last branch carries
|
|
162
|
+
-- the raw error number and text on purpose — an unrecognised refusal must
|
|
163
|
+
-- arrive as itself, not as the most plausible-sounding of the known causes.
|
|
164
|
+
set probe to my windowProbe()
|
|
165
|
+
set winCount to item 1 of probe
|
|
166
|
+
if winCount > 0 then return ""
|
|
167
|
+
if winCount is 0 then return "Conductor is running but exposes no window - reopen and a Dock click both drew nothing. Open or restart it on your Mac and try again." & my windowEvidence()
|
|
168
|
+
set refusal to my refusalReason(probe)
|
|
169
|
+
if refusal is not "" then return refusal
|
|
170
|
+
if not (my conductorRunning()) then return "Conductor is not running - the relay started it, but it did not come up in time. Try again in a few seconds."
|
|
171
|
+
return "couldn't read Conductor's windows (" & (item 2 of probe) & "): " & (item 3 of probe)
|
|
172
|
+
end windowFailure
|
|
173
|
+
|
|
174
|
+
on requireWindow()
|
|
175
|
+
set reason to my windowFailure()
|
|
176
|
+
if reason is not "" then error reason
|
|
113
177
|
end requireWindow
|
|
114
178
|
|
|
179
|
+
on dockClick()
|
|
180
|
+
-- Last resort when the "reopen" AppleEvent draws nothing: click the app's Dock
|
|
181
|
+
-- tile. Same intent, different delivery — the Dock raises it through the app's
|
|
182
|
+
-- own event loop rather than as an Apple event we send, which is what an app
|
|
183
|
+
-- that never wired up reopen can still answer. (There is no "New Window" menu
|
|
184
|
+
-- item to press: Conductor is single-window and single-instance.)
|
|
185
|
+
try
|
|
186
|
+
tell application "System Events" to tell application process "Dock"
|
|
187
|
+
click UI element "Conductor" of list 1
|
|
188
|
+
end tell
|
|
189
|
+
return true
|
|
190
|
+
on error
|
|
191
|
+
return false
|
|
192
|
+
end try
|
|
193
|
+
end dockClick
|
|
194
|
+
|
|
195
|
+
on restartConductor()
|
|
196
|
+
-- The last lever: Conductor is single-window and single-instance, so when a
|
|
197
|
+
-- running app answers neither reopen nor a Dock click there is nothing left to
|
|
198
|
+
-- press. Deliberately fire-and-forget — a cold launch is 5-10s on its own,
|
|
199
|
+
-- past what the phone's budget leaves once the send's own delays are counted —
|
|
200
|
+
-- so this returns as soon as the relaunch is under way and the caller tells the
|
|
201
|
+
-- phone to send again into a warm app. Gated by RELAY_ALLOW_RESTART, which the
|
|
202
|
+
-- relay only sets when the DB says no session is mid-turn.
|
|
203
|
+
try
|
|
204
|
+
tell application "Conductor" to quit
|
|
205
|
+
on error errText
|
|
206
|
+
return "couldn't quit Conductor to restart it: " & errText
|
|
207
|
+
end try
|
|
208
|
+
repeat with attempt from 1 to 16
|
|
209
|
+
if not (my conductorRunning()) then exit repeat
|
|
210
|
+
delay 0.25
|
|
211
|
+
end repeat
|
|
212
|
+
if my conductorRunning() then return "Conductor ignored the quit, so the relay left it alone - quit it on your Mac and try again."
|
|
213
|
+
try
|
|
214
|
+
tell application "Conductor" to activate
|
|
215
|
+
on error errText
|
|
216
|
+
return "quit Conductor but couldn't start it again: " & errText
|
|
217
|
+
end try
|
|
218
|
+
return ""
|
|
219
|
+
end restartConductor
|
|
220
|
+
|
|
115
221
|
on waitForWindow(attempts)
|
|
116
|
-
-- "reopen" is the dock-click event
|
|
117
|
-
-- "activate" on its own does not. Nudge a few times
|
|
118
|
-
-- launch can reach "process exists" long before it
|
|
222
|
+
-- "reopen" is the dock-click Apple event, and for most apps that is what
|
|
223
|
+
-- recreates a closed window ("activate" on its own does not). Nudge a few times
|
|
224
|
+
-- across the wait: a cold launch can reach "process exists" long before it
|
|
225
|
+
-- draws anything. If reopen has drawn nothing by ~3s the app isn't answering
|
|
226
|
+
-- that event, so escalate to a real Dock click rather than repeating what has
|
|
227
|
+
-- already failed twice.
|
|
119
228
|
repeat with attempt from 1 to attempts
|
|
120
229
|
if my hasWindow() then return true
|
|
121
230
|
if attempt is 2 or attempt is 8 or attempt is 20 then
|
|
@@ -124,6 +233,7 @@ on waitForWindow(attempts)
|
|
|
124
233
|
tell application "Conductor" to activate
|
|
125
234
|
end try
|
|
126
235
|
end if
|
|
236
|
+
if attempt is 12 or attempt is 28 then my dockClick()
|
|
127
237
|
delay 0.25
|
|
128
238
|
end repeat
|
|
129
239
|
return false
|
|
@@ -136,11 +246,13 @@ on activateConductor()
|
|
|
136
246
|
-- handler happened to run first. So: launch-or-front it ("activate" starts it
|
|
137
247
|
-- when it isn't running), wait for a real window, then say what actually went
|
|
138
248
|
-- wrong in words the phone can act on.
|
|
139
|
-
-- Permissions first: a
|
|
140
|
-
--
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
set
|
|
249
|
+
-- Permissions first: a refusal reads as "no process, no window" from every
|
|
250
|
+
-- probe below, and waiting out the patience budget to say the wrong thing
|
|
251
|
+
-- helps nobody. A refusal is the one failure launching can't fix.
|
|
252
|
+
set firstProbe to my windowProbe()
|
|
253
|
+
set refusal to my refusalReason(firstProbe)
|
|
254
|
+
if refusal is not "" then error refusal
|
|
255
|
+
set wasRunning to (item 1 of firstProbe) >= 0
|
|
144
256
|
try
|
|
145
257
|
tell application "Conductor" to activate
|
|
146
258
|
on error errText
|
|
@@ -155,6 +267,14 @@ on activateConductor()
|
|
|
155
267
|
set patience to 16
|
|
156
268
|
if not wasRunning then set patience to 36
|
|
157
269
|
if my waitForWindow(patience) then return
|
|
270
|
+
-- Running, readable, and still zero windows: reopen and the Dock click have
|
|
271
|
+
-- both drawn nothing, so a restart is the only thing left. "Open it on your
|
|
272
|
+
-- Mac" is not advice a phone can act on — that is the whole point of this app.
|
|
273
|
+
if (system attribute "RELAY_ALLOW_RESTART") is "1" and (item 1 of my windowProbe()) is 0 then
|
|
274
|
+
set restartError to my restartConductor()
|
|
275
|
+
if restartError is not "" then error restartError
|
|
276
|
+
error "Conductor was running with no window and ignored both reopen and a Dock click, so the relay restarted it. Give it a few seconds and send again."
|
|
277
|
+
end if
|
|
158
278
|
my requireWindow()
|
|
159
279
|
end activateConductor
|
|
160
280
|
|
|
@@ -679,9 +799,33 @@ function sidebarTitles(ws) {
|
|
|
679
799
|
ws.directory_name
|
|
680
800
|
].filter((t) => Boolean(t));
|
|
681
801
|
}
|
|
802
|
+
/**
|
|
803
|
+
* May the actuator restart Conductor to force a window into existence?
|
|
804
|
+
*
|
|
805
|
+
* Restarting is the only lever left when a running, windowless Conductor ignores
|
|
806
|
+
* both `reopen` and a Dock click — it is single-window and single-instance, so
|
|
807
|
+
* there is nothing else to press. It is also the one step here that can destroy
|
|
808
|
+
* work: quitting takes any agent mid-turn down with it. "Nothing is running" is a
|
|
809
|
+
* fact only the read side has, so server.ts wires this to a DB read rather than
|
|
810
|
+
* writes.ts guessing. Unset → never restart, which is the safe default for any
|
|
811
|
+
* caller that hasn't opted in.
|
|
812
|
+
*/
|
|
813
|
+
let restartGuard = null;
|
|
814
|
+
export function setRestartGuard(guard) {
|
|
815
|
+
restartGuard = guard;
|
|
816
|
+
}
|
|
817
|
+
function restartAllowed() {
|
|
818
|
+
try {
|
|
819
|
+
return restartGuard?.() ?? false;
|
|
820
|
+
}
|
|
821
|
+
catch {
|
|
822
|
+
return false;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
682
825
|
/** The target rides in on the environment, like RELAY_WS_QUERY, to dodge AppleScript escaping. */
|
|
683
826
|
function targetEnv(target) {
|
|
684
827
|
return {
|
|
828
|
+
RELAY_ALLOW_RESTART: restartAllowed() ? '1' : '',
|
|
685
829
|
RELAY_TAB_INDEX: String(target.tab?.index ?? 0),
|
|
686
830
|
RELAY_TAB_COUNT: String(target.tab?.count ?? 0),
|
|
687
831
|
RELAY_TAB_TITLE: target.tab?.title ?? '',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "conductor-remote",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.28.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"packageManager": "yarn@4.15.0",
|
|
6
6
|
"description": "Phone control panel for local Conductor agents. Reads ride SQLite + git; prompts ride Conductor's own dispatch path.",
|