@phnx-labs/agents-cli 1.22.92 → 1.22.94
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/CHANGELOG.md +12 -0
- package/README.md +5 -0
- package/dist/commands/browser.d.ts +1 -0
- package/dist/commands/browser.js +145 -11
- package/dist/lib/browser/arc-discovery.d.ts +60 -0
- package/dist/lib/browser/arc-discovery.js +187 -0
- package/dist/lib/browser/arc-dom.d.ts +14 -0
- package/dist/lib/browser/arc-dom.js +121 -0
- package/dist/lib/browser/drivers/arc.d.ts +57 -0
- package/dist/lib/browser/drivers/arc.js +281 -0
- package/dist/lib/browser/profiles.d.ts +5 -1
- package/dist/lib/browser/profiles.js +94 -9
- package/dist/lib/browser/refs.d.ts +2 -0
- package/dist/lib/browser/refs.js +2 -1
- package/dist/lib/browser/resolve-target.d.ts +2 -0
- package/dist/lib/browser/resolve-target.js +8 -0
- package/dist/lib/browser/runtime-state.js +9 -1
- package/dist/lib/browser/service.d.ts +37 -3
- package/dist/lib/browser/service.js +571 -41
- package/dist/lib/browser/types.d.ts +55 -1
- package/dist/lib/claude-account-token.d.ts +13 -0
- package/dist/lib/claude-account-token.js +29 -0
- package/dist/lib/daemon/attention-notify-service.d.ts +5 -2
- package/dist/lib/daemon/attention-notify-service.js +9 -2
- package/dist/lib/exec.js +11 -0
- package/dist/lib/feed/answer.js +9 -0
- package/dist/lib/feed/attention.d.ts +18 -2
- package/dist/lib/feed/attention.js +0 -0
- package/dist/lib/feed/feed.d.ts +6 -2
- package/dist/lib/feed/feed.js +10 -4
- package/dist/lib/harness/adapter.d.ts +23 -0
- package/dist/lib/harness/adapter.js +29 -0
- package/dist/lib/harness/adapters/claude.js +5 -3
- package/dist/lib/harness/adapters/copilot.js +4 -2
- package/dist/lib/harness/adapters/grok.js +4 -2
- package/dist/lib/harness/adapters/kimi.js +4 -2
- package/dist/lib/harness/adapters/muse.js +4 -3
- package/dist/lib/harness/adapters/opencode.js +4 -2
- package/dist/lib/helper-download.js +1 -1
- package/dist/lib/helper-versions.js +1 -1
- package/dist/lib/installations/shims.d.ts +2 -2
- package/dist/lib/installations/shims.js +16 -8
- package/dist/lib/menubar/download-menubar.js +4 -1
- package/dist/lib/menubar/install-menubar.d.ts +10 -9
- package/dist/lib/menubar/install-menubar.js +17 -15
- package/dist/lib/secrets-policy.d.ts +5 -2
- package/dist/lib/secrets-policy.js +18 -4
- package/dist/lib/session/active.d.ts +18 -11
- package/dist/lib/session/active.js +52 -44
- package/dist/lib/session/session-cache.d.ts +1 -1
- package/dist/lib/session/session-cache.js +1 -0
- package/dist/lib/session/state.d.ts +26 -0
- package/dist/lib/session/state.js +43 -27
- package/dist/lib/types.d.ts +7 -0
- package/package.json +1 -1
|
@@ -16,12 +16,14 @@ import { taskMatchesCaller } from './caller-identity.js';
|
|
|
16
16
|
import { getRefs, resolveRefToCoords, describeRefs, healRef } from './refs.js';
|
|
17
17
|
import { clickAtCoords, hoverAtCoords, scrollAtCoords, typeText, pressKey, focusNode } from './input.js';
|
|
18
18
|
import { typeEditorText } from './editor.js';
|
|
19
|
+
import { arcClickExpression, arcFillExpression, arcRefsExpression, arcScrollExpression, parseArcRefsResult, } from './arc-dom.js';
|
|
19
20
|
import { detectUploadPattern, stageUploadFile, uploadToDropTarget, uploadToFileInput, uploadViaFileChooser, } from './upload.js';
|
|
20
21
|
import { emit } from '../feed/events.js';
|
|
21
22
|
import { resolveActor } from '../actor.js';
|
|
22
23
|
import { recordBrowserSession } from '../session/db.js';
|
|
23
24
|
import { sshExecAsync } from '../ssh-exec.js';
|
|
24
25
|
import { resolveFfmpeg } from './ffmpeg.js';
|
|
26
|
+
import { ArcNativeCapabilityError, executeJavaScript, navigateArcTab, closeArcTab, createArcTab, enumerateArcSpaces, isArcRunning, resolveArcTab, restoreArcSelection, selectWindowTab, selectArcTab, } from './drivers/arc.js';
|
|
25
27
|
/**
|
|
26
28
|
* Canonical form for comparing a requested URL against what CDP reports for a
|
|
27
29
|
* live target. `new URL('https://example.com').href` is `https://example.com/`,
|
|
@@ -229,6 +231,10 @@ function expandHome(p) {
|
|
|
229
231
|
* can make; if it doesn't round-trip within 1s the connection is dead.
|
|
230
232
|
*/
|
|
231
233
|
async function isConnHealthy(conn, timeoutMs = 1000) {
|
|
234
|
+
// Native Arc connections: healthy if Arc is still running (no CDP socket to probe).
|
|
235
|
+
if (conn.backend === 'arc-native') {
|
|
236
|
+
return isArcRunning();
|
|
237
|
+
}
|
|
232
238
|
if (!conn.cdp.isOpen)
|
|
233
239
|
return false;
|
|
234
240
|
try {
|
|
@@ -242,6 +248,10 @@ async function isConnHealthy(conn, timeoutMs = 1000) {
|
|
|
242
248
|
return false;
|
|
243
249
|
}
|
|
244
250
|
}
|
|
251
|
+
function requireCdp(conn, capability) {
|
|
252
|
+
if (conn.backend === 'arc-native')
|
|
253
|
+
throw new ArcNativeCapabilityError(capability);
|
|
254
|
+
}
|
|
245
255
|
/** Join error lines so callers get a next command, not a dead-end message. */
|
|
246
256
|
export function actionable(...lines) {
|
|
247
257
|
return lines.filter((l) => l != null && l !== '').join('\n');
|
|
@@ -294,6 +304,20 @@ export function resolveTaskIdentity(forwarded, resolveLocalActor) {
|
|
|
294
304
|
sessionId: forwarded.sessionId,
|
|
295
305
|
};
|
|
296
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* The unique URL a native Arc tab is created at before it is navigated to its
|
|
309
|
+
* real target (PHNX-2399). Arc's `make new tab` accepts only http(s) URLs (it
|
|
310
|
+
* rejects `data:` and `about:` outright), so the marker is an https URL on the
|
|
311
|
+
* reserved `.invalid` TLD: DNS fails locally, no request leaves the machine,
|
|
312
|
+
* and the tab keeps the exact URL for the enumerate-and-match that follows.
|
|
313
|
+
*/
|
|
314
|
+
const ARC_CREATE_MARKER_PREFIX = 'https://agents-browser.invalid/';
|
|
315
|
+
export function arcCreateMarker() {
|
|
316
|
+
return `${ARC_CREATE_MARKER_PREFIX}${crypto.randomUUID()}`;
|
|
317
|
+
}
|
|
318
|
+
export function isArcCreateMarker(url) {
|
|
319
|
+
return url.startsWith(ARC_CREATE_MARKER_PREFIX);
|
|
320
|
+
}
|
|
297
321
|
export class BrowserService {
|
|
298
322
|
static SOURCE_PREFIX = {
|
|
299
323
|
'rush-app': 'rush-app-',
|
|
@@ -435,6 +459,9 @@ export class BrowserService {
|
|
|
435
459
|
endpointName: opts.endpointName,
|
|
436
460
|
probe: opts.probe,
|
|
437
461
|
});
|
|
462
|
+
if (!routed.local && routed.commandDispatch) {
|
|
463
|
+
throw new Error(`Native Arc profile "${profileName}" is owned by ${routed.device}; dispatch the complete browser command there.`);
|
|
464
|
+
}
|
|
438
465
|
const composite = routed.key;
|
|
439
466
|
const effectiveProfile = routed.profile;
|
|
440
467
|
const taskId = generateTaskId();
|
|
@@ -509,7 +536,7 @@ export class BrowserService {
|
|
|
509
536
|
// pile-up in RUSH-2622. Tabs the daemon did NOT open are still left alone —
|
|
510
537
|
// the branch only fires when the profile has no page target at all.
|
|
511
538
|
let startupBlankTargetId;
|
|
512
|
-
if (!opts.url && !conn.electron) {
|
|
539
|
+
if (!opts.url && !conn.electron && conn.backend !== 'arc-native') {
|
|
513
540
|
const { targetInfos } = (await conn.cdp.send('Target.getTargets'));
|
|
514
541
|
if (!targetInfos.some((t) => t.type === 'page')) {
|
|
515
542
|
const created = await this.createPageTarget(conn, {
|
|
@@ -538,13 +565,23 @@ export class BrowserService {
|
|
|
538
565
|
// actor (the RUSH-2020 bug).
|
|
539
566
|
...resolveTaskIdentity({ actor: opts.actor, launchId: opts.launchId, sessionId: opts.sessionId }, () => resolveActor().id),
|
|
540
567
|
};
|
|
568
|
+
if (conn.backend === 'arc-native') {
|
|
569
|
+
const selected = await this.resolveArcSpace(conn.arcProfile);
|
|
570
|
+
task.arcNative = {
|
|
571
|
+
profileId: conn.arcProfile.profileId,
|
|
572
|
+
windowId: selected.windowId,
|
|
573
|
+
spaceId: selected.spaceId,
|
|
574
|
+
spaceTitle: selected.spaceTitle,
|
|
575
|
+
tabs: {},
|
|
576
|
+
};
|
|
577
|
+
}
|
|
541
578
|
if (startupBlankTargetId) {
|
|
542
579
|
const shortId = generateShortId();
|
|
543
580
|
task.tabs[shortId] = startupBlankTargetId;
|
|
544
581
|
task.currentTabId = shortId;
|
|
545
582
|
}
|
|
546
583
|
// For Electron, get the existing window as the tab
|
|
547
|
-
if (conn.electron) {
|
|
584
|
+
if (conn.electron && conn.backend !== 'arc-native') {
|
|
548
585
|
const windowId = await this.getOrCreateWindow(conn);
|
|
549
586
|
if (windowId) {
|
|
550
587
|
const shortId = generateShortId();
|
|
@@ -596,7 +633,7 @@ export class BrowserService {
|
|
|
596
633
|
// opening a fresh target is created:true.
|
|
597
634
|
let tabId;
|
|
598
635
|
let firstOpen;
|
|
599
|
-
if (opts.url && !conn.electron && conn.browserType !== 'arc') {
|
|
636
|
+
if (opts.url && conn.backend !== 'arc-native' && !conn.electron && conn.browserType !== 'arc') {
|
|
600
637
|
const adopted = opts.fresh ? undefined : await this.adoptTabShowing(conn, opts.url);
|
|
601
638
|
const targetId = adopted ?? (await this.createPageTarget(conn, { url: opts.url })).targetId;
|
|
602
639
|
const shortId = generateShortId();
|
|
@@ -607,7 +644,7 @@ export class BrowserService {
|
|
|
607
644
|
tabId = shortId;
|
|
608
645
|
firstOpen = { tabId: shortId, created: !adopted, refreshed: false };
|
|
609
646
|
}
|
|
610
|
-
else if (opts.url && (conn.electron || conn.browserType === 'arc')) {
|
|
647
|
+
else if (opts.url && (conn.backend === 'arc-native' || conn.electron || conn.browserType === 'arc')) {
|
|
611
648
|
// Electron and Arc share the reuse-in-place path: neither may open a fresh
|
|
612
649
|
// tab here (Electron drives its one window; Arc crashes on Target.createTarget),
|
|
613
650
|
// so the implicit first navigate attaches to an existing tab — honoring the
|
|
@@ -830,36 +867,55 @@ export class BrowserService {
|
|
|
830
867
|
const mapKey = this.taskMapKey(conn, task) ?? taskName;
|
|
831
868
|
// Get domains from tabs before closing (for history)
|
|
832
869
|
const domains = new Set();
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
for
|
|
836
|
-
|
|
837
|
-
|
|
870
|
+
if (conn.backend === 'arc-native') {
|
|
871
|
+
// Resolve exact durable refs for history. Missing/moved tabs are not
|
|
872
|
+
// adopted and never become candidates for a best-effort close.
|
|
873
|
+
for (const ref of Object.values(this.requireArcTask(task).tabs)) {
|
|
874
|
+
const live = await resolveArcTab(ref);
|
|
875
|
+
if (live?.url) {
|
|
838
876
|
try {
|
|
839
|
-
const domain = new URL(
|
|
877
|
+
const domain = new URL(live.url).hostname.replace(/^www\./, '');
|
|
840
878
|
if (domain && domain !== 'blank')
|
|
841
879
|
domains.add(domain);
|
|
842
880
|
}
|
|
843
|
-
catch {
|
|
844
|
-
// invalid URL
|
|
845
|
-
}
|
|
881
|
+
catch { /* invalid URL */ }
|
|
846
882
|
}
|
|
847
883
|
}
|
|
884
|
+
await this.saveToHistory(task, Array.from(domains));
|
|
885
|
+
await this.closeArcNativeTabs(conn, task);
|
|
848
886
|
}
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
887
|
+
else {
|
|
888
|
+
try {
|
|
889
|
+
const { targetInfos } = (await conn.cdp.send('Target.getTargets'));
|
|
890
|
+
for (const cdpId of Object.values(task.tabs)) {
|
|
891
|
+
const target = targetInfos.find((t) => t.targetId === cdpId);
|
|
892
|
+
if (target?.url) {
|
|
893
|
+
try {
|
|
894
|
+
const domain = new URL(target.url).hostname.replace(/^www\./, '');
|
|
895
|
+
if (domain && domain !== 'blank')
|
|
896
|
+
domains.add(domain);
|
|
897
|
+
}
|
|
898
|
+
catch {
|
|
899
|
+
// invalid URL
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
catch {
|
|
905
|
+
// CDP not responding
|
|
906
|
+
}
|
|
907
|
+
// Save to history before closing
|
|
908
|
+
await this.saveToHistory(task, Array.from(domains));
|
|
909
|
+
// Close task's tabs (not the window - it's shared, and not a tab the
|
|
910
|
+
// task merely borrowed - that one was open before the task existed).
|
|
911
|
+
await Promise.all(this.closeableTargetIds(task).map((cdpId) => conn.cdp.send('Target.closeTarget', { targetId: cdpId }).catch(() => {
|
|
912
|
+
// Tab already closed
|
|
913
|
+
})));
|
|
914
|
+
for (const cdpId of Object.values(task.tabs)) {
|
|
915
|
+
conn.sessionCache.delete(cdpId);
|
|
916
|
+
}
|
|
917
|
+
this.invalidateTargetCache(conn);
|
|
861
918
|
}
|
|
862
|
-
this.invalidateTargetCache(conn);
|
|
863
919
|
conn.tasks.delete(mapKey);
|
|
864
920
|
await this.saveTaskState(key, conn.tasks);
|
|
865
921
|
// Feed + usage rows are read by humans: bare profile name (RUSH-2709).
|
|
@@ -875,8 +931,10 @@ export class BrowserService {
|
|
|
875
931
|
});
|
|
876
932
|
}).catch(() => { });
|
|
877
933
|
if (conn.forkedFrom && conn.tasks.size === 0) {
|
|
878
|
-
conn.
|
|
879
|
-
|
|
934
|
+
if (conn.backend !== 'arc-native') {
|
|
935
|
+
conn.cdp.close();
|
|
936
|
+
killChrome(conn.pid);
|
|
937
|
+
}
|
|
880
938
|
conn.cleanup?.();
|
|
881
939
|
this.connections.delete(key);
|
|
882
940
|
clearProfileRuntime(key);
|
|
@@ -908,6 +966,16 @@ export class BrowserService {
|
|
|
908
966
|
const conn = this.connections.get(key);
|
|
909
967
|
if (!conn)
|
|
910
968
|
continue;
|
|
969
|
+
// Native Arc connections: close owned tabs but NEVER kill the Arc process.
|
|
970
|
+
// The CLI does not own the Arc process — the user started it.
|
|
971
|
+
if (conn.backend === 'arc-native') {
|
|
972
|
+
for (const task of conn.tasks.values()) {
|
|
973
|
+
await this.closeArcNativeTabs(conn, task);
|
|
974
|
+
}
|
|
975
|
+
this.connections.delete(key);
|
|
976
|
+
clearProfileRuntime(key);
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
911
979
|
conn.cdp.close();
|
|
912
980
|
killChrome(conn.pid);
|
|
913
981
|
conn.cleanup?.();
|
|
@@ -934,6 +1002,11 @@ export class BrowserService {
|
|
|
934
1002
|
}
|
|
935
1003
|
async navigateLocked(conn, task, url) {
|
|
936
1004
|
this.maybeUpdateLabelFromUrl(conn, task, url);
|
|
1005
|
+
// Native Arc backend (PHNX-2399): route through the native driver instead of CDP.
|
|
1006
|
+
if (conn.backend === 'arc-native') {
|
|
1007
|
+
return this.navigateArcNative(conn, task, url);
|
|
1008
|
+
}
|
|
1009
|
+
requireCdp(conn, 'navigate');
|
|
937
1010
|
// Same-task reopen (PHNX-2399): the URL is already live in a tab this task
|
|
938
1011
|
// owns → reload THAT tab in place and keep its id, before the create/reuse
|
|
939
1012
|
// paths below. Chrome and Arc alike; Electron drives one window and never
|
|
@@ -1035,6 +1108,10 @@ export class BrowserService {
|
|
|
1035
1108
|
return this.runExclusive(`task:${task.profile}:${task.name}`, () => this.tabAddLocked(conn, task, url));
|
|
1036
1109
|
}
|
|
1037
1110
|
async tabAddLocked(conn, task, url) {
|
|
1111
|
+
if (conn.backend === 'arc-native') {
|
|
1112
|
+
return this.navigateArcNative(conn, task, url, true);
|
|
1113
|
+
}
|
|
1114
|
+
requireCdp(conn, 'createTab');
|
|
1038
1115
|
if (conn.electron) {
|
|
1039
1116
|
throw new Error('Electron apps do not support opening additional tabs');
|
|
1040
1117
|
}
|
|
@@ -1057,12 +1134,27 @@ export class BrowserService {
|
|
|
1057
1134
|
async tabFocus(taskId, tabHint) {
|
|
1058
1135
|
const { conn, task } = await this.findTask(taskId);
|
|
1059
1136
|
const resolvedTabId = await this.resolveTabHint(conn, task, tabHint);
|
|
1137
|
+
if (conn.backend === 'arc-native') {
|
|
1138
|
+
await this.runExclusive('arc-native:host-app', async () => {
|
|
1139
|
+
const ref = this.requireArcTask(task).tabs[resolvedTabId];
|
|
1140
|
+
if (!ref)
|
|
1141
|
+
throw new Error(`Tab ${resolvedTabId} not found`);
|
|
1142
|
+
await selectArcTab(ref);
|
|
1143
|
+
task.currentTabId = resolvedTabId;
|
|
1144
|
+
await this.saveTaskState(task.profile, conn.tasks);
|
|
1145
|
+
});
|
|
1146
|
+
return { tabId: resolvedTabId };
|
|
1147
|
+
}
|
|
1060
1148
|
task.currentTabId = resolvedTabId;
|
|
1061
1149
|
await this.saveTaskState(task.profile, conn.tasks);
|
|
1062
1150
|
return { tabId: resolvedTabId };
|
|
1063
1151
|
}
|
|
1064
1152
|
async tabList(taskId) {
|
|
1065
1153
|
const { conn, task } = await this.findTask(taskId);
|
|
1154
|
+
if (conn.backend === 'arc-native') {
|
|
1155
|
+
return (await this.listArcTaskTabs(task)).map((tab) => ({ ...tab, current: tab.current === true }));
|
|
1156
|
+
}
|
|
1157
|
+
requireCdp(conn, 'enumerate');
|
|
1066
1158
|
const targets = (await conn.cdp.send('Target.getTargets'));
|
|
1067
1159
|
const tabs = [];
|
|
1068
1160
|
for (const [shortId, cdpId] of Object.entries(task.tabs)) {
|
|
@@ -1089,6 +1181,10 @@ export class BrowserService {
|
|
|
1089
1181
|
if (byPrefix.length > 1) {
|
|
1090
1182
|
throw new Error(`Ambiguous tab hint "${hint}" — matches ${byPrefix.length} tabs`);
|
|
1091
1183
|
}
|
|
1184
|
+
// Native Arc addresses owned tabs only by the task's stable short id. URL
|
|
1185
|
+
// and title are display data and must never become fallback identities.
|
|
1186
|
+
if (conn.backend === 'arc-native')
|
|
1187
|
+
throw new Error(`Tab "${hint}" not found`);
|
|
1092
1188
|
// URL substring match
|
|
1093
1189
|
const targets = (await conn.cdp.send('Target.getTargets'));
|
|
1094
1190
|
const matches = [];
|
|
@@ -1121,12 +1217,16 @@ export class BrowserService {
|
|
|
1121
1217
|
async tabs(taskId, profileRef) {
|
|
1122
1218
|
if (taskId) {
|
|
1123
1219
|
const { conn, task } = await this.findTask(taskId, profileRef);
|
|
1220
|
+
if (conn.backend === 'arc-native')
|
|
1221
|
+
return this.listArcTaskTabs(task);
|
|
1124
1222
|
return this.getTabsForTask(conn.cdp, task);
|
|
1125
1223
|
}
|
|
1126
1224
|
const allTabs = [];
|
|
1127
1225
|
for (const [, conn] of this.connections) {
|
|
1128
1226
|
for (const [, task] of conn.tasks) {
|
|
1129
|
-
const tabs =
|
|
1227
|
+
const tabs = conn.backend === 'arc-native'
|
|
1228
|
+
? await this.listArcTaskTabs(task)
|
|
1229
|
+
: await this.getTabsForTask(conn.cdp, task);
|
|
1130
1230
|
allTabs.push(...tabs);
|
|
1131
1231
|
}
|
|
1132
1232
|
}
|
|
@@ -1134,6 +1234,30 @@ export class BrowserService {
|
|
|
1134
1234
|
}
|
|
1135
1235
|
async tabClose(taskId, tabHint) {
|
|
1136
1236
|
const { conn, task } = await this.findTask(taskId);
|
|
1237
|
+
if (conn.backend === 'arc-native') {
|
|
1238
|
+
await this.runExclusive('arc-native:host-app', async () => {
|
|
1239
|
+
await this.reconcileArcCreateIntents(conn, task);
|
|
1240
|
+
const native = this.requireArcTask(task);
|
|
1241
|
+
const ids = tabHint === undefined
|
|
1242
|
+
? Object.keys(native.tabs)
|
|
1243
|
+
: [await this.resolveTabHint(conn, task, tabHint)];
|
|
1244
|
+
for (const shortId of ids) {
|
|
1245
|
+
const ref = native.tabs[shortId];
|
|
1246
|
+
if (ref)
|
|
1247
|
+
await closeArcTab(ref);
|
|
1248
|
+
delete native.tabs[shortId];
|
|
1249
|
+
delete task.tabs[shortId];
|
|
1250
|
+
delete task.refDescriptors?.[shortId];
|
|
1251
|
+
}
|
|
1252
|
+
if (!task.currentTabId || ids.includes(task.currentTabId)) {
|
|
1253
|
+
const remaining = Object.keys(native.tabs);
|
|
1254
|
+
task.currentTabId = remaining.at(-1);
|
|
1255
|
+
}
|
|
1256
|
+
await this.saveTaskState(task.profile, conn.tasks);
|
|
1257
|
+
});
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
requireCdp(conn, 'closeTab');
|
|
1137
1261
|
if (tabHint !== undefined) {
|
|
1138
1262
|
const shortId = await this.resolveTabHint(conn, task, tabHint);
|
|
1139
1263
|
const cdpId = task.tabs[shortId];
|
|
@@ -1170,6 +1294,10 @@ export class BrowserService {
|
|
|
1170
1294
|
async evaluate(taskId, tabHint, expression) {
|
|
1171
1295
|
const { conn, task } = await this.findTask(taskId);
|
|
1172
1296
|
const shortId = tabHint ? await this.resolveTabHint(conn, task, tabHint) : this.resolveCurrentTab(task);
|
|
1297
|
+
// Native Arc backend (PHNX-2399): synchronous JS only, isolated world.
|
|
1298
|
+
if (conn.backend === 'arc-native') {
|
|
1299
|
+
return this.evaluateArcNative(conn, task, shortId, expression);
|
|
1300
|
+
}
|
|
1173
1301
|
const cdpTargetId = this.getCdpTargetId(task, shortId);
|
|
1174
1302
|
const target = await this.getTarget(conn, cdpTargetId);
|
|
1175
1303
|
if (!target) {
|
|
@@ -1199,6 +1327,13 @@ export class BrowserService {
|
|
|
1199
1327
|
}
|
|
1200
1328
|
async screenshot(taskId, tabHint, outputPath, quality = 'compressed') {
|
|
1201
1329
|
const { conn, task, key: runtimeKey } = await this.findTask(taskId);
|
|
1330
|
+
// Native Arc backend: screenshots cannot select/activate the user's tab.
|
|
1331
|
+
if (conn.backend === 'arc-native') {
|
|
1332
|
+
throw new ArcNativeCapabilityError('screenshot', 'Screenshot is unavailable for native Arc automation. ' +
|
|
1333
|
+
'Screenshots require activating the tab, which would disrupt user selection. ' +
|
|
1334
|
+
'This task still belongs to the Arc profile. ' +
|
|
1335
|
+
'Use a Chromium-family browser for screenshot capability.');
|
|
1336
|
+
}
|
|
1202
1337
|
const shortId = tabHint ? await this.resolveTabHint(conn, task, tabHint) : this.resolveCurrentTab(task);
|
|
1203
1338
|
const cdpTargetId = this.getCdpTargetId(task, shortId);
|
|
1204
1339
|
const target = await this.getTarget(conn, cdpTargetId);
|
|
@@ -1259,6 +1394,7 @@ export class BrowserService {
|
|
|
1259
1394
|
*/
|
|
1260
1395
|
async printToPdf(taskId, tabHint, outputPath) {
|
|
1261
1396
|
const { conn, task, key: runtimeKey } = await this.findTask(taskId);
|
|
1397
|
+
requireCdp(conn, 'pdf');
|
|
1262
1398
|
const shortId = tabHint ? await this.resolveTabHint(conn, task, tabHint) : this.resolveCurrentTab(task);
|
|
1263
1399
|
const cdpTargetId = this.getCdpTargetId(task, shortId);
|
|
1264
1400
|
const target = await this.getTarget(conn, cdpTargetId);
|
|
@@ -1287,6 +1423,7 @@ export class BrowserService {
|
|
|
1287
1423
|
throw new Error(`Task "${taskId}" is already recording. Call record stop first.`);
|
|
1288
1424
|
}
|
|
1289
1425
|
const { conn, task, key: runtimeKey } = await this.findTask(taskId);
|
|
1426
|
+
requireCdp(conn, 'screenshot');
|
|
1290
1427
|
const shortId = tabHint ? await this.resolveTabHint(conn, task, tabHint) : this.resolveCurrentTab(task);
|
|
1291
1428
|
const cdpTargetId = this.getCdpTargetId(task, shortId);
|
|
1292
1429
|
const target = await this.getTarget(conn, cdpTargetId);
|
|
@@ -1488,6 +1625,9 @@ export class BrowserService {
|
|
|
1488
1625
|
return { path: outputPath, fps, durationCapSec: durationSec, maxMb };
|
|
1489
1626
|
}
|
|
1490
1627
|
async recordStop(taskId, reason = 'manual') {
|
|
1628
|
+
const activeTask = await this.findTask(taskId).catch(() => undefined);
|
|
1629
|
+
if (activeTask)
|
|
1630
|
+
requireCdp(activeTask.conn, 'screenshot');
|
|
1491
1631
|
const rec = this.recordings.get(taskId);
|
|
1492
1632
|
if (!rec) {
|
|
1493
1633
|
throw new Error(`Task "${taskId}" is not currently recording`);
|
|
@@ -1570,6 +1710,13 @@ export class BrowserService {
|
|
|
1570
1710
|
return { path: rec.outputPath, bytes, durationMs, reason };
|
|
1571
1711
|
}
|
|
1572
1712
|
async recordStatus(taskId) {
|
|
1713
|
+
// The idle reaper calls this while deciding whether a task is stale. Do
|
|
1714
|
+
// not route an already-live task through findTask(), which stamps it as
|
|
1715
|
+
// active and defeats idle detection. A cold daemon may still rehydrate for
|
|
1716
|
+
// an explicit user query.
|
|
1717
|
+
const live = this.findTaskByHandle(taskId) ?? await this.findTask(taskId).catch(() => undefined);
|
|
1718
|
+
if (live)
|
|
1719
|
+
requireCdp(live.conn, 'screenshot');
|
|
1573
1720
|
const rec = this.recordings.get(taskId);
|
|
1574
1721
|
if (!rec)
|
|
1575
1722
|
return { recording: false };
|
|
@@ -1578,6 +1725,13 @@ export class BrowserService {
|
|
|
1578
1725
|
async refs(taskId, tabHint, opts = {}) {
|
|
1579
1726
|
const { conn, task } = await this.findTask(taskId);
|
|
1580
1727
|
const shortId = tabHint ? await this.resolveTabHint(conn, task, tabHint) : this.resolveCurrentTab(task);
|
|
1728
|
+
if (conn.backend === 'arc-native') {
|
|
1729
|
+
const result = parseArcRefsResult(await this.evaluateArcNative(conn, task, shortId, arcRefsExpression(opts)), opts);
|
|
1730
|
+
this.cacheRefDescriptors(task, shortId, result.nodeMap, result.opts);
|
|
1731
|
+
await this.saveTaskState(task.profile, conn.tasks);
|
|
1732
|
+
return result;
|
|
1733
|
+
}
|
|
1734
|
+
requireCdp(conn, 'enumerate');
|
|
1581
1735
|
const cdpTargetId = this.getCdpTargetId(task, shortId);
|
|
1582
1736
|
const target = await this.getTarget(conn, cdpTargetId);
|
|
1583
1737
|
if (!target)
|
|
@@ -1614,7 +1768,7 @@ export class BrowserService {
|
|
|
1614
1768
|
const stillMatches = fresh !== undefined &&
|
|
1615
1769
|
fresh.role === cached.role &&
|
|
1616
1770
|
fresh.name === cached.name &&
|
|
1617
|
-
fresh.backendNodeId !== undefined;
|
|
1771
|
+
(fresh.backendNodeId !== undefined || fresh.selector !== undefined);
|
|
1618
1772
|
if (stillMatches)
|
|
1619
1773
|
return { targetRef: ref };
|
|
1620
1774
|
const newRef = healRef(cached, nodeMap);
|
|
@@ -1632,6 +1786,18 @@ export class BrowserService {
|
|
|
1632
1786
|
async click(taskId, ref, tabHint) {
|
|
1633
1787
|
const { conn, task } = await this.findTask(taskId);
|
|
1634
1788
|
const shortId = tabHint ? await this.resolveTabHint(conn, task, tabHint) : this.resolveCurrentTab(task);
|
|
1789
|
+
if (conn.backend === 'arc-native') {
|
|
1790
|
+
const snapshot = task.refDescriptors?.[shortId];
|
|
1791
|
+
const buildOpts = snapshot?.opts ?? { interactive: true, limit: 500 };
|
|
1792
|
+
const { nodeMap } = parseArcRefsResult(await this.evaluateArcNative(conn, task, shortId, arcRefsExpression(buildOpts)), buildOpts);
|
|
1793
|
+
const { targetRef, healed } = this.resolveHealedRef(snapshot, nodeMap, ref);
|
|
1794
|
+
const selector = nodeMap.get(targetRef)?.selector;
|
|
1795
|
+
if (!selector)
|
|
1796
|
+
throw new Error(`Ref ${ref} has no DOM selector`);
|
|
1797
|
+
await this.evaluateArcNative(conn, task, shortId, arcClickExpression(selector));
|
|
1798
|
+
return healed ? { healed } : {};
|
|
1799
|
+
}
|
|
1800
|
+
requireCdp(conn, 'click');
|
|
1635
1801
|
const cdpTargetId = this.getCdpTargetId(task, shortId);
|
|
1636
1802
|
const target = await this.getTarget(conn, cdpTargetId);
|
|
1637
1803
|
if (!target)
|
|
@@ -1663,6 +1829,7 @@ export class BrowserService {
|
|
|
1663
1829
|
*/
|
|
1664
1830
|
async clickAt(taskId, x, y, tabHint) {
|
|
1665
1831
|
const { conn, task } = await this.findTask(taskId);
|
|
1832
|
+
requireCdp(conn, 'trustedInput');
|
|
1666
1833
|
const shortId = tabHint ? await this.resolveTabHint(conn, task, tabHint) : this.resolveCurrentTab(task);
|
|
1667
1834
|
const cdpTargetId = this.getCdpTargetId(task, shortId);
|
|
1668
1835
|
const target = await this.getTarget(conn, cdpTargetId);
|
|
@@ -1674,6 +1841,18 @@ export class BrowserService {
|
|
|
1674
1841
|
async type(taskId, ref, text, tabHint, clear) {
|
|
1675
1842
|
const { conn, task } = await this.findTask(taskId);
|
|
1676
1843
|
const shortId = tabHint ? await this.resolveTabHint(conn, task, tabHint) : this.resolveCurrentTab(task);
|
|
1844
|
+
if (conn.backend === 'arc-native') {
|
|
1845
|
+
const snapshot = task.refDescriptors?.[shortId];
|
|
1846
|
+
const buildOpts = snapshot?.opts ?? { interactive: true, limit: 500 };
|
|
1847
|
+
const { nodeMap } = parseArcRefsResult(await this.evaluateArcNative(conn, task, shortId, arcRefsExpression(buildOpts)), buildOpts);
|
|
1848
|
+
const { targetRef } = this.resolveHealedRef(snapshot, nodeMap, ref);
|
|
1849
|
+
const selector = nodeMap.get(targetRef)?.selector;
|
|
1850
|
+
if (!selector)
|
|
1851
|
+
throw new Error(`Ref ${ref} has no DOM selector`);
|
|
1852
|
+
await this.evaluateArcNative(conn, task, shortId, arcFillExpression(selector, text, clear ?? false));
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1855
|
+
requireCdp(conn, 'type');
|
|
1677
1856
|
const cdpTargetId = this.getCdpTargetId(task, shortId);
|
|
1678
1857
|
const target = await this.getTarget(conn, cdpTargetId);
|
|
1679
1858
|
if (!target)
|
|
@@ -1701,6 +1880,7 @@ export class BrowserService {
|
|
|
1701
1880
|
}
|
|
1702
1881
|
async press(taskId, key, tabHint) {
|
|
1703
1882
|
const { conn, task } = await this.findTask(taskId);
|
|
1883
|
+
requireCdp(conn, 'trustedInput');
|
|
1704
1884
|
const shortId = tabHint ? await this.resolveTabHint(conn, task, tabHint) : this.resolveCurrentTab(task);
|
|
1705
1885
|
const cdpTargetId = this.getCdpTargetId(task, shortId);
|
|
1706
1886
|
const target = await this.getTarget(conn, cdpTargetId);
|
|
@@ -1711,6 +1891,7 @@ export class BrowserService {
|
|
|
1711
1891
|
}
|
|
1712
1892
|
async hover(taskId, ref, tabHint) {
|
|
1713
1893
|
const { conn, task } = await this.findTask(taskId);
|
|
1894
|
+
requireCdp(conn, 'trustedInput');
|
|
1714
1895
|
const shortId = tabHint ? await this.resolveTabHint(conn, task, tabHint) : this.resolveCurrentTab(task);
|
|
1715
1896
|
const cdpTargetId = this.getCdpTargetId(task, shortId);
|
|
1716
1897
|
const target = await this.getTarget(conn, cdpTargetId);
|
|
@@ -1724,6 +1905,13 @@ export class BrowserService {
|
|
|
1724
1905
|
async scroll(taskId, deltaX, deltaY, atX, atY, tabHint) {
|
|
1725
1906
|
const { conn, task } = await this.findTask(taskId);
|
|
1726
1907
|
const shortId = tabHint ? await this.resolveTabHint(conn, task, tabHint) : this.resolveCurrentTab(task);
|
|
1908
|
+
if (conn.backend === 'arc-native') {
|
|
1909
|
+
if (atX !== undefined || atY !== undefined)
|
|
1910
|
+
throw new ArcNativeCapabilityError('trustedInput');
|
|
1911
|
+
await this.evaluateArcNative(conn, task, shortId, arcScrollExpression(deltaX, deltaY));
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
requireCdp(conn, 'scroll');
|
|
1727
1915
|
const cdpTargetId = this.getCdpTargetId(task, shortId);
|
|
1728
1916
|
const target = await this.getTarget(conn, cdpTargetId);
|
|
1729
1917
|
if (!target)
|
|
@@ -1733,6 +1921,7 @@ export class BrowserService {
|
|
|
1733
1921
|
}
|
|
1734
1922
|
async upload(taskId, files, options) {
|
|
1735
1923
|
const { conn, task } = await this.findTask(taskId);
|
|
1924
|
+
requireCdp(conn, 'upload');
|
|
1736
1925
|
const shortId = options.tabHint
|
|
1737
1926
|
? await this.resolveTabHint(conn, task, options.tabHint)
|
|
1738
1927
|
: this.resolveCurrentTab(task);
|
|
@@ -1883,6 +2072,7 @@ export class BrowserService {
|
|
|
1883
2072
|
// ─── Viewport & Device Emulation ──────────────────────────────────────────────
|
|
1884
2073
|
async setViewport(taskId, width, height, options = {}) {
|
|
1885
2074
|
const { conn, task } = await this.findTask(taskId);
|
|
2075
|
+
requireCdp(conn, 'viewport');
|
|
1886
2076
|
const shortId = options.tabHint ? await this.resolveTabHint(conn, task, options.tabHint) : this.resolveCurrentTab(task);
|
|
1887
2077
|
const cdpTargetId = this.getCdpTargetId(task, shortId);
|
|
1888
2078
|
const target = await this.getTarget(conn, cdpTargetId);
|
|
@@ -1963,6 +2153,7 @@ export class BrowserService {
|
|
|
1963
2153
|
}
|
|
1964
2154
|
async getConsoleLogs(taskId, options = {}) {
|
|
1965
2155
|
const { conn, task } = await this.findTask(taskId);
|
|
2156
|
+
requireCdp(conn, 'consoleCapture');
|
|
1966
2157
|
const shortId = options.tabHint ? await this.resolveTabHint(conn, task, options.tabHint) : this.resolveCurrentTab(task);
|
|
1967
2158
|
const cdpTargetId = this.getCdpTargetId(task, shortId);
|
|
1968
2159
|
const target = await this.getTarget(conn, cdpTargetId);
|
|
@@ -1981,6 +2172,7 @@ export class BrowserService {
|
|
|
1981
2172
|
}
|
|
1982
2173
|
async getErrors(taskId, options = {}) {
|
|
1983
2174
|
const { conn, task } = await this.findTask(taskId);
|
|
2175
|
+
requireCdp(conn, 'consoleCapture');
|
|
1984
2176
|
const shortId = options.tabHint ? await this.resolveTabHint(conn, task, options.tabHint) : this.resolveCurrentTab(task);
|
|
1985
2177
|
const cdpTargetId = this.getCdpTargetId(task, shortId);
|
|
1986
2178
|
const target = await this.getTarget(conn, cdpTargetId);
|
|
@@ -2033,6 +2225,7 @@ export class BrowserService {
|
|
|
2033
2225
|
}
|
|
2034
2226
|
async getNetworkRequests(taskId, options = {}) {
|
|
2035
2227
|
const { conn, task } = await this.findTask(taskId);
|
|
2228
|
+
requireCdp(conn, 'networkCapture');
|
|
2036
2229
|
const shortId = options.tabHint ? await this.resolveTabHint(conn, task, options.tabHint) : this.resolveCurrentTab(task);
|
|
2037
2230
|
const cdpTargetId = this.getCdpTargetId(task, shortId);
|
|
2038
2231
|
const target = await this.getTarget(conn, cdpTargetId);
|
|
@@ -2052,6 +2245,7 @@ export class BrowserService {
|
|
|
2052
2245
|
}
|
|
2053
2246
|
async getResponseBody(taskId, urlPattern, options = {}) {
|
|
2054
2247
|
const { conn, task } = await this.findTask(taskId);
|
|
2248
|
+
requireCdp(conn, 'networkCapture');
|
|
2055
2249
|
const shortId = options.tabHint ? await this.resolveTabHint(conn, task, options.tabHint) : this.resolveCurrentTab(task);
|
|
2056
2250
|
const cdpTargetId = this.getCdpTargetId(task, shortId);
|
|
2057
2251
|
const target = await this.getTarget(conn, cdpTargetId);
|
|
@@ -2125,11 +2319,12 @@ export class BrowserService {
|
|
|
2125
2319
|
// ─── Wait Conditions ─────────────────────────────────────────────────────────
|
|
2126
2320
|
async wait(taskId, type, value, options = {}) {
|
|
2127
2321
|
const timeout = options.timeout ?? 30000;
|
|
2322
|
+
const { conn, task } = await this.findTask(taskId);
|
|
2323
|
+
requireCdp(conn, 'asyncEvaluate');
|
|
2128
2324
|
if (type === 'time') {
|
|
2129
2325
|
await new Promise((r) => setTimeout(r, typeof value === 'number' ? value : parseInt(value, 10)));
|
|
2130
2326
|
return;
|
|
2131
2327
|
}
|
|
2132
|
-
const { conn, task } = await this.findTask(taskId);
|
|
2133
2328
|
const shortId = options.tabHint ? await this.resolveTabHint(conn, task, options.tabHint) : this.resolveCurrentTab(task);
|
|
2134
2329
|
const cdpTargetId = this.getCdpTargetId(task, shortId);
|
|
2135
2330
|
const target = await this.getTarget(conn, cdpTargetId);
|
|
@@ -2202,6 +2397,7 @@ export class BrowserService {
|
|
|
2202
2397
|
}
|
|
2203
2398
|
async setDownloadPath(taskId, downloadPath, tabHint) {
|
|
2204
2399
|
const { conn, task, key: runtimeKey } = await this.findTask(taskId);
|
|
2400
|
+
requireCdp(conn, 'download');
|
|
2205
2401
|
const shortId = tabHint ? await this.resolveTabHint(conn, task, tabHint) : this.resolveCurrentTab(task);
|
|
2206
2402
|
const cdpTargetId = this.getCdpTargetId(task, shortId);
|
|
2207
2403
|
const target = await this.getTarget(conn, cdpTargetId);
|
|
@@ -2230,6 +2426,8 @@ export class BrowserService {
|
|
|
2230
2426
|
return resolvedPath;
|
|
2231
2427
|
}
|
|
2232
2428
|
async waitForDownload(taskId, timeout = 60000) {
|
|
2429
|
+
const { conn } = await this.findTask(taskId);
|
|
2430
|
+
requireCdp(conn, 'download');
|
|
2233
2431
|
const start = Date.now();
|
|
2234
2432
|
while (Date.now() - start < timeout) {
|
|
2235
2433
|
const dl = this.pendingDownloads.get(taskId);
|
|
@@ -2284,7 +2482,8 @@ export class BrowserService {
|
|
|
2284
2482
|
this.recordings.delete(taskId);
|
|
2285
2483
|
}
|
|
2286
2484
|
for (const [, conn] of this.connections) {
|
|
2287
|
-
conn.
|
|
2485
|
+
if (conn.backend !== 'arc-native')
|
|
2486
|
+
conn.cdp.close();
|
|
2288
2487
|
conn.cleanup?.();
|
|
2289
2488
|
}
|
|
2290
2489
|
this.connections.clear();
|
|
@@ -2310,6 +2509,7 @@ export class BrowserService {
|
|
|
2310
2509
|
await cdp.connect(wsUrl);
|
|
2311
2510
|
await this.enableDomains(cdp);
|
|
2312
2511
|
const connection = {
|
|
2512
|
+
backend: 'cdp',
|
|
2313
2513
|
cdp,
|
|
2314
2514
|
port,
|
|
2315
2515
|
pid,
|
|
@@ -2357,6 +2557,7 @@ export class BrowserService {
|
|
|
2357
2557
|
await this.enableDomains(cdp);
|
|
2358
2558
|
const tasks = this.loadTaskState(key);
|
|
2359
2559
|
return {
|
|
2560
|
+
backend: 'cdp',
|
|
2360
2561
|
cdp,
|
|
2361
2562
|
port: existingInfo.port,
|
|
2362
2563
|
pid: existingInfo.pid,
|
|
@@ -2389,6 +2590,7 @@ export class BrowserService {
|
|
|
2389
2590
|
const conn = await connectLocal(endpoint, profile, key);
|
|
2390
2591
|
await this.enableDomains(conn.cdp);
|
|
2391
2592
|
return {
|
|
2593
|
+
backend: 'cdp',
|
|
2392
2594
|
cdp: conn.cdp,
|
|
2393
2595
|
port: conn.port,
|
|
2394
2596
|
pid: conn.pid,
|
|
@@ -2405,6 +2607,7 @@ export class BrowserService {
|
|
|
2405
2607
|
});
|
|
2406
2608
|
await this.enableDomains(conn.cdp);
|
|
2407
2609
|
return {
|
|
2610
|
+
backend: 'cdp',
|
|
2408
2611
|
cdp: conn.cdp,
|
|
2409
2612
|
port: conn.port,
|
|
2410
2613
|
pid: conn.pid,
|
|
@@ -2427,6 +2630,7 @@ export class BrowserService {
|
|
|
2427
2630
|
}
|
|
2428
2631
|
await this.enableDomains(cdp);
|
|
2429
2632
|
return {
|
|
2633
|
+
backend: 'cdp',
|
|
2430
2634
|
cdp,
|
|
2431
2635
|
port: 0,
|
|
2432
2636
|
pid: 0,
|
|
@@ -2445,6 +2649,7 @@ export class BrowserService {
|
|
|
2445
2649
|
await cdp.connect(wsUrl);
|
|
2446
2650
|
await this.enableDomains(cdp);
|
|
2447
2651
|
return {
|
|
2652
|
+
backend: 'cdp',
|
|
2448
2653
|
cdp,
|
|
2449
2654
|
port,
|
|
2450
2655
|
pid: 0,
|
|
@@ -2455,6 +2660,32 @@ export class BrowserService {
|
|
|
2455
2660
|
sessionCache: new Map(),
|
|
2456
2661
|
};
|
|
2457
2662
|
}
|
|
2663
|
+
// Native Arc backend (PHNX-2399): `arc-native:` protocol connects through
|
|
2664
|
+
// Apple Events instead of CDP. No debugging port, no browser launch.
|
|
2665
|
+
if (url.protocol === 'arc-native:') {
|
|
2666
|
+
if (process.platform !== 'darwin') {
|
|
2667
|
+
throw new Error('Native Arc automation is only available on macOS');
|
|
2668
|
+
}
|
|
2669
|
+
if (!(await isArcRunning())) {
|
|
2670
|
+
throw new Error('Arc is not running. Start Arc normally, then retry.\n' +
|
|
2671
|
+
'Native Arc automation drives your existing browser — it does not launch one.');
|
|
2672
|
+
}
|
|
2673
|
+
if (!profile.arc) {
|
|
2674
|
+
throw new Error(`Native Arc profile "${profile.name}" has no stable native profile metadata. ` +
|
|
2675
|
+
`Re-select it with: agents browser use arc-<profile-id>`);
|
|
2676
|
+
}
|
|
2677
|
+
return {
|
|
2678
|
+
port: 0,
|
|
2679
|
+
pid: 0,
|
|
2680
|
+
electron: false,
|
|
2681
|
+
browserType: 'arc',
|
|
2682
|
+
targetFilter: profile.targetFilter,
|
|
2683
|
+
tasks: this.loadTaskState(key),
|
|
2684
|
+
sessionCache: new Map(),
|
|
2685
|
+
backend: 'arc-native',
|
|
2686
|
+
arcProfile: profile.arc,
|
|
2687
|
+
};
|
|
2688
|
+
}
|
|
2458
2689
|
return null;
|
|
2459
2690
|
}
|
|
2460
2691
|
async enableDomains(cdp) {
|
|
@@ -2660,6 +2891,7 @@ export class BrowserService {
|
|
|
2660
2891
|
const matches = this.listTasksForCaller(caller);
|
|
2661
2892
|
if (matches.length === 1) {
|
|
2662
2893
|
const m = matches[0];
|
|
2894
|
+
await this.prepareTask(m.conn, m.task);
|
|
2663
2895
|
await this.touchTask(m.conn, m.task);
|
|
2664
2896
|
return { conn: m.conn, task: m.task, key: m.key, created: false };
|
|
2665
2897
|
}
|
|
@@ -2790,6 +3022,18 @@ export class BrowserService {
|
|
|
2790
3022
|
// `recordBrowserSession` call in `start` uses for the same reason.
|
|
2791
3023
|
}
|
|
2792
3024
|
}
|
|
3025
|
+
async prepareTask(conn, task) {
|
|
3026
|
+
if (conn.backend !== 'arc-native')
|
|
3027
|
+
return;
|
|
3028
|
+
await this.runExclusive('arc-native:host-app', async () => {
|
|
3029
|
+
await this.reconcileArcCreateIntents(conn, task);
|
|
3030
|
+
const native = this.requireArcTask(task);
|
|
3031
|
+
if (native.profileId !== conn.arcProfile.profileId) {
|
|
3032
|
+
throw new Error(`Arc task profile ${JSON.stringify(native.profileId)} does not match connected profile ` +
|
|
3033
|
+
`${JSON.stringify(conn.arcProfile.profileId)}.`);
|
|
3034
|
+
}
|
|
3035
|
+
});
|
|
3036
|
+
}
|
|
2793
3037
|
async findTask(taskId, profileRef) {
|
|
2794
3038
|
if (profileRef) {
|
|
2795
3039
|
// Accept bare profile names against composite connection keys.
|
|
@@ -2801,6 +3045,7 @@ export class BrowserService {
|
|
|
2801
3045
|
const conn = this.connections.get(key);
|
|
2802
3046
|
const task = this.lookupTaskOnConn(conn, taskId);
|
|
2803
3047
|
if (task) {
|
|
3048
|
+
await this.prepareTask(conn, task);
|
|
2804
3049
|
await this.touchTask(conn, task);
|
|
2805
3050
|
return { conn, task, key: conn.key ?? key };
|
|
2806
3051
|
}
|
|
@@ -2808,6 +3053,7 @@ export class BrowserService {
|
|
|
2808
3053
|
// RAM miss → rehydrate from disk for this profile.
|
|
2809
3054
|
const rehydrated = await this.rehydrateTaskFromDisk(taskId, profileRef);
|
|
2810
3055
|
if (rehydrated) {
|
|
3056
|
+
await this.prepareTask(rehydrated.conn, rehydrated.task);
|
|
2811
3057
|
await this.touchTask(rehydrated.conn, rehydrated.task);
|
|
2812
3058
|
return rehydrated;
|
|
2813
3059
|
}
|
|
@@ -2816,6 +3062,7 @@ export class BrowserService {
|
|
|
2816
3062
|
for (const [key, conn] of this.connections) {
|
|
2817
3063
|
const task = this.lookupTaskOnConn(conn, taskId);
|
|
2818
3064
|
if (task) {
|
|
3065
|
+
await this.prepareTask(conn, task);
|
|
2819
3066
|
await this.touchTask(conn, task);
|
|
2820
3067
|
return { conn, task, key: conn.key ?? key };
|
|
2821
3068
|
}
|
|
@@ -2824,6 +3071,7 @@ export class BrowserService {
|
|
|
2824
3071
|
// meta.json still has the browser pid/port. Reconnect and adopt.
|
|
2825
3072
|
const rehydrated = await this.rehydrateTaskFromDisk(taskId);
|
|
2826
3073
|
if (rehydrated) {
|
|
3074
|
+
await this.prepareTask(rehydrated.conn, rehydrated.task);
|
|
2827
3075
|
await this.touchTask(rehydrated.conn, rehydrated.task);
|
|
2828
3076
|
return rehydrated;
|
|
2829
3077
|
}
|
|
@@ -2861,6 +3109,33 @@ export class BrowserService {
|
|
|
2861
3109
|
catch {
|
|
2862
3110
|
return null;
|
|
2863
3111
|
}
|
|
3112
|
+
// Native Arc endpoints (PHNX-2399): reconnect via the native driver if Arc
|
|
3113
|
+
// is still running. No port, no CDP — just verify Arc is alive and restore
|
|
3114
|
+
// the task state.
|
|
3115
|
+
if (resolved.target.startsWith('arc-native:')) {
|
|
3116
|
+
if (process.platform !== 'darwin' || !(await isArcRunning()))
|
|
3117
|
+
return null;
|
|
3118
|
+
const tasks = this.loadTaskState(key);
|
|
3119
|
+
for (const [k, t] of diskTasks) {
|
|
3120
|
+
if (!tasks.has(k))
|
|
3121
|
+
tasks.set(k, t);
|
|
3122
|
+
}
|
|
3123
|
+
if (!profile.arc)
|
|
3124
|
+
return null;
|
|
3125
|
+
return {
|
|
3126
|
+
port: 0,
|
|
3127
|
+
pid: 0,
|
|
3128
|
+
electron: false,
|
|
3129
|
+
browserType: 'arc',
|
|
3130
|
+
targetFilter: profile.targetFilter,
|
|
3131
|
+
key,
|
|
3132
|
+
profile: bare,
|
|
3133
|
+
tasks,
|
|
3134
|
+
sessionCache: new Map(),
|
|
3135
|
+
backend: 'arc-native',
|
|
3136
|
+
arcProfile: profile.arc,
|
|
3137
|
+
};
|
|
3138
|
+
}
|
|
2864
3139
|
const existingInfo = getRunningChromeInfo(key);
|
|
2865
3140
|
const parsed = parseEndpointUrl(resolved.target);
|
|
2866
3141
|
const port = existingInfo?.port ?? parsed?.port;
|
|
@@ -2886,6 +3161,7 @@ export class BrowserService {
|
|
|
2886
3161
|
tasks.set(k, t);
|
|
2887
3162
|
}
|
|
2888
3163
|
return {
|
|
3164
|
+
backend: 'cdp',
|
|
2889
3165
|
cdp: conn.cdp,
|
|
2890
3166
|
port: conn.port,
|
|
2891
3167
|
pid: conn.pid,
|
|
@@ -2918,6 +3194,7 @@ export class BrowserService {
|
|
|
2918
3194
|
tasks.set(k, t);
|
|
2919
3195
|
}
|
|
2920
3196
|
const conn = {
|
|
3197
|
+
backend: 'cdp',
|
|
2921
3198
|
cdp,
|
|
2922
3199
|
port,
|
|
2923
3200
|
pid: existingInfo?.pid ?? 0,
|
|
@@ -2975,10 +3252,12 @@ export class BrowserService {
|
|
|
2975
3252
|
else {
|
|
2976
3253
|
// Shared/borrowed tunnel (or a local connection with no tunnel): close
|
|
2977
3254
|
// only our own CDP client; killing the tunnel would break the winner.
|
|
2978
|
-
|
|
2979
|
-
|
|
3255
|
+
if (conn.backend !== 'arc-native') {
|
|
3256
|
+
try {
|
|
3257
|
+
conn.cdp.close();
|
|
3258
|
+
}
|
|
3259
|
+
catch { /* best effort */ }
|
|
2980
3260
|
}
|
|
2981
|
-
catch { /* best effort */ }
|
|
2982
3261
|
}
|
|
2983
3262
|
return existing;
|
|
2984
3263
|
}
|
|
@@ -3019,7 +3298,8 @@ export class BrowserService {
|
|
|
3019
3298
|
if (registered !== conn)
|
|
3020
3299
|
continue;
|
|
3021
3300
|
try {
|
|
3022
|
-
|
|
3301
|
+
if (conn.backend !== 'arc-native')
|
|
3302
|
+
await this.applyDefaultDownloadBehavior(conn, key);
|
|
3023
3303
|
}
|
|
3024
3304
|
catch {
|
|
3025
3305
|
// Non-fatal for rehydrate.
|
|
@@ -3080,7 +3360,8 @@ export class BrowserService {
|
|
|
3080
3360
|
conn = this.registerRehydratedConnection(key, attached);
|
|
3081
3361
|
if (conn === attached) {
|
|
3082
3362
|
try {
|
|
3083
|
-
|
|
3363
|
+
if (conn.backend !== 'arc-native')
|
|
3364
|
+
await this.applyDefaultDownloadBehavior(conn, key);
|
|
3084
3365
|
}
|
|
3085
3366
|
catch {
|
|
3086
3367
|
// Non-fatal.
|
|
@@ -3117,6 +3398,41 @@ export class BrowserService {
|
|
|
3117
3398
|
const conn = this.connections.get(key);
|
|
3118
3399
|
if (!conn)
|
|
3119
3400
|
return null;
|
|
3401
|
+
if (conn.backend === 'arc-native') {
|
|
3402
|
+
const tasks = [];
|
|
3403
|
+
for (const task of conn.tasks.values()) {
|
|
3404
|
+
const tabs = await this.listArcTaskTabs(task);
|
|
3405
|
+
const domains = tabs.flatMap((tab) => {
|
|
3406
|
+
try {
|
|
3407
|
+
const domain = new URL(tab.url).hostname.replace(/^www\./, '');
|
|
3408
|
+
return domain && domain !== 'blank' ? [domain] : [];
|
|
3409
|
+
}
|
|
3410
|
+
catch {
|
|
3411
|
+
return [];
|
|
3412
|
+
}
|
|
3413
|
+
});
|
|
3414
|
+
tasks.push({
|
|
3415
|
+
id: task.id,
|
|
3416
|
+
name: task.name,
|
|
3417
|
+
label: task.label ?? task.name,
|
|
3418
|
+
tabCount: Object.keys(task.tabs).length,
|
|
3419
|
+
currentTabId: task.currentTabId,
|
|
3420
|
+
createdAt: task.createdAt,
|
|
3421
|
+
tabs: tabs.length ? tabs : undefined,
|
|
3422
|
+
domains: [...new Set(domains)],
|
|
3423
|
+
});
|
|
3424
|
+
}
|
|
3425
|
+
const parsed = parseConnectionKey(key);
|
|
3426
|
+
return {
|
|
3427
|
+
name: conn.profile ?? parsed.profile,
|
|
3428
|
+
endpoint: parsed.endpoint,
|
|
3429
|
+
key,
|
|
3430
|
+
running: await isArcRunning(),
|
|
3431
|
+
port: 0,
|
|
3432
|
+
pid: 0,
|
|
3433
|
+
tasks,
|
|
3434
|
+
};
|
|
3435
|
+
}
|
|
3120
3436
|
// Fetch all targets once for efficiency
|
|
3121
3437
|
let targets = [];
|
|
3122
3438
|
try {
|
|
@@ -3339,10 +3655,12 @@ export class BrowserService {
|
|
|
3339
3655
|
return undefined;
|
|
3340
3656
|
if (await isConnHealthy(conn))
|
|
3341
3657
|
return conn;
|
|
3342
|
-
|
|
3343
|
-
|
|
3658
|
+
if (conn.backend !== 'arc-native') {
|
|
3659
|
+
try {
|
|
3660
|
+
conn.cdp.close();
|
|
3661
|
+
}
|
|
3662
|
+
catch { /* already closed */ }
|
|
3344
3663
|
}
|
|
3345
|
-
catch { /* already closed */ }
|
|
3346
3664
|
conn.cleanup?.();
|
|
3347
3665
|
this.connections.delete(key);
|
|
3348
3666
|
return undefined;
|
|
@@ -3373,13 +3691,219 @@ export class BrowserService {
|
|
|
3373
3691
|
}
|
|
3374
3692
|
return undefined;
|
|
3375
3693
|
}
|
|
3694
|
+
// ---------------------------------------------------------------------------
|
|
3695
|
+
// Native Arc backend helpers (PHNX-2399)
|
|
3696
|
+
// ---------------------------------------------------------------------------
|
|
3697
|
+
/**
|
|
3698
|
+
* Bind a task to the profile's Space in the visible Arc window that holds it.
|
|
3699
|
+
* The Space is the profile (PHNX-2399); only its live window id is looked up.
|
|
3700
|
+
*/
|
|
3701
|
+
async resolveArcSpace(profile) {
|
|
3702
|
+
const live = (await enumerateArcSpaces()).filter((space) => space.spaceId === profile.spaceId);
|
|
3703
|
+
if (live.length !== 1) {
|
|
3704
|
+
throw new Error(live.length === 0
|
|
3705
|
+
? `Arc Space ${JSON.stringify(profile.spaceTitle)} (${profile.spaceId}) is not open in a visible Arc window.`
|
|
3706
|
+
: `Arc Space ${JSON.stringify(profile.spaceTitle)} is open in more than one window; refusing an ambiguous native address.`);
|
|
3707
|
+
}
|
|
3708
|
+
return {
|
|
3709
|
+
windowId: live[0].windowId,
|
|
3710
|
+
spaceId: live[0].spaceId,
|
|
3711
|
+
spaceTitle: profile.spaceTitle,
|
|
3712
|
+
};
|
|
3713
|
+
}
|
|
3714
|
+
requireArcTask(task) {
|
|
3715
|
+
const native = task.arcNative;
|
|
3716
|
+
if (!native || typeof native.profileId !== 'string' || typeof native.windowId !== 'string' ||
|
|
3717
|
+
typeof native.spaceId !== 'string' || !native.tabs || typeof native.tabs !== 'object') {
|
|
3718
|
+
throw new Error(`Task ${JSON.stringify(task.name)} has no valid durable Arc identity.`);
|
|
3719
|
+
}
|
|
3720
|
+
for (const [shortId, ref] of Object.entries(native.tabs)) {
|
|
3721
|
+
if (!ref || typeof ref.windowId !== 'string' || typeof ref.spaceId !== 'string' ||
|
|
3722
|
+
typeof ref.tabId !== 'string' || ref.windowId !== native.windowId || ref.spaceId !== native.spaceId) {
|
|
3723
|
+
throw new Error(`Owned Arc tab ${JSON.stringify(shortId)} has no stable id in its original window/Space.`);
|
|
3724
|
+
}
|
|
3725
|
+
}
|
|
3726
|
+
for (const [shortId, intent] of Object.entries(native.createIntents ?? {})) {
|
|
3727
|
+
if (!intent || intent.tabId !== shortId || typeof intent.markerUrl !== 'string' ||
|
|
3728
|
+
!isArcCreateMarker(intent.markerUrl) ||
|
|
3729
|
+
typeof intent.targetUrl !== 'string' || typeof intent.createdAt !== 'number') {
|
|
3730
|
+
throw new Error(`Arc creation intent ${JSON.stringify(shortId)} is invalid; refusing recovery.`);
|
|
3731
|
+
}
|
|
3732
|
+
if (intent.ref && (intent.ref.windowId !== native.windowId || intent.ref.spaceId !== native.spaceId ||
|
|
3733
|
+
typeof intent.ref.tabId !== 'string')) {
|
|
3734
|
+
throw new Error(`Arc creation intent ${JSON.stringify(shortId)} left its original window/Space.`);
|
|
3735
|
+
}
|
|
3736
|
+
}
|
|
3737
|
+
return native;
|
|
3738
|
+
}
|
|
3739
|
+
async reconcileArcCreateIntents(conn, task) {
|
|
3740
|
+
const native = this.requireArcTask(task);
|
|
3741
|
+
for (const intent of Object.values(native.createIntents ?? {})) {
|
|
3742
|
+
let ref = intent.ref;
|
|
3743
|
+
if (ref) {
|
|
3744
|
+
const live = await resolveArcTab(ref);
|
|
3745
|
+
if (!live) {
|
|
3746
|
+
throw new Error(`Owned Arc tab ${intent.tabId} left its original window/Space while creation was incomplete; refusing to adopt it elsewhere.`);
|
|
3747
|
+
}
|
|
3748
|
+
if (live.url === intent.targetUrl) {
|
|
3749
|
+
native.tabs[intent.tabId] = ref;
|
|
3750
|
+
task.tabs[intent.tabId] = ref.tabId;
|
|
3751
|
+
if (intent.previousTabId) {
|
|
3752
|
+
await restoreArcSelection(ref, intent.previousTabId);
|
|
3753
|
+
}
|
|
3754
|
+
delete native.createIntents?.[intent.tabId];
|
|
3755
|
+
await this.saveTaskState(task.profile, conn.tasks);
|
|
3756
|
+
continue;
|
|
3757
|
+
}
|
|
3758
|
+
if (live.url !== intent.markerUrl) {
|
|
3759
|
+
throw new Error(`Owned Arc tab ${intent.tabId} changed outside this task during creation; refusing to navigate it.`);
|
|
3760
|
+
}
|
|
3761
|
+
}
|
|
3762
|
+
else {
|
|
3763
|
+
const originalSpace = (await enumerateArcSpaces()).find((space) => space.windowId === native.windowId && space.spaceId === native.spaceId);
|
|
3764
|
+
if (!originalSpace) {
|
|
3765
|
+
throw new Error('The original Arc window/Space for this task is no longer present.');
|
|
3766
|
+
}
|
|
3767
|
+
const markerMatches = originalSpace.tabs.filter((tab) => tab.url === intent.markerUrl);
|
|
3768
|
+
if (markerMatches.length > 1) {
|
|
3769
|
+
throw new Error(`Arc creation marker ${JSON.stringify(intent.markerUrl)} is not unique in the original Space.`);
|
|
3770
|
+
}
|
|
3771
|
+
if (markerMatches[0]) {
|
|
3772
|
+
ref = markerMatches[0];
|
|
3773
|
+
}
|
|
3774
|
+
else {
|
|
3775
|
+
try {
|
|
3776
|
+
ref = await createArcTab({ windowId: native.windowId, spaceId: native.spaceId }, intent.markerUrl);
|
|
3777
|
+
}
|
|
3778
|
+
catch (error) {
|
|
3779
|
+
// Arc selects the Space it was asked to create in even when the
|
|
3780
|
+
// creation itself fails. Put the owner back on their tab before
|
|
3781
|
+
// surfacing the error, so a failed agent verb never leaves them
|
|
3782
|
+
// staring at the wrong Space.
|
|
3783
|
+
if (intent.previousTabId) {
|
|
3784
|
+
await selectWindowTab(native.windowId, intent.previousTabId).catch(() => undefined);
|
|
3785
|
+
}
|
|
3786
|
+
throw error;
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
intent.ref = ref;
|
|
3790
|
+
native.tabs[intent.tabId] = ref;
|
|
3791
|
+
task.tabs[intent.tabId] = ref.tabId;
|
|
3792
|
+
await this.saveTaskState(task.profile, conn.tasks);
|
|
3793
|
+
}
|
|
3794
|
+
await navigateArcTab(ref, intent.targetUrl);
|
|
3795
|
+
if (intent.previousTabId) {
|
|
3796
|
+
await restoreArcSelection(ref, intent.previousTabId);
|
|
3797
|
+
}
|
|
3798
|
+
delete native.createIntents?.[intent.tabId];
|
|
3799
|
+
task.currentTabId = intent.tabId;
|
|
3800
|
+
await this.saveTaskState(task.profile, conn.tasks);
|
|
3801
|
+
}
|
|
3802
|
+
}
|
|
3803
|
+
async createArcOwnedTab(conn, task, url) {
|
|
3804
|
+
const native = this.requireArcTask(task);
|
|
3805
|
+
const tabId = generateShortId();
|
|
3806
|
+
const markerUrl = arcCreateMarker();
|
|
3807
|
+
const originalSpace = (await enumerateArcSpaces()).find((space) => space.windowId === native.windowId && space.spaceId === native.spaceId);
|
|
3808
|
+
if (!originalSpace)
|
|
3809
|
+
throw new Error('The original Arc window/Space for this task is no longer present.');
|
|
3810
|
+
native.createIntents ??= {};
|
|
3811
|
+
native.createIntents[tabId] = {
|
|
3812
|
+
tabId,
|
|
3813
|
+
markerUrl,
|
|
3814
|
+
targetUrl: url,
|
|
3815
|
+
createdAt: Date.now(),
|
|
3816
|
+
previousTabId: originalSpace.activeTabId,
|
|
3817
|
+
};
|
|
3818
|
+
await this.saveTaskState(task.profile, conn.tasks);
|
|
3819
|
+
await this.reconcileArcCreateIntents(conn, task);
|
|
3820
|
+
return tabId;
|
|
3821
|
+
}
|
|
3822
|
+
async navigateArcNative(conn, task, url, addTab = false) {
|
|
3823
|
+
return this.runExclusive('arc-native:host-app', async () => {
|
|
3824
|
+
await this.reconcileArcCreateIntents(conn, task);
|
|
3825
|
+
const native = this.requireArcTask(task);
|
|
3826
|
+
if (!addTab) {
|
|
3827
|
+
for (const [shortId, ref] of Object.entries(native.tabs)) {
|
|
3828
|
+
const live = await resolveArcTab(ref);
|
|
3829
|
+
if (live && canonicalTabUrl(live.url) === canonicalTabUrl(url)) {
|
|
3830
|
+
await navigateArcTab(ref, url);
|
|
3831
|
+
task.currentTabId = shortId;
|
|
3832
|
+
await this.saveTaskState(task.profile, conn.tasks);
|
|
3833
|
+
return { tabId: shortId, url, created: false, refreshed: true, message: 'Tab already open—refreshed' };
|
|
3834
|
+
}
|
|
3835
|
+
}
|
|
3836
|
+
}
|
|
3837
|
+
const currentId = task.currentTabId;
|
|
3838
|
+
if (!addTab && currentId) {
|
|
3839
|
+
const ref = native.tabs[currentId];
|
|
3840
|
+
if (!ref || !(await resolveArcTab(ref))) {
|
|
3841
|
+
throw new Error(`Owned Arc tab ${currentId} is missing from its original window/Space; refusing to adopt a moved tab.`);
|
|
3842
|
+
}
|
|
3843
|
+
await navigateArcTab(ref, url);
|
|
3844
|
+
return { tabId: currentId, url, created: false, refreshed: false };
|
|
3845
|
+
}
|
|
3846
|
+
const tabId = await this.createArcOwnedTab(conn, task, url);
|
|
3847
|
+
return { tabId, url, created: true, refreshed: false };
|
|
3848
|
+
});
|
|
3849
|
+
}
|
|
3850
|
+
/**
|
|
3851
|
+
* Execute JavaScript in a native Arc tab. Sync JS only, isolated world —
|
|
3852
|
+
* async/promise evaluation fails honestly with an AppleScript error.
|
|
3853
|
+
*/
|
|
3854
|
+
async evaluateArcNative(conn, task, shortId, expression) {
|
|
3855
|
+
const ref = this.requireArcTask(task).tabs[shortId];
|
|
3856
|
+
if (!ref || !(await resolveArcTab(ref))) {
|
|
3857
|
+
throw new Error(`Owned Arc tab ${shortId} is missing from its original window/Space.`);
|
|
3858
|
+
}
|
|
3859
|
+
return executeJavaScript(ref, expression);
|
|
3860
|
+
}
|
|
3861
|
+
async listArcTaskTabs(task) {
|
|
3862
|
+
const native = this.requireArcTask(task);
|
|
3863
|
+
const tabs = [];
|
|
3864
|
+
for (const [shortId, ref] of Object.entries(native.tabs)) {
|
|
3865
|
+
const live = await resolveArcTab(ref);
|
|
3866
|
+
if (!live) {
|
|
3867
|
+
throw new Error(`Owned Arc tab ${shortId} is missing from its original window/Space; refusing to adopt a moved tab.`);
|
|
3868
|
+
}
|
|
3869
|
+
tabs.push({
|
|
3870
|
+
id: shortId,
|
|
3871
|
+
url: live.url,
|
|
3872
|
+
title: live.title,
|
|
3873
|
+
task: task.name,
|
|
3874
|
+
current: shortId === task.currentTabId,
|
|
3875
|
+
});
|
|
3876
|
+
}
|
|
3877
|
+
return tabs;
|
|
3878
|
+
}
|
|
3879
|
+
/**
|
|
3880
|
+
* Close native Arc tabs owned by a task. Only closes tabs the task created
|
|
3881
|
+
* (not borrowed ones). Never kills Arc. Identifies tabs only by stable ids in
|
|
3882
|
+
* the original window and Space.
|
|
3883
|
+
*/
|
|
3884
|
+
async closeArcNativeTabs(conn, task) {
|
|
3885
|
+
await this.runExclusive('arc-native:host-app', async () => {
|
|
3886
|
+
await this.reconcileArcCreateIntents(conn, task);
|
|
3887
|
+
const native = this.requireArcTask(task);
|
|
3888
|
+
for (const [shortId, ref] of Object.entries(native.tabs)) {
|
|
3889
|
+
await closeArcTab(ref);
|
|
3890
|
+
delete native.tabs[shortId];
|
|
3891
|
+
delete task.tabs[shortId];
|
|
3892
|
+
delete task.refDescriptors?.[shortId];
|
|
3893
|
+
}
|
|
3894
|
+
task.currentTabId = undefined;
|
|
3895
|
+
});
|
|
3896
|
+
}
|
|
3376
3897
|
/** Connect a profile at `target`, register it under `key`, and arm downloads. */
|
|
3377
3898
|
async openConnection(profile, target, key, profileName, opts = {}) {
|
|
3378
3899
|
const conn = await this.connectProfile(profile, target, key, opts);
|
|
3379
3900
|
conn.key = key;
|
|
3380
3901
|
conn.profile = profileName;
|
|
3381
3902
|
this.connections.set(key, conn);
|
|
3382
|
-
|
|
3903
|
+
// Download behavior configuration is CDP-specific; skip for native Arc.
|
|
3904
|
+
if (conn.backend !== 'arc-native') {
|
|
3905
|
+
await this.applyDefaultDownloadBehavior(conn, key);
|
|
3906
|
+
}
|
|
3383
3907
|
return conn;
|
|
3384
3908
|
}
|
|
3385
3909
|
/**
|
|
@@ -3402,6 +3926,9 @@ export class BrowserService {
|
|
|
3402
3926
|
endpointName: opts.endpointName,
|
|
3403
3927
|
probe: opts.probe,
|
|
3404
3928
|
});
|
|
3929
|
+
if (!routed.local && routed.commandDispatch) {
|
|
3930
|
+
throw new Error(`Native Arc profile "${profileName}" is owned by ${routed.device}; dispatch the complete browser command there.`);
|
|
3931
|
+
}
|
|
3405
3932
|
const key = routed.key;
|
|
3406
3933
|
const effectiveProfile = routed.profile;
|
|
3407
3934
|
const reused = await this.reuseProfileConnection(profileName, key, routed.local);
|
|
@@ -3411,6 +3938,9 @@ export class BrowserService {
|
|
|
3411
3938
|
adoptLegacyRuntimeIfLocal(routed.local, profileName, key, getBrowserRuntimeDir());
|
|
3412
3939
|
conn = await this.openConnection(effectiveProfile, routed.target, key, profileName, { persistRemote: !routed.local });
|
|
3413
3940
|
}
|
|
3941
|
+
if (conn.backend === 'arc-native') {
|
|
3942
|
+
throw new ArcNativeCapabilityError('show', 'Task-less viewer tabs are unavailable for native Arc because creation intent cannot be bound to an owning task.');
|
|
3943
|
+
}
|
|
3414
3944
|
// createPageTarget refuses Arc with an actionable error rather than crashing it.
|
|
3415
3945
|
const created = await this.createPageTarget(conn, { url });
|
|
3416
3946
|
this.invalidateTargetCache(conn);
|