conductor-remote 1.27.4 → 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-DgCZgSTk.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 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:"83ffcb8926c341d0bda6b343b1649bf7"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-DnE5ulTu.css",revision:null},{url:"assets/index-DgCZgSTk.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\//]}))});
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
@@ -109,6 +109,52 @@ on conductorRunning()
109
109
  end try
110
110
  end conductorRunning
111
111
 
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
+
112
158
  on windowFailure()
113
159
  -- "" when a window is there; otherwise why not, in words the phone can act on.
114
160
  -- ASCII on purpose: these reach the phone, and osascript decodes -e by the
@@ -118,7 +164,7 @@ on windowFailure()
118
164
  set probe to my windowProbe()
119
165
  set winCount to item 1 of probe
120
166
  if winCount > 0 then return ""
121
- if winCount is 0 then return "Conductor has no open window - open it on your Mac and try again"
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()
122
168
  set refusal to my refusalReason(probe)
123
169
  if refusal is not "" then return refusal
124
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."
@@ -130,30 +176,55 @@ on requireWindow()
130
176
  if reason is not "" then error reason
131
177
  end requireWindow
132
178
 
133
- on openNewWindow()
134
- -- Last resort when "reopen" draws nothing: press the app's own New Window.
135
- -- Exact match only "New Workspace" is a different command and would create a
136
- -- workspace nobody asked for so an app without that item fails closed and
137
- -- silent, leaving windowFailure() to report the honest "no open window".
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.)
138
185
  try
139
- tell application "System Events" to tell process "Conductor"
140
- repeat with topMenu in (menu bar items of menu bar 1)
141
- try
142
- click (first menu item of menu 1 of topMenu whose name is "New Window")
143
- return true
144
- end try
145
- end repeat
186
+ tell application "System Events" to tell application process "Dock"
187
+ click UI element "Conductor" of list 1
146
188
  end tell
189
+ return true
190
+ on error
191
+ return false
147
192
  end try
148
- return false
149
- end openNewWindow
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
150
220
 
151
221
  on waitForWindow(attempts)
152
- -- "reopen" is the dock-click event that is what recreates a closed window;
153
- -- "activate" on its own does not. Nudge a few times across the wait: a cold
154
- -- launch can reach "process exists" long before it draws anything. If reopen
155
- -- has drawn nothing by ~3s, the app isn't answering that event at all, so
156
- -- escalate to its own menu item rather than repeating what already failed.
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.
157
228
  repeat with attempt from 1 to attempts
158
229
  if my hasWindow() then return true
159
230
  if attempt is 2 or attempt is 8 or attempt is 20 then
@@ -162,7 +233,7 @@ on waitForWindow(attempts)
162
233
  tell application "Conductor" to activate
163
234
  end try
164
235
  end if
165
- if attempt is 12 or attempt is 28 then my openNewWindow()
236
+ if attempt is 12 or attempt is 28 then my dockClick()
166
237
  delay 0.25
167
238
  end repeat
168
239
  return false
@@ -196,6 +267,14 @@ on activateConductor()
196
267
  set patience to 16
197
268
  if not wasRunning then set patience to 36
198
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
199
278
  my requireWindow()
200
279
  end activateConductor
201
280
 
@@ -720,9 +799,33 @@ function sidebarTitles(ws) {
720
799
  ws.directory_name
721
800
  ].filter((t) => Boolean(t));
722
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
+ }
723
825
  /** The target rides in on the environment, like RELAY_WS_QUERY, to dodge AppleScript escaping. */
724
826
  function targetEnv(target) {
725
827
  return {
828
+ RELAY_ALLOW_RESTART: restartAllowed() ? '1' : '',
726
829
  RELAY_TAB_INDEX: String(target.tab?.index ?? 0),
727
830
  RELAY_TAB_COUNT: String(target.tab?.count ?? 0),
728
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.4",
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.",