lazy-gravity 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bot/index.js +62 -17
- package/dist/bot/telegramMessageHandler.js +3 -0
- package/dist/services/cdpService.js +962 -105
- package/dist/services/responseMonitor.js +235 -48
- package/package.json +1 -1
|
@@ -55,6 +55,68 @@ const SELECTORS = {
|
|
|
55
55
|
/** Keyword to identify message injection target context */
|
|
56
56
|
CONTEXT_URL_KEYWORD: 'cascade-panel',
|
|
57
57
|
};
|
|
58
|
+
const WORKSPACE_STATE_SCRIPT = `(() => {
|
|
59
|
+
const panel = document.querySelector('.antigravity-agent-side-panel');
|
|
60
|
+
const scopes = [panel, document].filter(Boolean);
|
|
61
|
+
|
|
62
|
+
let isGenerating = false;
|
|
63
|
+
for (const scope of scopes) {
|
|
64
|
+
const stopEl = scope.querySelector('[data-tooltip-id="input-send-button-cancel-tooltip"]');
|
|
65
|
+
if (stopEl) {
|
|
66
|
+
isGenerating = true;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!isGenerating) {
|
|
72
|
+
const normalize = (value) => (value || '').toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
73
|
+
const stopPatterns = [
|
|
74
|
+
/^stop$/,
|
|
75
|
+
/^stop generating$/,
|
|
76
|
+
/^stop response$/,
|
|
77
|
+
/^停止$/,
|
|
78
|
+
/^生成を停止$/,
|
|
79
|
+
/^応答を停止$/,
|
|
80
|
+
];
|
|
81
|
+
for (const scope of scopes) {
|
|
82
|
+
const buttons = scope.querySelectorAll('button, [role="button"]');
|
|
83
|
+
for (let i = 0; i < buttons.length; i++) {
|
|
84
|
+
const btn = buttons[i];
|
|
85
|
+
const labels = [
|
|
86
|
+
btn.textContent || '',
|
|
87
|
+
btn.getAttribute('aria-label') || '',
|
|
88
|
+
btn.getAttribute('title') || '',
|
|
89
|
+
];
|
|
90
|
+
if (labels.some((label) => stopPatterns.some((re) => re.test(normalize(label))))) {
|
|
91
|
+
isGenerating = true;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (isGenerating) break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!panel) {
|
|
100
|
+
return { isGenerating, sessionTitle: '', hasActiveChat: false };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const header = panel.querySelector('div[class*="border-b"]');
|
|
104
|
+
const titleEl = header?.querySelector('div[class*="text-ellipsis"]');
|
|
105
|
+
const rawTitle = titleEl ? (titleEl.textContent || '').trim() : '';
|
|
106
|
+
const hasActiveChat = rawTitle.length > 0 && rawTitle !== 'Agent';
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
isGenerating,
|
|
110
|
+
sessionTitle: rawTitle || '(Untitled)',
|
|
111
|
+
hasActiveChat,
|
|
112
|
+
};
|
|
113
|
+
})()`;
|
|
114
|
+
const CHAT_READY_TIMEOUT_MS = 6000;
|
|
115
|
+
const CHAT_READY_POLL_MS = 250;
|
|
116
|
+
const INJECT_RETRY_READY_TIMEOUT_MS = 2500;
|
|
117
|
+
const INJECT_RETRY_BACKOFF_MS = 500;
|
|
118
|
+
const MODE_READY_TIMEOUT_MS = 4000;
|
|
119
|
+
const MODE_RETRY_READY_TIMEOUT_MS = 2000;
|
|
58
120
|
class CdpService extends events_1.EventEmitter {
|
|
59
121
|
ports;
|
|
60
122
|
isConnectedFlag = false;
|
|
@@ -64,6 +126,7 @@ class CdpService extends events_1.EventEmitter {
|
|
|
64
126
|
idCounter = 1;
|
|
65
127
|
cdpCallTimeout = 30000;
|
|
66
128
|
targetUrl = null;
|
|
129
|
+
targetId = null;
|
|
67
130
|
/** Number of auto-reconnect attempts on disconnect */
|
|
68
131
|
maxReconnectAttempts;
|
|
69
132
|
/** Delay between reconnect attempts (ms) */
|
|
@@ -78,14 +141,34 @@ class CdpService extends events_1.EventEmitter {
|
|
|
78
141
|
currentWorkspacePath = null;
|
|
79
142
|
/** Workspace switching flag (suppresses disconnected event) */
|
|
80
143
|
isSwitchingWorkspace = false;
|
|
144
|
+
accountName;
|
|
145
|
+
accountPorts;
|
|
146
|
+
accountUserDataDirs;
|
|
81
147
|
constructor(options = {}) {
|
|
82
148
|
super();
|
|
83
|
-
this.
|
|
149
|
+
this.accountName = options.accountName || 'default';
|
|
150
|
+
this.accountPorts = options.accountPorts || {};
|
|
151
|
+
this.accountUserDataDirs = options.accountUserDataDirs || {};
|
|
152
|
+
this.ports = options.portsToScan || this.resolveAccountPorts(this.accountName);
|
|
84
153
|
if (options.cdpCallTimeout)
|
|
85
154
|
this.cdpCallTimeout = options.cdpCallTimeout;
|
|
86
155
|
this.maxReconnectAttempts = options.maxReconnectAttempts ?? 3;
|
|
87
156
|
this.reconnectDelayMs = options.reconnectDelayMs ?? 2000;
|
|
88
157
|
}
|
|
158
|
+
resolveAccountPorts(accountName) {
|
|
159
|
+
const explicitPort = this.accountPorts[accountName];
|
|
160
|
+
if (Number.isInteger(explicitPort) && explicitPort > 0) {
|
|
161
|
+
return [explicitPort];
|
|
162
|
+
}
|
|
163
|
+
return [...cdpPorts_1.CDP_PORTS];
|
|
164
|
+
}
|
|
165
|
+
resolveConfiguredUserDataDir(accountName) {
|
|
166
|
+
const configured = this.accountUserDataDirs[accountName];
|
|
167
|
+
if (typeof configured === 'string' && configured.trim().length > 0) {
|
|
168
|
+
return configured.trim();
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
89
172
|
async getJson(url) {
|
|
90
173
|
return new Promise((resolve, reject) => {
|
|
91
174
|
http.get(url, (res) => {
|
|
@@ -141,6 +224,7 @@ class CdpService extends events_1.EventEmitter {
|
|
|
141
224
|
}
|
|
142
225
|
if (target && target.webSocketDebuggerUrl) {
|
|
143
226
|
this.targetUrl = target.webSocketDebuggerUrl;
|
|
227
|
+
this.targetId = typeof target.id === 'string' ? target.id : null;
|
|
144
228
|
// Extract workspace name from title (e.g., "ProjectName — Antigravity")
|
|
145
229
|
if (target.title && !this.currentWorkspaceName) {
|
|
146
230
|
const titleParts = target.title.split(/\\s[—–-]\\s/);
|
|
@@ -266,10 +350,48 @@ class CdpService extends events_1.EventEmitter {
|
|
|
266
350
|
}
|
|
267
351
|
this.isConnectedFlag = false;
|
|
268
352
|
this.contexts = [];
|
|
353
|
+
this.targetId = null;
|
|
269
354
|
this.currentWorkspacePath = null;
|
|
270
355
|
this.currentWorkspaceName = null;
|
|
271
356
|
this.clearPendingCalls(new Error('disconnect() was called'));
|
|
272
357
|
}
|
|
358
|
+
async evaluateAcrossContexts(expression, accept, options) {
|
|
359
|
+
const awaitPromise = options?.awaitPromise ?? true;
|
|
360
|
+
const contexts = this.getContexts();
|
|
361
|
+
let firstValue = null;
|
|
362
|
+
if (contexts.length === 0) {
|
|
363
|
+
const result = await this.call('Runtime.evaluate', {
|
|
364
|
+
expression,
|
|
365
|
+
returnByValue: true,
|
|
366
|
+
awaitPromise,
|
|
367
|
+
});
|
|
368
|
+
return {
|
|
369
|
+
value: (result?.result?.value ?? null),
|
|
370
|
+
contextId: null,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
for (const ctx of contexts) {
|
|
374
|
+
try {
|
|
375
|
+
const result = await this.call('Runtime.evaluate', {
|
|
376
|
+
expression,
|
|
377
|
+
returnByValue: true,
|
|
378
|
+
awaitPromise,
|
|
379
|
+
contextId: ctx.id,
|
|
380
|
+
});
|
|
381
|
+
const value = (result?.result?.value ?? null);
|
|
382
|
+
if (!firstValue) {
|
|
383
|
+
firstValue = { value, contextId: ctx.id };
|
|
384
|
+
}
|
|
385
|
+
if (accept(value)) {
|
|
386
|
+
return { value, contextId: ctx.id };
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
// Try the next context.
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return firstValue ?? { value: null, contextId: null };
|
|
394
|
+
}
|
|
273
395
|
/**
|
|
274
396
|
* Return the currently connected workspace name.
|
|
275
397
|
*/
|
|
@@ -302,6 +424,19 @@ class CdpService extends events_1.EventEmitter {
|
|
|
302
424
|
this.isSwitchingWorkspace = false;
|
|
303
425
|
}
|
|
304
426
|
}
|
|
427
|
+
async openWorkspace(workspacePath) {
|
|
428
|
+
const projectName = (0, pathUtils_1.extractProjectNameFromPath)(workspacePath);
|
|
429
|
+
this.currentWorkspacePath = workspacePath;
|
|
430
|
+
try {
|
|
431
|
+
return await this.discoverAndConnectForWorkspace(workspacePath);
|
|
432
|
+
}
|
|
433
|
+
catch (error) {
|
|
434
|
+
logger_1.logger.info(`[CdpService] Explicit open requested for workspace "${projectName}" ` +
|
|
435
|
+
`on account "${this.accountName}" — retrying with launch ` +
|
|
436
|
+
`(reason: ${error?.message || String(error)})`);
|
|
437
|
+
return this.launchAndConnectWorkspace(workspacePath, projectName, true);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
305
440
|
/**
|
|
306
441
|
* Verify whether the currently attached page still represents the expected workspace.
|
|
307
442
|
*/
|
|
@@ -386,6 +521,7 @@ class CdpService extends events_1.EventEmitter {
|
|
|
386
521
|
}
|
|
387
522
|
this.disconnectQuietly();
|
|
388
523
|
this.targetUrl = page.webSocketDebuggerUrl;
|
|
524
|
+
this.targetId = typeof page.id === 'string' ? page.id : null;
|
|
389
525
|
await this.connect();
|
|
390
526
|
this.currentWorkspaceName = projectName;
|
|
391
527
|
logger_1.logger.debug(`[CdpService] Connected to workspace "${projectName}"`);
|
|
@@ -407,6 +543,7 @@ class CdpService extends events_1.EventEmitter {
|
|
|
407
543
|
// Temporarily connect to retrieve document.title
|
|
408
544
|
this.disconnectQuietly();
|
|
409
545
|
this.targetUrl = page.webSocketDebuggerUrl;
|
|
546
|
+
this.targetId = typeof page.id === 'string' ? page.id : null;
|
|
410
547
|
await this.connect();
|
|
411
548
|
const result = await this.call('Runtime.evaluate', {
|
|
412
549
|
expression: 'document.title',
|
|
@@ -511,20 +648,53 @@ class CdpService extends events_1.EventEmitter {
|
|
|
511
648
|
/**
|
|
512
649
|
* Launch Antigravity and wait for a new workbench page to appear, then connect.
|
|
513
650
|
*/
|
|
514
|
-
async launchAndConnectWorkspace(workspacePath, projectName) {
|
|
651
|
+
async launchAndConnectWorkspace(workspacePath, projectName, explicitOpen = false) {
|
|
652
|
+
const targetPort = this.ports[0] ?? null;
|
|
653
|
+
const configuredUserDataDir = this.resolveConfiguredUserDataDir(this.accountName);
|
|
654
|
+
const resolvedUserDataDir = explicitOpen && targetPort !== null
|
|
655
|
+
? (configuredUserDataDir ?? await this.resolveRunningUserDataDirForPort(targetPort))
|
|
656
|
+
: null;
|
|
657
|
+
if (explicitOpen && this.accountName !== 'default' && targetPort !== null && !resolvedUserDataDir) {
|
|
658
|
+
throw new Error(`Could not determine user-data-dir for running account "${this.accountName}" ` +
|
|
659
|
+
`(CDP port ${targetPort}). Make sure that Antigravity instance is already running with that port.`);
|
|
660
|
+
}
|
|
661
|
+
if (!explicitOpen && this.accountName !== 'default' && targetPort !== null) {
|
|
662
|
+
logger_1.logger.warn(`[CdpService] Workspace "${projectName}" not found on account "${this.accountName}" ` +
|
|
663
|
+
`(port=${targetPort}). Skipping auto-launch to avoid opening the wrong Antigravity instance.`);
|
|
664
|
+
throw new Error(`Workspace "${projectName}" is not open in account "${this.accountName}" ` +
|
|
665
|
+
`(CDP port ${targetPort}). Open it with Antigravity Cockpit or use "/project reopen" ` +
|
|
666
|
+
`command (Telegram: /project_reopen).`);
|
|
667
|
+
}
|
|
515
668
|
// Open as folder using Antigravity CLI (not as workspace mode).
|
|
516
669
|
// `open -a Antigravity` may open as workspace, resulting in title "Untitled (Workspace)".
|
|
517
670
|
// CLI --new-window opens as folder, immediately reflecting directory name in title.
|
|
518
671
|
const antigravityCli = (0, pathUtils_1.getAntigravityCliPath)();
|
|
519
|
-
|
|
672
|
+
const launchArgs = [
|
|
673
|
+
...(resolvedUserDataDir ? ['--user-data-dir', resolvedUserDataDir] : []),
|
|
674
|
+
...(targetPort !== null ? [`--remote-debugging-port=${targetPort}`] : []),
|
|
675
|
+
'--new-window',
|
|
676
|
+
workspacePath,
|
|
677
|
+
];
|
|
678
|
+
logger_1.logger.debug(`[CdpService] Launching Antigravity for account "${this.accountName}" ` +
|
|
679
|
+
`(port=${targetPort ?? 'unknown'}, userDataDir=${resolvedUserDataDir ?? 'default'}) ` +
|
|
680
|
+
`${antigravityCli} ${launchArgs.join(' ')}`);
|
|
520
681
|
try {
|
|
521
|
-
await this.runCommand(antigravityCli,
|
|
682
|
+
await this.runCommand(antigravityCli, launchArgs);
|
|
522
683
|
}
|
|
523
684
|
catch (error) {
|
|
524
685
|
// Fall back to open -a if CLI not found (macOS only)
|
|
525
686
|
logger_1.logger.warn(`[CdpService] CLI launch failed, falling back to open -a (if macOS): ${error?.message || String(error)}`);
|
|
526
687
|
if (process.platform === 'darwin') {
|
|
527
|
-
|
|
688
|
+
const openArgs = [
|
|
689
|
+
'-n',
|
|
690
|
+
'-a',
|
|
691
|
+
'Antigravity',
|
|
692
|
+
'--args',
|
|
693
|
+
...(resolvedUserDataDir ? ['--user-data-dir', resolvedUserDataDir] : []),
|
|
694
|
+
...(targetPort !== null ? [`--remote-debugging-port=${targetPort}`] : []),
|
|
695
|
+
workspacePath,
|
|
696
|
+
];
|
|
697
|
+
await this.runCommand('open', openArgs);
|
|
528
698
|
}
|
|
529
699
|
else {
|
|
530
700
|
throw error;
|
|
@@ -693,6 +863,62 @@ class CdpService extends events_1.EventEmitter {
|
|
|
693
863
|
});
|
|
694
864
|
});
|
|
695
865
|
}
|
|
866
|
+
async resolveRunningUserDataDirForPort(port) {
|
|
867
|
+
const commandLines = await this.getProcessCommandLines();
|
|
868
|
+
if (commandLines.length === 0)
|
|
869
|
+
return null;
|
|
870
|
+
const portPattern = new RegExp(`--remote-debugging-port(?:=|\\s+)${port}(?:\\s|$)`);
|
|
871
|
+
for (const line of commandLines) {
|
|
872
|
+
const lowerLine = line.toLowerCase();
|
|
873
|
+
if (!lowerLine.includes('antigravity') || !portPattern.test(line) || !line.includes('--user-data-dir')) {
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
const match = line.match(/--user-data-dir(?:=|\s+)("([^"]+)"|'([^']+)'|([^\s]+))/);
|
|
877
|
+
const userDataDir = match?.[2] || match?.[3] || match?.[4];
|
|
878
|
+
if (userDataDir) {
|
|
879
|
+
logger_1.logger.info(`[CdpService] Resolved user-data-dir for CDP port ${port}: ${userDataDir}`);
|
|
880
|
+
return userDataDir;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
return null;
|
|
884
|
+
}
|
|
885
|
+
async getProcessCommandLines() {
|
|
886
|
+
const run = (command, args) => new Promise((resolve, reject) => {
|
|
887
|
+
(0, child_process_1.execFile)(command, args, { windowsHide: true, maxBuffer: 20 * 1024 * 1024 }, (error, stdout) => {
|
|
888
|
+
if (error) {
|
|
889
|
+
reject(error);
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
resolve(stdout);
|
|
893
|
+
});
|
|
894
|
+
});
|
|
895
|
+
const parseLines = (output) => output
|
|
896
|
+
.split('\n')
|
|
897
|
+
.map((line) => line.trim())
|
|
898
|
+
.filter((line) => line.length > 0);
|
|
899
|
+
try {
|
|
900
|
+
if (process.platform === 'win32') {
|
|
901
|
+
try {
|
|
902
|
+
const psOutput = await run('powershell.exe', [
|
|
903
|
+
'-NoProfile',
|
|
904
|
+
'-Command',
|
|
905
|
+
'Get-CimInstance Win32_Process | Select-Object -ExpandProperty CommandLine | Where-Object { $_ }',
|
|
906
|
+
]);
|
|
907
|
+
return parseLines(psOutput);
|
|
908
|
+
}
|
|
909
|
+
catch {
|
|
910
|
+
const wmicOutput = await run('wmic', ['process', 'get', 'CommandLine']);
|
|
911
|
+
return parseLines(wmicOutput).filter((line) => line !== 'CommandLine');
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
const psOutput = await run('ps', ['-axo', 'command=']);
|
|
915
|
+
return parseLines(psOutput);
|
|
916
|
+
}
|
|
917
|
+
catch (error) {
|
|
918
|
+
logger_1.logger.warn(`[CdpService] Failed to inspect Antigravity processes: ${error instanceof Error ? error.message : String(error)}`);
|
|
919
|
+
return [];
|
|
920
|
+
}
|
|
921
|
+
}
|
|
696
922
|
/**
|
|
697
923
|
* Quietly disconnect the existing connection (no reconnect attempts).
|
|
698
924
|
* Used during workspace switching.
|
|
@@ -711,6 +937,81 @@ class CdpService extends events_1.EventEmitter {
|
|
|
711
937
|
this.contexts = [];
|
|
712
938
|
this.clearPendingCalls(new Error('Disconnected for workspace switch'));
|
|
713
939
|
this.targetUrl = null;
|
|
940
|
+
this.targetId = null;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
async closeCurrentTarget() {
|
|
944
|
+
if (!this.targetId) {
|
|
945
|
+
return false;
|
|
946
|
+
}
|
|
947
|
+
try {
|
|
948
|
+
await this.call('Target.closeTarget', { targetId: this.targetId });
|
|
949
|
+
return true;
|
|
950
|
+
}
|
|
951
|
+
finally {
|
|
952
|
+
await this.disconnect();
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
async inspectWorkspaceRuntimeState() {
|
|
956
|
+
const result = await this.evaluateAcrossContexts(WORKSPACE_STATE_SCRIPT, (value) => !!(value && typeof value === 'object' && value.hasActiveChat));
|
|
957
|
+
const value = result.value;
|
|
958
|
+
return {
|
|
959
|
+
isGenerating: !!(value && typeof value === 'object' && value.isGenerating),
|
|
960
|
+
sessionTitle: value && typeof value === 'object' && typeof value.sessionTitle === 'string'
|
|
961
|
+
? value.sessionTitle
|
|
962
|
+
: '(Untitled)',
|
|
963
|
+
hasActiveChat: !!(value && typeof value === 'object' && value.hasActiveChat),
|
|
964
|
+
contextId: result.contextId,
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
async closeCurrentTargetGracefully(timeoutMs = 5000) {
|
|
968
|
+
if (!this.targetId) {
|
|
969
|
+
return false;
|
|
970
|
+
}
|
|
971
|
+
const closingTargetId = this.targetId;
|
|
972
|
+
try {
|
|
973
|
+
await this.call('Page.bringToFront', {}).catch(() => { });
|
|
974
|
+
const modifiers = process.platform === 'darwin' ? 4 : 2;
|
|
975
|
+
await this.call('Input.dispatchKeyEvent', {
|
|
976
|
+
type: 'keyDown',
|
|
977
|
+
key: 'w',
|
|
978
|
+
code: 'KeyW',
|
|
979
|
+
modifiers,
|
|
980
|
+
windowsVirtualKeyCode: 87,
|
|
981
|
+
nativeVirtualKeyCode: 87,
|
|
982
|
+
});
|
|
983
|
+
await this.call('Input.dispatchKeyEvent', {
|
|
984
|
+
type: 'keyUp',
|
|
985
|
+
key: 'w',
|
|
986
|
+
code: 'KeyW',
|
|
987
|
+
modifiers,
|
|
988
|
+
windowsVirtualKeyCode: 87,
|
|
989
|
+
nativeVirtualKeyCode: 87,
|
|
990
|
+
});
|
|
991
|
+
const deadline = Date.now() + timeoutMs;
|
|
992
|
+
while (Date.now() <= deadline) {
|
|
993
|
+
let stillOpen = false;
|
|
994
|
+
for (const port of this.ports) {
|
|
995
|
+
try {
|
|
996
|
+
const list = await this.getJson(`http://127.0.0.1:${port}/json/list`);
|
|
997
|
+
if (list.some((page) => page?.id === closingTargetId)) {
|
|
998
|
+
stillOpen = true;
|
|
999
|
+
break;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
catch {
|
|
1003
|
+
// Ignore port failures while polling close state.
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
if (!stillOpen) {
|
|
1007
|
+
return true;
|
|
1008
|
+
}
|
|
1009
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
1010
|
+
}
|
|
1011
|
+
return false;
|
|
1012
|
+
}
|
|
1013
|
+
finally {
|
|
1014
|
+
await this.disconnect();
|
|
714
1015
|
}
|
|
715
1016
|
}
|
|
716
1017
|
/**
|
|
@@ -896,6 +1197,112 @@ class CdpService extends events_1.EventEmitter {
|
|
|
896
1197
|
}
|
|
897
1198
|
return { ok: false, error: 'Chat input field not found' };
|
|
898
1199
|
}
|
|
1200
|
+
async waitForChatInputReady(timeoutMs = CHAT_READY_TIMEOUT_MS) {
|
|
1201
|
+
const deadline = Date.now() + timeoutMs;
|
|
1202
|
+
let lastError = 'Chat input field not found';
|
|
1203
|
+
while (Date.now() < deadline) {
|
|
1204
|
+
const focusResult = await this.focusChatInput();
|
|
1205
|
+
if (focusResult.ok) {
|
|
1206
|
+
return focusResult;
|
|
1207
|
+
}
|
|
1208
|
+
lastError = focusResult.error || lastError;
|
|
1209
|
+
await new Promise((r) => setTimeout(r, CHAT_READY_POLL_MS));
|
|
1210
|
+
}
|
|
1211
|
+
return { ok: false, error: lastError };
|
|
1212
|
+
}
|
|
1213
|
+
isTransientInjectError(error) {
|
|
1214
|
+
const message = String(error || '');
|
|
1215
|
+
return [
|
|
1216
|
+
'No editor found',
|
|
1217
|
+
'Chat input field not found',
|
|
1218
|
+
'WebSocket is not connected',
|
|
1219
|
+
'WebSocket disconnected',
|
|
1220
|
+
].some((fragment) => message.includes(fragment));
|
|
1221
|
+
}
|
|
1222
|
+
async isModeToggleReady() {
|
|
1223
|
+
const expression = '(() => {'
|
|
1224
|
+
+ ' const uiNameMap = { fast: "Fast", plan: "Planning" };'
|
|
1225
|
+
+ ' const knownModes = Object.values(uiNameMap).map(n => n.toLowerCase());'
|
|
1226
|
+
+ ' const allBtns = Array.from(document.querySelectorAll("button"));'
|
|
1227
|
+
+ ' const visibleBtns = allBtns.filter(b => b.offsetParent !== null);'
|
|
1228
|
+
+ ' const modeToggleBtn = visibleBtns.find(b => {'
|
|
1229
|
+
+ ' const text = (b.textContent || "").trim().toLowerCase();'
|
|
1230
|
+
+ ' const hasChevron = b.querySelector("svg[class*=\\"chevron\\"]");'
|
|
1231
|
+
+ ' return knownModes.some(m => text === m) && hasChevron;'
|
|
1232
|
+
+ ' });'
|
|
1233
|
+
+ ' return !!modeToggleBtn;'
|
|
1234
|
+
+ '})()';
|
|
1235
|
+
try {
|
|
1236
|
+
const contextId = this.getPrimaryContextId();
|
|
1237
|
+
const callParams = {
|
|
1238
|
+
expression,
|
|
1239
|
+
returnByValue: true,
|
|
1240
|
+
awaitPromise: false,
|
|
1241
|
+
};
|
|
1242
|
+
if (contextId !== null)
|
|
1243
|
+
callParams.contextId = contextId;
|
|
1244
|
+
const res = await this.call('Runtime.evaluate', callParams);
|
|
1245
|
+
return Boolean(res?.result?.value);
|
|
1246
|
+
}
|
|
1247
|
+
catch {
|
|
1248
|
+
return false;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
async waitForModeToggleReady(timeoutMs = MODE_READY_TIMEOUT_MS) {
|
|
1252
|
+
const deadline = Date.now() + timeoutMs;
|
|
1253
|
+
while (Date.now() < deadline) {
|
|
1254
|
+
if (await this.isModeToggleReady()) {
|
|
1255
|
+
return true;
|
|
1256
|
+
}
|
|
1257
|
+
await new Promise((r) => setTimeout(r, CHAT_READY_POLL_MS));
|
|
1258
|
+
}
|
|
1259
|
+
return false;
|
|
1260
|
+
}
|
|
1261
|
+
isTransientModeError(error) {
|
|
1262
|
+
const message = String(error || '');
|
|
1263
|
+
return [
|
|
1264
|
+
'Mode toggle button not found',
|
|
1265
|
+
'WebSocket is not connected',
|
|
1266
|
+
'WebSocket disconnected',
|
|
1267
|
+
].some((fragment) => message.includes(fragment));
|
|
1268
|
+
}
|
|
1269
|
+
async injectMessageCore(text, imageFilePaths) {
|
|
1270
|
+
const focusResult = await this.waitForChatInputReady();
|
|
1271
|
+
if (!focusResult.ok) {
|
|
1272
|
+
return { ok: false, error: focusResult.error || 'Chat input field not found' };
|
|
1273
|
+
}
|
|
1274
|
+
// Clear any existing text in the input field before injecting.
|
|
1275
|
+
await this.clearInputField();
|
|
1276
|
+
if (imageFilePaths && imageFilePaths.length > 0) {
|
|
1277
|
+
const attachResult = await this.attachImageFiles(imageFilePaths, focusResult.contextId);
|
|
1278
|
+
if (!attachResult.ok) {
|
|
1279
|
+
return { ok: false, error: attachResult.error || 'Failed to attach images' };
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
await this.call('Input.insertText', { text });
|
|
1283
|
+
await new Promise(r => setTimeout(r, 200));
|
|
1284
|
+
await this.pressEnterToSend();
|
|
1285
|
+
return {
|
|
1286
|
+
ok: true,
|
|
1287
|
+
method: 'enter',
|
|
1288
|
+
contextId: focusResult.contextId,
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
async retryInjectOnce(text, firstError, imageFilePaths) {
|
|
1292
|
+
logger_1.logger.warn(`[CdpService] Initial message injection failed: ${firstError}. Retrying once after readiness check...`);
|
|
1293
|
+
try {
|
|
1294
|
+
await this.reconnectOnDemand(INJECT_RETRY_READY_TIMEOUT_MS);
|
|
1295
|
+
}
|
|
1296
|
+
catch {
|
|
1297
|
+
// Ignore reconnect failures here; readiness check below will produce the final error.
|
|
1298
|
+
}
|
|
1299
|
+
await new Promise((r) => setTimeout(r, INJECT_RETRY_BACKOFF_MS));
|
|
1300
|
+
const ready = await this.waitForChatInputReady(INJECT_RETRY_READY_TIMEOUT_MS);
|
|
1301
|
+
if (!ready.ok) {
|
|
1302
|
+
return { ok: false, error: ready.error || firstError };
|
|
1303
|
+
}
|
|
1304
|
+
return this.injectMessageCore(text, imageFilePaths);
|
|
1305
|
+
}
|
|
899
1306
|
/**
|
|
900
1307
|
* Select all text in the focused input and delete it to ensure a clean state.
|
|
901
1308
|
* Uses Meta+A (select all) then Backspace (delete) via CDP key events.
|
|
@@ -1080,18 +1487,11 @@ class CdpService extends events_1.EventEmitter {
|
|
|
1080
1487
|
if (!this.isConnectedFlag || !this.ws) {
|
|
1081
1488
|
throw new Error('Not connected to CDP. Call connect() first.');
|
|
1082
1489
|
}
|
|
1083
|
-
const
|
|
1084
|
-
if (!
|
|
1085
|
-
return
|
|
1490
|
+
const result = await this.injectMessageCore(text);
|
|
1491
|
+
if (result.ok || !this.isTransientInjectError(result.error)) {
|
|
1492
|
+
return result;
|
|
1086
1493
|
}
|
|
1087
|
-
|
|
1088
|
-
await this.clearInputField();
|
|
1089
|
-
// 1. Input text via CDP Input.insertText
|
|
1090
|
-
await this.call('Input.insertText', { text });
|
|
1091
|
-
await new Promise(r => setTimeout(r, 200));
|
|
1092
|
-
// 2. Send via Enter key
|
|
1093
|
-
await this.pressEnterToSend();
|
|
1094
|
-
return { ok: true, method: 'enter', contextId: focusResult.contextId };
|
|
1494
|
+
return this.retryInjectOnce(text, result.error || 'Message injection failed');
|
|
1095
1495
|
}
|
|
1096
1496
|
/**
|
|
1097
1497
|
* Attach image files to the UI and send the specified text.
|
|
@@ -1100,20 +1500,11 @@ class CdpService extends events_1.EventEmitter {
|
|
|
1100
1500
|
if (!this.isConnectedFlag || !this.ws) {
|
|
1101
1501
|
throw new Error('Not connected to CDP. Call connect() first.');
|
|
1102
1502
|
}
|
|
1103
|
-
const
|
|
1104
|
-
if (!
|
|
1105
|
-
return
|
|
1106
|
-
}
|
|
1107
|
-
// Clear any existing text in the input field before injecting
|
|
1108
|
-
await this.clearInputField();
|
|
1109
|
-
const attachResult = await this.attachImageFiles(imageFilePaths, focusResult.contextId);
|
|
1110
|
-
if (!attachResult.ok) {
|
|
1111
|
-
return { ok: false, error: attachResult.error || 'Failed to attach images' };
|
|
1503
|
+
const result = await this.injectMessageCore(text, imageFilePaths);
|
|
1504
|
+
if (result.ok || !this.isTransientInjectError(result.error)) {
|
|
1505
|
+
return result;
|
|
1112
1506
|
}
|
|
1113
|
-
|
|
1114
|
-
await new Promise(r => setTimeout(r, 200));
|
|
1115
|
-
await this.pressEnterToSend();
|
|
1116
|
-
return { ok: true, method: 'enter', contextId: focusResult.contextId };
|
|
1507
|
+
return this.retryInjectOnce(text, result.error || 'Image message injection failed', imageFilePaths);
|
|
1117
1508
|
}
|
|
1118
1509
|
/**
|
|
1119
1510
|
* Extract images from the latest AI response.
|
|
@@ -1351,6 +1742,7 @@ class CdpService extends events_1.EventEmitter {
|
|
|
1351
1742
|
if (!this.isConnectedFlag || !this.ws) {
|
|
1352
1743
|
await this.reconnectOnDemand();
|
|
1353
1744
|
}
|
|
1745
|
+
await this.waitForModeToggleReady();
|
|
1354
1746
|
const safeMode = JSON.stringify(modeName);
|
|
1355
1747
|
// Internal mode name -> Antigravity UI display name mapping
|
|
1356
1748
|
const uiNameMap = JSON.stringify({ fast: 'Fast', plan: 'Planning' });
|
|
@@ -1443,7 +1835,24 @@ class CdpService extends events_1.EventEmitter {
|
|
|
1443
1835
|
if (value?.ok) {
|
|
1444
1836
|
return { ok: true, mode: value.mode };
|
|
1445
1837
|
}
|
|
1446
|
-
|
|
1838
|
+
const errorMessage = value?.error || 'UI operation failed (setUiMode)';
|
|
1839
|
+
if (!this.isTransientModeError(errorMessage)) {
|
|
1840
|
+
return { ok: false, error: errorMessage };
|
|
1841
|
+
}
|
|
1842
|
+
logger_1.logger.warn(`[CdpService] setUiMode initial attempt failed: ${errorMessage}. Retrying once after readiness check...`);
|
|
1843
|
+
try {
|
|
1844
|
+
await this.reconnectOnDemand(MODE_RETRY_READY_TIMEOUT_MS);
|
|
1845
|
+
}
|
|
1846
|
+
catch {
|
|
1847
|
+
// Fall through to the readiness wait and retry below.
|
|
1848
|
+
}
|
|
1849
|
+
await this.waitForModeToggleReady(MODE_RETRY_READY_TIMEOUT_MS);
|
|
1850
|
+
const retryRes = await this.call('Runtime.evaluate', callParams);
|
|
1851
|
+
const retryValue = retryRes?.result?.value;
|
|
1852
|
+
if (retryValue?.ok) {
|
|
1853
|
+
return { ok: true, mode: retryValue.mode };
|
|
1854
|
+
}
|
|
1855
|
+
return { ok: false, error: retryValue?.error || errorMessage };
|
|
1447
1856
|
}
|
|
1448
1857
|
catch (error) {
|
|
1449
1858
|
return { ok: false, error: error?.message || String(error) };
|
|
@@ -1457,26 +1866,152 @@ class CdpService extends events_1.EventEmitter {
|
|
|
1457
1866
|
throw new Error('Not connected to CDP.');
|
|
1458
1867
|
}
|
|
1459
1868
|
const expression = `(async () => {
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1869
|
+
const normalize = (text) => (text || '').trim().replace(/New$/, '').trim();
|
|
1870
|
+
const isBlockedModelLabel = (text) => /assist|open in terminal|terminal|code change|claude code|\\bchange\\b/i.test(text || '');
|
|
1871
|
+
const looksLikeModel = (text) => {
|
|
1872
|
+
const normalized = normalize(text).toLowerCase();
|
|
1873
|
+
if (!normalized || isBlockedModelLabel(normalized)) return false;
|
|
1874
|
+
return /gemini\\s*\\d|gpt(?:[-\\s]?\\d|[-\\s]?oss)|claude\\s+(?:opus|sonnet|haiku)|(?:opus|sonnet|haiku)\\s*\\d/i.test(normalized);
|
|
1875
|
+
};
|
|
1876
|
+
const isVisible = (el) => {
|
|
1877
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
1878
|
+
const style = window.getComputedStyle(el);
|
|
1879
|
+
const rect = el.getBoundingClientRect();
|
|
1880
|
+
return style.visibility !== 'hidden'
|
|
1881
|
+
&& style.display !== 'none'
|
|
1882
|
+
&& rect.width > 0
|
|
1883
|
+
&& rect.height > 0;
|
|
1884
|
+
};
|
|
1885
|
+
const getLabel = (el) => {
|
|
1886
|
+
if (!(el instanceof HTMLElement)) return '';
|
|
1887
|
+
const parts = [
|
|
1888
|
+
el.textContent || '',
|
|
1889
|
+
el.getAttribute('aria-label') || '',
|
|
1890
|
+
el.getAttribute('title') || '',
|
|
1891
|
+
el.getAttribute('data-value') || '',
|
|
1892
|
+
];
|
|
1893
|
+
return normalize(parts.find((part) => normalize(part).length > 0) || '');
|
|
1894
|
+
};
|
|
1895
|
+
const scopeSelectors = [
|
|
1896
|
+
'[role="dialog"]',
|
|
1897
|
+
'[role="listbox"]',
|
|
1898
|
+
'[role="menu"]',
|
|
1899
|
+
'[data-radix-popper-content-wrapper]',
|
|
1900
|
+
'[data-slot*="popover"]',
|
|
1901
|
+
'[data-state="open"]',
|
|
1902
|
+
'[class*="popover"]',
|
|
1903
|
+
'[class*="dropdown"]',
|
|
1904
|
+
'[class*="menu"]',
|
|
1905
|
+
];
|
|
1906
|
+
const itemSelectors = [
|
|
1907
|
+
'[role="option"]',
|
|
1908
|
+
'[role="menuitem"]',
|
|
1909
|
+
'[aria-selected]',
|
|
1910
|
+
'[aria-checked]',
|
|
1911
|
+
'button',
|
|
1912
|
+
'[role="button"]',
|
|
1913
|
+
'div.cursor-pointer',
|
|
1914
|
+
'div[class*="cursor-pointer"]',
|
|
1915
|
+
];
|
|
1916
|
+
const getScopes = () => {
|
|
1917
|
+
const scopes = [document];
|
|
1918
|
+
for (const el of document.querySelectorAll(scopeSelectors.join(','))) {
|
|
1919
|
+
if (isVisible(el)) scopes.push(el);
|
|
1920
|
+
}
|
|
1921
|
+
return Array.from(new Set(scopes));
|
|
1922
|
+
};
|
|
1923
|
+
const hasOpenPicker = () => getScopes().some((scope) => scope !== document);
|
|
1924
|
+
const closePicker = async () => {
|
|
1925
|
+
const active = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
1926
|
+
if (active) active.blur();
|
|
1927
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true }));
|
|
1928
|
+
document.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape', bubbles: true, cancelable: true }));
|
|
1929
|
+
document.body?.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }));
|
|
1930
|
+
document.body?.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true }));
|
|
1931
|
+
document.body?.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
1932
|
+
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
1933
|
+
};
|
|
1934
|
+
const collectModels = () => {
|
|
1935
|
+
const seen = new Set();
|
|
1936
|
+
const labels = [];
|
|
1937
|
+
for (const scope of getScopes()) {
|
|
1938
|
+
for (const el of scope.querySelectorAll(itemSelectors.join(','))) {
|
|
1939
|
+
if (!isVisible(el)) continue;
|
|
1940
|
+
const label = getLabel(el);
|
|
1941
|
+
if (!looksLikeModel(label)) continue;
|
|
1942
|
+
const key = label.toLowerCase();
|
|
1943
|
+
if (seen.has(key)) continue;
|
|
1944
|
+
seen.add(key);
|
|
1945
|
+
labels.push(label);
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
return labels;
|
|
1949
|
+
};
|
|
1950
|
+
const triggerSelectors = [
|
|
1951
|
+
'[role="combobox"]',
|
|
1952
|
+
'[aria-haspopup="listbox"]',
|
|
1953
|
+
'[aria-haspopup="menu"]',
|
|
1954
|
+
'[aria-expanded]',
|
|
1955
|
+
'button',
|
|
1956
|
+
'[role="button"]',
|
|
1957
|
+
'div.cursor-pointer',
|
|
1958
|
+
'div[class*="cursor-pointer"]',
|
|
1959
|
+
];
|
|
1960
|
+
|
|
1961
|
+
let models = collectModels();
|
|
1962
|
+
if (models.length > 1) {
|
|
1963
|
+
if (hasOpenPicker()) await closePicker();
|
|
1964
|
+
return models;
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
const triggers = Array.from(document.querySelectorAll(triggerSelectors.join(',')))
|
|
1968
|
+
.filter(isVisible)
|
|
1969
|
+
.filter((el) => {
|
|
1970
|
+
const label = getLabel(el);
|
|
1971
|
+
const attrs = normalize([
|
|
1972
|
+
el.getAttribute('aria-label') || '',
|
|
1973
|
+
el.getAttribute('title') || '',
|
|
1974
|
+
el.getAttribute('aria-haspopup') || '',
|
|
1975
|
+
el.className || '',
|
|
1976
|
+
].join(' '));
|
|
1977
|
+
return looksLikeModel(label) || /model|listbox|combobox|popover|dropdown/.test(attrs);
|
|
1978
|
+
});
|
|
1979
|
+
|
|
1980
|
+
for (const trigger of triggers) {
|
|
1981
|
+
trigger.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
1982
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
1983
|
+
models = collectModels();
|
|
1984
|
+
if (models.length > 1) {
|
|
1985
|
+
if (hasOpenPicker()) await closePicker();
|
|
1986
|
+
return models;
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
if (hasOpenPicker()) await closePicker();
|
|
1991
|
+
return models;
|
|
1464
1992
|
})()`;
|
|
1465
1993
|
try {
|
|
1466
|
-
const
|
|
1467
|
-
const
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1994
|
+
const contexts = this.getContexts();
|
|
1995
|
+
const contextIds = [
|
|
1996
|
+
this.getPrimaryContextId(),
|
|
1997
|
+
...contexts.map((ctx) => ctx.id),
|
|
1998
|
+
].filter((value, index, arr) => typeof value === 'number' && arr.indexOf(value) === index);
|
|
1999
|
+
const targets = contextIds.length > 0 ? contextIds : [null];
|
|
2000
|
+
for (const contextId of targets) {
|
|
2001
|
+
const params = {
|
|
2002
|
+
expression,
|
|
2003
|
+
returnByValue: true,
|
|
2004
|
+
awaitPromise: true,
|
|
2005
|
+
};
|
|
2006
|
+
if (typeof contextId === 'number')
|
|
2007
|
+
params.contextId = contextId;
|
|
2008
|
+
const res = await this.call('Runtime.evaluate', params);
|
|
2009
|
+
const value = res?.result?.value;
|
|
2010
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
2011
|
+
return Array.from(new Set(value));
|
|
2012
|
+
}
|
|
1479
2013
|
}
|
|
2014
|
+
logger_1.logger.warn('[CdpService] getUiModels returned no models from any execution context.');
|
|
1480
2015
|
return [];
|
|
1481
2016
|
}
|
|
1482
2017
|
catch (error) {
|
|
@@ -1492,17 +2027,81 @@ class CdpService extends events_1.EventEmitter {
|
|
|
1492
2027
|
return null;
|
|
1493
2028
|
}
|
|
1494
2029
|
const expression = `(() => {
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
2030
|
+
const normalize = (text) => (text || '').trim().replace(/New$/, '').trim();
|
|
2031
|
+
const isBlockedModelLabel = (text) => /assist|open in terminal|terminal|code change|claude code|\\bchange\\b/i.test(text || '');
|
|
2032
|
+
const looksLikeModel = (text) => {
|
|
2033
|
+
const normalized = normalize(text).toLowerCase();
|
|
2034
|
+
if (!normalized || isBlockedModelLabel(normalized)) return false;
|
|
2035
|
+
return /gemini\\s*\\d|gpt(?:[-\\s]?\\d|[-\\s]?oss)|claude\\s+(?:opus|sonnet|haiku)|(?:opus|sonnet|haiku)\\s*\\d/i.test(normalized);
|
|
2036
|
+
};
|
|
2037
|
+
const isVisible = (el) => {
|
|
2038
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
2039
|
+
const style = window.getComputedStyle(el);
|
|
2040
|
+
const rect = el.getBoundingClientRect();
|
|
2041
|
+
return style.visibility !== 'hidden'
|
|
2042
|
+
&& style.display !== 'none'
|
|
2043
|
+
&& rect.width > 0
|
|
2044
|
+
&& rect.height > 0;
|
|
2045
|
+
};
|
|
2046
|
+
const getLabel = (el) => {
|
|
2047
|
+
if (!(el instanceof HTMLElement)) return '';
|
|
2048
|
+
const parts = [
|
|
2049
|
+
el.textContent || '',
|
|
2050
|
+
el.getAttribute('aria-label') || '',
|
|
2051
|
+
el.getAttribute('title') || '',
|
|
2052
|
+
el.getAttribute('data-value') || '',
|
|
2053
|
+
];
|
|
2054
|
+
return normalize(parts.find((part) => normalize(part).length > 0) || '');
|
|
2055
|
+
};
|
|
2056
|
+
const getScore = (el) => {
|
|
2057
|
+
if (!(el instanceof HTMLElement)) return 0;
|
|
2058
|
+
let score = 0;
|
|
2059
|
+
if (el.getAttribute('aria-selected') === 'true') score += 5;
|
|
2060
|
+
if (el.getAttribute('aria-checked') === 'true') score += 5;
|
|
2061
|
+
if (el.getAttribute('aria-current') === 'true') score += 4;
|
|
2062
|
+
const dataState = normalize(el.getAttribute('data-state') || '');
|
|
2063
|
+
if (/(checked|active|selected|on|open)/.test(dataState)) score += 4;
|
|
2064
|
+
const classes = normalize(el.className || '');
|
|
2065
|
+
if (classes.includes('bg-gray-500/20')) score += 3;
|
|
2066
|
+
if (classes.includes('selected')) score += 3;
|
|
2067
|
+
if (classes.includes('active')) score += 2;
|
|
2068
|
+
if (el.getAttribute('aria-expanded') === 'true') score += 1;
|
|
2069
|
+
return score;
|
|
2070
|
+
};
|
|
2071
|
+
const candidates = Array.from(document.querySelectorAll(
|
|
2072
|
+
'[role="option"], [role="menuitem"], [role="combobox"], [aria-selected], [aria-checked], ' +
|
|
2073
|
+
'[aria-current], button, [role="button"], div.cursor-pointer, div[class*="cursor-pointer"]'
|
|
2074
|
+
))
|
|
2075
|
+
.filter(isVisible)
|
|
2076
|
+
.map((el) => ({ el, label: getLabel(el), score: getScore(el) }))
|
|
2077
|
+
.filter((entry) => looksLikeModel(entry.label))
|
|
2078
|
+
.sort((a, b) => b.score - a.score || a.label.length - b.label.length);
|
|
2079
|
+
if (candidates.length === 0) return null;
|
|
2080
|
+
if (candidates[0].score > 0) return candidates[0].label;
|
|
2081
|
+
return candidates[0].label;
|
|
1498
2082
|
})()`;
|
|
1499
2083
|
try {
|
|
1500
|
-
const
|
|
1501
|
-
const
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
2084
|
+
const contexts = this.getContexts();
|
|
2085
|
+
const contextIds = [
|
|
2086
|
+
this.getPrimaryContextId(),
|
|
2087
|
+
...contexts.map((ctx) => ctx.id),
|
|
2088
|
+
].filter((value, index, arr) => typeof value === 'number' && arr.indexOf(value) === index);
|
|
2089
|
+
const targets = contextIds.length > 0 ? contextIds : [null];
|
|
2090
|
+
for (const contextId of targets) {
|
|
2091
|
+
const params = {
|
|
2092
|
+
expression,
|
|
2093
|
+
returnByValue: true,
|
|
2094
|
+
awaitPromise: true,
|
|
2095
|
+
};
|
|
2096
|
+
if (typeof contextId === 'number')
|
|
2097
|
+
params.contextId = contextId;
|
|
2098
|
+
const res = await this.call('Runtime.evaluate', params);
|
|
2099
|
+
const value = res?.result?.value;
|
|
2100
|
+
if (typeof value === 'string' && value.trim().length > 0) {
|
|
2101
|
+
return value;
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
return null;
|
|
1506
2105
|
}
|
|
1507
2106
|
catch (e) {
|
|
1508
2107
|
return null;
|
|
@@ -1515,6 +2114,7 @@ class CdpService extends events_1.EventEmitter {
|
|
|
1515
2114
|
* @param modelName Model name to set (e.g., 'gpt-4o', 'claude-3-opus')
|
|
1516
2115
|
*/
|
|
1517
2116
|
async setUiModel(modelName) {
|
|
2117
|
+
logger_1.logger.info(`[CdpService] setUiModel requested account=${this.accountName} target="${modelName}" connected=${this.isConnectedFlag}`);
|
|
1518
2118
|
if (!this.isConnectedFlag || !this.ws) {
|
|
1519
2119
|
await this.reconnectOnDemand();
|
|
1520
2120
|
}
|
|
@@ -1525,68 +2125,325 @@ class CdpService extends events_1.EventEmitter {
|
|
|
1525
2125
|
const safeModel = JSON.stringify(modelName);
|
|
1526
2126
|
const expression = `(async () => {
|
|
1527
2127
|
const targetModel = ${safeModel};
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
const
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
2128
|
+
const normalize = (text) => (text || '').trim().replace(/New$/, '').trim();
|
|
2129
|
+
const isBlockedModelLabel = (text) => /assist|open in terminal|terminal|code change|claude code|\\bchange\\b/i.test(text || '');
|
|
2130
|
+
const looksLikeModel = (text) => {
|
|
2131
|
+
const normalized = normalize(text).toLowerCase();
|
|
2132
|
+
if (!normalized || isBlockedModelLabel(normalized)) return false;
|
|
2133
|
+
return /gemini\\s*\\d|gpt(?:[-\\s]?\\d|[-\\s]?oss)|claude\\s+(?:opus|sonnet|haiku)|(?:opus|sonnet|haiku)\\s*\\d/i.test(normalized);
|
|
2134
|
+
};
|
|
2135
|
+
const isVisible = (el) => {
|
|
2136
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
2137
|
+
const style = window.getComputedStyle(el);
|
|
2138
|
+
const rect = el.getBoundingClientRect();
|
|
2139
|
+
return style.visibility !== 'hidden'
|
|
2140
|
+
&& style.display !== 'none'
|
|
2141
|
+
&& rect.width > 0
|
|
2142
|
+
&& rect.height > 0;
|
|
2143
|
+
};
|
|
2144
|
+
const getLabel = (el) => {
|
|
2145
|
+
if (!(el instanceof HTMLElement)) return '';
|
|
2146
|
+
const parts = [
|
|
2147
|
+
el.textContent || '',
|
|
2148
|
+
el.getAttribute('aria-label') || '',
|
|
2149
|
+
el.getAttribute('title') || '',
|
|
2150
|
+
el.getAttribute('data-value') || '',
|
|
2151
|
+
];
|
|
2152
|
+
return normalize(parts.find((part) => normalize(part).length > 0) || '');
|
|
2153
|
+
};
|
|
2154
|
+
const itemSelectors = [
|
|
2155
|
+
'[role="option"]',
|
|
2156
|
+
'[role="menuitem"]',
|
|
2157
|
+
'[aria-selected]',
|
|
2158
|
+
'[aria-checked]',
|
|
2159
|
+
'button',
|
|
2160
|
+
'[role="button"]',
|
|
2161
|
+
'div.cursor-pointer',
|
|
2162
|
+
'div[class*="cursor-pointer"]',
|
|
2163
|
+
];
|
|
2164
|
+
const triggerSelectors = [
|
|
2165
|
+
'[role="combobox"]',
|
|
2166
|
+
'[aria-haspopup="listbox"]',
|
|
2167
|
+
'[aria-haspopup="menu"]',
|
|
2168
|
+
'[aria-expanded]',
|
|
2169
|
+
'button',
|
|
2170
|
+
'[role="button"]',
|
|
2171
|
+
'div.cursor-pointer',
|
|
2172
|
+
'div[class*="cursor-pointer"]',
|
|
2173
|
+
];
|
|
2174
|
+
const scopeSelectors = [
|
|
2175
|
+
'[role="dialog"]',
|
|
2176
|
+
'[role="listbox"]',
|
|
2177
|
+
'[role="menu"]',
|
|
2178
|
+
'[data-radix-popper-content-wrapper]',
|
|
2179
|
+
'[data-slot*="popover"]',
|
|
2180
|
+
'[data-state="open"]',
|
|
2181
|
+
'[class*="popover"]',
|
|
2182
|
+
'[class*="dropdown"]',
|
|
2183
|
+
'[class*="menu"]',
|
|
2184
|
+
];
|
|
2185
|
+
const getScopes = () => {
|
|
2186
|
+
const scopes = [document];
|
|
2187
|
+
for (const el of document.querySelectorAll(scopeSelectors.join(','))) {
|
|
2188
|
+
if (isVisible(el)) scopes.push(el);
|
|
2189
|
+
}
|
|
2190
|
+
return Array.from(new Set(scopes));
|
|
2191
|
+
};
|
|
2192
|
+
const getOpenPickerScopes = () => getScopes().filter((scope) => scope !== document);
|
|
2193
|
+
const hasOpenPicker = () => getOpenPickerScopes().length > 0;
|
|
2194
|
+
const isSelected = (el) => {
|
|
2195
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
2196
|
+
const classes = normalize(el.className || '');
|
|
2197
|
+
const state = normalize(el.getAttribute('data-state') || '');
|
|
2198
|
+
return el.getAttribute('aria-selected') === 'true'
|
|
2199
|
+
|| el.getAttribute('aria-checked') === 'true'
|
|
2200
|
+
|| el.getAttribute('aria-current') === 'true'
|
|
2201
|
+
|| /(checked|active|selected|on)/.test(state)
|
|
2202
|
+
|| classes.includes('bg-gray-500/20')
|
|
2203
|
+
|| classes.includes('selected')
|
|
2204
|
+
|| classes.includes('active');
|
|
2205
|
+
};
|
|
2206
|
+
const isModelTrigger = (el) => {
|
|
2207
|
+
if (!(el instanceof HTMLElement) || !isVisible(el)) return false;
|
|
2208
|
+
const label = getLabel(el);
|
|
2209
|
+
const attrs = normalize([
|
|
2210
|
+
el.getAttribute('aria-label') || '',
|
|
2211
|
+
el.getAttribute('title') || '',
|
|
2212
|
+
el.getAttribute('aria-haspopup') || '',
|
|
2213
|
+
el.getAttribute('role') || '',
|
|
2214
|
+
el.getAttribute('data-state') || '',
|
|
2215
|
+
el.className || '',
|
|
2216
|
+
].join(' '));
|
|
2217
|
+
return looksLikeModel(label)
|
|
2218
|
+
|| /select model|current:|model|listbox|combobox|dropdown|popover|dialog/.test(attrs);
|
|
2219
|
+
};
|
|
2220
|
+
const collectModelItems = ({ includeClosedTrigger = false } = {}) => {
|
|
2221
|
+
const items = [];
|
|
2222
|
+
const seen = new Set();
|
|
2223
|
+
const openScopes = getOpenPickerScopes();
|
|
2224
|
+
const scopes = openScopes.length > 0
|
|
2225
|
+
? openScopes
|
|
2226
|
+
: includeClosedTrigger
|
|
2227
|
+
? [document]
|
|
2228
|
+
: [];
|
|
2229
|
+
for (const scope of scopes) {
|
|
2230
|
+
for (const el of scope.querySelectorAll(itemSelectors.join(','))) {
|
|
2231
|
+
if (!isVisible(el)) continue;
|
|
2232
|
+
if (scope === document && isModelTrigger(el)) continue;
|
|
2233
|
+
const label = getLabel(el);
|
|
2234
|
+
if (!looksLikeModel(label)) continue;
|
|
2235
|
+
const key = label.toLowerCase() + '::' + normalize((el.getAttribute('role') || '') + ' ' + (el.className || ''));
|
|
2236
|
+
if (seen.has(key)) continue;
|
|
2237
|
+
seen.add(key);
|
|
2238
|
+
items.push({ el, label });
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
return items;
|
|
2242
|
+
};
|
|
2243
|
+
const summarizeElement = (el) => {
|
|
2244
|
+
if (!(el instanceof HTMLElement)) return null;
|
|
2245
|
+
return {
|
|
2246
|
+
label: getLabel(el),
|
|
2247
|
+
role: el.getAttribute('role') || '',
|
|
2248
|
+
ariaLabel: el.getAttribute('aria-label') || '',
|
|
2249
|
+
title: el.getAttribute('title') || '',
|
|
2250
|
+
ariaExpanded: el.getAttribute('aria-expanded') || '',
|
|
2251
|
+
ariaHaspopup: el.getAttribute('aria-haspopup') || '',
|
|
2252
|
+
ariaSelected: el.getAttribute('aria-selected') || '',
|
|
2253
|
+
dataState: el.getAttribute('data-state') || '',
|
|
2254
|
+
className: String(el.className || '').slice(0, 120),
|
|
2255
|
+
};
|
|
2256
|
+
};
|
|
2257
|
+
const diagnostics = () => {
|
|
2258
|
+
const triggers = Array.from(document.querySelectorAll(triggerSelectors.join(',')))
|
|
2259
|
+
.filter(isVisible)
|
|
2260
|
+
.map((el) => summarizeElement(el))
|
|
2261
|
+
.filter((entry) => entry && (looksLikeModel(entry.label) || /select model|current:|model|listbox|combobox|dropdown|popover|dialog/.test(normalize(entry.className + ' ' + entry.ariaLabel + ' ' + entry.title + ' ' + entry.ariaHaspopup))))
|
|
2262
|
+
.slice(0, 8);
|
|
2263
|
+
const items = collectModelItems({ includeClosedTrigger: true })
|
|
2264
|
+
.map(({ el }) => summarizeElement(el))
|
|
2265
|
+
.filter(Boolean)
|
|
2266
|
+
.slice(0, 12);
|
|
2267
|
+
return JSON.stringify({ triggers, items, openPicker: hasOpenPicker() });
|
|
2268
|
+
};
|
|
2269
|
+
const getCurrentModelLabel = () => {
|
|
2270
|
+
const selectedItem = collectModelItems({ includeClosedTrigger: true }).find(({ el }) => isSelected(el));
|
|
2271
|
+
if (selectedItem) return selectedItem.label;
|
|
2272
|
+
const trigger = Array.from(document.querySelectorAll(triggerSelectors.join(',')))
|
|
2273
|
+
.filter(isVisible)
|
|
2274
|
+
.map((el) => ({ el, label: getLabel(el) }))
|
|
2275
|
+
.find((entry) => isModelTrigger(entry.el) && looksLikeModel(entry.label));
|
|
2276
|
+
return trigger ? trigger.label : null;
|
|
2277
|
+
};
|
|
2278
|
+
const triggerScore = (el) => {
|
|
2279
|
+
if (!(el instanceof HTMLElement)) return -1;
|
|
2280
|
+
const label = getLabel(el);
|
|
2281
|
+
const ariaLabel = normalize(el.getAttribute('aria-label') || '');
|
|
2282
|
+
const title = normalize(el.getAttribute('title') || '');
|
|
2283
|
+
const role = normalize(el.getAttribute('role') || '');
|
|
2284
|
+
const ariaHaspopup = normalize(el.getAttribute('aria-haspopup') || '');
|
|
2285
|
+
const ariaExpanded = normalize(el.getAttribute('aria-expanded') || '');
|
|
2286
|
+
const classes = normalize(el.className || '');
|
|
2287
|
+
let score = 0;
|
|
2288
|
+
if (/select model|current:/.test(ariaLabel)) score += 10;
|
|
2289
|
+
if (ariaHaspopup === 'dialog') score += 8;
|
|
2290
|
+
if (ariaHaspopup === 'listbox' || ariaHaspopup === 'menu') score += 7;
|
|
2291
|
+
if (role === 'combobox') score += 6;
|
|
2292
|
+
if (ariaExpanded === 'false' || ariaExpanded === 'true') score += 4;
|
|
2293
|
+
if (looksLikeModel(label)) score += 3;
|
|
2294
|
+
if (/model|popover|dropdown/.test(title + ' ' + classes)) score += 2;
|
|
2295
|
+
return score;
|
|
2296
|
+
};
|
|
2297
|
+
const activateTrigger = async (trigger) => {
|
|
2298
|
+
if (!(trigger instanceof HTMLElement)) return false;
|
|
2299
|
+
const tryClick = () => {
|
|
2300
|
+
trigger.focus?.();
|
|
2301
|
+
trigger.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, cancelable: true, pointerType: 'mouse' }));
|
|
2302
|
+
trigger.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }));
|
|
2303
|
+
trigger.dispatchEvent(new PointerEvent('pointerup', { bubbles: true, cancelable: true, pointerType: 'mouse' }));
|
|
2304
|
+
trigger.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true }));
|
|
2305
|
+
trigger.click?.();
|
|
2306
|
+
trigger.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
2307
|
+
};
|
|
2308
|
+
tryClick();
|
|
2309
|
+
for (const delay of [250, 450, 700]) {
|
|
2310
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
2311
|
+
if (hasOpenPicker()) return true;
|
|
2312
|
+
}
|
|
2313
|
+
if (trigger.parentElement instanceof HTMLElement) {
|
|
2314
|
+
try {
|
|
2315
|
+
const rect = trigger.getBoundingClientRect();
|
|
2316
|
+
const parentRect = trigger.parentElement.getBoundingClientRect();
|
|
2317
|
+
if (parentRect.width > rect.width && parentRect.height >= rect.height) {
|
|
2318
|
+
trigger.parentElement.click?.();
|
|
2319
|
+
trigger.parentElement.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
2320
|
+
}
|
|
2321
|
+
} catch {}
|
|
2322
|
+
for (const delay of [250, 450]) {
|
|
2323
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
2324
|
+
if (hasOpenPicker()) return true;
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
return hasOpenPicker();
|
|
2328
|
+
};
|
|
2329
|
+
const clickPossibleTrigger = async () => {
|
|
2330
|
+
const triggers = Array.from(document.querySelectorAll(triggerSelectors.join(',')))
|
|
2331
|
+
.filter(isVisible)
|
|
2332
|
+
.filter((el) => isModelTrigger(el))
|
|
2333
|
+
.sort((a, b) => triggerScore(b) - triggerScore(a));
|
|
2334
|
+
for (const trigger of triggers) {
|
|
2335
|
+
const opened = await activateTrigger(trigger);
|
|
2336
|
+
const candidateItems = collectModelItems();
|
|
2337
|
+
if (opened || candidateItems.length > 0) {
|
|
2338
|
+
return true;
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
return false;
|
|
2342
|
+
};
|
|
2343
|
+
|
|
2344
|
+
let modelItems = collectModelItems();
|
|
2345
|
+
if (modelItems.length === 0 || (modelItems.length <= 1 && !hasOpenPicker())) {
|
|
2346
|
+
await clickPossibleTrigger();
|
|
2347
|
+
modelItems = collectModelItems();
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
if (modelItems.length === 0 || (modelItems.length <= 1 && !hasOpenPicker())) {
|
|
2351
|
+
return {
|
|
2352
|
+
ok: false,
|
|
2353
|
+
error: 'Model list not found. The dropdown may not be open.',
|
|
2354
|
+
diagnostics: diagnostics(),
|
|
2355
|
+
};
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
const targetLabel = normalize(targetModel).toLowerCase();
|
|
2359
|
+
const targetItem = modelItems.find(({ label }) => {
|
|
2360
|
+
const text = normalize(label).toLowerCase();
|
|
2361
|
+
return text === targetLabel;
|
|
1541
2362
|
});
|
|
1542
|
-
|
|
2363
|
+
|
|
1543
2364
|
if (!targetItem) {
|
|
1544
|
-
const available = modelItems.map(
|
|
1545
|
-
return {
|
|
2365
|
+
const available = modelItems.map(({ label }) => normalize(label)).join(', ');
|
|
2366
|
+
return {
|
|
2367
|
+
ok: false,
|
|
2368
|
+
error: 'Model "' + targetModel + '" not found. Available: ' + available,
|
|
2369
|
+
diagnostics: diagnostics(),
|
|
2370
|
+
};
|
|
1546
2371
|
}
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
if (targetItem.className.includes('bg-gray-500/20') && !targetItem.className.includes('hover:bg-gray-500/20')) {
|
|
2372
|
+
|
|
2373
|
+
if (isSelected(targetItem.el) || getCurrentModelLabel()?.toLowerCase() === targetLabel) {
|
|
1550
2374
|
return { ok: true, model: targetModel, alreadySelected: true };
|
|
1551
2375
|
}
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
targetItem.click();
|
|
2376
|
+
|
|
2377
|
+
targetItem.el.click();
|
|
1555
2378
|
await new Promise(r => setTimeout(r, 500));
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
.filter(e => e.className.includes('px-2 py-1 flex items-center justify-between'));
|
|
1560
|
-
const selectedItem = updatedItems.find(el => {
|
|
1561
|
-
const text = (el.textContent || '').trim().replace(/New$/, '').trim();
|
|
1562
|
-
return text === targetModel || text.toLowerCase() === targetModel.toLowerCase();
|
|
1563
|
-
});
|
|
1564
|
-
|
|
1565
|
-
if (selectedItem && selectedItem.className.includes('bg-gray-500/20') && !selectedItem.className.includes('hover:bg-gray-500/20')) {
|
|
2379
|
+
|
|
2380
|
+
const currentModel = normalize(getCurrentModelLabel() || '').toLowerCase();
|
|
2381
|
+
if (currentModel === targetLabel) {
|
|
1566
2382
|
return { ok: true, model: targetModel, verified: true };
|
|
1567
2383
|
}
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
2384
|
+
|
|
2385
|
+
const updatedTarget = collectModelItems().find(({ label }) => normalize(label).toLowerCase() === targetLabel);
|
|
2386
|
+
if (updatedTarget && isSelected(updatedTarget.el)) {
|
|
2387
|
+
return { ok: true, model: targetModel, verified: true };
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
return {
|
|
2391
|
+
ok: false,
|
|
2392
|
+
error: 'Model click did not update selected state.',
|
|
2393
|
+
diagnostics: diagnostics(),
|
|
2394
|
+
};
|
|
1571
2395
|
})()`;
|
|
1572
2396
|
try {
|
|
1573
|
-
const
|
|
1574
|
-
const
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
2397
|
+
const contexts = this.getContexts();
|
|
2398
|
+
const contextIds = [
|
|
2399
|
+
this.getPrimaryContextId(),
|
|
2400
|
+
...contexts.map((ctx) => ctx.id),
|
|
2401
|
+
].filter((value, index, arr) => typeof value === 'number' && arr.indexOf(value) === index);
|
|
2402
|
+
const targets = contextIds.length > 0 ? contextIds : [null];
|
|
2403
|
+
let lastError = 'UI operation failed (setUiModel)';
|
|
2404
|
+
for (const contextId of targets) {
|
|
2405
|
+
logger_1.logger.debug(`[CdpService] setUiModel evaluating account=${this.accountName} context=${contextId ?? 'default'} target="${modelName}"`);
|
|
2406
|
+
const params = {
|
|
2407
|
+
expression,
|
|
2408
|
+
returnByValue: true,
|
|
2409
|
+
awaitPromise: true,
|
|
2410
|
+
};
|
|
2411
|
+
if (typeof contextId === 'number')
|
|
2412
|
+
params.contextId = contextId;
|
|
2413
|
+
const res = await this.call('Runtime.evaluate', params);
|
|
2414
|
+
const value = res?.result?.value;
|
|
2415
|
+
if (value?.ok) {
|
|
2416
|
+
logger_1.logger.info(`[CdpService] setUiModel result account=${this.accountName} context=${contextId ?? 'default'} ` +
|
|
2417
|
+
`target="${modelName}" applied="${value.model}" verified=${value.verified === true} ` +
|
|
2418
|
+
`alreadySelected=${value.alreadySelected === true}`);
|
|
2419
|
+
if (value.verified === false) {
|
|
2420
|
+
logger_1.logger.warn(`[CdpService] setUiModel verification did not confirm selection account=${this.accountName} ` +
|
|
2421
|
+
`context=${contextId ?? 'default'} target="${modelName}"`);
|
|
2422
|
+
}
|
|
2423
|
+
return {
|
|
2424
|
+
ok: true,
|
|
2425
|
+
model: value.model,
|
|
2426
|
+
verified: value.verified,
|
|
2427
|
+
alreadySelected: value.alreadySelected,
|
|
2428
|
+
};
|
|
2429
|
+
}
|
|
2430
|
+
if (value?.error) {
|
|
2431
|
+
if (value?.diagnostics) {
|
|
2432
|
+
logger_1.logger.warn(`[CdpService] setUiModel diagnostics account=${this.accountName} context=${contextId ?? 'default'} ` +
|
|
2433
|
+
`target="${modelName}" details=${value.diagnostics}`);
|
|
2434
|
+
}
|
|
2435
|
+
logger_1.logger.warn(`[CdpService] setUiModel context failure account=${this.accountName} context=${contextId ?? 'default'} ` +
|
|
2436
|
+
`target="${modelName}" error="${value.error}"`);
|
|
2437
|
+
lastError = value.error;
|
|
2438
|
+
}
|
|
1585
2439
|
}
|
|
1586
|
-
|
|
2440
|
+
logger_1.logger.warn(`[CdpService] setUiModel failed account=${this.accountName} target="${modelName}" error="${lastError}"`);
|
|
2441
|
+
return { ok: false, error: lastError };
|
|
1587
2442
|
}
|
|
1588
2443
|
catch (error) {
|
|
1589
|
-
|
|
2444
|
+
const errorMessage = error?.message || String(error);
|
|
2445
|
+
logger_1.logger.error(`[CdpService] setUiModel exception account=${this.accountName} target="${modelName}": ${errorMessage}`);
|
|
2446
|
+
return { ok: false, error: errorMessage };
|
|
1590
2447
|
}
|
|
1591
2448
|
}
|
|
1592
2449
|
}
|