conductor-remote 1.116.0 → 1.118.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/README.md +10 -8
- package/dist/assets/{PierrePatch-BPp5Cf-p.js → PierrePatch-D4YbKD8Q.js} +1 -1
- package/dist/assets/index-BRzsSW8g.css +1 -0
- package/dist/assets/index-BaS6qXUW.js +65 -0
- package/dist/index.html +2 -2
- package/dist/push-sw.js +2 -2
- package/dist/sw.js +1 -1
- package/dist-node/scripts/nosleep-setup.js +2 -2
- package/dist-node/scripts/nosleep.js +3 -3
- package/dist-node/scripts/service/commands.js +32 -0
- package/dist-node/scripts/service/configuration.js +205 -0
- package/dist-node/scripts/service/environment.js +59 -0
- package/dist-node/scripts/service/flags.js +45 -0
- package/dist-node/scripts/service/installation.js +91 -0
- package/dist-node/scripts/service/launch-agent.js +134 -0
- package/dist-node/scripts/service/network.js +244 -0
- package/dist-node/scripts/service/presentation.js +101 -0
- package/dist-node/scripts/service/voice.js +113 -0
- package/dist-node/scripts/service.js +5 -980
- package/dist-node/src/{agent-config.js → agents/agent-config.js} +1 -1
- package/dist-node/src/{model-cache.js → agents/model-cache.js} +1 -1
- package/dist-node/src/{roles.js → agents/roles.js} +3 -3
- package/dist-node/src/db.js +1 -1
- package/dist-node/src/{parked.js → delivery/parked.js} +1 -1
- package/dist-node/src/{dev-server.js → dev-server/controller.js} +8 -291
- package/dist-node/src/dev-server/ports.js +63 -0
- package/dist-node/src/dev-server/processes.js +73 -0
- package/dist-node/src/dev-server/proxy.js +158 -0
- package/dist-node/src/{run-activity.js → dev-server/run-activity.js} +2 -2
- package/dist-node/src/dev-server/types.js +1 -0
- package/dist-node/src/{file-preview.js → files/file-preview.js} +1 -1
- package/dist-node/src/{change-stats.js → git/change-stats.js} +1 -1
- package/dist-node/src/{git.js → git/diff.js} +1 -1
- package/dist-node/src/{autoupdate.js → host/autoupdate.js} +1 -1
- package/dist-node/src/{funnel-watchdog.js → host/funnel-watchdog.js} +4 -4
- package/dist-node/src/{logbuf.js → host/logbuf.js} +1 -1
- package/dist-node/src/{nosleep-helper.js → host/nosleep-helper.js} +1 -1
- package/dist-node/src/{tailscale.js → host/tailscale.js} +1 -1
- package/dist-node/src/http/router-types.js +2 -0
- package/dist-node/src/http/router.js +78 -0
- package/dist-node/src/http/routes/create-workspace.js +90 -0
- package/dist-node/src/http/routes/files.js +53 -0
- package/dist-node/src/http/routes/prompts.js +363 -0
- package/dist-node/src/http/routes/sessions.js +279 -0
- package/dist-node/src/http/routes/state.js +186 -0
- package/dist-node/src/http/routes/system.js +220 -0
- package/dist-node/src/http/routes/voice.js +117 -0
- package/dist-node/src/http/routes/workflows.js +248 -0
- package/dist-node/src/http/routes/workspaces.js +267 -0
- package/dist-node/src/http/services/base.js +252 -0
- package/dist-node/src/http/services/delegations.js +301 -0
- package/dist-node/src/http/services/delivery.js +433 -0
- package/dist-node/src/http/services/files.js +199 -0
- package/dist-node/src/http/services/mcp.js +94 -0
- package/dist-node/src/http/services/responses.js +120 -0
- package/dist-node/src/http/services/voice.js +147 -0
- package/dist-node/src/http/services/workflow-probes.js +215 -0
- package/dist-node/src/http/services/workflow-state.js +195 -0
- package/dist-node/src/http/services/workflow.js +366 -0
- package/dist-node/src/http/services.js +35 -0
- package/dist-node/src/mcp/arguments.js +14 -0
- package/dist-node/src/mcp/dispatcher.js +56 -0
- package/dist-node/src/mcp/formatters.js +110 -0
- package/dist-node/src/mcp/protocol.js +7 -0
- package/dist-node/src/mcp/registry.js +66 -0
- package/dist-node/src/mcp/tools/agent-options.js +122 -0
- package/dist-node/src/mcp/tools/chat-actions.js +194 -0
- package/dist-node/src/mcp/tools/chats.js +194 -0
- package/dist-node/src/mcp/tools/delegation.js +135 -0
- package/dist-node/src/mcp/tools/system.js +135 -0
- package/dist-node/src/mcp/tools/voice.js +136 -0
- package/dist-node/src/mcp/tools/workspaces.js +259 -0
- package/dist-node/src/mcp/types.js +1 -0
- package/dist-node/src/mcp.js +6 -4
- package/dist-node/src/{notify.js → notifications/notify.js} +5 -5
- package/dist-node/src/{webpush.js → notifications/webpush.js} +1 -1
- package/dist-node/src/orchestration/delegation/codec.js +201 -0
- package/dist-node/src/{delegation-intake.js → orchestration/delegation/intake.js} +2 -2
- package/dist-node/src/{delegation-prompt.js → orchestration/delegation/prompt.js} +1 -1
- package/dist-node/src/orchestration/delegation/queue.js +255 -0
- package/dist-node/src/orchestration/delegation/store.js +111 -0
- package/dist-node/src/orchestration/delegation/types.js +2 -0
- package/dist-node/src/orchestration/persistence/capabilities.js +109 -0
- package/dist-node/src/orchestration/persistence/codecs.js +148 -0
- package/dist-node/src/orchestration/persistence/connection.js +205 -0
- package/dist-node/src/orchestration/persistence/db.js +101 -0
- package/dist-node/src/orchestration/persistence/effect-recovery.js +227 -0
- package/dist-node/src/orchestration/persistence/effect-transitions.js +157 -0
- package/dist-node/src/orchestration/persistence/effects.js +109 -0
- package/dist-node/src/orchestration/persistence/errors.js +39 -0
- package/dist-node/src/orchestration/persistence/events.js +29 -0
- package/dist-node/src/orchestration/persistence/idempotency.js +82 -0
- package/dist-node/src/orchestration/persistence/job-attempts.js +114 -0
- package/dist-node/src/orchestration/persistence/jobs.js +159 -0
- package/dist-node/src/orchestration/persistence/projections.js +190 -0
- package/dist-node/src/orchestration/persistence/quarantine.js +46 -0
- package/dist-node/src/orchestration/persistence/records.js +115 -0
- package/dist-node/src/orchestration/persistence/relays.js +78 -0
- package/dist-node/src/orchestration/persistence/runs.js +156 -0
- package/dist-node/src/{orchestration-schema.js → orchestration/persistence/schema.js} +1 -1
- package/dist-node/src/orchestration/persistence/types.js +1 -0
- package/dist-node/src/orchestration/persistence/ui-lease.js +197 -0
- package/dist-node/src/orchestration/persistence/values.js +114 -0
- package/dist-node/src/orchestration/workflow/commands.js +309 -0
- package/dist-node/src/orchestration/workflow/coordinator.js +28 -0
- package/dist-node/src/orchestration/workflow/effect-recovery.js +125 -0
- package/dist-node/src/orchestration/workflow/effects.js +307 -0
- package/dist-node/src/orchestration/workflow/errors.js +13 -0
- package/dist-node/src/orchestration/workflow/helpers.js +154 -0
- package/dist-node/src/orchestration/workflow/job-dispatch.js +312 -0
- package/dist-node/src/orchestration/workflow/job-results.js +280 -0
- package/dist-node/src/{workflow.js → orchestration/workflow/prompts.js} +2 -2
- package/dist-node/src/orchestration/workflow/root.js +293 -0
- package/dist-node/src/orchestration/workflow/start.js +99 -0
- package/dist-node/src/orchestration/workflow/state.js +105 -0
- package/dist-node/src/orchestration/workflow/types.js +1 -0
- package/dist-node/src/orchestration/workflow/wake.js +164 -0
- package/dist-node/src/prefs.js +1 -1
- package/dist-node/src/{background-tasks.js → reads/background-tasks.js} +1 -1
- package/dist-node/src/reads/messages.js +362 -0
- package/dist-node/src/reads/repository.js +87 -0
- package/dist-node/src/reads/sessions.js +165 -0
- package/dist-node/src/reads/types.js +1 -0
- package/dist-node/src/reads/workspaces.js +230 -0
- package/dist-node/src/reads/worktrees.js +45 -0
- package/dist-node/src/routes.js +5 -5
- package/dist-node/src/{search.js → search/coordinator.js} +4 -4
- package/dist-node/src/{search-worker.js → search/worker.js} +3 -3
- package/dist-node/src/server.js +14 -3930
- package/dist-node/src/settings.js +1 -1
- package/dist-node/src/shared.js +130 -9
- package/dist-node/src/{transcript.js → transcript/parser.js} +4 -124
- package/dist-node/src/{tool-usage-worker.js → usage/tool-usage-worker.js} +1 -1
- package/dist-node/src/{tool-usage.js → usage/tool-usage.js} +1 -1
- package/dist-node/src/voice/brief.js +22 -50
- package/dist-node/src/voice/context.js +1 -1
- package/dist-node/src/voice/dates.js +38 -0
- package/dist-node/src/voice/history.js +14 -5
- package/dist-node/src/voice/prompt.js +10 -9
- package/dist-node/src/voice/recall.js +115 -0
- package/dist-node/src/{speech.js → voice/speech.js} +1 -1
- package/dist-node/src/voice/tools.js +92 -3
- package/dist-node/src/voice/webrtc.js +1 -1
- package/dist-node/src/wire.js +2 -2
- package/dist-node/src/writes/actuator.js +124 -0
- package/dist-node/src/writes/agent-options.js +151 -0
- package/dist-node/src/writes/applescript/agent-options.applescript +334 -0
- package/dist-node/src/writes/applescript/chat-targeting.applescript +149 -0
- package/dist-node/src/writes/applescript/composer.applescript +195 -0
- package/dist-node/src/writes/applescript/hotspot.applescript +155 -0
- package/dist-node/src/writes/applescript/lifecycle.applescript +266 -0
- package/dist-node/src/writes/applescript/run-tasks.applescript +208 -0
- package/dist-node/src/writes/applescript/source.js +52 -0
- package/dist-node/src/writes/applescript/window.applescript +436 -0
- package/dist-node/src/writes/applescript/workspace-status.applescript +177 -0
- package/dist-node/src/writes/applescript/workspace-targeting.applescript +359 -0
- package/dist-node/src/writes/chats.js +160 -0
- package/dist-node/src/writes/guards.js +77 -0
- package/dist-node/src/writes/runner.js +67 -0
- package/dist-node/src/{sidecar.js → writes/sidecar.js} +1 -1
- package/dist-node/src/writes/system.js +81 -0
- package/dist-node/src/writes/targeting.js +121 -0
- package/dist-node/src/writes/types.js +1 -0
- package/dist-node/src/writes/ui-lock.js +229 -0
- package/dist-node/src/writes/workspaces.js +233 -0
- package/docs/voice-setup.md +34 -12
- package/package.json +2 -2
- package/dist/assets/index-DNLdIfZ2.css +0 -1
- package/dist/assets/index-QJR7tn2I.js +0 -61
- package/dist-node/src/conductor.applescript +0 -2279
- package/dist-node/src/delegations.js +0 -557
- package/dist-node/src/mcp-tools.js +0 -1319
- package/dist-node/src/orchestration-db.js +0 -2186
- package/dist-node/src/reads.js +0 -787
- package/dist-node/src/workflow-coordinator.js +0 -2119
- package/dist-node/src/writes.js +0 -1223
- /package/dist-node/src/{conductor-settings.js → agents/conductor-settings.js} +0 -0
- /package/dist-node/src/{firstprompt.js → delivery/firstprompt.js} +0 -0
- /package/dist-node/src/{sendonce.js → delivery/sendonce.js} +0 -0
- /package/dist-node/src/{preview-urls.js → dev-server/preview-urls.js} +0 -0
- /package/dist-node/src/{run-configs.js → dev-server/run-configs.js} +0 -0
- /package/dist-node/src/{attachments.js → files/attachments.js} +0 -0
- /package/dist-node/src/{icons.js → files/icons.js} +0 -0
- /package/dist-node/src/{staged-attachments.js → files/staged-attachments.js} +0 -0
- /package/dist-node/src/{fork-workspace.js → git/fork-workspace.js} +0 -0
- /package/dist-node/src/{merge.js → git/merge.js} +0 -0
- /package/dist-node/src/{pr.js → git/pr.js} +0 -0
- /package/dist-node/src/{nosleep.js → host/nosleep.js} +0 -0
- /package/dist-node/src/{relay-processes.js → host/relay-processes.js} +0 -0
- /package/dist-node/src/{ui-lease-watchdog.js → host/ui-lease-watchdog.js} +0 -0
- /package/dist-node/src/{wifi.js → host/wifi.js} +0 -0
- /package/dist-node/src/{orchestration-schema.generated.js → orchestration/persistence/schema.generated.js} +0 -0
- /package/dist-node/src/{workflow-effect-runner.js → orchestration/workflow/effect-runner.js} +0 -0
- /package/dist-node/src/{workflow-http.js → orchestration/workflow/http.js} +0 -0
- /package/dist-node/src/{workflow-machine.js → orchestration/workflow/machine.js} +0 -0
- /package/dist-node/src/{session-poller.js → reads/session-poller.js} +0 -0
- /package/dist-node/src/{context-breakdown.js → transcript/context-breakdown.js} +0 -0
- /package/dist-node/src/{chat-cursor.js → transcript/cursor.js} +0 -0
- /package/dist-node/src/{plan-usage.js → usage/plan-usage.js} +0 -0
- /package/dist-node/src/{tool-usage-service.js → usage/tool-usage-service.js} +0 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
|
|
2
|
+
on composerControls()
|
|
3
|
+
set strips to my tabGroups()
|
|
4
|
+
if (count of strips) is 0 then error "couldn't find the composer"
|
|
5
|
+
tell application "System Events" to tell process "Conductor"
|
|
6
|
+
set pane to value of attribute "AXParent" of (item 1 of strips)
|
|
7
|
+
set composerBox to item 1 of (UI elements of pane whose name is "composer")
|
|
8
|
+
set out to {}
|
|
9
|
+
repeat with e in (UI elements of composerBox)
|
|
10
|
+
set end of out to contents of e
|
|
11
|
+
repeat with e2 in (UI elements of e)
|
|
12
|
+
set end of out to contents of e2
|
|
13
|
+
end repeat
|
|
14
|
+
end repeat
|
|
15
|
+
return out
|
|
16
|
+
end tell
|
|
17
|
+
end composerControls
|
|
18
|
+
|
|
19
|
+
on controlNamed(wanted)
|
|
20
|
+
repeat with entry in my composerControls()
|
|
21
|
+
set c to contents of entry
|
|
22
|
+
if my tabLabel(c) is wanted then return c
|
|
23
|
+
end repeat
|
|
24
|
+
return missing value
|
|
25
|
+
end controlNamed
|
|
26
|
+
|
|
27
|
+
on effortButton()
|
|
28
|
+
-- The effort control is a button whose *label is its current value*, so it is
|
|
29
|
+
-- identified by that label rather than a stable name. Claude calls its top
|
|
30
|
+
-- level Ultracode while Codex calls the corresponding level Ultra. Codex's
|
|
31
|
+
-- None state is the exception: its button has no accessible name at all, so
|
|
32
|
+
-- accept one (and only one) unnamed button *after* the proven model picker.
|
|
33
|
+
-- The main composer also contains unnamed attachment/stop buttons before that
|
|
34
|
+
-- picker, and pressing one of those would be worse than failing.
|
|
35
|
+
set levels to {"Light", "Low", "Medium", "High", "Extra high", "Max", "Ultracode", "Ultra"}
|
|
36
|
+
set sawModel to false
|
|
37
|
+
set unnamed to {}
|
|
38
|
+
repeat with entry in my composerControls()
|
|
39
|
+
set c to contents of entry
|
|
40
|
+
if (my axRole(c)) is "AXPopUpButton" and (my axName(c)) contains "Change agent" then
|
|
41
|
+
set sawModel to true
|
|
42
|
+
else if sawModel and (my axRole(c)) is "AXButton" then
|
|
43
|
+
set label to my axName(c)
|
|
44
|
+
if label is in levels then return c
|
|
45
|
+
if label is "" then set end of unnamed to c
|
|
46
|
+
end if
|
|
47
|
+
end repeat
|
|
48
|
+
if (count of unnamed) is 1 then return item 1 of unnamed
|
|
49
|
+
return missing value
|
|
50
|
+
end effortButton
|
|
51
|
+
|
|
52
|
+
on setEffort(wanted)
|
|
53
|
+
-- Claude owns a six-value ring ending in Ultracode; Codex adds None and ends
|
|
54
|
+
-- in Ultra. Step around either ring once and confirm the label landed.
|
|
55
|
+
set btn to my effortButton()
|
|
56
|
+
if btn is missing value then error "couldn't find the effort control"
|
|
57
|
+
repeat 8 times
|
|
58
|
+
set currentLabel to my axName(btn)
|
|
59
|
+
if currentLabel is wanted or (wanted is "__UNNAMED_EFFORT__" and currentLabel is "") then return
|
|
60
|
+
tell application "System Events" to tell process "Conductor"
|
|
61
|
+
perform action "AXPress" of btn
|
|
62
|
+
end tell
|
|
63
|
+
delay 0.35
|
|
64
|
+
set btn to my effortButton()
|
|
65
|
+
if btn is missing value then error "the effort control vanished mid-cycle"
|
|
66
|
+
end repeat
|
|
67
|
+
error "couldn't set effort to " & wanted
|
|
68
|
+
end setEffort
|
|
69
|
+
|
|
70
|
+
on setPlan(wanted)
|
|
71
|
+
set box to my controlNamed("Plan")
|
|
72
|
+
if box is missing value then error "couldn't find the Plan toggle"
|
|
73
|
+
tell application "System Events" to tell process "Conductor"
|
|
74
|
+
set current to ((value of box) as text)
|
|
75
|
+
if (wanted is "1" and current is "0") or (wanted is "0" and current is not "0") then
|
|
76
|
+
perform action "AXPress" of box
|
|
77
|
+
delay 0.4
|
|
78
|
+
if ((value of box) as text) is current then error "the Plan toggle didn't change"
|
|
79
|
+
end if
|
|
80
|
+
end tell
|
|
81
|
+
end setPlan
|
|
82
|
+
|
|
83
|
+
on pressFast()
|
|
84
|
+
-- Fast has no AX state to read (its label is always "Fast"), so the caller
|
|
85
|
+
-- decides whether a press is needed and re-checks the DB afterwards.
|
|
86
|
+
set btn to my controlNamed("Fast")
|
|
87
|
+
if btn is missing value then error "this model has no Fast toggle"
|
|
88
|
+
tell application "System Events" to tell process "Conductor"
|
|
89
|
+
perform action "AXPress" of btn
|
|
90
|
+
end tell
|
|
91
|
+
delay 0.4
|
|
92
|
+
end pressFast
|
|
93
|
+
|
|
94
|
+
on firstLine(s)
|
|
95
|
+
set saved to AppleScript's text item delimiters
|
|
96
|
+
set AppleScript's text item delimiters to linefeed
|
|
97
|
+
set parts to text items of s
|
|
98
|
+
set AppleScript's text item delimiters to saved
|
|
99
|
+
return item 1 of parts
|
|
100
|
+
end firstLine
|
|
101
|
+
|
|
102
|
+
on pickerShowsModel(popup, wanted)
|
|
103
|
+
set pickerLabel to my tabLabel(popup)
|
|
104
|
+
if pickerLabel contains ("(" & wanted & ")") then return true
|
|
105
|
+
-- The menu can label a newly released model "Opus 5 NEW", while the
|
|
106
|
+
-- composer deliberately drops that temporary badge and says "Opus 5".
|
|
107
|
+
-- Treat that exact display form as the same choice; the surrounding
|
|
108
|
+
-- parentheses keep "Opus 5" distinct from a longer model name.
|
|
109
|
+
if wanted ends with " NEW" then
|
|
110
|
+
set baseName to text 1 thru -5 of wanted
|
|
111
|
+
if pickerLabel contains ("(" & baseName & ")") then return true
|
|
112
|
+
end if
|
|
113
|
+
return false
|
|
114
|
+
end pickerShowsModel
|
|
115
|
+
|
|
116
|
+
on modelMenuItem(wanted)
|
|
117
|
+
-- Resolve one model from the picker that is already open. The menu can append
|
|
118
|
+
-- `NEW` to a freshly released model while the relay deliberately exposes the
|
|
119
|
+
-- stable name without it, so the same unique-prefix rule as setModel applies.
|
|
120
|
+
set chosen to missing value
|
|
121
|
+
set loose to {}
|
|
122
|
+
set wa to my webArea()
|
|
123
|
+
tell application "System Events" to tell process "Conductor"
|
|
124
|
+
repeat with m in (get UI elements of wa whose role is "AXMenu")
|
|
125
|
+
repeat with mi in (get UI elements of m whose role is "AXMenuItem")
|
|
126
|
+
set label to my firstLine(my tabLabel(mi))
|
|
127
|
+
if label is wanted then
|
|
128
|
+
set chosen to contents of mi
|
|
129
|
+
else if label starts with wanted then
|
|
130
|
+
set end of loose to contents of mi
|
|
131
|
+
end if
|
|
132
|
+
end repeat
|
|
133
|
+
end repeat
|
|
134
|
+
end tell
|
|
135
|
+
if chosen is missing value and (count of loose) is 1 then set chosen to item 1 of loose
|
|
136
|
+
if chosen is missing value then
|
|
137
|
+
if (count of loose) > 1 then error "several models match " & wanted
|
|
138
|
+
error "no model named " & wanted
|
|
139
|
+
end if
|
|
140
|
+
return chosen
|
|
141
|
+
end modelMenuItem
|
|
142
|
+
|
|
143
|
+
on modelDefaultButton(menuItem)
|
|
144
|
+
-- Every non-default row has a child star button named "Set <model> as
|
|
145
|
+
-- default and select". The starred row has no button at all. Read the role and
|
|
146
|
+
-- label together so an unrelated control a future picker grows cannot be
|
|
147
|
+
-- mistaken for a global preference write.
|
|
148
|
+
repeat with childNode in my axKids(menuItem)
|
|
149
|
+
set childEl to contents of childNode
|
|
150
|
+
if (my axRole(childEl)) is "AXButton" and (my axName(childEl)) contains " as default" then return childEl
|
|
151
|
+
end repeat
|
|
152
|
+
return missing value
|
|
153
|
+
end modelDefaultButton
|
|
154
|
+
|
|
155
|
+
on modelMenuItems()
|
|
156
|
+
set found to {}
|
|
157
|
+
set wa to my webArea()
|
|
158
|
+
tell application "System Events" to tell process "Conductor"
|
|
159
|
+
repeat with m in (get UI elements of wa whose role is "AXMenu")
|
|
160
|
+
set found to found & (get UI elements of m whose role is "AXMenuItem")
|
|
161
|
+
end repeat
|
|
162
|
+
end tell
|
|
163
|
+
return found
|
|
164
|
+
end modelMenuItems
|
|
165
|
+
|
|
166
|
+
on defaultModelInOpenPicker()
|
|
167
|
+
-- The selected star is exposed by *removing* the star button from that row.
|
|
168
|
+
-- Require exactly one such row. If managed settings disable every star, or a
|
|
169
|
+
-- Conductor release changes this tree, failing closed is safer than reporting
|
|
170
|
+
-- that whichever model was requested became the user's global default.
|
|
171
|
+
set defaults to {}
|
|
172
|
+
repeat with mi in my modelMenuItems()
|
|
173
|
+
set menuItem to contents of mi
|
|
174
|
+
if (my modelDefaultButton(menuItem)) is missing value then set end of defaults to my firstLine(my tabLabel(menuItem))
|
|
175
|
+
end repeat
|
|
176
|
+
if (count of defaults) is not 1 then error "couldn't identify Conductor's default model"
|
|
177
|
+
return item 1 of defaults
|
|
178
|
+
end defaultModelInOpenPicker
|
|
179
|
+
|
|
180
|
+
on modelPickerButton()
|
|
181
|
+
set popup to missing value
|
|
182
|
+
repeat with entry in my composerControls()
|
|
183
|
+
set c to contents of entry
|
|
184
|
+
if my tabLabel(c) contains "Change agent" then set popup to c
|
|
185
|
+
end repeat
|
|
186
|
+
if popup is missing value then error "couldn't find the model picker"
|
|
187
|
+
return popup
|
|
188
|
+
end modelPickerButton
|
|
189
|
+
|
|
190
|
+
on openModelPicker()
|
|
191
|
+
set popup to my modelPickerButton()
|
|
192
|
+
tell application "System Events" to tell process "Conductor"
|
|
193
|
+
perform action "AXPress" of popup
|
|
194
|
+
end tell
|
|
195
|
+
delay 1.0
|
|
196
|
+
end openModelPicker
|
|
197
|
+
|
|
198
|
+
on setDefaultModel(wanted)
|
|
199
|
+
-- Conductor's star is one combined action: its accessible name is "Set … as
|
|
200
|
+
-- default and select". Mirror that rather than pretending this only changes a
|
|
201
|
+
-- preference — the target chat switches model too, and the picker label is
|
|
202
|
+
-- read back below as the receipt for that half.
|
|
203
|
+
set initialPopup to my modelPickerButton()
|
|
204
|
+
set alreadySelected to my pickerShowsModel(initialPopup, wanted)
|
|
205
|
+
tell application "System Events" to tell process "Conductor"
|
|
206
|
+
perform action "AXPress" of initialPopup
|
|
207
|
+
end tell
|
|
208
|
+
delay 1.0
|
|
209
|
+
try
|
|
210
|
+
set chosen to my modelMenuItem(wanted)
|
|
211
|
+
set chosenLabel to my firstLine(my tabLabel(chosen))
|
|
212
|
+
set currentDefault to my defaultModelInOpenPicker()
|
|
213
|
+
if currentDefault is chosenLabel then
|
|
214
|
+
if alreadySelected then
|
|
215
|
+
tell application "System Events" to key code 53
|
|
216
|
+
return chosenLabel
|
|
217
|
+
end if
|
|
218
|
+
-- The star is already selected, but this chat is not. Its row is now
|
|
219
|
+
-- the remaining half of Conductor's combined action.
|
|
220
|
+
tell application "System Events" to tell process "Conductor"
|
|
221
|
+
perform action "AXPress" of chosen
|
|
222
|
+
end tell
|
|
223
|
+
delay 0.8
|
|
224
|
+
set selectedPopup to my modelPickerButton()
|
|
225
|
+
if not my pickerShowsModel(selectedPopup, chosenLabel) then error "the model didn't switch to " & chosenLabel
|
|
226
|
+
return chosenLabel
|
|
227
|
+
end if
|
|
228
|
+
set starButton to my modelDefaultButton(chosen)
|
|
229
|
+
if starButton is missing value then error "the default-model star is missing for " & chosenLabel
|
|
230
|
+
tell application "System Events" to tell process "Conductor"
|
|
231
|
+
perform action "AXPress" of starButton
|
|
232
|
+
end tell
|
|
233
|
+
delay 0.8
|
|
234
|
+
set selectedPopup to my modelPickerButton()
|
|
235
|
+
if not my pickerShowsModel(selectedPopup, chosenLabel) then error "the model didn't switch to " & chosenLabel
|
|
236
|
+
-- Re-open and read the star back. Pressing is fire-and-forget; the only safe
|
|
237
|
+
-- success is the requested row becoming the unique one with no star button.
|
|
238
|
+
my openModelPicker()
|
|
239
|
+
set verified to my modelMenuItem(wanted)
|
|
240
|
+
set verifiedLabel to my firstLine(my tabLabel(verified))
|
|
241
|
+
set currentDefault to my defaultModelInOpenPicker()
|
|
242
|
+
tell application "System Events" to key code 53
|
|
243
|
+
if currentDefault is not verifiedLabel then error "Conductor didn't mark " & verifiedLabel & " as the default model"
|
|
244
|
+
return verifiedLabel
|
|
245
|
+
on error errText number errNum
|
|
246
|
+
tell application "System Events" to key code 53
|
|
247
|
+
error errText number errNum
|
|
248
|
+
end try
|
|
249
|
+
end setDefaultModel
|
|
250
|
+
|
|
251
|
+
on setModel(wanted)
|
|
252
|
+
set popup to missing value
|
|
253
|
+
repeat with entry in my composerControls()
|
|
254
|
+
set c to contents of entry
|
|
255
|
+
if my tabLabel(c) contains "Change agent" then set popup to c
|
|
256
|
+
end repeat
|
|
257
|
+
if popup is missing value then error "couldn't find the model picker"
|
|
258
|
+
if my pickerShowsModel(popup, wanted) then return
|
|
259
|
+
tell application "System Events" to tell process "Conductor"
|
|
260
|
+
perform action "AXPress" of popup
|
|
261
|
+
end tell
|
|
262
|
+
delay 1.0
|
|
263
|
+
-- The relay removes a temporary `NEW` badge before it sends this label back,
|
|
264
|
+
-- so a prefix match resolves that menu item. It is accepted only when unique:
|
|
265
|
+
-- "Sonnet 4.6" would
|
|
266
|
+
-- otherwise also match "Sonnet 4.6 1M"), which must fail rather than guess.
|
|
267
|
+
set chosen to missing value
|
|
268
|
+
set loose to {}
|
|
269
|
+
set wa to my webArea()
|
|
270
|
+
tell application "System Events" to tell process "Conductor"
|
|
271
|
+
repeat with m in (get UI elements of wa whose role is "AXMenu")
|
|
272
|
+
repeat with mi in (get UI elements of m whose role is "AXMenuItem")
|
|
273
|
+
set label to my firstLine(my tabLabel(mi))
|
|
274
|
+
if label is wanted then
|
|
275
|
+
set chosen to contents of mi
|
|
276
|
+
else if label starts with wanted then
|
|
277
|
+
set end of loose to contents of mi
|
|
278
|
+
end if
|
|
279
|
+
end repeat
|
|
280
|
+
end repeat
|
|
281
|
+
end tell
|
|
282
|
+
if chosen is missing value and (count of loose) is 1 then set chosen to item 1 of loose
|
|
283
|
+
if chosen is missing value then
|
|
284
|
+
tell application "System Events" to key code 53
|
|
285
|
+
if (count of loose) > 1 then error "several models match " & wanted
|
|
286
|
+
error "no model named " & wanted
|
|
287
|
+
end if
|
|
288
|
+
set chosenLabel to my firstLine(my tabLabel(chosen))
|
|
289
|
+
tell application "System Events" to tell process "Conductor"
|
|
290
|
+
perform action "AXPress" of chosen
|
|
291
|
+
end tell
|
|
292
|
+
delay 0.8
|
|
293
|
+
set popup to missing value
|
|
294
|
+
repeat with entry in my composerControls()
|
|
295
|
+
set c to contents of entry
|
|
296
|
+
if my tabLabel(c) contains "Change agent" then set popup to c
|
|
297
|
+
end repeat
|
|
298
|
+
if popup is missing value then error "the model picker vanished"
|
|
299
|
+
if not my pickerShowsModel(popup, chosenLabel) then error "the model didn't switch to " & chosenLabel
|
|
300
|
+
end setModel
|
|
301
|
+
|
|
302
|
+
on listModels()
|
|
303
|
+
-- Enumerate the picker rather than hard-coding a model list that would rot on
|
|
304
|
+
-- every Conductor release. The first tagged line carries the unique starred
|
|
305
|
+
-- row, followed by the ordinary labels. Opens the menu, reads, closes it again.
|
|
306
|
+
my openModelPicker()
|
|
307
|
+
try
|
|
308
|
+
set labels to {}
|
|
309
|
+
repeat with mi in my modelMenuItems()
|
|
310
|
+
set end of labels to my firstLine(my tabLabel(mi))
|
|
311
|
+
end repeat
|
|
312
|
+
set defaultLabel to my defaultModelInOpenPicker()
|
|
313
|
+
tell application "System Events" to key code 53
|
|
314
|
+
set saved to AppleScript's text item delimiters
|
|
315
|
+
set AppleScript's text item delimiters to linefeed
|
|
316
|
+
set joined to labels as text
|
|
317
|
+
set AppleScript's text item delimiters to saved
|
|
318
|
+
return "__CONDUCTOR_DEFAULT_MODEL__" & tab & defaultLabel & linefeed & joined
|
|
319
|
+
on error errText number errNum
|
|
320
|
+
tell application "System Events" to key code 53
|
|
321
|
+
error errText number errNum
|
|
322
|
+
end try
|
|
323
|
+
end listModels
|
|
324
|
+
|
|
325
|
+
on applyAgentOptions()
|
|
326
|
+
set wantEffort to system attribute "RELAY_SET_EFFORT"
|
|
327
|
+
set wantPlan to system attribute "RELAY_SET_PLAN"
|
|
328
|
+
set wantFast to system attribute "RELAY_SET_FAST"
|
|
329
|
+
set wantModel to system attribute "RELAY_SET_MODEL"
|
|
330
|
+
if wantModel is not "" then my setModel(wantModel)
|
|
331
|
+
if wantEffort is not "" then my setEffort(wantEffort)
|
|
332
|
+
if wantPlan is not "" then my setPlan(wantPlan)
|
|
333
|
+
if wantFast is "1" then my pressFast()
|
|
334
|
+
end applyAgentOptions
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
|
|
2
|
+
on tabGroups()
|
|
3
|
+
-- Level-order search, returning every tab group at the shallowest depth that
|
|
4
|
+
-- has one (the chat strip and the terminal strip are siblings). Bounded: the
|
|
5
|
+
-- pane sits ~5 levels down, and we must never descend into the transcript.
|
|
6
|
+
my requireWindow()
|
|
7
|
+
tell application "System Events" to tell process "Conductor"
|
|
8
|
+
set level to {window 1}
|
|
9
|
+
set depth to 0
|
|
10
|
+
repeat while (count of level) > 0 and depth < 8
|
|
11
|
+
set found to {}
|
|
12
|
+
set nextLevel to {}
|
|
13
|
+
repeat with entry in level
|
|
14
|
+
set node to contents of entry
|
|
15
|
+
try
|
|
16
|
+
set found to found & (get UI elements of node whose role is "AXTabGroup")
|
|
17
|
+
set nextLevel to nextLevel & (get UI elements of node)
|
|
18
|
+
end try
|
|
19
|
+
end repeat
|
|
20
|
+
if (count of found) > 0 then return found
|
|
21
|
+
set level to nextLevel
|
|
22
|
+
set depth to depth + 1
|
|
23
|
+
end repeat
|
|
24
|
+
end tell
|
|
25
|
+
return {}
|
|
26
|
+
end tabGroups
|
|
27
|
+
|
|
28
|
+
on stripRadios(tg)
|
|
29
|
+
-- Every radio button in a tab strip, whether it is a direct child or sits one
|
|
30
|
+
-- group deep, in tree order. Both shapes are live at once: the terminal strip's
|
|
31
|
+
-- tabs are direct children, while a chat tab is wrapped in its own AXGroup with
|
|
32
|
+
-- the file-view toggle beside it as a plain child. Reading only the direct ones
|
|
33
|
+
-- (and stopping as soon as any turned up) is what made a chat strip look like a
|
|
34
|
+
-- one-tab strip called "Close file view".
|
|
35
|
+
tell application "System Events" to tell process "Conductor"
|
|
36
|
+
set acc to {}
|
|
37
|
+
repeat with k in (UI elements of tg)
|
|
38
|
+
set node to contents of k
|
|
39
|
+
if (role of node) is "AXRadioButton" then
|
|
40
|
+
set end of acc to node
|
|
41
|
+
else
|
|
42
|
+
try
|
|
43
|
+
set acc to acc & (get UI elements of node whose role is "AXRadioButton")
|
|
44
|
+
end try
|
|
45
|
+
end if
|
|
46
|
+
end repeat
|
|
47
|
+
return acc
|
|
48
|
+
end tell
|
|
49
|
+
end stripRadios
|
|
50
|
+
|
|
51
|
+
on chatTabs(tg)
|
|
52
|
+
-- Of those, a chat tab is the one whose accessible name is its own close action,
|
|
53
|
+
-- "Close chat <title>". The strip's other radio button is "Close file view",
|
|
54
|
+
-- which is not a chat and must not shift the indices this file addresses tabs
|
|
55
|
+
-- by — off by one is how a prompt lands in the wrong conversation. Falls back to
|
|
56
|
+
-- every radio button when none carries the prefix, so a renamed label costs the
|
|
57
|
+
-- filter rather than emptying the strip.
|
|
58
|
+
set radios to my stripRadios(tg)
|
|
59
|
+
set chats to {}
|
|
60
|
+
repeat with entry in radios
|
|
61
|
+
set node to contents of entry
|
|
62
|
+
if (my axName(node)) starts with "Close chat" then set end of chats to node
|
|
63
|
+
end repeat
|
|
64
|
+
if (count of chats) > 0 then return chats
|
|
65
|
+
return radios
|
|
66
|
+
end chatTabs
|
|
67
|
+
|
|
68
|
+
on tabLabel(t)
|
|
69
|
+
tell application "System Events" to tell process "Conductor"
|
|
70
|
+
return (name of t) as text
|
|
71
|
+
end tell
|
|
72
|
+
end tabLabel
|
|
73
|
+
|
|
74
|
+
on pickChatStrip(strips, wantCount, wantTitle)
|
|
75
|
+
-- Score each candidate strip: a label match outweighs a tab-count match, and
|
|
76
|
+
-- a tie means we cannot tell the chat strip from the terminal strip.
|
|
77
|
+
set best to missing value
|
|
78
|
+
set bestTabs to {}
|
|
79
|
+
set bestScore to 0
|
|
80
|
+
set tied to false
|
|
81
|
+
repeat with entry in strips
|
|
82
|
+
set tg to contents of entry
|
|
83
|
+
set strip to my chatTabs(tg)
|
|
84
|
+
set score to 0
|
|
85
|
+
if wantCount > 0 and (count of strip) is wantCount then set score to score + 1
|
|
86
|
+
if wantTitle is not "" then
|
|
87
|
+
repeat with t in strip
|
|
88
|
+
if (my tabLabel(t)) contains wantTitle then
|
|
89
|
+
set score to score + 2
|
|
90
|
+
exit repeat
|
|
91
|
+
end if
|
|
92
|
+
end repeat
|
|
93
|
+
end if
|
|
94
|
+
if score > bestScore then
|
|
95
|
+
set bestScore to score
|
|
96
|
+
set best to tg
|
|
97
|
+
set bestTabs to strip
|
|
98
|
+
set tied to false
|
|
99
|
+
else if score is bestScore and score > 0 then
|
|
100
|
+
set tied to true
|
|
101
|
+
end if
|
|
102
|
+
end repeat
|
|
103
|
+
if bestScore is 0 then error "couldn't identify the chat tab strip"
|
|
104
|
+
if tied then error "can't tell which tab strip holds the target chat"
|
|
105
|
+
return {best, bestTabs}
|
|
106
|
+
end pickChatStrip
|
|
107
|
+
|
|
108
|
+
on lastPathSegment(s)
|
|
109
|
+
set saved to AppleScript's text item delimiters
|
|
110
|
+
set AppleScript's text item delimiters to "/"
|
|
111
|
+
set parts to text items of s
|
|
112
|
+
set AppleScript's text item delimiters to saved
|
|
113
|
+
return item -1 of parts
|
|
114
|
+
end lastPathSegment
|
|
115
|
+
|
|
116
|
+
on paneLabels(tg, wantRole)
|
|
117
|
+
-- Kept out of the caller's scope on purpose: inside a System Events tell,
|
|
118
|
+
-- ordinary-looking names (tabs, count) resolve as app terms instead of vars.
|
|
119
|
+
tell application "System Events" to tell process "Conductor"
|
|
120
|
+
set pane to value of attribute "AXParent" of tg
|
|
121
|
+
return (name of (UI elements of pane whose role is wantRole))
|
|
122
|
+
end tell
|
|
123
|
+
end paneLabels
|
|
124
|
+
|
|
125
|
+
on anyContains(haystack, needle)
|
|
126
|
+
repeat with entry in haystack
|
|
127
|
+
try
|
|
128
|
+
if (entry as text) contains needle then return true
|
|
129
|
+
end try
|
|
130
|
+
end repeat
|
|
131
|
+
return false
|
|
132
|
+
end anyContains
|
|
133
|
+
|
|
134
|
+
on assertWorkspace(tg)
|
|
135
|
+
-- The pane holding the chat strip also labels the open workspace: an
|
|
136
|
+
-- AXStaticText with the branch (sans owner prefix) and a repo popup button.
|
|
137
|
+
set wantBranch to system attribute "RELAY_WS_BRANCH"
|
|
138
|
+
if wantBranch is "" then return
|
|
139
|
+
set tail to my lastPathSegment(wantBranch)
|
|
140
|
+
if not (my anyContains(my paneLabels(tg, "AXStaticText"), tail)) then
|
|
141
|
+
error "the palette didn't land on " & tail
|
|
142
|
+
end if
|
|
143
|
+
set wantRepo to system attribute "RELAY_WS_REPO"
|
|
144
|
+
if wantRepo is not "" then
|
|
145
|
+
if not (my anyContains(my paneLabels(tg, "AXPopUpButton"), wantRepo)) then
|
|
146
|
+
error "the palette didn't land in " & wantRepo
|
|
147
|
+
end if
|
|
148
|
+
end if
|
|
149
|
+
end assertWorkspace
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
|
|
2
|
+
on normalizeNewlines(s)
|
|
3
|
+
-- "do shell script" hands back CR-delimited text; the composer reads back LF.
|
|
4
|
+
-- Without this the verification below never matches a multi-line prompt.
|
|
5
|
+
set saved to AppleScript's text item delimiters
|
|
6
|
+
set AppleScript's text item delimiters to return
|
|
7
|
+
set parts to text items of s
|
|
8
|
+
set AppleScript's text item delimiters to linefeed
|
|
9
|
+
set joined to parts as text
|
|
10
|
+
set AppleScript's text item delimiters to saved
|
|
11
|
+
return joined
|
|
12
|
+
end normalizeNewlines
|
|
13
|
+
|
|
14
|
+
on composerField()
|
|
15
|
+
-- The composer's AXTextArea: an AXGroup named "composer" in the chat pane, one
|
|
16
|
+
-- text area inside it. Resolved once and handed to both the fill and the send
|
|
17
|
+
-- confirm below, because reaching it means walking the pane from the tab strip
|
|
18
|
+
-- and that walk measured ~0.5s on a live window. Returns missing value when the
|
|
19
|
+
-- composer isn't there at all, which is the caller's cue to fall back to pasting.
|
|
20
|
+
set strips to my tabGroups()
|
|
21
|
+
if (count of strips) is 0 then return missing value
|
|
22
|
+
try
|
|
23
|
+
tell application "System Events" to tell process "Conductor"
|
|
24
|
+
set pane to value of attribute "AXParent" of (item 1 of strips)
|
|
25
|
+
set composerBox to item 1 of (UI elements of pane whose name is "composer")
|
|
26
|
+
return item 1 of (UI elements of composerBox whose role is "AXTextArea")
|
|
27
|
+
end tell
|
|
28
|
+
end try
|
|
29
|
+
return missing value
|
|
30
|
+
end composerField
|
|
31
|
+
|
|
32
|
+
on composerTextOf(textBox)
|
|
33
|
+
-- What the box holds now, or missing value when it cannot be read at all. A pane
|
|
34
|
+
-- that re-rendered leaves the old reference pointing at a dead element, and
|
|
35
|
+
-- "can't read it" must never be reported as "still holding the prompt": that is
|
|
36
|
+
-- the one state that presses Enter again, and it would do so at the moment the
|
|
37
|
+
-- box may already be empty.
|
|
38
|
+
try
|
|
39
|
+
tell application "System Events" to tell process "Conductor"
|
|
40
|
+
return (value of textBox) as text
|
|
41
|
+
end tell
|
|
42
|
+
end try
|
|
43
|
+
return missing value
|
|
44
|
+
end composerTextOf
|
|
45
|
+
|
|
46
|
+
on fillComposer(textBox, promptText)
|
|
47
|
+
-- Write the prompt straight into the composer's AXTextArea instead of
|
|
48
|
+
-- stashing the clipboard, pressing Cmd+L and pasting. AXFocused and AXValue
|
|
49
|
+
-- are both settable, so this needs no keystrokes and no clipboard hijack.
|
|
50
|
+
-- Returns false (→ caller falls back to pasting) if anything looks off, but
|
|
51
|
+
-- *clears whatever it wrote first*: leaving half a prompt behind would make
|
|
52
|
+
-- the fallback paste append to it and send a garbled prompt.
|
|
53
|
+
if promptText is "" then return false
|
|
54
|
+
if textBox is missing value then return false
|
|
55
|
+
try
|
|
56
|
+
tell application "System Events" to tell process "Conductor"
|
|
57
|
+
set value of attribute "AXFocused" of textBox to true
|
|
58
|
+
set value of textBox to promptText
|
|
59
|
+
delay 0.25
|
|
60
|
+
if ((value of textBox) as text) does not contain promptText then
|
|
61
|
+
set value of textBox to ""
|
|
62
|
+
return false
|
|
63
|
+
end if
|
|
64
|
+
end tell
|
|
65
|
+
on error
|
|
66
|
+
try
|
|
67
|
+
my clearComposer(textBox)
|
|
68
|
+
end try
|
|
69
|
+
return false
|
|
70
|
+
end try
|
|
71
|
+
return true
|
|
72
|
+
end fillComposer
|
|
73
|
+
|
|
74
|
+
on clearComposer(textBox)
|
|
75
|
+
if textBox is missing value then return
|
|
76
|
+
tell application "System Events" to tell process "Conductor"
|
|
77
|
+
set value of textBox to ""
|
|
78
|
+
end tell
|
|
79
|
+
end clearComposer
|
|
80
|
+
|
|
81
|
+
on pressSend(queueIt)
|
|
82
|
+
-- Return sends, Command-Return queues the prompt behind the running answer.
|
|
83
|
+
-- The choice is made out here because inside a System Events tell an ordinary
|
|
84
|
+
-- parameter name can resolve as one of its own terms.
|
|
85
|
+
if queueIt then
|
|
86
|
+
tell application "System Events" to key code 36 using {command down}
|
|
87
|
+
else
|
|
88
|
+
tell application "System Events" to key code 36
|
|
89
|
+
end if
|
|
90
|
+
end pressSend
|
|
91
|
+
|
|
92
|
+
on submitComposer(textBox, promptText, queueIt)
|
|
93
|
+
-- Press Enter, then read the composer back. Conductor consumes the draft when it
|
|
94
|
+
-- accepts a prompt, so a box still holding the text is a keystroke that did
|
|
95
|
+
-- nothing — and that read is the only receipt a run can collect about its own
|
|
96
|
+
-- send.
|
|
97
|
+
--
|
|
98
|
+
-- Why it earns its place: the caller's other receipt is the transcript row, which
|
|
99
|
+
-- costs a 6s watch (CONFIRM_WINDOW_MS in server.ts) and then a whole second run
|
|
100
|
+
-- through activate, focus, tab and fill. Measured from this Mac's own relay logs,
|
|
101
|
+
-- a send whose single fault was a first Enter Conductor ignored took ~10s from the
|
|
102
|
+
-- prompt appearing in the composer to it being sent, with the text sitting on
|
|
103
|
+
-- screen the whole way. Exactly one user row was written, so the keystroke was
|
|
104
|
+
-- lost rather than delayed.
|
|
105
|
+
--
|
|
106
|
+
-- Pressing again is safe *because* of the read, not in spite of it: the prompt is
|
|
107
|
+
-- still in the box, so nothing was consumed, so there is nothing to duplicate.
|
|
108
|
+
-- The one state that would break that is a Conductor which queues on Command-Return
|
|
109
|
+
-- and *keeps* the draft, since the box would then read as unsent after a send that
|
|
110
|
+
-- worked. Unverified against the live app, and the only path that could reach it is
|
|
111
|
+
-- Command-Return in the phone's composer (Composer.tsx, a hardware keyboard); a tap
|
|
112
|
+
-- always sends. Worth checking before anything else starts queueing.
|
|
113
|
+
-- A Conductor too busy to have handled the first Enter is also too busy to answer
|
|
114
|
+
-- an AX read, which blocks here rather than handing back stale text.
|
|
115
|
+
--
|
|
116
|
+
-- **Two presses, not three, and the second one is on probation.** The first
|
|
117
|
+
-- occurrence in the wild (2026-09-01 16:59:57) had the box survive all three, and
|
|
118
|
+
-- no send has ever been recorded as rescued by a later press — so the value here
|
|
119
|
+
-- is the *detection*, which lets the caller stop watching for a row that cannot
|
|
120
|
+
-- come (`sendNeverStarted` in writes.ts) rather than spending the 6s window on it.
|
|
121
|
+
-- Every press past the first that doesn't work is pure delay added to a send that
|
|
122
|
+
-- is already failing, which is why the budget shrank from 3×1.2s to 2×0.8s.
|
|
123
|
+
--
|
|
124
|
+
-- Returns the press that cleared the box, or 0 for a box that never cleared. The
|
|
125
|
+
-- count is the point: a rescued send is otherwise indistinguishable from one that
|
|
126
|
+
-- worked first time, so the failure this handler exists for would stop appearing
|
|
127
|
+
-- in the log the day it stopped costing 10s, and nobody could tell whether it was
|
|
128
|
+
-- fixed or merely hidden.
|
|
129
|
+
if promptText is "" then
|
|
130
|
+
my pressSend(queueIt)
|
|
131
|
+
return 1
|
|
132
|
+
end if
|
|
133
|
+
repeat with pressCount from 1 to 2
|
|
134
|
+
my pressSend(queueIt)
|
|
135
|
+
if textBox is missing value then return pressCount
|
|
136
|
+
repeat with tick from 1 to 4
|
|
137
|
+
delay 0.2
|
|
138
|
+
set seen to my composerTextOf(textBox)
|
|
139
|
+
if seen is missing value then return pressCount
|
|
140
|
+
if seen does not contain promptText then return pressCount
|
|
141
|
+
end repeat
|
|
142
|
+
end repeat
|
|
143
|
+
return 0
|
|
144
|
+
end submitComposer
|
|
145
|
+
|
|
146
|
+
on pasteComposer()
|
|
147
|
+
-- Fallback for when the composer isn't reachable: Cmd+L focuses it (after the
|
|
148
|
+
-- palette, focus sits on a button, not the text box), then paste.
|
|
149
|
+
tell application "System Events"
|
|
150
|
+
set the clipboard to (do shell script "cat" & " " & quoted form of (system attribute "RELAY_PROMPT_FILE"))
|
|
151
|
+
keystroke "l" using {command down}
|
|
152
|
+
delay 0.3
|
|
153
|
+
keystroke "v" using {command down}
|
|
154
|
+
delay 0.15
|
|
155
|
+
end tell
|
|
156
|
+
end pasteComposer
|
|
157
|
+
|
|
158
|
+
on selectChatTab()
|
|
159
|
+
set wantIndex to (system attribute "RELAY_TAB_INDEX") as integer
|
|
160
|
+
if wantIndex is 0 then return
|
|
161
|
+
set wantCount to (system attribute "RELAY_TAB_COUNT") as integer
|
|
162
|
+
set wantTitle to my targetField(1)
|
|
163
|
+
set strips to my tabGroups()
|
|
164
|
+
if (count of strips) is 0 then
|
|
165
|
+
-- A lone chat has no ambiguity to resolve; more than one and we must not guess.
|
|
166
|
+
if wantCount <= 1 then return
|
|
167
|
+
error "couldn't find the chat tab strip"
|
|
168
|
+
end if
|
|
169
|
+
-- Assert the workspace first: every strip lives in the same pane, so this
|
|
170
|
+
-- reports "wrong workspace" rather than a confusing "no chat strip".
|
|
171
|
+
my assertWorkspace(item 1 of strips)
|
|
172
|
+
set picked to my pickChatStrip(strips, wantCount, wantTitle)
|
|
173
|
+
set tabs to item 2 of picked
|
|
174
|
+
tell application "System Events" to tell process "Conductor"
|
|
175
|
+
set target to missing value
|
|
176
|
+
if wantIndex <= (count of tabs) then
|
|
177
|
+
set candidate to contents of (item wantIndex of tabs)
|
|
178
|
+
if wantTitle is "" or (name of candidate) contains wantTitle then set target to candidate
|
|
179
|
+
end if
|
|
180
|
+
if target is missing value and wantTitle is not "" then
|
|
181
|
+
repeat with t in tabs
|
|
182
|
+
if (name of t) contains wantTitle then
|
|
183
|
+
if target is not missing value then error "several chat tabs match " & wantTitle
|
|
184
|
+
set target to contents of t
|
|
185
|
+
end if
|
|
186
|
+
end repeat
|
|
187
|
+
end if
|
|
188
|
+
if target is missing value then error "chat tab " & wantIndex & " not found"
|
|
189
|
+
if (value of target) is not true then
|
|
190
|
+
perform action "AXPress" of target
|
|
191
|
+
delay 0.5
|
|
192
|
+
end if
|
|
193
|
+
if (value of target) is not true then error "couldn't switch to the target chat tab"
|
|
194
|
+
end tell
|
|
195
|
+
end selectChatTab
|