conductor-remote 1.34.1 → 1.34.2

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-k8Rjxn09.js"></script>
16
+ <script type="module" crossorigin src="/assets/index-BvDsOp0S.js"></script>
17
17
  <link rel="stylesheet" crossorigin href="/assets/index--7Dp2aq8.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,s={};const i=(i,n)=>(i=new URL(i+".js",n).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(n,r)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(s[o])return;let l={};const t=e=>i(e,o),c={module:{uri:o},exports:l,require:t};s[o]=Promise.all(n.map(e=>c[e]||t(e))).then(e=>(r(...e),l))}}define(["./workbox-9c191d2f"],function(e){"use strict";importScripts("/push-sw.js"),self.addEventListener("message",e=>{e.data&&"SKIP_WAITING"===e.data.type&&self.skipWaiting()}),e.clientsClaim(),e.precacheAndRoute([{url:"self-heal.js",revision:"49bd63adb25a09341f8d2610e8bd3c76"},{url:"push-sw.js",revision:"890f6a70e97d144f4d025254e90892a1"},{url:"index.html",revision:"d47260b82fc6fd5b56fd4b617a654174"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-k8Rjxn09.js",revision:null},{url:"assets/index--7Dp2aq8.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,s={};const i=(i,n)=>(i=new URL(i+".js",n).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(n,r)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(s[o])return;let c={};const l=e=>i(e,o),t={module:{uri:o},exports:c,require:l};s[o]=Promise.all(n.map(e=>t[e]||l(e))).then(e=>(r(...e),c))}}define(["./workbox-9c191d2f"],function(e){"use strict";importScripts("/push-sw.js"),self.addEventListener("message",e=>{e.data&&"SKIP_WAITING"===e.data.type&&self.skipWaiting()}),e.clientsClaim(),e.precacheAndRoute([{url:"self-heal.js",revision:"49bd63adb25a09341f8d2610e8bd3c76"},{url:"push-sw.js",revision:"890f6a70e97d144f4d025254e90892a1"},{url:"index.html",revision:"3ba36ccccb232cf75c6793b748ad42c3"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-BvDsOp0S.js",revision:null},{url:"assets/index--7Dp2aq8.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\//]}))});
@@ -33,9 +33,13 @@ function uiTurn(op) {
33
33
  * ordinary sends were being killed mid-run and reported as "Conductor took too long
34
34
  * to respond". The cost is Accessibility round trips, not waiting: activating a
35
35
  * backgrounded Conductor and reading the pane cost ~10s cold, and finding the
36
- * sidebar row to press another ~10s (see `atTargetWorkspace`, which is why a warm
37
- * send is now ~2s). A ceiling costs nothing when a send is fast — only a doomed one
38
- * waits it out, and the caller's own deadline is what bounds that.
36
+ * sidebar row to press another ~10s.
37
+ *
38
+ * `openViaDeepLink` took the row scan out of the common path a whole send now
39
+ * measures ~4s, and focusing alone ~2s against the ~18s the same focus costs when
40
+ * the link is unavailable — so this budget is really the *fallback's*, kept at the
41
+ * size that fallback still needs. A ceiling costs nothing when a send is fast; only
42
+ * a doomed one waits it out, and the caller's own deadline is what bounds that.
39
43
  */
40
44
  export const SEND_ATTEMPT_MS = 28_000;
41
45
  /**
@@ -108,9 +112,11 @@ export class SidecarActuator {
108
112
  /**
109
113
  * AppleScript handlers that pin down *which chat* the prompt goes to.
110
114
  *
111
- * The palette (Cmd+K) only gets us to the right workspace a workspace holds
112
- * several chats, and Conductor keeps typing in whichever tab is already active.
113
- * So a send addressed to a non-active chat would land in the wrong agent.
115
+ * `openViaDeepLink` normally selects it already, by id these handlers are what
116
+ * *confirm* that, and what still does the work for the fallback focus paths,
117
+ * which only get us to the right workspace. A workspace holds several chats and
118
+ * Conductor keeps typing in whichever tab is already active, so a send addressed
119
+ * to a non-active chat would land in the wrong agent.
114
120
  *
115
121
  * Conductor's webview exposes the whole tree through macOS Accessibility. The
116
122
  * chat strip is an AXTabGroup whose AXRadioButtons are the tabs (`AXValue`
@@ -118,7 +124,14 @@ export class SidecarActuator {
118
124
  * chat). The strip's order matches `reads.listSessions` (created_at ASC), so
119
125
  * the caller addresses a tab by 1-based index and we cross-check the label.
120
126
  *
121
- * Two traps this has to survive:
127
+ * Three traps this has to survive:
128
+ * - **The tabs are not the strip's direct children.** Each sits one AXGroup
129
+ * deep and is named for its own close action ("Close chat <title>"), while a
130
+ * "Close file view" radio button — not a chat — *is* a direct child. Reading
131
+ * the direct children and stopping there made every chat strip look like a
132
+ * one-tab strip called "Close file view", which then lost the scoring below to
133
+ * the terminal panel and failed every send with "chat tab 1 not found". So
134
+ * `stripRadios` reads both levels and `chatTabs` keeps the "Close chat" ones.
122
135
  * - **The terminal panel is an AXTabGroup too** (radio buttons named Setup /
123
136
  * Run / Terminal 1), a sibling of the chat strip in the same pane. Picking
124
137
  * "the first tab group" would sometimes press a terminal tab, so we *score*
@@ -461,8 +474,44 @@ on atTargetWorkspace()
461
474
  return my paneAgrees()
462
475
  end atTargetWorkspace
463
476
 
477
+ on openViaDeepLink()
478
+ -- Conductor's *own* workspace link, the one its sidebar row menu copies under
479
+ -- "Copy link" (Cmd+Shift+C): one URL carrying the workspace id and the chat id,
480
+ -- addressed the way the relay already reads them rather than by any label a
481
+ -- release can rename. It replaces the entire focus dance below - no sidebar
482
+ -- scan, no palette keystrokes, no title precedence to reproduce - and measures
483
+ -- ~0.6s against the 5-10s the row press costs on a 30-workspace sidebar.
484
+ --
485
+ -- Two things it fixes outright rather than merely speeding up. It navigates by
486
+ -- *id*, so a collapsed sidebar section (which hides the row from Accessibility
487
+ -- entirely) stops mattering; and it opens the chat tab explicitly, so a pane
488
+ -- left on a file view or a terminal - where the chat strip does not exist in
489
+ -- the AX tree at all, and every send failed with "couldn't identify the chat
490
+ -- tab strip" - comes back to the composer on its own.
491
+ --
492
+ -- Still fail-closed: the URL is fire-and-forget, so what counts as success is
493
+ -- the same pane assertion every other path here has to pass. A Conductor
494
+ -- without the route (before 0.71) ignores it silently, and a scheme macOS
495
+ -- can't resolve fails the "open" outright - both fall through to the sidebar.
496
+ set linkURL to system attribute "RELAY_WS_LINK"
497
+ if linkURL is "" then return false
498
+ try
499
+ do shell script "open " & quoted form of linkURL
500
+ on error
501
+ return false
502
+ end try
503
+ -- Bounded on purpose: this poll is also the cost of *not* having the route, and
504
+ -- the fallback below still has to run inside the same budget afterwards.
505
+ repeat with attempt from 1 to 10
506
+ delay 0.15
507
+ if my paneAgrees() then return true
508
+ end repeat
509
+ return false
510
+ end openViaDeepLink
511
+
464
512
  on focusWorkspace()
465
513
  if (system attribute "RELAY_WS_QUERY") is "" then return
514
+ if my openViaDeepLink() then return
466
515
  if my atTargetWorkspace() then return
467
516
  if my focusViaSidebar() then
468
517
  if my paneAgrees() then return
@@ -498,18 +547,46 @@ on tabGroups()
498
547
  return {}
499
548
  end tabGroups
500
549
 
501
- on chatTabs(tg)
550
+ on stripRadios(tg)
551
+ -- Every radio button in a tab strip, whether it is a direct child or sits one
552
+ -- group deep, in tree order. Both shapes are live at once: the terminal strip's
553
+ -- tabs are direct children, while a chat tab is wrapped in its own AXGroup with
554
+ -- the file-view toggle beside it as a plain child. Reading only the direct ones
555
+ -- (and stopping as soon as any turned up) is what made a chat strip look like a
556
+ -- one-tab strip called "Close file view".
502
557
  tell application "System Events" to tell process "Conductor"
503
- set direct to (UI elements of tg whose role is "AXRadioButton")
504
- if (count of direct) > 0 then return direct
505
- set nested to {}
506
- repeat with g in (UI elements of tg)
507
- repeat with r in (UI elements of g whose role is "AXRadioButton")
508
- set end of nested to contents of r
509
- end repeat
558
+ set acc to {}
559
+ repeat with k in (UI elements of tg)
560
+ set node to contents of k
561
+ if (role of node) is "AXRadioButton" then
562
+ set end of acc to node
563
+ else
564
+ try
565
+ repeat with r in (UI elements of node whose role is "AXRadioButton")
566
+ set end of acc to contents of r
567
+ end repeat
568
+ end try
569
+ end if
510
570
  end repeat
511
- return nested
571
+ return acc
512
572
  end tell
573
+ end stripRadios
574
+
575
+ on chatTabs(tg)
576
+ -- Of those, a chat tab is the one whose accessible name is its own close action,
577
+ -- "Close chat <title>". The strip's other radio button is "Close file view",
578
+ -- which is not a chat and must not shift the indices this file addresses tabs
579
+ -- by — off by one is how a prompt lands in the wrong conversation. Falls back to
580
+ -- every radio button when none carries the prefix, so a renamed label costs the
581
+ -- filter rather than emptying the strip.
582
+ set radios to my stripRadios(tg)
583
+ set chats to {}
584
+ repeat with entry in radios
585
+ set node to contents of entry
586
+ if (my axName(node)) starts with "Close chat" then set end of chats to node
587
+ end repeat
588
+ if (count of chats) > 0 then return chats
589
+ return radios
513
590
  end chatTabs
514
591
 
515
592
  on tabLabel(t)
@@ -1031,6 +1108,46 @@ on setWorkspaceStatus()
1031
1108
  end tell
1032
1109
  delay 0.6
1033
1110
  end setWorkspaceStatus`;
1111
+ /**
1112
+ * The URL scheme Conductor registers, which is per *release channel*: the
1113
+ * production build answers `conductor://`, the pre-release ones
1114
+ * `conductor-alpha://`, `conductor-beta://`, `conductor-dev://` and friends.
1115
+ * Everything else in this file addresses `application "Conductor"` by name, so
1116
+ * production is the only channel the write path works against anyway — the
1117
+ * override exists so a channel build needs a variable rather than a patch.
1118
+ */
1119
+ const CONDUCTOR_SCHEME = process.env.RELAY_CONDUCTOR_SCHEME || 'conductor';
1120
+ /**
1121
+ * Conductor's own link to a workspace, and optionally to one chat inside it —
1122
+ * exactly what its sidebar row menu copies under "Copy link" (Cmd+Shift+C).
1123
+ *
1124
+ * `conductor://workspace?id=<workspace>&session=<chat>` is the shape that works,
1125
+ * and the near misses all fail *badly*, so this is the one place that builds it:
1126
+ * the parameters sit behind a real `?` (unlike the create-workspace links, which
1127
+ * are flat after the scheme), the workspace id is `id` rather than `workspace`,
1128
+ * and `workspace` must be the URL's **host** — `conductor:///workspace/<id>`,
1129
+ * with the id in the path, falls through to the flat-parameter parser and
1130
+ * **creates a new workspace in the first repo**. A path-shaped link with the
1131
+ * right host (`conductor://workspace/<id>`) is merely ignored.
1132
+ *
1133
+ * `session` is optional and names a chat by `sessions.id`, the same id the relay
1134
+ * already serves; omitted, Conductor opens whichever tab that workspace had.
1135
+ * A hidden chat (`sessions.is_hidden`) has no tab, and Conductor keeps the
1136
+ * workspace's current one rather than reporting anything, so the caller's own
1137
+ * tab assertion is still what catches it.
1138
+ *
1139
+ * The https form Conductor copies for sharing —
1140
+ * `https://app.conductor.build/workspace/<id>?session=<chat>` — reaches the same
1141
+ * handler, but only once macOS has decided to hand it to the app; the desktop
1142
+ * build declares no associated domain, so a browser gets it first. Locally the
1143
+ * scheme form is the one that always lands.
1144
+ */
1145
+ function workspaceLink(workspaceId, sessionId) {
1146
+ const params = new URLSearchParams({ id: workspaceId });
1147
+ if (sessionId)
1148
+ params.set('session', sessionId);
1149
+ return `${CONDUCTOR_SCHEME}://workspace?${params.toString()}`;
1150
+ }
1034
1151
  /** Conductor's command palette matches workspaces by branch — its unique key. A
1035
1152
  * looser query (directory name) can match a command like unarchive, so prefer
1036
1153
  * branch and only fall back when it's absent. */
@@ -1086,6 +1203,7 @@ function targetEnv(target) {
1086
1203
  RELAY_WS_BRANCH: target.workspace.branch ?? '',
1087
1204
  RELAY_WS_REPO: target.workspace.repo_name ?? '',
1088
1205
  RELAY_WS_QUERY: focusQuery(target.workspace),
1206
+ RELAY_WS_LINK: workspaceLink(target.workspace.id, target.sessionId),
1089
1207
  RELAY_WS_TITLES: sidebarTitles(target.workspace).join('\n')
1090
1208
  };
1091
1209
  }
@@ -1105,18 +1223,20 @@ function osaError(err) {
1105
1223
  * permission mode the session already has (zero risk of altering the agent),
1106
1224
  * which is why it's the default.
1107
1225
  *
1108
- * Precise targeting comes from focusing the intended workspace first through
1109
- * Conductor's command palette (Cmd+K branch → Enter) and then selecting the
1110
- * target chat's tab (Accessibility, see SELECT_CHAT_TAB_HANDLERS), so the prompt
1111
- * lands in the right session regardless of what was focused — no private IPC and
1112
- * nothing to rebreak on a Conductor update (unlike the sidecar).
1226
+ * Precise targeting comes from opening Conductor's own workspace link first
1227
+ * (`conductor://workspace?id=…&session=…`, see `workspaceLink`) and then
1228
+ * confirming the pane and the chat tab through Accessibility (see
1229
+ * SELECT_CHAT_TAB_HANDLERS), so the prompt lands in the right session regardless
1230
+ * of what was focused. The link is public and id-addressed; the AX reads only
1231
+ * check it, and pressing the sidebar row or the command palette remains the
1232
+ * fallback for a Conductor that doesn't answer it.
1113
1233
  */
1114
1234
  export class AppleScriptActuator {
1115
1235
  name = 'applescript';
1116
- caveat = 'Focuses the target workspace (Cmd+K) and its chat tab before sending.';
1236
+ caveat = "Opens the target workspace's own Conductor link, then confirms the chat tab before sending.";
1117
1237
  precise = true;
1118
1238
  async send(target, text, deadline = Date.now() + SEND_ATTEMPT_MS) {
1119
- // Focus the target workspace, select its chat tab, fill the composer, send.
1239
+ // Open the target workspace's own link, confirm its chat tab, fill the composer, send.
1120
1240
  // Filling is an Accessibility write (no keystrokes, no clipboard); the
1121
1241
  // clipboard paste is kept only as a fallback, and stashes/restores around it.
1122
1242
  const script = `
@@ -1316,7 +1436,7 @@ export async function createWorkspace(prompt, repoPath) {
1316
1436
  try {
1317
1437
  // Serialized with the AX writes: creating a workspace pulls Conductor forward and
1318
1438
  // switches which one is showing, which is precisely what a concurrent send assumes.
1319
- await uiTurn(() => exec('open', [`conductor://${query}`], { timeout: 15000 }));
1439
+ await uiTurn(() => exec('open', [`${CONDUCTOR_SCHEME}://${query}`], { timeout: 15000 }));
1320
1440
  return { ok: true, strategy: 'deeplink' };
1321
1441
  }
1322
1442
  catch (err) {
@@ -1325,8 +1445,8 @@ export async function createWorkspace(prompt, repoPath) {
1325
1445
  }
1326
1446
  /**
1327
1447
  * Open a new chat in the target workspace — Conductor's "New chat, same files"
1328
- * (Cmd+T). Focuses the workspace first (command palette branch), then Cmd+T; the
1329
- * caller detects the freshly-created session id from the DB.
1448
+ * (Cmd+T). Focuses the workspace first (its own link, see `workspaceLink`), then
1449
+ * Cmd+T; the caller detects the freshly-created session id from the DB.
1330
1450
  */
1331
1451
  export async function newChat(workspace) {
1332
1452
  if (!focusQuery(workspace))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conductor-remote",
3
- "version": "1.34.1",
3
+ "version": "1.34.2",
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.",