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-DodAVrF-.js"></script>
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 c={};const l=e=>n(e,o),t={module:{uri:o},exports:c,require:l};i[o]=Promise.all(s.map(e=>t[e]||l(e))).then(e=>(r(...e),c))}}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:"0d9b7c2e83c2d2ac18a82c2accca260a"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-DodAVrF-.js",revision:null},{url:"assets/index-DnE5ulTu.css",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\//]}))});
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\//]}))});
@@ -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
@@ -67,32 +67,39 @@ on splitLines(s)
67
67
  return parts
68
68
  end splitLines
69
69
 
70
- on hasWindow()
71
- -- Also false when the process itself is gone: "tell process" errors there.
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
- return (count of windows) > 0
77
+ set winCount to (count of windows)
75
78
  end tell
76
- on error
77
- return false
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 scriptingBlocked()
82
- -- hasWindow() cannot tell "no window" from "macOS refused us" it swallows
83
- -- every error so without this a missing permission reads as "no open
84
- -- window" and sends the user off to open a window that is already open.
85
- -- These are the two separate grants the write path needs: Automation (may we
86
- -- talk to System Events at all) and Accessibility (may we read the UI tree).
87
- -- Returns "" when both are in place.
88
- try
89
- tell application "System Events" to set axTrusted to (UI elements enabled)
90
- on error errText number errNum
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 scriptingBlocked
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 requireWindow()
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
- if my hasWindow() then return
109
- set blocked to my scriptingBlocked()
110
- if blocked is not "" then error blocked
111
- if not (my conductorRunning()) then error "Conductor is not running - the relay started it, but it did not come up in time. Try again in a few seconds."
112
- error "Conductor has no open window - open it on your Mac and try again"
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 that is what recreates a closed window;
117
- -- "activate" on its own does not. Nudge a few times across the wait: a cold
118
- -- launch can reach "process exists" long before it draws anything.
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 blocked relay reads as "no process, no window" from
140
- -- every probe below, and waiting 9s to say the wrong thing helps nobody.
141
- set blocked to my scriptingBlocked()
142
- if blocked is not "" then error blocked
143
- set wasRunning to my conductorRunning()
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.27.3",
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.",