conductor-remote 1.32.0 → 1.32.1
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/assets/{index-CqXxJtQ2.js → index-D883fUSq.js} +22 -22
- package/dist/index.html +1 -1
- package/dist/sw.js +1 -1
- package/dist-node/src/server.js +127 -38
- package/dist-node/src/writes.js +84 -13
- package/package.json +1 -1
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-D883fUSq.js"></script>
|
|
17
17
|
<link rel="stylesheet" crossorigin href="/assets/index-DwrzkcPG.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
|
|
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 c=e=>i(e,o),t={module:{uri:o},exports:l,require:c};s[o]=Promise.all(n.map(e=>t[e]||c(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:"77e0c625cdd4aa58ad272304a943de7b"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-DwrzkcPG.css",revision:null},{url:"assets/index-D883fUSq.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
|
@@ -15,7 +15,7 @@ import { notifyDevice, pushConfig, startNotifier, subscribeDevice, unsubscribeDe
|
|
|
15
15
|
import { attachPrStatus } from "./pr.js";
|
|
16
16
|
import { Reads } from "./reads.js";
|
|
17
17
|
import { driftWarningLines, tailscaleBin } from "./tailscale.js";
|
|
18
|
-
import { createWorkspace, describeActuator, EFFORT_LABELS, listAgentModels, newChat, pickActuator, setAgentOptions, setRestartGuard } from "./writes.js";
|
|
18
|
+
import { createWorkspace, describeActuator, EFFORT_LABELS, listAgentModels, newChat, pickActuator, retryWontHelp, setAgentOptions, setRestartGuard } from "./writes.js";
|
|
19
19
|
// Before anything that logs: from here on every console line is also kept in memory for
|
|
20
20
|
// `GET /api/logs`, so the phone can read why a send failed without ssh-ing into the Mac.
|
|
21
21
|
installLogCapture();
|
|
@@ -31,23 +31,74 @@ const actuator = pickActuator(cfg.writeStrategy);
|
|
|
31
31
|
setRestartGuard(() => !reads.listWorkspaces().some(w => w.session_status === 'working'));
|
|
32
32
|
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* a short poll catches a real send fast and only the failure path waits the full
|
|
40
|
-
* window. A queued prompt still writes a user row, so it counts as delivered.
|
|
34
|
+
* Has the prompt shown up as a user row yet? The receipt everything below is built
|
|
35
|
+
* on. The AppleScript actuator reports `ok` on `osascript` exit 0 — which only
|
|
36
|
+
* means the script *ran*, not that Conductor accepted the keystrokes — so without
|
|
37
|
+
* this a dropped send (asleep/unfocused Mac) looks delivered. A queued prompt still
|
|
38
|
+
* writes a user row, so it counts as delivered.
|
|
41
39
|
*/
|
|
42
|
-
|
|
40
|
+
function deliveredSince(sessionId, text, sinceRowid) {
|
|
43
41
|
const target = text.trim();
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
const { entries } = reads.getMessages(sessionId, sinceRowid);
|
|
43
|
+
return entries.some(e => e.role === 'user' && e.text.trim() === target);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Watch for that row, ending on a check rather than a sleep, and never past
|
|
47
|
+
* `budgetDeadline`. Conductor writes the row right after the send presses Enter, so
|
|
48
|
+
* a real send is confirmed in a tick and only the failure path waits the window out.
|
|
49
|
+
*
|
|
50
|
+
* The window is *also* what makes a retry safe — it is deliberately longer than the
|
|
51
|
+
* row takes to appear, because everything past it is allowed to type into the
|
|
52
|
+
* composer again — so note which end of the budget gets clipped when the two
|
|
53
|
+
* compete: a retry only happens with `MIN_ATTEMPT_MS + CONFIRM_WINDOW_MS` left, so
|
|
54
|
+
* a confirm *followed by another attempt* always gets its full window. Only the
|
|
55
|
+
* last confirm of all can be cut short, and nothing follows it to duplicate a row.
|
|
56
|
+
*/
|
|
57
|
+
async function confirmDelivery(sessionId, text, sinceRowid, budgetDeadline) {
|
|
58
|
+
const stopAt = Math.min(Date.now() + CONFIRM_WINDOW_MS, budgetDeadline);
|
|
59
|
+
for (;;) {
|
|
60
|
+
if (deliveredSince(sessionId, text, sinceRowid))
|
|
47
61
|
return true;
|
|
48
|
-
|
|
62
|
+
if (Date.now() >= stopAt)
|
|
63
|
+
return false;
|
|
64
|
+
await sleep(300);
|
|
49
65
|
}
|
|
50
|
-
|
|
66
|
+
}
|
|
67
|
+
/** How long we watch the transcript after a run before deciding it didn't land. */
|
|
68
|
+
const CONFIRM_WINDOW_MS = 6_000;
|
|
69
|
+
/** Ceiling on a whole send, retries included — no phone should hold a request open longer. */
|
|
70
|
+
const SEND_BUDGET_MS = 55_000;
|
|
71
|
+
/** Below this there isn't room for a run that could plausibly succeed, so don't start one. */
|
|
72
|
+
const MIN_ATTEMPT_MS = 12_000;
|
|
73
|
+
/**
|
|
74
|
+
* The least a confirm is worth doing at all. Held back from every run so a send that
|
|
75
|
+
* lands can be *seen* to have landed — an unconfirmed send is indistinguishable from
|
|
76
|
+
* a lost one, which is the failure this whole path exists to avoid.
|
|
77
|
+
*/
|
|
78
|
+
const MIN_CONFIRM_MS = 2_000;
|
|
79
|
+
/** Leaves the response itself time to get home before the caller's own timer fires. */
|
|
80
|
+
const RESPONSE_MARGIN_MS = 5_000;
|
|
81
|
+
/**
|
|
82
|
+
* Budget for a caller that didn't say how long it would wait — a PWA build from
|
|
83
|
+
* before `x-client-timeout-ms`, which aborted a send at a flat 25s. Sized so that
|
|
84
|
+
* such a phone is no worse off than it was: one run with a ceiling like the old one,
|
|
85
|
+
* and no retry (there was never room for a retry inside 25s).
|
|
86
|
+
*/
|
|
87
|
+
const LEGACY_SEND_BUDGET_MS = 20_000;
|
|
88
|
+
/**
|
|
89
|
+
* Never outlast the caller. The relay giving up *after* the phone has is the worst
|
|
90
|
+
* available outcome: the phone shows a failure while the send goes on to land, and
|
|
91
|
+
* the user can't tell that from a send that really didn't. Pairing our budget to
|
|
92
|
+
* the PWA's by hand wouldn't hold — the relay updates itself (autoupdate.ts) while
|
|
93
|
+
* the app sits in a service-worker cache — so the caller states its own deadline
|
|
94
|
+
* and we retry inside it.
|
|
95
|
+
*/
|
|
96
|
+
function sendBudget(req) {
|
|
97
|
+
const asked = Number(req.headers['x-client-timeout-ms']);
|
|
98
|
+
if (!Number.isFinite(asked) || asked <= 0)
|
|
99
|
+
return LEGACY_SEND_BUDGET_MS;
|
|
100
|
+
// Floor at one confirmable attempt: a caller in a hurry still gets a real try.
|
|
101
|
+
return Math.max(MIN_ATTEMPT_MS + CONFIRM_WINDOW_MS, Math.min(SEND_BUDGET_MS, asked - RESPONSE_MARGIN_MS));
|
|
51
102
|
}
|
|
52
103
|
/**
|
|
53
104
|
* Where a chat sits in Conductor's tab strip. Both write paths need it: the
|
|
@@ -79,32 +130,67 @@ async function confirmAgentOptions(ws, sessionId, opts) {
|
|
|
79
130
|
return false;
|
|
80
131
|
}
|
|
81
132
|
/**
|
|
82
|
-
* Deliver a prompt to one chat and confirm it landed
|
|
83
|
-
* phone's own sends go through it, and so
|
|
84
|
-
* get the same targeting, the same read-back
|
|
133
|
+
* Deliver a prompt to one chat and confirm it landed, retrying until the caller's
|
|
134
|
+
* budget runs out. The single write path: the phone's own sends go through it, and so
|
|
135
|
+
* does the first-prompt queue, so both get the same targeting, the same read-back,
|
|
136
|
+
* the same retries and the same errors.
|
|
137
|
+
*
|
|
138
|
+
* Retrying here rather than handing the phone a Retry button is the point: the
|
|
139
|
+
* failures this path hits are overwhelmingly warm-up costs — a cold or busy Conductor
|
|
140
|
+
* makes the first AppleScript run slow enough to be killed, and the second finds an
|
|
141
|
+
* activated app and lands — which is exactly why tapping Retry always worked. Two
|
|
142
|
+
* things make doing it automatically safe rather than a way to send a prompt twice:
|
|
143
|
+
* - **The transcript is the receipt.** Every run is followed by a full
|
|
144
|
+
* `CONFIRM_WINDOW_MS` of watching for the matching user row, *including* runs that
|
|
145
|
+
* reported an error, and the last of those checks is the moment before we type
|
|
146
|
+
* again. A run that actually landed — even one killed just after pressing Enter,
|
|
147
|
+
* or one whose row appeared after we'd stopped looking — is reported as delivered.
|
|
148
|
+
* - **The composer is written, not appended to** (`fillComposer` sets AXValue), so a
|
|
149
|
+
* retry replaces a half-finished attempt's text instead of doubling it.
|
|
150
|
+
*
|
|
151
|
+
* Bounded by a wall clock rather than an attempt count, because someone is holding
|
|
152
|
+
* this request open: runs are bounded by the caller's deadline, and we stop rather
|
|
153
|
+
* than start one the budget could not also confirm. The queue's own 3-sends-over-15-
|
|
154
|
+
* minutes schedule sits *outside* this and is unaffected — it retries a delivery that
|
|
155
|
+
* never got off the ground (worktree still setting up), not one Conductor fumbled.
|
|
85
156
|
*/
|
|
86
|
-
async function deliverPrompt(ws, sessionId, text) {
|
|
157
|
+
async function deliverPrompt(ws, sessionId, text, budgetMs = SEND_BUDGET_MS) {
|
|
87
158
|
const located = locateChat(ws, sessionId);
|
|
88
159
|
if ('error' in located)
|
|
89
|
-
return { ok: false, strategy: actuator.name, error: located.error };
|
|
90
|
-
// Snapshot the
|
|
160
|
+
return { ok: false, strategy: actuator.name, attempts: 0, error: located.error };
|
|
161
|
+
// Snapshot the cursor once: every check below asks "did *this* prompt arrive since
|
|
162
|
+
// we started", so a retry can't be fooled by an older identical prompt.
|
|
91
163
|
const beforeRowid = reads.getMessages(sessionId).cursor;
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
// The
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
164
|
+
const label = ws.branch ?? ws.id;
|
|
165
|
+
const deadline = Date.now() + budgetMs;
|
|
166
|
+
let attempts = 0;
|
|
167
|
+
let last = { ok: false, strategy: actuator.name };
|
|
168
|
+
for (;;) {
|
|
169
|
+
attempts++;
|
|
170
|
+
// The run gets the deadline, not a duration: `uiTurn` may hold it behind another
|
|
171
|
+
// write, and only the run knows what was left of the budget when it started. Minus
|
|
172
|
+
// the confirm, so a caller on a tight budget spends it on the run rather than on
|
|
173
|
+
// watching — a 25s-era phone gets one full-length attempt, not two too short to finish.
|
|
174
|
+
last = await actuator.send({ workspace: ws, sessionId, tab: located.tab }, text, deadline - MIN_CONFIRM_MS);
|
|
175
|
+
if (await confirmDelivery(sessionId, text, beforeRowid, deadline)) {
|
|
176
|
+
if (attempts > 1)
|
|
177
|
+
console.info(`[relay] send to ${label} landed on attempt ${attempts}`);
|
|
178
|
+
return { ok: true, strategy: last.strategy, attempts };
|
|
179
|
+
}
|
|
180
|
+
if (retryWontHelp(last.error))
|
|
181
|
+
break;
|
|
182
|
+
if (deadline - Date.now() < MIN_ATTEMPT_MS + CONFIRM_WINDOW_MS)
|
|
183
|
+
break;
|
|
184
|
+
// The phone only ever sees the outcome; why a send goes missing lives on this
|
|
185
|
+
// side, so leave the trail in relay.log rather than nothing at all.
|
|
186
|
+
console.warn(`[relay] send to ${label} attempt ${attempts} didn’t land (${last.error ?? 'no user row appeared'}) — retrying`);
|
|
106
187
|
}
|
|
107
|
-
|
|
188
|
+
const tried = attempts > 1 ? ` (tried ${attempts}×)` : '';
|
|
189
|
+
const error = last.ok
|
|
190
|
+
? `Send didn’t land in the chat — Conductor may have been asleep or unfocused${tried}. Try again.`
|
|
191
|
+
: `${last.error}${tried}`;
|
|
192
|
+
console.warn(`[relay] send to ${label} failed after ${attempts} attempt(s): ${error}`);
|
|
193
|
+
return { ok: false, strategy: last.strategy, attempts, error };
|
|
108
194
|
}
|
|
109
195
|
/**
|
|
110
196
|
* Undelivered first prompts, owned by this process rather than by the phone (see
|
|
@@ -359,8 +445,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
359
445
|
}
|
|
360
446
|
// Return as soon as the row exists (~2s) — waiting for delivery would block the
|
|
361
447
|
// request through Conductor's whole setup, measured at 30s+ on a real repo and
|
|
362
|
-
// past
|
|
363
|
-
// the phone watches it in /api/state; `send:true` opts API
|
|
448
|
+
// past any budget a phone should hold a request open for. The queue delivers on
|
|
449
|
+
// its own schedule and the phone watches it in /api/state; `send:true` opts API
|
|
450
|
+
// callers into waiting.
|
|
364
451
|
// Whatever happens, the prompt is already pre-filled in Conductor's composer.
|
|
365
452
|
const settled = prompt ? firstPrompts.enqueue(created.id, prompt) : null;
|
|
366
453
|
const failed = settled && body.send === true ? await settled : null;
|
|
@@ -500,7 +587,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
500
587
|
: (reads.listWorkspaces().find(w => w.active_session_id === sessionId) ?? null);
|
|
501
588
|
if (!ws)
|
|
502
589
|
return json(req, res, 404, { error: 'workspace for session not found' });
|
|
503
|
-
|
|
590
|
+
// Retries live inside deliverPrompt, confirmed against the transcript each time,
|
|
591
|
+
// and inside the deadline this phone told us it would wait.
|
|
592
|
+
const result = await deliverPrompt(ws, sessionId, text, sendBudget(req));
|
|
504
593
|
// Whatever the queue was still holding for this workspace has now been said by
|
|
505
594
|
// hand — including a failed entry the user retried from the chat.
|
|
506
595
|
if (result.ok)
|
package/dist-node/src/writes.js
CHANGED
|
@@ -25,6 +25,55 @@ function uiTurn(op) {
|
|
|
25
25
|
uiTail = turn.catch(() => undefined);
|
|
26
26
|
return turn;
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* How long one AppleScript run may take before it's killed.
|
|
30
|
+
*
|
|
31
|
+
* Sized from measurement, not taste. A send that *worked* measured 23.6s end to
|
|
32
|
+
* end on a 30-workspace sidebar — past the 20s ceiling this replaces, which is why
|
|
33
|
+
* ordinary sends were being killed mid-run and reported as "Conductor took too long
|
|
34
|
+
* to respond". The cost is Accessibility round trips, not waiting: activating a
|
|
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.
|
|
39
|
+
*/
|
|
40
|
+
export const SEND_ATTEMPT_MS = 28_000;
|
|
41
|
+
/**
|
|
42
|
+
* A run's own ceiling, taken off the caller's deadline at the moment it actually
|
|
43
|
+
* starts.
|
|
44
|
+
*
|
|
45
|
+
* Both halves matter. `uiTurn` above means a run can sit in the queue behind
|
|
46
|
+
* another write, so a duration computed when it was *requested* would let a queued
|
|
47
|
+
* run overshoot a deadline the caller is still holding a phone open on.
|
|
48
|
+
* `SEND_ATTEMPT_MS` then caps it, because a caller with a minute of budget still
|
|
49
|
+
* shouldn't spend all of it on one doomed run when a retry is the thing that works.
|
|
50
|
+
* The floor keeps a squeezed run honest instead of passing `timeout: 0`, which node
|
|
51
|
+
* reads as "no timeout at all".
|
|
52
|
+
*/
|
|
53
|
+
function runCeiling(deadline) {
|
|
54
|
+
return Math.max(5_000, Math.min(SEND_ATTEMPT_MS, deadline - Date.now()));
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Failures no amount of retrying will fix, so a caller that retries can stop at
|
|
58
|
+
* once instead of spending a whole budget to arrive at the same sentence.
|
|
59
|
+
*
|
|
60
|
+
* Matched on phrases this file writes itself — the first two from `refusalReason`,
|
|
61
|
+
* the rest from the target checks below — never on macOS's own wording, which we
|
|
62
|
+
* quote verbatim precisely because it drifts. The refusals are the ones a node
|
|
63
|
+
* upgrade causes by silently revoking Accessibility: they fail instantly and
|
|
64
|
+
* identically every time, so making the phone sit through a whole retry budget to
|
|
65
|
+
* be told a permission is missing is worse than being told at once. The rest are
|
|
66
|
+
* malformed targets — no session, no branch — which no attempt can turn into one.
|
|
67
|
+
*/
|
|
68
|
+
const TERMINAL_ERRORS = [
|
|
69
|
+
'not trusted for Accessibility',
|
|
70
|
+
'blocked the relay from controlling the UI',
|
|
71
|
+
'no session id to target',
|
|
72
|
+
'workspace has no branch to focus'
|
|
73
|
+
];
|
|
74
|
+
export function retryWontHelp(error) {
|
|
75
|
+
return error !== undefined && TERMINAL_ERRORS.some(phrase => error.includes(phrase));
|
|
76
|
+
}
|
|
28
77
|
/**
|
|
29
78
|
* The sidecar IPC path — the precise, per-session write. Delivers straight to
|
|
30
79
|
* `sessionId` over Conductor's own dispatch socket (see sidecar.ts), so it needs
|
|
@@ -42,7 +91,8 @@ export class SidecarActuator {
|
|
|
42
91
|
available() {
|
|
43
92
|
return sidecarAvailable();
|
|
44
93
|
}
|
|
45
|
-
|
|
94
|
+
/** `deadline` is ignored — the sidecar is one socket write, with no UI to wait on. */
|
|
95
|
+
async send(target, text, _deadline) {
|
|
46
96
|
const sessionId = target.sessionId ?? target.workspace.active_session_id;
|
|
47
97
|
if (!sessionId)
|
|
48
98
|
return { ok: false, strategy: this.name, error: 'no session id to target' };
|
|
@@ -382,16 +432,35 @@ on focusViaPalette()
|
|
|
382
432
|
end tell
|
|
383
433
|
end focusViaPalette
|
|
384
434
|
|
|
435
|
+
on paneAgrees()
|
|
436
|
+
-- Nothing in the open pane contradicts the target: a chat pane is there and its
|
|
437
|
+
-- header assertion passed (which is a no-op when there's no branch to check it
|
|
438
|
+
-- against). This is what confirms a sidebar press landed.
|
|
439
|
+
try
|
|
440
|
+
set strips to my tabGroups()
|
|
441
|
+
if (count of strips) is 0 then return false
|
|
442
|
+
my assertWorkspace(item 1 of strips)
|
|
443
|
+
return true
|
|
444
|
+
end try
|
|
445
|
+
return false
|
|
446
|
+
end paneAgrees
|
|
447
|
+
|
|
448
|
+
on atTargetWorkspace()
|
|
449
|
+
-- "Are we already there?" — the *strict* form of the question, so it needs a
|
|
450
|
+
-- branch to have been checked and not merely skipped. Worth asking first because
|
|
451
|
+
-- the pane header answers in ~0.5s where reading the sidebar's rows to press one
|
|
452
|
+
-- measured 5-7s, the single largest cost in a send: a phone sends to the same
|
|
453
|
+
-- workspace over and over, and Conductor stays where it was left. Safe as a
|
|
454
|
+
-- shortcut because it *is* the assertion the send makes before typing anyway.
|
|
455
|
+
if (system attribute "RELAY_WS_BRANCH") is "" then return false
|
|
456
|
+
return my paneAgrees()
|
|
457
|
+
end atTargetWorkspace
|
|
458
|
+
|
|
385
459
|
on focusWorkspace()
|
|
386
460
|
if (system attribute "RELAY_WS_QUERY") is "" then return
|
|
461
|
+
if my atTargetWorkspace() then return
|
|
387
462
|
if my focusViaSidebar() then
|
|
388
|
-
|
|
389
|
-
set strips to my tabGroups()
|
|
390
|
-
if (count of strips) > 0 then
|
|
391
|
-
my assertWorkspace(item 1 of strips)
|
|
392
|
-
return
|
|
393
|
-
end if
|
|
394
|
-
end try
|
|
463
|
+
if my paneAgrees() then return
|
|
395
464
|
end if
|
|
396
465
|
my focusViaPalette()
|
|
397
466
|
end focusWorkspace
|
|
@@ -884,7 +953,7 @@ export class AppleScriptActuator {
|
|
|
884
953
|
name = 'applescript';
|
|
885
954
|
caveat = 'Focuses the target workspace (Cmd+K) and its chat tab before sending.';
|
|
886
955
|
precise = true;
|
|
887
|
-
async send(target, text) {
|
|
956
|
+
async send(target, text, deadline = Date.now() + SEND_ATTEMPT_MS) {
|
|
888
957
|
// Focus the target workspace, select its chat tab, fill the composer, send.
|
|
889
958
|
// Filling is an Accessibility write (no keystrokes, no clipboard); the
|
|
890
959
|
// clipboard paste is kept only as a fallback, and stashes/restores around it.
|
|
@@ -914,7 +983,7 @@ end tell
|
|
|
914
983
|
try {
|
|
915
984
|
await uiTurn(() => exec('osascript', ['-e', script], {
|
|
916
985
|
env: { ...process.env, RELAY_PROMPT_FILE: tmp, ...targetEnv(target) },
|
|
917
|
-
timeout:
|
|
986
|
+
timeout: runCeiling(deadline)
|
|
918
987
|
}));
|
|
919
988
|
return { ok: true, strategy: this.name };
|
|
920
989
|
}
|
|
@@ -968,7 +1037,7 @@ return "ok"`.trim();
|
|
|
968
1037
|
RELAY_SET_FAST: opts.toggleFast ? '1' : '',
|
|
969
1038
|
RELAY_SET_MODEL: opts.model ?? ''
|
|
970
1039
|
},
|
|
971
|
-
timeout:
|
|
1040
|
+
timeout: SEND_ATTEMPT_MS
|
|
972
1041
|
}));
|
|
973
1042
|
return { ok: true, strategy: 'applescript' };
|
|
974
1043
|
}
|
|
@@ -988,7 +1057,7 @@ return my listModels()`.trim();
|
|
|
988
1057
|
try {
|
|
989
1058
|
const { stdout } = await uiTurn(() => exec('osascript', ['-e', script], {
|
|
990
1059
|
env: { ...process.env, ...targetEnv(target) },
|
|
991
|
-
timeout:
|
|
1060
|
+
timeout: SEND_ATTEMPT_MS
|
|
992
1061
|
}));
|
|
993
1062
|
const models = stdout
|
|
994
1063
|
.split('\n')
|
|
@@ -1060,9 +1129,11 @@ tell application "System Events"
|
|
|
1060
1129
|
keystroke "t" using {command down}
|
|
1061
1130
|
end tell`.trim();
|
|
1062
1131
|
try {
|
|
1132
|
+
// Shares the focus path with a send, so it needs the same ceiling: 15s was under
|
|
1133
|
+
// the cost of activating a cold Conductor and finding the row on its own.
|
|
1063
1134
|
await uiTurn(() => exec('osascript', ['-e', script], {
|
|
1064
1135
|
env: { ...process.env, ...targetEnv({ workspace, sessionId: null }) },
|
|
1065
|
-
timeout:
|
|
1136
|
+
timeout: SEND_ATTEMPT_MS
|
|
1066
1137
|
}));
|
|
1067
1138
|
return { ok: true, strategy: 'applescript' };
|
|
1068
1139
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "conductor-remote",
|
|
3
|
-
"version": "1.32.
|
|
3
|
+
"version": "1.32.1",
|
|
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.",
|