conductor-remote 1.67.0 → 1.68.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-BqCdHovG.js +47 -0
- package/dist/index.html +1 -1
- package/dist/sw.js +1 -1
- package/dist-node/src/conductor.applescript +266 -41
- package/dist-node/src/mcp-tools.js +1 -1
- package/dist-node/src/server.js +38 -5
- package/dist-node/src/shared.js +33 -0
- package/dist-node/src/writes.js +15 -7
- package/package.json +1 -1
- package/dist/assets/index-P1erC-f5.js +0 -47
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-BqCdHovG.js"></script>
|
|
17
17
|
<link rel="stylesheet" crossorigin href="/assets/index-YPRuGHP4.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:"e1e682e2e5e88fa03b7808ae9db8e098"},{url:"index.html",revision:"
|
|
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:"e1e682e2e5e88fa03b7808ae9db8e098"},{url:"index.html",revision:"82beeb823118f5808af7be9a2f306f5d"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-YPRuGHP4.css",revision:null},{url:"assets/index-BqCdHovG.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\//]}))});
|
|
@@ -340,7 +340,7 @@ on activateConductor()
|
|
|
340
340
|
-- retry loop polls this cheaply until its deadline, so an unlock mid-budget
|
|
341
341
|
-- lands the send by itself — that is the observed recovery, not a theory.
|
|
342
342
|
if (item 1 of firstProbe) < 1 and my screenLocked() is "locked" then
|
|
343
|
-
error "The Mac is locked - the lock screen hides Conductor from the relay, so
|
|
343
|
+
error "The Mac is locked - the lock screen hides Conductor from the relay, so nothing can be sent or pressed. Unlock the Mac and try again." & my windowEvidence()
|
|
344
344
|
end if
|
|
345
345
|
try
|
|
346
346
|
tell application "Conductor" to activate
|
|
@@ -379,14 +379,14 @@ on activateConductor()
|
|
|
379
379
|
-- window server can tell the difference, so a window it CAN see blocks the
|
|
380
380
|
-- restart — this is a human's problem, said in words, not the relay's.
|
|
381
381
|
if my serverWindowCount() > 0 then
|
|
382
|
-
if lockState is "locked" then error "The Mac is locked - Conductor's window is open behind the lock screen, where the relay can't reach it. Unlock the Mac and
|
|
382
|
+
if lockState is "locked" then error "The Mac is locked - Conductor's window is open behind the lock screen, where the relay can't reach it. Unlock the Mac and try again." & ev
|
|
383
383
|
error "Conductor's window exists but is not reachable - it is probably full screen on another Space. Take Conductor out of full screen, then send again." & ev
|
|
384
384
|
end if
|
|
385
385
|
-- No window anywhere and the lock screen up: don't restart. Every relaunch
|
|
386
386
|
-- fired behind the lock screen in the live incident came up windowless, and
|
|
387
387
|
-- the last one came up wedged — answering deep links, hanging every AX
|
|
388
388
|
-- read. A restart here is not a lever, it is how the wreckage happened.
|
|
389
|
-
if lockState is "locked" then error "The Mac is locked and Conductor has no window. Relaunching behind the lock screen is what leaves it wedged, so the relay won't - unlock the Mac and
|
|
389
|
+
if lockState is "locked" then error "The Mac is locked and Conductor has no window. Relaunching behind the lock screen is what leaves it wedged, so the relay won't - unlock the Mac and try again." & ev
|
|
390
390
|
set restartError to my restartConductor()
|
|
391
391
|
if restartError is not "" then error restartError & ev
|
|
392
392
|
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." & ev
|
|
@@ -430,39 +430,243 @@ on sidebarLinks()
|
|
|
430
430
|
end tell
|
|
431
431
|
end sidebarLinks
|
|
432
432
|
|
|
433
|
-
on
|
|
434
|
-
-- The workspace's own sidebar row. Only rows that are actually rendered exist
|
|
435
|
-
-- in the AX tree (a collapsed section has none), and the title follows
|
|
436
|
-
-- Conductor's precedence, so we try each candidate and require a *unique* hit —
|
|
437
|
-
-- anything ambiguous returns nothing rather than guessing at a neighbour.
|
|
433
|
+
on findSidebarRowOnce()
|
|
438
434
|
set titles to my targetSidebarTitles()
|
|
439
435
|
if (count of titles) is 0 then return missing value
|
|
440
436
|
set repoName to system attribute "RELAY_WS_REPO"
|
|
441
|
-
set
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
-- "+adds -dels"), and one row going stale mid-scan must not take the search down.
|
|
447
|
-
set rowNames to {}
|
|
448
|
-
repeat with entry in rows
|
|
449
|
-
set end of rowNames to my axName(contents of entry)
|
|
450
|
-
end repeat
|
|
437
|
+
set pair to my sidebarRowsAndNames()
|
|
438
|
+
set refs to item 1 of pair
|
|
439
|
+
set nms to item 2 of pair
|
|
440
|
+
set total to count of refs
|
|
441
|
+
if total is 0 or (count of nms) is not total then return missing value
|
|
451
442
|
repeat with candidate in titles
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
443
|
+
set needle to candidate as text
|
|
444
|
+
if needle is not "" then
|
|
445
|
+
set hits to {}
|
|
446
|
+
repeat with i from 1 to total
|
|
447
|
+
set rowName to my textOrEmpty(item i of nms)
|
|
448
|
+
if rowName contains needle then
|
|
449
|
+
if repoName is "" or rowName contains repoName then set end of hits to i
|
|
458
450
|
end if
|
|
459
451
|
end repeat
|
|
460
|
-
if (count of
|
|
452
|
+
if (count of hits) is 1 then
|
|
453
|
+
set idx to item 1 of hits
|
|
454
|
+
set hit to item idx of refs
|
|
455
|
+
-- The references and the names are two reads, so a sidebar that
|
|
456
|
+
-- re-rendered between them (every commit rewrites a row's
|
|
457
|
+
-- "+adds -dels") would have shifted the index under us — and pressing
|
|
458
|
+
-- the neighbour sets the wrong workspace's status, which is the one
|
|
459
|
+
-- failure this whole file is built to refuse. Confirm the element
|
|
460
|
+
-- still carries the name it was picked for.
|
|
461
|
+
if (my axName(hit)) is (my textOrEmpty(item idx of nms)) then return hit
|
|
462
|
+
return missing value
|
|
463
|
+
end if
|
|
461
464
|
end if
|
|
462
465
|
end repeat
|
|
463
466
|
return missing value
|
|
467
|
+
end findSidebarRowOnce
|
|
468
|
+
|
|
469
|
+
on findSidebarRow()
|
|
470
|
+
-- The workspace's own sidebar row. Only rows that are actually rendered exist
|
|
471
|
+
-- in the AX tree (a collapsed section has none — expandSections is what opens
|
|
472
|
+
-- those), and the title follows Conductor's precedence, so we try each
|
|
473
|
+
-- candidate and require a *unique* hit — anything ambiguous returns nothing
|
|
474
|
+
-- rather than guessing at a neighbour. Two passes because the only thing that
|
|
475
|
+
-- fails the check inside is a sidebar that moved mid-read, and that is worth
|
|
476
|
+
-- one cheap retry.
|
|
477
|
+
set hit to my findSidebarRowOnce()
|
|
478
|
+
if hit is not missing value then return hit
|
|
479
|
+
return my findSidebarRowOnce()
|
|
464
480
|
end findSidebarRow
|
|
465
481
|
|
|
482
|
+
on textOrEmpty(v)
|
|
483
|
+
try
|
|
484
|
+
if v is missing value then return ""
|
|
485
|
+
return v as text
|
|
486
|
+
on error
|
|
487
|
+
return ""
|
|
488
|
+
end try
|
|
489
|
+
end textOrEmpty
|
|
490
|
+
|
|
491
|
+
on childRoles(el)
|
|
492
|
+
-- Every child's role in *one* Apple event. Every AX read here is a round trip
|
|
493
|
+
-- to another process, so asking element by element is what turned a sidebar
|
|
494
|
+
-- scan into 15 seconds — more, by itself, than this write's whole budget.
|
|
495
|
+
tell application "System Events" to tell process "Conductor"
|
|
496
|
+
try
|
|
497
|
+
return role of (UI elements of el)
|
|
498
|
+
on error
|
|
499
|
+
return {}
|
|
500
|
+
end try
|
|
501
|
+
end tell
|
|
502
|
+
end childRoles
|
|
503
|
+
|
|
504
|
+
on childRolesAndNames(el)
|
|
505
|
+
tell application "System Events" to tell process "Conductor"
|
|
506
|
+
try
|
|
507
|
+
return {role of (UI elements of el), name of (UI elements of el)}
|
|
508
|
+
on error
|
|
509
|
+
return {{}, {}}
|
|
510
|
+
end try
|
|
511
|
+
end tell
|
|
512
|
+
end childRolesAndNames
|
|
513
|
+
|
|
514
|
+
on looksLikeSidebarList(el)
|
|
515
|
+
-- The sidebar's workspace list is one flat run of section headers
|
|
516
|
+
-- (AXStaticText) and workspace rows (AXLink), with nothing else beside them.
|
|
517
|
+
-- That shape is the whole test, because the *transcript* hangs off the same
|
|
518
|
+
-- web area and holds AXLinks of its own — "the element with links in it"
|
|
519
|
+
-- would happily hand back a message bubble, and every press below would then
|
|
520
|
+
-- land in one.
|
|
521
|
+
set roleList to my childRoles(el)
|
|
522
|
+
if (count of roleList) is 0 then return false
|
|
523
|
+
set seenLink to false
|
|
524
|
+
repeat with r in roleList
|
|
525
|
+
set rt to my textOrEmpty(contents of r)
|
|
526
|
+
if rt is "AXLink" then
|
|
527
|
+
set seenLink to true
|
|
528
|
+
else if rt is not "AXStaticText" then
|
|
529
|
+
return false
|
|
530
|
+
end if
|
|
531
|
+
end repeat
|
|
532
|
+
return seenLink
|
|
533
|
+
end looksLikeSidebarList
|
|
534
|
+
|
|
535
|
+
on sidebarList()
|
|
536
|
+
-- Found by that shape rather than by an index, since the chrome above it
|
|
537
|
+
-- (project pickers, the resource-usage button) comes and goes. Two levels
|
|
538
|
+
-- deep, the same reach sidebarLinks() uses.
|
|
539
|
+
set wa to my webArea()
|
|
540
|
+
repeat with a in (my axKids(wa))
|
|
541
|
+
set nodeA to contents of a
|
|
542
|
+
if my looksLikeSidebarList(nodeA) then return nodeA
|
|
543
|
+
repeat with b in (my axKids(nodeA))
|
|
544
|
+
set nodeB to contents of b
|
|
545
|
+
if my looksLikeSidebarList(nodeB) then return nodeB
|
|
546
|
+
end repeat
|
|
547
|
+
end repeat
|
|
548
|
+
return missing value
|
|
549
|
+
end sidebarList
|
|
550
|
+
|
|
551
|
+
on sidebarRowsAndNames()
|
|
552
|
+
-- Every sidebar row paired with its name. Two Apple events off the list found
|
|
553
|
+
-- above, against one per row before.
|
|
554
|
+
set listEl to my sidebarList()
|
|
555
|
+
if listEl is not missing value then
|
|
556
|
+
tell application "System Events" to tell process "Conductor"
|
|
557
|
+
try
|
|
558
|
+
return {(UI elements of listEl whose role is "AXLink"), name of (UI elements of listEl whose role is "AXLink")}
|
|
559
|
+
end try
|
|
560
|
+
end tell
|
|
561
|
+
end if
|
|
562
|
+
-- No list matched the shape, so Conductor has reorganised its sidebar. Fall
|
|
563
|
+
-- back to the broad two-level sweep and one name read per row: slow, and the
|
|
564
|
+
-- reason the fast path exists, but it assumes nothing about the layout.
|
|
565
|
+
set refs to my sidebarLinks()
|
|
566
|
+
set nms to {}
|
|
567
|
+
repeat with entry in refs
|
|
568
|
+
set end of nms to my axName(contents of entry)
|
|
569
|
+
end repeat
|
|
570
|
+
return {refs, nms}
|
|
571
|
+
end sidebarRowsAndNames
|
|
572
|
+
|
|
573
|
+
on looksNumeric(t)
|
|
574
|
+
-- A folded section draws its row count where its rows would be. That number is
|
|
575
|
+
-- a sibling of the header, not a child of it, so it has to be told apart from a
|
|
576
|
+
-- header by hand — digits only, which no status or repo name is.
|
|
577
|
+
if t is "" then return false
|
|
578
|
+
repeat with c in (characters of t)
|
|
579
|
+
if "0123456789" does not contain (c as text) then return false
|
|
580
|
+
end repeat
|
|
581
|
+
return true
|
|
582
|
+
end looksNumeric
|
|
583
|
+
|
|
584
|
+
on collapsedSectionNames()
|
|
585
|
+
-- The sections someone has folded shut. A section's rows are *siblings* of
|
|
586
|
+
-- its header, so an expanded one is followed by an AXLink and a collapsed one
|
|
587
|
+
-- by its count, by the next header, or by nothing — and a collapsed section
|
|
588
|
+
-- renders no rows at all, which is why its workspaces are invisible to
|
|
589
|
+
-- Accessibility. Names, not element references: everything below re-finds the
|
|
590
|
+
-- header when it needs it, because the list re-renders under us (a status
|
|
591
|
+
-- change moves a row to another group) and a System Events reference is an
|
|
592
|
+
-- index into a tree that has already changed.
|
|
593
|
+
set listEl to my sidebarList()
|
|
594
|
+
if listEl is missing value then return {}
|
|
595
|
+
set pair to my childRolesAndNames(listEl)
|
|
596
|
+
set roleList to item 1 of pair
|
|
597
|
+
set nameList to item 2 of pair
|
|
598
|
+
set total to count of roleList
|
|
599
|
+
if total is 0 or (count of nameList) is not total then return {}
|
|
600
|
+
set out to {}
|
|
601
|
+
repeat with i from 1 to total
|
|
602
|
+
if (my textOrEmpty(item i of roleList)) is "AXStaticText" then
|
|
603
|
+
set nm to my textOrEmpty(item i of nameList)
|
|
604
|
+
if nm is not "" and not (my looksNumeric(nm)) then
|
|
605
|
+
set nextRole to ""
|
|
606
|
+
if i < total then set nextRole to my textOrEmpty(item (i + 1) of roleList)
|
|
607
|
+
if nextRole is not "AXLink" then set end of out to nm
|
|
608
|
+
end if
|
|
609
|
+
end if
|
|
610
|
+
end repeat
|
|
611
|
+
return out
|
|
612
|
+
end collapsedSectionNames
|
|
613
|
+
|
|
614
|
+
on sectionHeaderNamed(wanted)
|
|
615
|
+
-- Unique or nothing, like every other lookup here: two sections cannot share
|
|
616
|
+
-- a name, so an ambiguous hit means we are reading the wrong list.
|
|
617
|
+
set listEl to my sidebarList()
|
|
618
|
+
if listEl is missing value then return missing value
|
|
619
|
+
tell application "System Events" to tell process "Conductor"
|
|
620
|
+
try
|
|
621
|
+
set hits to (UI elements of listEl whose role is "AXStaticText" and name is wanted)
|
|
622
|
+
on error
|
|
623
|
+
return missing value
|
|
624
|
+
end try
|
|
625
|
+
end tell
|
|
626
|
+
if (count of hits) is 1 then return item 1 of hits
|
|
627
|
+
return missing value
|
|
628
|
+
end sectionHeaderNamed
|
|
629
|
+
|
|
630
|
+
on toggleSection(headerName)
|
|
631
|
+
set head to my sectionHeaderNamed(headerName)
|
|
632
|
+
if head is missing value then return false
|
|
633
|
+
tell application "System Events" to tell process "Conductor"
|
|
634
|
+
try
|
|
635
|
+
perform action "AXScrollToVisible" of head
|
|
636
|
+
end try
|
|
637
|
+
try
|
|
638
|
+
perform action "AXPress" of head
|
|
639
|
+
return true
|
|
640
|
+
on error
|
|
641
|
+
return false
|
|
642
|
+
end try
|
|
643
|
+
end tell
|
|
644
|
+
end toggleSection
|
|
645
|
+
|
|
646
|
+
on expandSections()
|
|
647
|
+
-- Open every folded section and report which ones we opened, so the caller can
|
|
648
|
+
-- put the sidebar back the way it found it. The header is the whole control —
|
|
649
|
+
-- it carries AXPress and toggles the group — so this needs no chevron, which
|
|
650
|
+
-- the webview does not expose anyway.
|
|
651
|
+
set opened to {}
|
|
652
|
+
repeat with nm in (my collapsedSectionNames())
|
|
653
|
+
if my toggleSection(nm as text) then set end of opened to (nm as text)
|
|
654
|
+
end repeat
|
|
655
|
+
if (count of opened) > 0 then delay 0.6
|
|
656
|
+
return opened
|
|
657
|
+
end expandSections
|
|
658
|
+
|
|
659
|
+
on restoreSections(names)
|
|
660
|
+
-- Fold back exactly what we opened. Best-effort on purpose: this runs after
|
|
661
|
+
-- the write has already landed, so a header that moved or vanished must not
|
|
662
|
+
-- turn a status change that worked into a reported failure.
|
|
663
|
+
repeat with nm in names
|
|
664
|
+
try
|
|
665
|
+
my toggleSection(nm as text)
|
|
666
|
+
end try
|
|
667
|
+
end repeat
|
|
668
|
+
end restoreSections
|
|
669
|
+
|
|
466
670
|
on focusViaSidebar()
|
|
467
671
|
-- Press that row: no keystrokes at all, so nothing can be swallowed by a
|
|
468
672
|
-- focused field or fire a palette *command*. Being wrong is survivable here —
|
|
@@ -506,9 +710,10 @@ on atTargetWorkspace()
|
|
|
506
710
|
-- "Are we already there?" — the *strict* form of the question, so it needs a
|
|
507
711
|
-- branch to have been checked and not merely skipped. Worth asking first because
|
|
508
712
|
-- the pane header answers in ~0.5s where reading the sidebar's rows to press one
|
|
509
|
-
-- measured 5-7s,
|
|
510
|
-
--
|
|
511
|
-
--
|
|
713
|
+
-- measured 5-7s, and ~1s since that scan went to bulk reads — still the larger
|
|
714
|
+
-- cost of the two: a phone sends to the same workspace over and over, and
|
|
715
|
+
-- Conductor stays where it was left. Safe as a shortcut because it *is* the
|
|
716
|
+
-- assertion the send makes before typing anyway.
|
|
512
717
|
if (system attribute "RELAY_WS_BRANCH") is "" then return false
|
|
513
718
|
return my paneAgrees()
|
|
514
719
|
end atTargetWorkspace
|
|
@@ -1316,23 +1521,13 @@ on dismissMenus()
|
|
|
1316
1521
|
end tell
|
|
1317
1522
|
end dismissMenus
|
|
1318
1523
|
|
|
1319
|
-
on
|
|
1320
|
-
-- Conductor has no menu-bar or palette command for this, so the only lever is
|
|
1321
|
-
-- the sidebar row's own context menu (Mark as unread / Pin / Set status /
|
|
1322
|
-
-- Rename / Copy link / Archive). Right-clicking the row needs no focus change,
|
|
1323
|
-
-- so unlike a send this never disturbs which workspace is on screen.
|
|
1324
|
-
set wanted to system attribute "RELAY_SET_STATUS"
|
|
1325
|
-
if wanted is "" then error "no status requested"
|
|
1326
|
-
set theRow to my findSidebarRow()
|
|
1327
|
-
if theRow is missing value then
|
|
1328
|
-
error "couldn't find this workspace in the sidebar — a collapsed section hides its row from Accessibility"
|
|
1329
|
-
end if
|
|
1524
|
+
on pressStatusMenu(rowEl, wanted)
|
|
1330
1525
|
-- Scroll it into view first. A row that exists in the AX tree but sits outside
|
|
1331
1526
|
-- the sidebar's visible strip accepts AXShowMenu and draws nothing — which is
|
|
1332
1527
|
-- exactly what happens right after a status change moves it to another group.
|
|
1333
1528
|
tell application "System Events" to tell process "Conductor"
|
|
1334
1529
|
try
|
|
1335
|
-
perform action "AXScrollToVisible" of
|
|
1530
|
+
perform action "AXScrollToVisible" of rowEl
|
|
1336
1531
|
end try
|
|
1337
1532
|
end tell
|
|
1338
1533
|
delay 0.4
|
|
@@ -1341,7 +1536,7 @@ on setWorkspaceStatus()
|
|
|
1341
1536
|
tell application "System Events" to key code 53
|
|
1342
1537
|
delay 0.3
|
|
1343
1538
|
tell application "System Events" to tell process "Conductor"
|
|
1344
|
-
perform action "AXShowMenu" of
|
|
1539
|
+
perform action "AXShowMenu" of rowEl
|
|
1345
1540
|
end tell
|
|
1346
1541
|
-- Depth 4, not the whole tree: the row menu is a portal near the top of the web
|
|
1347
1542
|
-- area, and the *transcript* hangs off that same root — a deep sweep walks every
|
|
@@ -1381,6 +1576,36 @@ on setWorkspaceStatus()
|
|
|
1381
1576
|
perform action "AXPress" of choice
|
|
1382
1577
|
end tell
|
|
1383
1578
|
delay 0.6
|
|
1579
|
+
end pressStatusMenu
|
|
1580
|
+
|
|
1581
|
+
on setWorkspaceStatus()
|
|
1582
|
+
-- Conductor has no menu-bar or palette command for this, so the only lever is
|
|
1583
|
+
-- the sidebar row's own context menu (Mark as unread / Pin / Set status /
|
|
1584
|
+
-- Rename / Copy link / Archive). Right-clicking the row needs no focus change,
|
|
1585
|
+
-- so unlike a send this never disturbs which workspace is on screen.
|
|
1586
|
+
set wanted to system attribute "RELAY_SET_STATUS"
|
|
1587
|
+
if wanted is "" then error "no status requested"
|
|
1588
|
+
set theRow to my findSidebarRow()
|
|
1589
|
+
set opened to {}
|
|
1590
|
+
if theRow is missing value then
|
|
1591
|
+
-- A folded section renders no rows, so the row may simply not be in the tree
|
|
1592
|
+
-- yet. There is no palette command to fall back to, so open the sections
|
|
1593
|
+
-- ourselves and look again — then fold them back, since the one thing this
|
|
1594
|
+
-- write promises is that the sidebar is where you left it.
|
|
1595
|
+
set opened to my expandSections()
|
|
1596
|
+
if (count of opened) > 0 then set theRow to my findSidebarRow()
|
|
1597
|
+
end if
|
|
1598
|
+
if theRow is missing value then
|
|
1599
|
+
my restoreSections(opened)
|
|
1600
|
+
error "couldn't find this workspace in the sidebar"
|
|
1601
|
+
end if
|
|
1602
|
+
try
|
|
1603
|
+
my pressStatusMenu(theRow, wanted)
|
|
1604
|
+
on error errMsg
|
|
1605
|
+
my restoreSections(opened)
|
|
1606
|
+
error errMsg
|
|
1607
|
+
end try
|
|
1608
|
+
my restoreSections(opened)
|
|
1384
1609
|
end setWorkspaceStatus
|
|
1385
1610
|
|
|
1386
1611
|
(* -- Instant Hotspot ----------------------------------------------------------
|
|
@@ -613,7 +613,7 @@ export function createTools(call) {
|
|
|
613
613
|
},
|
|
614
614
|
{
|
|
615
615
|
name: 'set_workspace_status',
|
|
616
|
-
description: 'Set a workspace’s status in Conductor’s sidebar (backlog, in-progress, in-review, done, canceled). Drives the real UI through the sidebar row menu, but changes nothing on screen.
|
|
616
|
+
description: 'Set a workspace’s status in Conductor’s sidebar (backlog, in-progress, in-review, done, canceled). Drives the real UI through the sidebar row menu, but changes nothing on screen. A collapsed section hides its rows from Accessibility, so folded sections are opened, used, and folded back — which makes this run a few seconds longer.',
|
|
617
617
|
inputSchema: {
|
|
618
618
|
type: 'object',
|
|
619
619
|
properties: {
|
package/dist-node/src/server.js
CHANGED
|
@@ -27,6 +27,7 @@ import { isRoute, routeParam, routes } from "./routes.js";
|
|
|
27
27
|
import { foldHits, queryTokens, SearchIndex } from "./search.js";
|
|
28
28
|
import { SendOnce } from "./sendonce.js";
|
|
29
29
|
import { readSettings, writeSettings } from "./settings.js";
|
|
30
|
+
import { withoutWindowEvidence } from "./shared.js";
|
|
30
31
|
import { discardStagedAttachment, materializeStagedAttachments, stageAttachment, stagedAttachments } from "./staged-attachments.js";
|
|
31
32
|
import { driftWarningLines, readExposeMode, tailscaleBin } from "./tailscale.js";
|
|
32
33
|
import { renderTranscript } from "./transcript.js";
|
|
@@ -573,6 +574,31 @@ function previewRefusal(filePath) {
|
|
|
573
574
|
const MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
|
|
574
575
|
class PayloadTooLargeError extends Error {
|
|
575
576
|
}
|
|
577
|
+
/**
|
|
578
|
+
* The last thing that happens to an error before it leaves the relay: the diagnostic
|
|
579
|
+
* tail `windowEvidence()` appends comes off, and the full text goes to the log instead.
|
|
580
|
+
*
|
|
581
|
+
* One place rather than the dozen routes that hand back a UI-write failure, for the
|
|
582
|
+
* same reason `redactSecrets` sits in front of every served log line: a rule applied at
|
|
583
|
+
* the boundary cannot be forgotten by the next route someone adds. What it prevents is
|
|
584
|
+
* what a tap on Fork against a locked Mac used to answer with — the sentence, then
|
|
585
|
+
* "[window server: 6; screen: locked] [processes: conductor=0] [menus: Apple, Conductor,
|
|
586
|
+
* File, ...]", in 11px red, on a phone that can do nothing with any of it. The evidence
|
|
587
|
+
* is still the fastest way to tell a wedged Conductor from a hidden window, so it lands
|
|
588
|
+
* in relay.log, which `/api/logs` serves to the same phone on request.
|
|
589
|
+
*/
|
|
590
|
+
function forTheClient(body) {
|
|
591
|
+
if (!body || typeof body !== 'object' || Array.isArray(body))
|
|
592
|
+
return body;
|
|
593
|
+
const error = body.error;
|
|
594
|
+
if (typeof error !== 'string')
|
|
595
|
+
return body;
|
|
596
|
+
const clean = withoutWindowEvidence(error);
|
|
597
|
+
if (clean === error)
|
|
598
|
+
return body;
|
|
599
|
+
console.warn(`[relay] ${error}`);
|
|
600
|
+
return { ...body, error: clean };
|
|
601
|
+
}
|
|
576
602
|
/**
|
|
577
603
|
* Successful GETs are conditional + compressed to keep the phone's polling cheap.
|
|
578
604
|
* `no-cache` (not `no-store`) means the browser must revalidate on every tick —
|
|
@@ -581,7 +607,7 @@ class PayloadTooLargeError extends Error {
|
|
|
581
607
|
* ~1 KB go out gzipped. Errors and non-GETs stay unconditional `no-store`.
|
|
582
608
|
*/
|
|
583
609
|
function json(req, res, status, body) {
|
|
584
|
-
const payload = Buffer.from(JSON.stringify(body));
|
|
610
|
+
const payload = Buffer.from(JSON.stringify(forTheClient(body)));
|
|
585
611
|
if (status !== 200 || req.method !== 'GET') {
|
|
586
612
|
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
|
|
587
613
|
return void res.end(payload);
|
|
@@ -844,15 +870,21 @@ const server = http.createServer(async (req, res) => {
|
|
|
844
870
|
// instead of asking someone to type a network name from memory on a phone keyboard.
|
|
845
871
|
// `ssid` is best-effort and often null (macOS gates it behind Location Services).
|
|
846
872
|
if (isRoute(routes.settings, req.method, pathname)) {
|
|
847
|
-
//
|
|
873
|
+
// Five subprocesses, all concurrent: this is the one route that shells out more
|
|
848
874
|
// than once, and serialising them would put the phone's polls behind the sum.
|
|
849
|
-
const [known, current, autoJoinHotspot, nosleep] = await Promise.all([
|
|
875
|
+
const [known, current, autoJoinHotspot, nosleep, locked] = await Promise.all([
|
|
850
876
|
preferredNetworks(),
|
|
851
877
|
currentSsid(),
|
|
852
878
|
// macOS's own Auto-join Hotspot setting. On "Never" the Mac won't reach for
|
|
853
879
|
// your phone unprompted, which no amount of relay code can substitute for.
|
|
854
880
|
autoJoinHotspotMode(),
|
|
855
|
-
nosleepState()
|
|
881
|
+
nosleepState(),
|
|
882
|
+
// Read here rather than polled: this sheet is where someone goes when the Mac
|
|
883
|
+
// stopped answering, and a keep-awake window that says "automatic screen lock
|
|
884
|
+
// is off" beside a Mac that is locked right now reads as the relay lying. The
|
|
885
|
+
// assertion blocks the *idle* lock; it cannot lift one already up, and a lid
|
|
886
|
+
// close or a manual lock puts one up whatever it holds.
|
|
887
|
+
screenLocked()
|
|
856
888
|
]);
|
|
857
889
|
return json(req, res, 200, {
|
|
858
890
|
settings: readSettings(),
|
|
@@ -863,7 +895,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
863
895
|
likelyHotspots: known.filter(looksLikeHotspot),
|
|
864
896
|
autoJoinHotspot
|
|
865
897
|
},
|
|
866
|
-
nosleep: { ...nosleep, maxSeconds: NOSLEEP_MAX_SECONDS }
|
|
898
|
+
nosleep: { ...nosleep, maxSeconds: NOSLEEP_MAX_SECONDS },
|
|
899
|
+
screenLocked: locked
|
|
867
900
|
});
|
|
868
901
|
}
|
|
869
902
|
// PATCH /api/settings { fallbackSsids?, autoRejoin? } — merge and persist.
|
package/dist-node/src/shared.js
CHANGED
|
@@ -214,3 +214,36 @@ export function isPreviewableSource(filePath) {
|
|
|
214
214
|
export function isToolResult(e) {
|
|
215
215
|
return e.role === 'tool' && !e.tool && e.output !== undefined;
|
|
216
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* The sentence every locked-Mac refusal starts with (src/conductor.applescript), and
|
|
219
|
+
* the one thing two sides must agree it means.
|
|
220
|
+
*
|
|
221
|
+
* The relay decides control flow on it — `lockBlocked` (src/writes.ts) parks the prompt
|
|
222
|
+
* rather than burning a phone's retry budget against a screen that will not answer for
|
|
223
|
+
* hours — and the phone decides what to draw: a link to Screen Sharing, because the
|
|
224
|
+
* relay will never unlock the Mac itself. Two matchers over one phrase, spelled out in
|
|
225
|
+
* two files, is how one of them quietly stops matching. macOS's own wording is never
|
|
226
|
+
* matched anywhere here; this sentence is ours, so it cannot drift under us.
|
|
227
|
+
*/
|
|
228
|
+
export const MAC_LOCKED = 'The Mac is locked';
|
|
229
|
+
export function isLockedError(error) {
|
|
230
|
+
return (error ?? '').includes(MAC_LOCKED);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* The diagnostic tail `windowEvidence()` (src/conductor.applescript) appends to every
|
|
234
|
+
* window and lock refusal: the window server's count, the lock state, every process
|
|
235
|
+
* named Conductor with its window count, and the menu bar titles.
|
|
236
|
+
*
|
|
237
|
+
* It exists to separate "genuinely windowless" from "we are addressing the wrong
|
|
238
|
+
* process", which is a question for the relay's log. It reached the phone as well,
|
|
239
|
+
* where a tap on Fork against a locked Mac answered with four lines of red text ending
|
|
240
|
+
* in "[menus: Apple, Conductor, File, Edit, View, Window, Help]" and nothing to act on.
|
|
241
|
+
* So `json()` in src/server.ts cuts it on the way out and logs the full text instead.
|
|
242
|
+
*
|
|
243
|
+
* Anchored on our own format: the whole tail is one run of bracketed groups that starts
|
|
244
|
+
* at "[window server:", so a single cut takes all of it and never touches a message
|
|
245
|
+
* that carries none.
|
|
246
|
+
*/
|
|
247
|
+
export function withoutWindowEvidence(error) {
|
|
248
|
+
return error.replace(/\s*\[window server:.*$/s, '').trim();
|
|
249
|
+
}
|
package/dist-node/src/writes.js
CHANGED
|
@@ -3,7 +3,7 @@ import { execFile } from 'node:child_process';
|
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
|
-
import { modelPickerLabel } from "./shared.js";
|
|
6
|
+
import { isLockedError, modelPickerLabel } from "./shared.js";
|
|
7
7
|
import { sidecarAvailable, sidecarSendUserMessage } from "./sidecar.js";
|
|
8
8
|
const exec = promisify(execFile);
|
|
9
9
|
/** Waiting runs past this are refused rather than queued. */
|
|
@@ -145,10 +145,12 @@ export function retryWontHelp(error) {
|
|
|
145
145
|
* warm-up cost a retry loop should burn budget on — the caller parks the prompt
|
|
146
146
|
* instead (src/parked.ts) and the queue delivers it when the lock lifts. Matched
|
|
147
147
|
* on the phrase every lock refusal in conductor.applescript starts with; the
|
|
148
|
-
* words are ours, so they can't drift under us the way macOS's can.
|
|
148
|
+
* words are ours, so they can't drift under us the way macOS's can. The phrase
|
|
149
|
+
* itself lives in src/shared.ts because the phone matches it too — it is what
|
|
150
|
+
* puts the Screen Sharing link beside a refusal nobody else can clear.
|
|
149
151
|
*/
|
|
150
152
|
export function lockBlocked(error) {
|
|
151
|
-
return (error
|
|
153
|
+
return isLockedError(error);
|
|
152
154
|
}
|
|
153
155
|
/**
|
|
154
156
|
* A run that ended with the prompt still sitting in Conductor's composer
|
|
@@ -596,9 +598,15 @@ export const WORKSPACE_STATUS_LABELS = {
|
|
|
596
598
|
*
|
|
597
599
|
* Unlike every other write here this one never changes what's on screen: it
|
|
598
600
|
* right-clicks the workspace's *row* (AXShowMenu) and works the menu, so the
|
|
599
|
-
* workspace you were reading stays open. It does need the row to be rendered,
|
|
600
|
-
*
|
|
601
|
-
*
|
|
601
|
+
* workspace you were reading stays open. It does need the row to be rendered, and
|
|
602
|
+
* a collapsed sidebar section renders none — so the script opens the folded
|
|
603
|
+
* sections itself, looks again, and folds back exactly the ones it opened. That
|
|
604
|
+
* costs a second sidebar scan, which is affordable only because that scan reads
|
|
605
|
+
* every row's name in two Apple events rather than one per row (15s → ~1s on a
|
|
606
|
+
* 50-workspace sidebar; see findSidebarRow). Measured end to end on 2026-09-01:
|
|
607
|
+
* 8.2s through a folded section, 5.4s through an open one. The budget is 35s
|
|
608
|
+
* anyway, because sidebarRowsAndNames falls back to the per-row reads when the
|
|
609
|
+
* list no longer matches its shape, and that path pays the old 15s twice.
|
|
602
610
|
*/
|
|
603
611
|
export async function setWorkspaceStatus(workspace, status) {
|
|
604
612
|
const label = WORKSPACE_STATUS_LABELS[status];
|
|
@@ -617,7 +625,7 @@ return "ok"`.trim();
|
|
|
617
625
|
...targetEnvironment,
|
|
618
626
|
RELAY_SET_STATUS: label
|
|
619
627
|
},
|
|
620
|
-
timeout:
|
|
628
|
+
timeout: 35000
|
|
621
629
|
})));
|
|
622
630
|
return { ok: true, strategy: 'applescript' };
|
|
623
631
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "conductor-remote",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.68.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.",
|