@agentdeck/bridge 1.0.1 → 1.0.3
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/assets/fm-helper/agentdeck-fm-helper +0 -0
- package/dist/apme/collector.d.ts.map +1 -1
- package/dist/apme/collector.js +5 -2
- package/dist/apme/collector.js.map +1 -1
- package/dist/apme/runner.d.ts +11 -0
- package/dist/apme/runner.d.ts.map +1 -1
- package/dist/apme/runner.js +60 -4
- package/dist/apme/runner.js.map +1 -1
- package/dist/apme/settings.d.ts +10 -0
- package/dist/apme/settings.d.ts.map +1 -1
- package/dist/apme/settings.js +14 -0
- package/dist/apme/settings.js.map +1 -1
- package/dist/awaiting-overlay.d.ts +44 -10
- package/dist/awaiting-overlay.d.ts.map +1 -1
- package/dist/awaiting-overlay.js +96 -28
- package/dist/awaiting-overlay.js.map +1 -1
- package/dist/bridge-core.d.ts +10 -0
- package/dist/bridge-core.d.ts.map +1 -1
- package/dist/bridge-core.js +47 -12
- package/dist/bridge-core.js.map +1 -1
- package/dist/card-feed.d.ts +107 -0
- package/dist/card-feed.d.ts.map +1 -0
- package/dist/card-feed.js +277 -0
- package/dist/card-feed.js.map +1 -0
- package/dist/card-modules.d.ts +87 -0
- package/dist/card-modules.d.ts.map +1 -0
- package/dist/card-modules.js +179 -0
- package/dist/card-modules.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +93 -0
- package/dist/cli.js.map +1 -1
- package/dist/daemon-server.d.ts.map +1 -1
- package/dist/daemon-server.js +1025 -50
- package/dist/daemon-server.js.map +1 -1
- package/dist/daemon-ws-client.d.ts +2 -1
- package/dist/daemon-ws-client.d.ts.map +1 -1
- package/dist/daemon-ws-client.js +2 -1
- package/dist/daemon-ws-client.js.map +1 -1
- package/dist/daemon.js +1 -1
- package/dist/device-photo.d.ts +86 -0
- package/dist/device-photo.d.ts.map +1 -0
- package/dist/device-photo.js +200 -0
- package/dist/device-photo.js.map +1 -0
- package/dist/device-voice-reply.d.ts +124 -0
- package/dist/device-voice-reply.d.ts.map +1 -0
- package/dist/device-voice-reply.js +236 -0
- package/dist/device-voice-reply.js.map +1 -0
- package/dist/device-voice.d.ts +56 -0
- package/dist/device-voice.d.ts.map +1 -0
- package/dist/device-voice.js +134 -0
- package/dist/device-voice.js.map +1 -0
- package/dist/esp32-serial.d.ts +40 -0
- package/dist/esp32-serial.d.ts.map +1 -1
- package/dist/esp32-serial.js +109 -13
- package/dist/esp32-serial.js.map +1 -1
- package/dist/foundation-models-helper.d.ts +30 -0
- package/dist/foundation-models-helper.d.ts.map +1 -1
- package/dist/foundation-models-helper.js +45 -0
- package/dist/foundation-models-helper.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/mdns.d.ts +11 -0
- package/dist/mdns.d.ts.map +1 -1
- package/dist/mdns.js +13 -0
- package/dist/mdns.js.map +1 -1
- package/dist/observed-inject.d.ts +119 -0
- package/dist/observed-inject.d.ts.map +1 -0
- package/dist/observed-inject.js +405 -0
- package/dist/observed-inject.js.map +1 -0
- package/dist/passive-observer.d.ts +14 -0
- package/dist/passive-observer.d.ts.map +1 -1
- package/dist/passive-observer.js +35 -5
- package/dist/passive-observer.js.map +1 -1
- package/dist/peripheral-mapping.d.ts +65 -0
- package/dist/peripheral-mapping.d.ts.map +1 -0
- package/dist/peripheral-mapping.js +115 -0
- package/dist/peripheral-mapping.js.map +1 -0
- package/dist/session-aggregator.d.ts +4 -1
- package/dist/session-aggregator.d.ts.map +1 -1
- package/dist/session-aggregator.js +5 -5
- package/dist/session-aggregator.js.map +1 -1
- package/dist/session-id-resolve.d.ts +17 -0
- package/dist/session-id-resolve.d.ts.map +1 -0
- package/dist/session-id-resolve.js +35 -0
- package/dist/session-id-resolve.js.map +1 -0
- package/dist/session-registry.d.ts +12 -0
- package/dist/session-registry.d.ts.map +1 -1
- package/dist/session-registry.js +31 -0
- package/dist/session-registry.js.map +1 -1
- package/dist/session-transcript-timeline.d.ts +7 -0
- package/dist/session-transcript-timeline.d.ts.map +1 -1
- package/dist/session-transcript-timeline.js +16 -6
- package/dist/session-transcript-timeline.js.map +1 -1
- package/dist/timeline-store.js +2 -2
- package/dist/timeline-store.js.map +1 -1
- package/dist/usage-event.d.ts.map +1 -1
- package/dist/usage-event.js +14 -7
- package/dist/usage-event.js.map +1 -1
- package/dist/voice.d.ts +13 -10
- package/dist/voice.d.ts.map +1 -1
- package/dist/voice.js +26 -272
- package/dist/voice.js.map +1 -1
- package/dist/ws-server.d.ts +14 -0
- package/dist/ws-server.d.ts.map +1 -1
- package/dist/ws-server.js +66 -1
- package/dist/ws-server.js.map +1 -1
- package/fm-helper/AgentDeckFMHelper.swift +303 -0
- package/package.json +3 -3
- package/dist/d200h/hid-protocol.d.ts +0 -62
- package/dist/d200h/hid-protocol.d.ts.map +0 -1
- package/dist/d200h/hid-protocol.js +0 -124
- package/dist/d200h/hid-protocol.js.map +0 -1
- package/dist/d200h/image-renderer.d.ts +0 -24
- package/dist/d200h/image-renderer.d.ts.map +0 -1
- package/dist/d200h/image-renderer.js +0 -321
- package/dist/d200h/image-renderer.js.map +0 -1
- package/dist/hook-migration.d.ts +0 -2
- package/dist/hook-migration.d.ts.map +0 -1
- package/dist/hook-migration.js +0 -104
- package/dist/hook-migration.js.map +0 -1
- package/dist/modules/d200h-module.d.ts +0 -65
- package/dist/modules/d200h-module.d.ts.map +0 -1
- package/dist/modules/d200h-module.js +0 -495
- package/dist/modules/d200h-module.js.map +0 -1
- package/dist/modules/d200h-renderer.d.ts +0 -7
- package/dist/modules/d200h-renderer.d.ts.map +0 -1
- package/dist/modules/d200h-renderer.js +0 -321
- package/dist/modules/d200h-renderer.js.map +0 -1
- package/dist/modules/trmnl-module.d.ts +0 -38
- package/dist/modules/trmnl-module.d.ts.map +0 -1
- package/dist/modules/trmnl-module.js +0 -111
- package/dist/modules/trmnl-module.js.map +0 -1
- package/dist/trmnl/byos-server.d.ts +0 -39
- package/dist/trmnl/byos-server.d.ts.map +0 -1
- package/dist/trmnl/byos-server.js +0 -301
- package/dist/trmnl/byos-server.js.map +0 -1
- package/dist/trmnl/frame-cache.d.ts +0 -37
- package/dist/trmnl/frame-cache.d.ts.map +0 -1
- package/dist/trmnl/frame-cache.js +0 -157
- package/dist/trmnl/frame-cache.js.map +0 -1
- package/dist/trmnl/image-renderer.d.ts +0 -27
- package/dist/trmnl/image-renderer.d.ts.map +0 -1
- package/dist/trmnl/image-renderer.js +0 -235
- package/dist/trmnl/image-renderer.js.map +0 -1
- package/dist/trmnl/trmnl-settings.d.ts +0 -96
- package/dist/trmnl/trmnl-settings.d.ts.map +0 -1
- package/dist/trmnl/trmnl-settings.js +0 -177
- package/dist/trmnl/trmnl-settings.js.map +0 -1
- package/dist/trmnl/trmnl-telemetry.d.ts +0 -47
- package/dist/trmnl/trmnl-telemetry.d.ts.map +0 -1
- package/dist/trmnl/trmnl-telemetry.js +0 -56
- package/dist/trmnl/trmnl-telemetry.js.map +0 -1
- package/dist/whisper-server-manager.d.ts +0 -19
- package/dist/whisper-server-manager.d.ts.map +0 -1
- package/dist/whisper-server-manager.js +0 -171
- package/dist/whisper-server-manager.js.map +0 -1
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deliver an option selection INTO an observed session's own UI — the rung of
|
|
3
|
+
* the observed-steering ladder that answers a live prompt without any
|
|
4
|
+
* hold-and-timeout machinery: the prompt is already on screen; the daemon does
|
|
5
|
+
* what the user would do, and if it cannot reach the UI **nothing happens**
|
|
6
|
+
* (the prompt keeps waiting — never an auto-proceed).
|
|
7
|
+
*
|
|
8
|
+
* Two families of host, resolved by the passive observer:
|
|
9
|
+
*
|
|
10
|
+
* Terminal-hosted (`tty` known) — the session runs in a real terminal, so
|
|
11
|
+
* the answer is keystrokes into that terminal's own input:
|
|
12
|
+
* 1. tmux — the pane whose `#{pane_tty}` matches gets `send-keys`.
|
|
13
|
+
* 2. iTerm2 — the session whose `tty` matches gets `write text` (no focus
|
|
14
|
+
* change; iTerm2 writes straight into the session).
|
|
15
|
+
* 3. Terminal.app — has no write API, so: select the tab owning that tty
|
|
16
|
+
* (scriptable, no activation) and post keys to Terminal's pid.
|
|
17
|
+
*
|
|
18
|
+
* App-hosted (`appName` known, no tty) — Claude.app / ChatGPT.app run the
|
|
19
|
+
* agent inside a native window, so the answer is that window's own control:
|
|
20
|
+
* 4. Press the button whose title matches the chosen option's label
|
|
21
|
+
* (AXPress — precise; works for native AppKit dialogs).
|
|
22
|
+
* 5. Post keys to the app's pid (focus-free).
|
|
23
|
+
* 6. Raise + keys + restore, only if the app ignores posted events.
|
|
24
|
+
*
|
|
25
|
+
* Every rung is focus-free except the last: measured 2026-07-26, keys posted
|
|
26
|
+
* with `CGEventPostToPid` reached a background Terminal tab while iTerm2
|
|
27
|
+
* stayed frontmost.
|
|
28
|
+
*
|
|
29
|
+
* Two measured dead ends, recorded so they are not retried:
|
|
30
|
+
* • TIOCSTI — the one kernel path that would cover every emulator at once —
|
|
31
|
+
* is EPERM on macOS unless the tty is the caller's own controlling
|
|
32
|
+
* terminal.
|
|
33
|
+
* • ChatGPT.app exposes no content in its accessibility tree (its window is
|
|
34
|
+
* three untitled AXButtons plus empty AXGroups, and
|
|
35
|
+
* `AXManualAccessibility` returns kAXErrorAttributeUnsupported), so the
|
|
36
|
+
* button rung cannot see its controls — key posting is the working path
|
|
37
|
+
* for that host.
|
|
38
|
+
*
|
|
39
|
+
* Node-daemon only by design: every rung needs a subprocess (tmux/osascript),
|
|
40
|
+
* which the App Store Swift daemon must never spawn. See
|
|
41
|
+
* docs/appstore-feature-matrix.md.
|
|
42
|
+
*/
|
|
43
|
+
import { execFile } from 'child_process';
|
|
44
|
+
import { promisify } from 'util';
|
|
45
|
+
import { debug } from './logger.js';
|
|
46
|
+
const execFileAsync = promisify(execFile);
|
|
47
|
+
/** Known bundle ids for GUI agent hosts — matched before localized names,
|
|
48
|
+
* which differ per system language. */
|
|
49
|
+
const APP_BUNDLE_IDS = {
|
|
50
|
+
Claude: ['com.anthropic.claudefordesktop', 'com.anthropic.claude'],
|
|
51
|
+
ChatGPT: ['com.openai.chat'],
|
|
52
|
+
Terminal: ['com.apple.Terminal'],
|
|
53
|
+
iTerm2: ['com.googlecode.iterm2'],
|
|
54
|
+
};
|
|
55
|
+
/** AppleScript string literal escaping (quotes + backslashes). */
|
|
56
|
+
export function escapeAppleScript(s) {
|
|
57
|
+
return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
58
|
+
}
|
|
59
|
+
/** Parse `tmux list-panes -a -F "#{pane_tty}\t#{pane_id}"` output into a
|
|
60
|
+
* tty-suffix → paneId map ("/dev/ttys012" is matched by "ttys012"). */
|
|
61
|
+
export function parseTmuxPanes(output) {
|
|
62
|
+
const map = new Map();
|
|
63
|
+
for (const line of output.split('\n')) {
|
|
64
|
+
const [paneTty, paneId] = line.trim().split('\t');
|
|
65
|
+
if (!paneTty || !paneId)
|
|
66
|
+
continue;
|
|
67
|
+
map.set(paneTty.replace(/^\/dev\//, ''), paneId);
|
|
68
|
+
}
|
|
69
|
+
return map;
|
|
70
|
+
}
|
|
71
|
+
async function injectViaTmux(tty, downs) {
|
|
72
|
+
try {
|
|
73
|
+
const { stdout } = await execFileAsync('tmux', ['list-panes', '-a', '-F', '#{pane_tty}\t#{pane_id}'], { encoding: 'utf8', timeout: 2_000 });
|
|
74
|
+
const paneId = parseTmuxPanes(stdout).get(tty);
|
|
75
|
+
if (!paneId)
|
|
76
|
+
return false;
|
|
77
|
+
const keys = [...Array(downs).fill('Down'), 'Enter'];
|
|
78
|
+
await execFileAsync('tmux', ['send-keys', '-t', paneId, ...keys], { timeout: 2_000 });
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false; // no tmux server / pane gone — fall through the ladder
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** AppleScript that types the selection into the iTerm2 session owning `tty`. */
|
|
86
|
+
export function buildItermSelectScript(tty, downs) {
|
|
87
|
+
// ESC [ B per Down, then a bare `write text ""` for Enter (write text
|
|
88
|
+
// appends CR by default; the arrow writes suppress it with `newline NO`).
|
|
89
|
+
const writes = [];
|
|
90
|
+
for (let i = 0; i < downs; i++) {
|
|
91
|
+
writes.push('write s text ((character id 27) & "[B") newline NO');
|
|
92
|
+
}
|
|
93
|
+
writes.push('write s text ""');
|
|
94
|
+
return [
|
|
95
|
+
'tell application "iTerm2"',
|
|
96
|
+
' repeat with w in windows',
|
|
97
|
+
' repeat with t in tabs of w',
|
|
98
|
+
' repeat with s in sessions of t',
|
|
99
|
+
` if tty of s is "/dev/${tty}" then`,
|
|
100
|
+
...writes.map((l) => ` ${l}`),
|
|
101
|
+
' return "ok"',
|
|
102
|
+
' end if',
|
|
103
|
+
' end repeat',
|
|
104
|
+
' end repeat',
|
|
105
|
+
' end repeat',
|
|
106
|
+
'end tell',
|
|
107
|
+
'return "notfound"',
|
|
108
|
+
].join('\n');
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* AppleScript for Terminal.app: select the tab whose `tty` matches, WITHOUT
|
|
112
|
+
* activating the app. Keys are then posted straight to Terminal's pid
|
|
113
|
+
* (`buildPostKeysScript`), so answering a prompt never steals the user's
|
|
114
|
+
* focus — measured 2026-07-26: iTerm2 stayed frontmost while the Terminal
|
|
115
|
+
* tab received ESC[B + CR.
|
|
116
|
+
*/
|
|
117
|
+
export function buildTerminalAppSelectTabScript(tty) {
|
|
118
|
+
return [
|
|
119
|
+
'set found to false',
|
|
120
|
+
'tell application "Terminal"',
|
|
121
|
+
' repeat with w in windows',
|
|
122
|
+
' repeat with t in tabs of w',
|
|
123
|
+
` if tty of t is "/dev/${tty}" then`,
|
|
124
|
+
' set selected of t to true',
|
|
125
|
+
' set index of w to 1',
|
|
126
|
+
' set found to true',
|
|
127
|
+
' exit repeat',
|
|
128
|
+
' end if',
|
|
129
|
+
' end repeat',
|
|
130
|
+
' if found then exit repeat',
|
|
131
|
+
' end repeat',
|
|
132
|
+
'end tell',
|
|
133
|
+
'if found then',
|
|
134
|
+
' return "ok"',
|
|
135
|
+
'else',
|
|
136
|
+
' return "notfound"',
|
|
137
|
+
'end if',
|
|
138
|
+
].join('\n');
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* JXA that posts Down×n + Return directly to a running app's process with
|
|
142
|
+
* `CGEventPostToPid` — the event reaches that app's key window without
|
|
143
|
+
* raising it, so the user's foreground work is untouched.
|
|
144
|
+
*
|
|
145
|
+
* The app is matched by bundle id first and localized name second: on a
|
|
146
|
+
* Korean system `Terminal` is "터미널", so name matching alone silently
|
|
147
|
+
* missed it.
|
|
148
|
+
*
|
|
149
|
+
* Requires Accessibility permission for whatever process runs the daemon
|
|
150
|
+
* (the terminal that launched it, or the LaunchAgent itself).
|
|
151
|
+
*/
|
|
152
|
+
export function buildPostKeysScript(match, downs) {
|
|
153
|
+
const bundles = JSON.stringify(match.bundleIds ?? []);
|
|
154
|
+
const names = JSON.stringify(match.names ?? []);
|
|
155
|
+
return [
|
|
156
|
+
"ObjC.import('ApplicationServices'); ObjC.import('AppKit');",
|
|
157
|
+
`const wantBundles = ${bundles}; const wantNames = ${names};`,
|
|
158
|
+
'function findPid() {',
|
|
159
|
+
' const apps = $.NSWorkspace.sharedWorkspace.runningApplications;',
|
|
160
|
+
' for (let i = 0; i < apps.count; i++) {',
|
|
161
|
+
' const a = apps.objectAtIndex(i);',
|
|
162
|
+
' const b = ObjC.unwrap(a.bundleIdentifier);',
|
|
163
|
+
' if (b && wantBundles.indexOf(b) >= 0) return a.processIdentifier;',
|
|
164
|
+
' }',
|
|
165
|
+
' for (let i = 0; i < apps.count; i++) {',
|
|
166
|
+
' const a = apps.objectAtIndex(i);',
|
|
167
|
+
' const n = ObjC.unwrap(a.localizedName);',
|
|
168
|
+
' if (n && wantNames.indexOf(n) >= 0) return a.processIdentifier;',
|
|
169
|
+
' }',
|
|
170
|
+
' return -1;',
|
|
171
|
+
'}',
|
|
172
|
+
'const pid = findPid();',
|
|
173
|
+
'if (pid < 0) { "notfound" } else {',
|
|
174
|
+
' function key(code) {',
|
|
175
|
+
' const d = $.CGEventCreateKeyboardEvent($(), code, true);',
|
|
176
|
+
' const u = $.CGEventCreateKeyboardEvent($(), code, false);',
|
|
177
|
+
' $.CGEventPostToPid(pid, d); $.CGEventPostToPid(pid, u);',
|
|
178
|
+
' delay(0.12);',
|
|
179
|
+
' }',
|
|
180
|
+
// A background app drops the first posted events until its input queue is
|
|
181
|
+
// warm: measured with 0.05s pacing and no lead-in, two Downs vanished and
|
|
182
|
+
// only the Return landed (which would silently pick the WRONG option).
|
|
183
|
+
' delay(0.30);',
|
|
184
|
+
` for (let i = 0; i < ${downs}; i++) key(125);`,
|
|
185
|
+
' key(36);',
|
|
186
|
+
' "ok"',
|
|
187
|
+
'}',
|
|
188
|
+
].join('\n');
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* AppleScript for an app-hosted session: press the button whose title matches
|
|
192
|
+
* the option label inside the app's front window. Matching is
|
|
193
|
+
* case-insensitive and prefix-tolerant (native buttons often shorten a long
|
|
194
|
+
* option label). AXPress needs no focus change, so this is the preferred
|
|
195
|
+
* app-hosted rung.
|
|
196
|
+
*/
|
|
197
|
+
export function buildAppButtonPressScript(appName, label) {
|
|
198
|
+
const app = escapeAppleScript(appName);
|
|
199
|
+
// AppleScript text comparison ignores case by default — no lowercasing dance.
|
|
200
|
+
const want = escapeAppleScript(label.trim());
|
|
201
|
+
// `entire contents` yields a flat element list; AppleScript cannot filter it
|
|
202
|
+
// with `every button of …`, so iterate and test the accessibility role.
|
|
203
|
+
return [
|
|
204
|
+
`tell application "System Events" to tell process "${app}"`,
|
|
205
|
+
' if not (exists window 1) then return "nowindow"',
|
|
206
|
+
' set target to missing value',
|
|
207
|
+
' repeat with e in (entire contents of window 1)',
|
|
208
|
+
' try',
|
|
209
|
+
' if role of e is "AXButton" or role of e is "AXRadioButton" then',
|
|
210
|
+
' set bt to name of e',
|
|
211
|
+
' if bt is not missing value and bt is not "" then',
|
|
212
|
+
` if bt is "${want}" or bt starts with "${want}" or "${want}" starts with bt then`,
|
|
213
|
+
' set target to e',
|
|
214
|
+
' exit repeat',
|
|
215
|
+
' end if',
|
|
216
|
+
' end if',
|
|
217
|
+
' end if',
|
|
218
|
+
' end try',
|
|
219
|
+
' end repeat',
|
|
220
|
+
' if target is missing value then return "nobutton"',
|
|
221
|
+
' click target',
|
|
222
|
+
' return "ok"',
|
|
223
|
+
'end tell',
|
|
224
|
+
].join('\n');
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Last-resort key path: raise the app, post Down×n + Return through System
|
|
228
|
+
* Events, restore the previous frontmost app. Only used when the focus-free
|
|
229
|
+
* `CGEventPostToPid` route reports the app is not running or the app ignores
|
|
230
|
+
* posted events.
|
|
231
|
+
*/
|
|
232
|
+
export function buildAppKeysScript(appName, downs) {
|
|
233
|
+
const app = escapeAppleScript(appName);
|
|
234
|
+
const keyLines = [];
|
|
235
|
+
for (let i = 0; i < downs; i++)
|
|
236
|
+
keyLines.push('key code 125');
|
|
237
|
+
keyLines.push('key code 36');
|
|
238
|
+
return [
|
|
239
|
+
'set prevApp to ""',
|
|
240
|
+
'try',
|
|
241
|
+
' tell application "System Events" to set prevApp to name of first process whose frontmost is true',
|
|
242
|
+
'end try',
|
|
243
|
+
`tell application "${app}" to activate`,
|
|
244
|
+
'delay 0.15',
|
|
245
|
+
'tell application "System Events"',
|
|
246
|
+
...keyLines.map((l) => ` ${l}`),
|
|
247
|
+
'end tell',
|
|
248
|
+
'delay 0.05',
|
|
249
|
+
`if prevApp is not "" and prevApp is not "${app}" then`,
|
|
250
|
+
' try',
|
|
251
|
+
' tell application prevApp to activate',
|
|
252
|
+
' end try',
|
|
253
|
+
'end if',
|
|
254
|
+
'return "ok"',
|
|
255
|
+
].join('\n');
|
|
256
|
+
}
|
|
257
|
+
async function runJxa(script, timeoutMs = 8_000) {
|
|
258
|
+
try {
|
|
259
|
+
const { stdout } = await execFileAsync('osascript', ['-l', 'JavaScript', '-e', script], { encoding: 'utf8', timeout: timeoutMs });
|
|
260
|
+
return stdout.trim();
|
|
261
|
+
}
|
|
262
|
+
catch (err) {
|
|
263
|
+
debug('inject', `jxa failed: ${String(err).slice(0, 200)}`);
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
async function runOsa(script, timeoutMs = 8_000) {
|
|
268
|
+
try {
|
|
269
|
+
const { stdout } = await execFileAsync('osascript', ['-e', script], { encoding: 'utf8', timeout: timeoutMs });
|
|
270
|
+
return stdout.trim();
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
debug('inject', `osascript failed: ${String(err).slice(0, 200)}`);
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Answer the session's on-screen prompt by driving its host UI.
|
|
279
|
+
* `index` is the zero-based option position (Claude's pickers start with the
|
|
280
|
+
* cursor on the first option, so index == the number of Down presses).
|
|
281
|
+
*/
|
|
282
|
+
export async function injectObservedSelection(target, index) {
|
|
283
|
+
if (index < 0 || index > 16)
|
|
284
|
+
return { ok: false, reason: 'index out of range' };
|
|
285
|
+
const { tty, appName, label } = target;
|
|
286
|
+
if (!tty && !appName)
|
|
287
|
+
return { ok: false, reason: 'no tty or app host for session' };
|
|
288
|
+
if (tty) {
|
|
289
|
+
if (await injectViaTmux(tty, index))
|
|
290
|
+
return { ok: true, via: 'tmux' };
|
|
291
|
+
if (await runOsa(buildItermSelectScript(tty, index), 5_000) === 'ok') {
|
|
292
|
+
return { ok: true, via: 'iterm2' };
|
|
293
|
+
}
|
|
294
|
+
// Terminal.app: select the owning tab (no activation), then post keys to
|
|
295
|
+
// its pid — focus-free.
|
|
296
|
+
if (await runOsa(buildTerminalAppSelectTabScript(tty), 5_000) === 'ok') {
|
|
297
|
+
const posted = await runJxa(buildPostKeysScript({ bundleIds: ['com.apple.Terminal'], names: ['Terminal', '터미널'] }, index));
|
|
298
|
+
if (posted === 'ok')
|
|
299
|
+
return { ok: true, via: 'terminal-app' };
|
|
300
|
+
debug('inject', `Terminal.app tab selected but key post failed: ${posted ?? 'error'}`);
|
|
301
|
+
}
|
|
302
|
+
return { ok: false, reason: `no reachable terminal for ${tty}` };
|
|
303
|
+
}
|
|
304
|
+
// App-hosted. Try the labelled control first (works for native AppKit
|
|
305
|
+
// dialogs); then focus-free key posting; raise+restore only as last resort.
|
|
306
|
+
if (label) {
|
|
307
|
+
const r = await runOsa(buildAppButtonPressScript(appName, label));
|
|
308
|
+
if (r === 'ok')
|
|
309
|
+
return { ok: true, via: 'app-button' };
|
|
310
|
+
debug('inject', `app-button press on ${appName}: ${r ?? 'error'}`);
|
|
311
|
+
}
|
|
312
|
+
const posted = await runJxa(buildPostKeysScript({ bundleIds: APP_BUNDLE_IDS[appName] ?? [], names: [appName] }, index));
|
|
313
|
+
if (posted === 'ok')
|
|
314
|
+
return { ok: true, via: 'app-keys' };
|
|
315
|
+
if (await runOsa(buildAppKeysScript(appName, index)) === 'ok') {
|
|
316
|
+
return { ok: true, via: 'app-keys-raised' };
|
|
317
|
+
}
|
|
318
|
+
return { ok: false, reason: `no reachable UI in ${appName}` };
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Type a whole line of text into the session's terminal and submit it — the
|
|
322
|
+
* delivery path for a dictated prompt. Same host ladder as
|
|
323
|
+
* `injectObservedSelection`, but writing text instead of arrow keys.
|
|
324
|
+
*
|
|
325
|
+
* Only terminal hosts are supported: typing free text into a GUI app's focused
|
|
326
|
+
* field is far riskier (no way to know what has focus), and a spoken prompt
|
|
327
|
+
* that lands in the wrong place is worse than one that is refused.
|
|
328
|
+
*/
|
|
329
|
+
export async function injectObservedText(target, text) {
|
|
330
|
+
const trimmed = text.trim();
|
|
331
|
+
if (!trimmed)
|
|
332
|
+
return { ok: false, reason: 'empty text' };
|
|
333
|
+
// Newlines would submit early and split the prompt across turns.
|
|
334
|
+
const line = trimmed.replace(/[\r\n]+/g, ' ');
|
|
335
|
+
const { tty } = target;
|
|
336
|
+
if (!tty)
|
|
337
|
+
return { ok: false, reason: 'no tty for session (text injection is terminal-only)' };
|
|
338
|
+
// tmux: -l sends the payload literally, then a separate Enter submits it.
|
|
339
|
+
try {
|
|
340
|
+
const { stdout } = await execFileAsync('tmux', ['list-panes', '-a', '-F', '#{pane_tty}\t#{pane_id}'], { encoding: 'utf8', timeout: 2_000 });
|
|
341
|
+
const paneId = parseTmuxPanes(stdout).get(tty);
|
|
342
|
+
if (paneId) {
|
|
343
|
+
await execFileAsync('tmux', ['send-keys', '-t', paneId, '-l', line], { timeout: 2_000 });
|
|
344
|
+
// Same beat as above: process spawn alone happens to provide ~50 ms, which
|
|
345
|
+
// is not a guarantee worth relying on for the keystroke that commits.
|
|
346
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
347
|
+
await execFileAsync('tmux', ['send-keys', '-t', paneId, 'Enter'], { timeout: 2_000 });
|
|
348
|
+
return { ok: true, via: 'tmux' };
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
catch { /* no tmux server — fall through */ }
|
|
352
|
+
// iTerm2: type the line, pause, then submit with an explicit carriage return.
|
|
353
|
+
//
|
|
354
|
+
// `write text "..."` alone would do both — measured in raw mode it delivers
|
|
355
|
+
// the text followed by 0x0d, which is the right byte (0x0a would land as a
|
|
356
|
+
// newline *inside* the prompt instead of submitting it; the tty's ICRNL
|
|
357
|
+
// translation hides that difference from any canonical-mode probe, so test
|
|
358
|
+
// this with `stty raw` or the reading is meaningless). It is split anyway
|
|
359
|
+
// because a TUI that treats one burst as a paste can absorb a trailing return
|
|
360
|
+
// as content, and the same missing beat cost us the option keys earlier.
|
|
361
|
+
const iterm = [
|
|
362
|
+
'tell application "iTerm2"',
|
|
363
|
+
' repeat with w in windows',
|
|
364
|
+
' repeat with t in tabs of w',
|
|
365
|
+
' repeat with s in sessions of t',
|
|
366
|
+
` if tty of s is "/dev/${tty}" then`,
|
|
367
|
+
` write s text "${escapeAppleScript(line)}" newline NO`,
|
|
368
|
+
' delay 0.25',
|
|
369
|
+
' write s text (ASCII character 13) newline NO',
|
|
370
|
+
' return "ok"',
|
|
371
|
+
' end if',
|
|
372
|
+
' end repeat',
|
|
373
|
+
' end repeat',
|
|
374
|
+
' end repeat',
|
|
375
|
+
'end tell',
|
|
376
|
+
'return "notfound"',
|
|
377
|
+
].join('\n');
|
|
378
|
+
if (await runOsa(iterm, 5_000) === 'ok')
|
|
379
|
+
return { ok: true, via: 'iterm2' };
|
|
380
|
+
// Terminal.app: `do script … in <tab>` types into that tab and returns.
|
|
381
|
+
const term = [
|
|
382
|
+
'set done to false',
|
|
383
|
+
'tell application "Terminal"',
|
|
384
|
+
' repeat with w in windows',
|
|
385
|
+
' repeat with t in tabs of w',
|
|
386
|
+
` if tty of t is "/dev/${tty}" then`,
|
|
387
|
+
` do script "${escapeAppleScript(line)}" in t`,
|
|
388
|
+
' set done to true',
|
|
389
|
+
' exit repeat',
|
|
390
|
+
' end if',
|
|
391
|
+
' end repeat',
|
|
392
|
+
' if done then exit repeat',
|
|
393
|
+
' end repeat',
|
|
394
|
+
'end tell',
|
|
395
|
+
'if done then',
|
|
396
|
+
' return "ok"',
|
|
397
|
+
'else',
|
|
398
|
+
' return "notfound"',
|
|
399
|
+
'end if',
|
|
400
|
+
].join('\n');
|
|
401
|
+
if (await runOsa(term, 5_000) === 'ok')
|
|
402
|
+
return { ok: true, via: 'terminal-app' };
|
|
403
|
+
return { ok: false, reason: `no reachable terminal for ${tty}` };
|
|
404
|
+
}
|
|
405
|
+
//# sourceMappingURL=observed-inject.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observed-inject.js","sourceRoot":"","sources":["../src/observed-inject.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,6 +3,8 @@ export type ObservedState = 'idle' | 'processing';
|
|
|
3
3
|
export interface ProcInfo {
|
|
4
4
|
pid: number;
|
|
5
5
|
ppid: number;
|
|
6
|
+
/** Controlling terminal short name ("ttys008") or "??" for none. */
|
|
7
|
+
tty?: string;
|
|
6
8
|
rssKb: number;
|
|
7
9
|
command: string;
|
|
8
10
|
}
|
|
@@ -22,6 +24,11 @@ export interface TranscriptSummary {
|
|
|
22
24
|
*/
|
|
23
25
|
export declare function cleanGoal(raw: string): string;
|
|
24
26
|
export interface ObservedSession extends EnrichedSession {
|
|
27
|
+
/** Controlling terminal ("ttys008") — the observed-answer injection target. */
|
|
28
|
+
tty?: string;
|
|
29
|
+
/** Owning .app for GUI-hosted sessions ("Claude", "ChatGPT") — the app-hosted
|
|
30
|
+
* injection target when there is no tty. */
|
|
31
|
+
appName?: string;
|
|
25
32
|
controlMode: 'observed';
|
|
26
33
|
pid: number;
|
|
27
34
|
cwd?: string;
|
|
@@ -65,6 +72,13 @@ export declare function parseCodexRollout(raw: string): TranscriptSummary & {
|
|
|
65
72
|
effort?: string;
|
|
66
73
|
hasPendingCalls?: boolean;
|
|
67
74
|
};
|
|
75
|
+
/** Extract the enclosing macOS app name from an executable path, e.g.
|
|
76
|
+
* "/Applications/ChatGPT.app/Contents/Resources/codex …" → "ChatGPT". */
|
|
77
|
+
export declare function appNameFromCommand(command: string): string | undefined;
|
|
78
|
+
/** Walk up the process tree (bounded) looking for the .app bundle that hosts
|
|
79
|
+
* this process. GUI-hosted agent sessions (Claude.app, ChatGPT.app) have no
|
|
80
|
+
* tty, so the owning app is how the injector finds their UI. */
|
|
81
|
+
export declare function resolveHostApp(pid: number, byPid: Map<number, ProcInfo>, maxDepth?: number): string | undefined;
|
|
68
82
|
export declare function isAntigravityProcessCommand(command: string): boolean;
|
|
69
83
|
export declare function parseLsofRollouts(output: string): Map<number, string>;
|
|
70
84
|
//# sourceMappingURL=passive-observer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"passive-observer.d.ts","sourceRoot":"","sources":["../src/passive-observer.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAW/D,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,YAAY,CAAC;AAElD,MAAM,WAAW,QAAQ;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,aAAa,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAgBD;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAU7C;AASD,MAAM,WAAW,eAAgB,SAAQ,eAAe;IACtD,WAAW,EAAE,UAAU,CAAC;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;sEAIkE;IAClE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAQD,qBAAa,sBAAsB;IACjC,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,YAAY,CAAS;IAE7B;;0DAEsD;IACtD,WAAW,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;IAEtC;;;;;;;;OAQG;IACH,OAAO,CAAC,eAAe,EAAE,eAAe,EAAE,GAAG,eAAe,EAAE;YAYhD,IAAI;CAenB;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,EAAE,
|
|
1
|
+
{"version":3,"file":"passive-observer.d.ts","sourceRoot":"","sources":["../src/passive-observer.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAW/D,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,YAAY,CAAC;AAElD,MAAM,WAAW,QAAQ;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,aAAa,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAgBD;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAU7C;AASD,MAAM,WAAW,eAAgB,SAAQ,eAAe;IACtD,+EAA+E;IAC/E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;iDAC6C;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,UAAU,CAAC;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;sEAIkE;IAClE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAQD,qBAAa,sBAAsB;IACjC,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,YAAY,CAAS;IAE7B;;0DAEsD;IACtD,WAAW,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;IAEtC;;;;;;;;OAQG;IACH,OAAO,CAAC,eAAe,EAAE,eAAe,EAAE,GAAG,eAAe,EAAE;YAYhD,IAAI;CAenB;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,EAAE,CAc5D;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,CAoFpE;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,GAAG;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,eAAe,CAAC,EAAE,OAAO,CAAA;CAAE,CA6HvK;AAeD;0EAC0E;AAC1E,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGtE;AAED;;iEAEiE;AACjE,wBAAgB,cAAc,CAC5B,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,EAC5B,QAAQ,SAAI,GACX,MAAM,GAAG,SAAS,CASpB;AAqKD,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAapE;AAkID,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAYrE"}
|
package/dist/passive-observer.js
CHANGED
|
@@ -5,7 +5,7 @@ import { closeSync, existsSync, lstatSync, openSync, readFileSync, readlinkSync,
|
|
|
5
5
|
import { basename, join } from 'node:path';
|
|
6
6
|
import { homedir } from 'node:os';
|
|
7
7
|
import { resolveProjectNameFromCwdCached } from './utils/project-name.js';
|
|
8
|
-
import { stripUnsafeText } from '@agentdeck/shared';
|
|
8
|
+
import { stripUnsafeText, rawSessionId } from '@agentdeck/shared';
|
|
9
9
|
import { parseAntigravityTranscript, antigravityDefaultModel, antigravityConversationId, antigravityTranscriptPath, } from './apme/antigravity-transcript.js';
|
|
10
10
|
/** Pull display text out of a Claude user message (string or text blocks). */
|
|
11
11
|
function claudeUserText(message) {
|
|
@@ -92,14 +92,15 @@ export class PassiveSessionObserver {
|
|
|
92
92
|
export function parseProcessTable(output) {
|
|
93
93
|
const rows = [];
|
|
94
94
|
for (const line of output.split('\n')) {
|
|
95
|
-
const match = line.match(/^\s*(\d+)\s+(\d+)\s+(\d+)\s+(.+?)\s*$/);
|
|
95
|
+
const match = line.match(/^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(.+?)\s*$/);
|
|
96
96
|
if (!match)
|
|
97
97
|
continue;
|
|
98
98
|
rows.push({
|
|
99
99
|
pid: Number(match[1]),
|
|
100
100
|
ppid: Number(match[2]),
|
|
101
101
|
rssKb: Number(match[3]),
|
|
102
|
-
|
|
102
|
+
tty: match[4] === '??' ? undefined : match[4],
|
|
103
|
+
command: match[5] ?? '',
|
|
103
104
|
});
|
|
104
105
|
}
|
|
105
106
|
return rows.filter((p) => p.pid > 0 && p.command.length > 0);
|
|
@@ -332,7 +333,7 @@ export function parseCodexRollout(raw) {
|
|
|
332
333
|
}
|
|
333
334
|
async function collectProcessInfo() {
|
|
334
335
|
try {
|
|
335
|
-
const { stdout } = await execFileAsync('ps', ['-ww', '-eo', 'pid=,ppid=,rss=,command='], {
|
|
336
|
+
const { stdout } = await execFileAsync('ps', ['-ww', '-eo', 'pid=,ppid=,rss=,tty=,command='], {
|
|
336
337
|
encoding: 'utf8',
|
|
337
338
|
timeout: 2_000,
|
|
338
339
|
maxBuffer: 2 * 1024 * 1024,
|
|
@@ -343,8 +344,30 @@ async function collectProcessInfo() {
|
|
|
343
344
|
return [];
|
|
344
345
|
}
|
|
345
346
|
}
|
|
347
|
+
/** Extract the enclosing macOS app name from an executable path, e.g.
|
|
348
|
+
* "/Applications/ChatGPT.app/Contents/Resources/codex …" → "ChatGPT". */
|
|
349
|
+
export function appNameFromCommand(command) {
|
|
350
|
+
const m = command.match(/\/([^/]+)\.app\//);
|
|
351
|
+
return m ? m[1] : undefined;
|
|
352
|
+
}
|
|
353
|
+
/** Walk up the process tree (bounded) looking for the .app bundle that hosts
|
|
354
|
+
* this process. GUI-hosted agent sessions (Claude.app, ChatGPT.app) have no
|
|
355
|
+
* tty, so the owning app is how the injector finds their UI. */
|
|
356
|
+
export function resolveHostApp(pid, byPid, maxDepth = 6) {
|
|
357
|
+
let cur = byPid.get(pid);
|
|
358
|
+
for (let i = 0; i < maxDepth && cur; i++) {
|
|
359
|
+
const name = appNameFromCommand(cur.command);
|
|
360
|
+
if (name)
|
|
361
|
+
return name;
|
|
362
|
+
if (!cur.ppid || cur.ppid <= 1)
|
|
363
|
+
break;
|
|
364
|
+
cur = byPid.get(cur.ppid);
|
|
365
|
+
}
|
|
366
|
+
return undefined;
|
|
367
|
+
}
|
|
346
368
|
function collectClaudeSessions(processes) {
|
|
347
369
|
const configDirs = claudeConfigDirs();
|
|
370
|
+
const byPid = new Map(processes.map((p) => [p.pid, p]));
|
|
348
371
|
const sessions = [];
|
|
349
372
|
for (const proc of processes) {
|
|
350
373
|
if (!cmdHasBinary(proc.command, 'claude') || proc.command.includes('--print'))
|
|
@@ -370,6 +393,8 @@ function collectClaudeSessions(processes) {
|
|
|
370
393
|
startedAt: new Date(sessionFile.startedAt).toISOString(),
|
|
371
394
|
controlMode: 'observed',
|
|
372
395
|
cwd: sessionFile.cwd,
|
|
396
|
+
tty: proc.tty,
|
|
397
|
+
appName: proc.tty ? undefined : resolveHostApp(proc.pid, byPid),
|
|
373
398
|
currentTask: summary.currentTask,
|
|
374
399
|
goal: summary.goal,
|
|
375
400
|
contextPercent: summary.contextPercent,
|
|
@@ -380,6 +405,7 @@ function collectClaudeSessions(processes) {
|
|
|
380
405
|
return sessions;
|
|
381
406
|
}
|
|
382
407
|
async function collectCodexSessions(processes) {
|
|
408
|
+
const byPid = new Map(processes.map((p) => [p.pid, p]));
|
|
383
409
|
const codex = processes.filter((p) => cmdHasBinary(p.command, 'codex') &&
|
|
384
410
|
!p.command.includes('app-server') &&
|
|
385
411
|
!p.command.includes('grep'));
|
|
@@ -410,6 +436,10 @@ async function collectCodexSessions(processes) {
|
|
|
410
436
|
pid: proc.pid,
|
|
411
437
|
projectName: cwd ? projectNameFromCwd(cwd) : 'Codex',
|
|
412
438
|
agentType: 'codex-cli',
|
|
439
|
+
// Same injection targets as observed Claude: answering or dictating into
|
|
440
|
+
// a Codex session means typing into the terminal that owns it.
|
|
441
|
+
tty: proc.tty,
|
|
442
|
+
appName: proc.tty ? undefined : resolveHostApp(proc.pid, byPid),
|
|
413
443
|
alive: true,
|
|
414
444
|
state,
|
|
415
445
|
modelName: parsed.modelName,
|
|
@@ -544,7 +574,7 @@ function dedupeObservedSessions(observed, managedSessions, processes) {
|
|
|
544
574
|
return observed.filter((session) => {
|
|
545
575
|
if (managedIds.has(session.id))
|
|
546
576
|
return false;
|
|
547
|
-
const rawId = session.id
|
|
577
|
+
const rawId = rawSessionId(session.id);
|
|
548
578
|
if (managedIds.has(rawId))
|
|
549
579
|
return false;
|
|
550
580
|
return !managedPids.some((pid) => pid === session.pid || isDescendantOf(session.pid, pid, byPid));
|