remoat 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +297 -0
- package/dist/bin/cli.d.ts +2 -0
- package/dist/bin/cli.js +80 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/bin/commands/doctor.d.ts +1 -0
- package/dist/bin/commands/doctor.js +211 -0
- package/dist/bin/commands/doctor.js.map +1 -0
- package/dist/bin/commands/open.d.ts +1 -0
- package/dist/bin/commands/open.js +187 -0
- package/dist/bin/commands/open.js.map +1 -0
- package/dist/bin/commands/setup.d.ts +1 -0
- package/dist/bin/commands/setup.js +267 -0
- package/dist/bin/commands/setup.js.map +1 -0
- package/dist/bin/commands/start.d.ts +2 -0
- package/dist/bin/commands/start.js +39 -0
- package/dist/bin/commands/start.js.map +1 -0
- package/dist/bot/index.d.ts +2 -0
- package/dist/bot/index.js +1393 -0
- package/dist/bot/index.js.map +1 -0
- package/dist/commands/chatCommandHandler.d.ts +20 -0
- package/dist/commands/chatCommandHandler.js +30 -0
- package/dist/commands/chatCommandHandler.js.map +1 -0
- package/dist/commands/cleanupCommandHandler.d.ts +21 -0
- package/dist/commands/cleanupCommandHandler.js +40 -0
- package/dist/commands/cleanupCommandHandler.js.map +1 -0
- package/dist/commands/joinCommandHandler.d.ts +19 -0
- package/dist/commands/joinCommandHandler.js +27 -0
- package/dist/commands/joinCommandHandler.js.map +1 -0
- package/dist/commands/messageParser.d.ts +7 -0
- package/dist/commands/messageParser.js +29 -0
- package/dist/commands/messageParser.js.map +1 -0
- package/dist/commands/slashCommandHandler.d.ts +21 -0
- package/dist/commands/slashCommandHandler.js +105 -0
- package/dist/commands/slashCommandHandler.js.map +1 -0
- package/dist/commands/workspaceCommandHandler.d.ts +16 -0
- package/dist/commands/workspaceCommandHandler.js +29 -0
- package/dist/commands/workspaceCommandHandler.js.map +1 -0
- package/dist/database/chatSessionRepository.d.ts +59 -0
- package/dist/database/chatSessionRepository.js +110 -0
- package/dist/database/chatSessionRepository.js.map +1 -0
- package/dist/database/scheduleRepository.d.ts +60 -0
- package/dist/database/scheduleRepository.js +106 -0
- package/dist/database/scheduleRepository.js.map +1 -0
- package/dist/database/templateRepository.d.ts +51 -0
- package/dist/database/templateRepository.js +90 -0
- package/dist/database/templateRepository.js.map +1 -0
- package/dist/database/workspaceBindingRepository.d.ts +48 -0
- package/dist/database/workspaceBindingRepository.js +92 -0
- package/dist/database/workspaceBindingRepository.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/auth.d.ts +5 -0
- package/dist/middleware/auth.js +14 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/sanitize.d.ts +1 -0
- package/dist/middleware/sanitize.js +18 -0
- package/dist/middleware/sanitize.js.map +1 -0
- package/dist/services/antigravityLauncher.d.ts +7 -0
- package/dist/services/antigravityLauncher.js +94 -0
- package/dist/services/antigravityLauncher.js.map +1 -0
- package/dist/services/approvalDetector.d.ts +97 -0
- package/dist/services/approvalDetector.js +394 -0
- package/dist/services/approvalDetector.js.map +1 -0
- package/dist/services/assistantDomExtractor.d.ts +49 -0
- package/dist/services/assistantDomExtractor.js +340 -0
- package/dist/services/assistantDomExtractor.js.map +1 -0
- package/dist/services/autoAcceptService.d.ts +14 -0
- package/dist/services/autoAcceptService.js +81 -0
- package/dist/services/autoAcceptService.js.map +1 -0
- package/dist/services/cdpBridgeManager.d.ts +50 -0
- package/dist/services/cdpBridgeManager.js +355 -0
- package/dist/services/cdpBridgeManager.js.map +1 -0
- package/dist/services/cdpConnectionPool.d.ts +88 -0
- package/dist/services/cdpConnectionPool.js +235 -0
- package/dist/services/cdpConnectionPool.js.map +1 -0
- package/dist/services/cdpService.d.ts +214 -0
- package/dist/services/cdpService.js +1423 -0
- package/dist/services/cdpService.js.map +1 -0
- package/dist/services/chatSessionService.d.ts +89 -0
- package/dist/services/chatSessionService.js +738 -0
- package/dist/services/chatSessionService.js.map +1 -0
- package/dist/services/errorPopupDetector.d.ts +89 -0
- package/dist/services/errorPopupDetector.js +274 -0
- package/dist/services/errorPopupDetector.js.map +1 -0
- package/dist/services/modeService.d.ts +44 -0
- package/dist/services/modeService.js +74 -0
- package/dist/services/modeService.js.map +1 -0
- package/dist/services/modelService.d.ts +36 -0
- package/dist/services/modelService.js +64 -0
- package/dist/services/modelService.js.map +1 -0
- package/dist/services/planningDetector.d.ts +87 -0
- package/dist/services/planningDetector.js +321 -0
- package/dist/services/planningDetector.js.map +1 -0
- package/dist/services/processManager.d.ts +18 -0
- package/dist/services/processManager.js +62 -0
- package/dist/services/processManager.js.map +1 -0
- package/dist/services/progressSender.d.ts +20 -0
- package/dist/services/progressSender.js +65 -0
- package/dist/services/progressSender.js.map +1 -0
- package/dist/services/promptDispatcher.d.ts +38 -0
- package/dist/services/promptDispatcher.js +42 -0
- package/dist/services/promptDispatcher.js.map +1 -0
- package/dist/services/quotaService.d.ts +21 -0
- package/dist/services/quotaService.js +191 -0
- package/dist/services/quotaService.js.map +1 -0
- package/dist/services/responseMonitor.d.ts +129 -0
- package/dist/services/responseMonitor.js +996 -0
- package/dist/services/responseMonitor.js.map +1 -0
- package/dist/services/scheduleService.d.ts +58 -0
- package/dist/services/scheduleService.js +135 -0
- package/dist/services/scheduleService.js.map +1 -0
- package/dist/services/screenshotService.d.ts +55 -0
- package/dist/services/screenshotService.js +86 -0
- package/dist/services/screenshotService.js.map +1 -0
- package/dist/services/telegramTopicManager.d.ts +40 -0
- package/dist/services/telegramTopicManager.js +103 -0
- package/dist/services/telegramTopicManager.js.map +1 -0
- package/dist/services/titleGeneratorService.d.ts +32 -0
- package/dist/services/titleGeneratorService.js +114 -0
- package/dist/services/titleGeneratorService.js.map +1 -0
- package/dist/services/updateCheckService.d.ts +16 -0
- package/dist/services/updateCheckService.js +148 -0
- package/dist/services/updateCheckService.js.map +1 -0
- package/dist/services/userMessageDetector.d.ts +57 -0
- package/dist/services/userMessageDetector.js +222 -0
- package/dist/services/userMessageDetector.js.map +1 -0
- package/dist/services/workspaceService.d.ts +33 -0
- package/dist/services/workspaceService.js +65 -0
- package/dist/services/workspaceService.js.map +1 -0
- package/dist/ui/autoAcceptUi.d.ts +6 -0
- package/dist/ui/autoAcceptUi.js +22 -0
- package/dist/ui/autoAcceptUi.js.map +1 -0
- package/dist/ui/modeUi.d.ts +12 -0
- package/dist/ui/modeUi.js +40 -0
- package/dist/ui/modeUi.js.map +1 -0
- package/dist/ui/modelsUi.d.ts +12 -0
- package/dist/ui/modelsUi.js +101 -0
- package/dist/ui/modelsUi.js.map +1 -0
- package/dist/ui/projectListUi.d.ts +11 -0
- package/dist/ui/projectListUi.js +59 -0
- package/dist/ui/projectListUi.js.map +1 -0
- package/dist/ui/screenshotUi.d.ts +6 -0
- package/dist/ui/screenshotUi.js +28 -0
- package/dist/ui/screenshotUi.js.map +1 -0
- package/dist/ui/sessionPickerUi.d.ts +8 -0
- package/dist/ui/sessionPickerUi.js +32 -0
- package/dist/ui/sessionPickerUi.js.map +1 -0
- package/dist/ui/templateUi.d.ts +5 -0
- package/dist/ui/templateUi.js +44 -0
- package/dist/ui/templateUi.js.map +1 -0
- package/dist/utils/cdpPorts.d.ts +2 -0
- package/dist/utils/cdpPorts.js +6 -0
- package/dist/utils/cdpPorts.js.map +1 -0
- package/dist/utils/config.d.ts +14 -0
- package/dist/utils/config.js +12 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/configLoader.d.ts +23 -0
- package/dist/utils/configLoader.js +153 -0
- package/dist/utils/configLoader.js.map +1 -0
- package/dist/utils/htmlToTelegramMarkdown.d.ts +6 -0
- package/dist/utils/htmlToTelegramMarkdown.js +189 -0
- package/dist/utils/htmlToTelegramMarkdown.js.map +1 -0
- package/dist/utils/i18n.d.ts +3 -0
- package/dist/utils/i18n.js +78 -0
- package/dist/utils/i18n.js.map +1 -0
- package/dist/utils/imageHandler.d.ts +35 -0
- package/dist/utils/imageHandler.js +155 -0
- package/dist/utils/imageHandler.js.map +1 -0
- package/dist/utils/lockfile.d.ts +7 -0
- package/dist/utils/lockfile.js +117 -0
- package/dist/utils/lockfile.js.map +1 -0
- package/dist/utils/logger.d.ts +23 -0
- package/dist/utils/logger.js +85 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/logo.d.ts +1 -0
- package/dist/utils/logo.js +14 -0
- package/dist/utils/logo.js.map +1 -0
- package/dist/utils/metadataExtractor.d.ts +5 -0
- package/dist/utils/metadataExtractor.js +16 -0
- package/dist/utils/metadataExtractor.js.map +1 -0
- package/dist/utils/pathUtils.d.ts +23 -0
- package/dist/utils/pathUtils.js +58 -0
- package/dist/utils/pathUtils.js.map +1 -0
- package/dist/utils/processLogBuffer.d.ts +17 -0
- package/dist/utils/processLogBuffer.js +108 -0
- package/dist/utils/processLogBuffer.js.map +1 -0
- package/dist/utils/streamMessageFormatter.d.ts +18 -0
- package/dist/utils/streamMessageFormatter.js +91 -0
- package/dist/utils/streamMessageFormatter.js.map +1 -0
- package/dist/utils/telegramFormatter.d.ts +37 -0
- package/dist/utils/telegramFormatter.js +445 -0
- package/dist/utils/telegramFormatter.js.map +1 -0
- package/dist/utils/voiceHandler.d.ts +23 -0
- package/dist/utils/voiceHandler.js +169 -0
- package/dist/utils/voiceHandler.js.map +1 -0
- package/locales/en.json +85 -0
- package/locales/ja.json +109 -0
- package/package.json +84 -0
|
@@ -0,0 +1,738 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ChatSessionService = void 0;
|
|
4
|
+
/** Script to get the state of the new chat button */
|
|
5
|
+
const GET_NEW_CHAT_BUTTON_SCRIPT = `(() => {
|
|
6
|
+
const btn = document.querySelector('[data-tooltip-id="new-conversation-tooltip"]');
|
|
7
|
+
if (!btn) return { found: false };
|
|
8
|
+
const cursor = window.getComputedStyle(btn).cursor;
|
|
9
|
+
const rect = btn.getBoundingClientRect();
|
|
10
|
+
return {
|
|
11
|
+
found: true,
|
|
12
|
+
enabled: cursor === 'pointer',
|
|
13
|
+
cursor,
|
|
14
|
+
x: Math.round(rect.x + rect.width / 2),
|
|
15
|
+
y: Math.round(rect.y + rect.height / 2),
|
|
16
|
+
};
|
|
17
|
+
})()`;
|
|
18
|
+
/**
|
|
19
|
+
* Script to get the chat title from the Cascade panel header.
|
|
20
|
+
* The title element is a div with the text-ellipsis class inside the header.
|
|
21
|
+
*/
|
|
22
|
+
const GET_CHAT_TITLE_SCRIPT = `(() => {
|
|
23
|
+
const panel = document.querySelector('.antigravity-agent-side-panel');
|
|
24
|
+
if (!panel) return { title: '', hasActiveChat: false };
|
|
25
|
+
const header = panel.querySelector('div[class*="border-b"]');
|
|
26
|
+
if (!header) return { title: '', hasActiveChat: false };
|
|
27
|
+
const titleEl = header.querySelector('div[class*="text-ellipsis"]');
|
|
28
|
+
const title = titleEl ? (titleEl.textContent || '').trim() : '';
|
|
29
|
+
// "Agent" is the default empty chat title
|
|
30
|
+
const hasActiveChat = title.length > 0 && title !== 'Agent';
|
|
31
|
+
return { title: title || '(Untitled)', hasActiveChat };
|
|
32
|
+
})()`;
|
|
33
|
+
/**
|
|
34
|
+
* Script to find the Past Conversations button and return its coordinates.
|
|
35
|
+
* We use coordinates so that the actual click is done via CDP Input.dispatchMouseEvent,
|
|
36
|
+
* which works reliably in Electron (DOM .click() can be ignored).
|
|
37
|
+
*
|
|
38
|
+
* Returns: { found: boolean, x: number, y: number }
|
|
39
|
+
*/
|
|
40
|
+
const FIND_PAST_CONVERSATIONS_BUTTON_SCRIPT = `(() => {
|
|
41
|
+
const isVisible = (el) => !!el && el instanceof HTMLElement && el.offsetParent !== null;
|
|
42
|
+
const getRect = (el) => {
|
|
43
|
+
const rect = el.getBoundingClientRect();
|
|
44
|
+
return { found: true, x: Math.round(rect.x + rect.width / 2), y: Math.round(rect.y + rect.height / 2) };
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Strategy 1 (primary): data-past-conversations-toggle attribute
|
|
48
|
+
const toggle = document.querySelector('[data-past-conversations-toggle]');
|
|
49
|
+
if (toggle && isVisible(toggle)) return getRect(toggle);
|
|
50
|
+
|
|
51
|
+
// Strategy 2: data-tooltip-id containing "history"
|
|
52
|
+
const tooltipEls = Array.from(document.querySelectorAll('[data-tooltip-id]'));
|
|
53
|
+
for (const el of tooltipEls) {
|
|
54
|
+
if (!isVisible(el)) continue;
|
|
55
|
+
const tid = (el.getAttribute('data-tooltip-id') || '').toLowerCase();
|
|
56
|
+
if (tid.includes('history') || tid.includes('past-conversations')) {
|
|
57
|
+
return getRect(el);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Strategy 3: SVG with lucide-history class
|
|
62
|
+
const icons = Array.from(document.querySelectorAll('svg.lucide-history, svg[class*="lucide-history"]'));
|
|
63
|
+
for (const icon of icons) {
|
|
64
|
+
const parent = icon.closest('a, button, [role="button"], div[class*="cursor-pointer"]');
|
|
65
|
+
const target = parent instanceof HTMLElement && isVisible(parent) ? parent : icon;
|
|
66
|
+
if (isVisible(target)) return getRect(target);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { found: false, x: 0, y: 0 };
|
|
70
|
+
})()`;
|
|
71
|
+
/**
|
|
72
|
+
* Script to scrape session items from the open Past Conversations panel.
|
|
73
|
+
* Expects the panel to already be visible.
|
|
74
|
+
*
|
|
75
|
+
* Returns: { sessions: SessionListItem[] }
|
|
76
|
+
*/
|
|
77
|
+
const SCRAPE_PAST_CONVERSATIONS_SCRIPT = `(() => {
|
|
78
|
+
const isVisible = (el) => !!el && el instanceof HTMLElement && el.offsetParent !== null;
|
|
79
|
+
const normalize = (text) => (text || '').trim();
|
|
80
|
+
|
|
81
|
+
const items = [];
|
|
82
|
+
const seen = new Set();
|
|
83
|
+
|
|
84
|
+
// Find the scrollable conversation list container
|
|
85
|
+
const containers = Array.from(document.querySelectorAll('div[class*="overflow-auto"], div[class*="overflow-y-scroll"]'));
|
|
86
|
+
const container = containers.find((c) => isVisible(c) && c.querySelectorAll('div[class*="cursor-pointer"]').length > 0) || document;
|
|
87
|
+
|
|
88
|
+
// Detect the "Other Conversations" section boundary.
|
|
89
|
+
// Sessions below this header belong to other projects and must be excluded.
|
|
90
|
+
let boundaryTop = Infinity;
|
|
91
|
+
const headerCandidates = container.querySelectorAll('div[class*="text-xs"][class*="opacity"]');
|
|
92
|
+
for (const el of headerCandidates) {
|
|
93
|
+
if (!isVisible(el)) continue;
|
|
94
|
+
const t = normalize(el.textContent || '');
|
|
95
|
+
if (/^Other\\s+Conversations?$/i.test(t)) {
|
|
96
|
+
boundaryTop = el.getBoundingClientRect().top;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Each session row is a div with cursor-pointer
|
|
102
|
+
const rows = Array.from(container.querySelectorAll('div[class*="cursor-pointer"]'));
|
|
103
|
+
for (const row of rows) {
|
|
104
|
+
if (!isVisible(row)) continue;
|
|
105
|
+
// Skip rows that are below the "Other Conversations" boundary
|
|
106
|
+
if (row.getBoundingClientRect().top >= boundaryTop) continue;
|
|
107
|
+
// Find the session title — nested span within the row
|
|
108
|
+
const spans = Array.from(row.querySelectorAll('span.text-sm span, span.text-sm'));
|
|
109
|
+
let title = '';
|
|
110
|
+
for (const span of spans) {
|
|
111
|
+
const t = normalize(span.textContent || '');
|
|
112
|
+
// Skip timestamp labels like "1 hr ago", "7 mins ago"
|
|
113
|
+
if (/^\\d+\\s+(min|hr|hour|day|sec|week|month|year)s?\\s+ago$/i.test(t)) continue;
|
|
114
|
+
// Skip very short or action-like labels
|
|
115
|
+
if (t.length < 2 || t.length > 200) continue;
|
|
116
|
+
if (/^(show\\s+\\d+\\s+more|new|past|history|settings|close|menu)\\b/i.test(t)) continue;
|
|
117
|
+
title = t;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
if (!title || seen.has(title)) continue;
|
|
121
|
+
seen.add(title);
|
|
122
|
+
// Detect if this is the active/current session (has focusBackground class)
|
|
123
|
+
const isActive = /focusBackground/i.test(row.className || '');
|
|
124
|
+
items.push({ title, isActive });
|
|
125
|
+
}
|
|
126
|
+
return { sessions: items };
|
|
127
|
+
})()`;
|
|
128
|
+
/**
|
|
129
|
+
* Script to find the "Show N more..." link and return its coordinates.
|
|
130
|
+
* Returns: { found: boolean, x: number, y: number }
|
|
131
|
+
*/
|
|
132
|
+
const FIND_SHOW_MORE_BUTTON_SCRIPT = `(() => {
|
|
133
|
+
const isVisible = (el) => !!el && el instanceof HTMLElement && el.offsetParent !== null;
|
|
134
|
+
const els = Array.from(document.querySelectorAll('div, span'));
|
|
135
|
+
for (const el of els) {
|
|
136
|
+
if (!isVisible(el)) continue;
|
|
137
|
+
const text = (el.textContent || '').trim();
|
|
138
|
+
if (/^Show\\s+\\d+\\s+more/i.test(text)) {
|
|
139
|
+
const rect = el.getBoundingClientRect();
|
|
140
|
+
return { found: true, x: Math.round(rect.x + rect.width / 2), y: Math.round(rect.y + rect.height / 2) };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return { found: false, x: 0, y: 0 };
|
|
144
|
+
})()`;
|
|
145
|
+
/**
|
|
146
|
+
* Build a script that activates an existing chat in the side panel by its title.
|
|
147
|
+
* Uses broad selector fallbacks because Antigravity's DOM structure can vary across versions.
|
|
148
|
+
*/
|
|
149
|
+
function buildActivateChatByTitleScript(title) {
|
|
150
|
+
const safeTitle = JSON.stringify(title);
|
|
151
|
+
return `(() => {
|
|
152
|
+
const wantedRaw = ${safeTitle};
|
|
153
|
+
const wanted = (wantedRaw || '').toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
154
|
+
if (!wanted) return { ok: false, error: 'Empty target title' };
|
|
155
|
+
|
|
156
|
+
const panel = document.querySelector('.antigravity-agent-side-panel') || document;
|
|
157
|
+
const normalize = (text) => (text || '').toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
158
|
+
const isVisible = (el) => !!el && el instanceof HTMLElement && el.offsetParent !== null;
|
|
159
|
+
const clickTarget = (el) => {
|
|
160
|
+
const clickable = el.closest('button, [role="button"], a, li, [data-testid*="conversation"]') || el;
|
|
161
|
+
if (!(clickable instanceof HTMLElement)) return false;
|
|
162
|
+
clickable.click();
|
|
163
|
+
return true;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const nodes = Array.from(panel.querySelectorAll('button, [role="button"], a, li, div, span'))
|
|
167
|
+
.filter(isVisible);
|
|
168
|
+
|
|
169
|
+
const exact = [];
|
|
170
|
+
const includes = [];
|
|
171
|
+
for (const node of nodes) {
|
|
172
|
+
const text = normalize(node.textContent || '');
|
|
173
|
+
if (!text) continue;
|
|
174
|
+
if (text === wanted) {
|
|
175
|
+
exact.push({ node, textLength: text.length });
|
|
176
|
+
} else if (text.includes(wanted)) {
|
|
177
|
+
includes.push({ node, textLength: text.length });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const pick = (list) => {
|
|
182
|
+
if (list.length === 0) return null;
|
|
183
|
+
list.sort((a, b) => a.textLength - b.textLength);
|
|
184
|
+
return list[0].node;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const target = pick(exact) || pick(includes);
|
|
188
|
+
if (!target) return { ok: false, error: 'Chat title not found in side panel' };
|
|
189
|
+
if (!clickTarget(target)) return { ok: false, error: 'Matched element is not clickable' };
|
|
190
|
+
return { ok: true };
|
|
191
|
+
})()`;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Build a script that opens Past Conversations and selects a conversation by title.
|
|
195
|
+
* This path is required for older chats that are not visible in the current side panel.
|
|
196
|
+
*/
|
|
197
|
+
function buildActivateViaPastConversationsScript(title) {
|
|
198
|
+
const safeTitle = JSON.stringify(title);
|
|
199
|
+
return `(() => {
|
|
200
|
+
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
201
|
+
const wantedRaw = ${safeTitle};
|
|
202
|
+
const normalize = (text) => (text || '')
|
|
203
|
+
.normalize('NFKC')
|
|
204
|
+
.toLowerCase()
|
|
205
|
+
.replace(/[\\u2018\\u2019\\u201C\\u201D'"\`]/g, '')
|
|
206
|
+
.replace(/\\s+/g, ' ')
|
|
207
|
+
.trim();
|
|
208
|
+
const normalizeLoose = (text) => normalize(text).replace(/[^a-z0-9\\u3040-\\u30ff\\u4e00-\\u9faf\\s]/g, '').replace(/\\s+/g, ' ').trim();
|
|
209
|
+
|
|
210
|
+
const wanted = normalize(wantedRaw || '');
|
|
211
|
+
const wantedLoose = normalizeLoose(wantedRaw || '');
|
|
212
|
+
if (!wanted) return { ok: false, error: 'Empty target title' };
|
|
213
|
+
|
|
214
|
+
const isVisible = (el) => !!el && el instanceof HTMLElement && el.offsetParent !== null;
|
|
215
|
+
const asArray = (nodeList) => Array.from(nodeList || []);
|
|
216
|
+
const getLabelText = (el) => {
|
|
217
|
+
if (!el || !(el instanceof Element)) return '';
|
|
218
|
+
const parts = [
|
|
219
|
+
el.textContent || '',
|
|
220
|
+
el.getAttribute('aria-label') || '',
|
|
221
|
+
el.getAttribute('title') || '',
|
|
222
|
+
el.getAttribute('placeholder') || '',
|
|
223
|
+
el.getAttribute('data-tooltip-content') || '',
|
|
224
|
+
el.getAttribute('data-testid') || '',
|
|
225
|
+
];
|
|
226
|
+
return parts.filter(Boolean).join(' ');
|
|
227
|
+
};
|
|
228
|
+
const getClickable = (el) => {
|
|
229
|
+
if (!el || !(el instanceof Element)) return null;
|
|
230
|
+
const clickable = el.closest('button, [role="button"], a, li, [role="option"], [data-testid*="conversation"]');
|
|
231
|
+
return clickable instanceof HTMLElement ? clickable : (el instanceof HTMLElement ? el : null);
|
|
232
|
+
};
|
|
233
|
+
const pickBest = (elements, patterns) => {
|
|
234
|
+
const matched = [];
|
|
235
|
+
for (const el of elements) {
|
|
236
|
+
if (!isVisible(el)) continue;
|
|
237
|
+
const text = normalize(getLabelText(el));
|
|
238
|
+
const textLoose = normalizeLoose(getLabelText(el));
|
|
239
|
+
if (!text) continue;
|
|
240
|
+
for (const pattern of patterns) {
|
|
241
|
+
if (!pattern) continue;
|
|
242
|
+
const p = normalize(pattern);
|
|
243
|
+
const pLoose = normalizeLoose(pattern);
|
|
244
|
+
if (
|
|
245
|
+
text === p ||
|
|
246
|
+
text.includes(p) ||
|
|
247
|
+
(pLoose && (textLoose === pLoose || textLoose.includes(pLoose)))
|
|
248
|
+
) {
|
|
249
|
+
matched.push({ el, score: Math.abs(text.length - pattern.length) });
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (matched.length === 0) return null;
|
|
255
|
+
matched.sort((a, b) => a.score - b.score);
|
|
256
|
+
return matched[0].el;
|
|
257
|
+
};
|
|
258
|
+
const clickByPatterns = (patterns, selector) => {
|
|
259
|
+
const nodes = asArray(document.querySelectorAll('button, [role="button"], a, li, div, span'));
|
|
260
|
+
const scopedNodes = selector ? asArray(document.querySelectorAll(selector)) : [];
|
|
261
|
+
const source = scopedNodes.length > 0 ? scopedNodes : nodes;
|
|
262
|
+
const target = pickBest(source, patterns);
|
|
263
|
+
const clickable = getClickable(target);
|
|
264
|
+
if (!clickable) return false;
|
|
265
|
+
clickable.click();
|
|
266
|
+
return true;
|
|
267
|
+
};
|
|
268
|
+
const setInputValue = (el, value) => {
|
|
269
|
+
if (!el) return false;
|
|
270
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
271
|
+
el.focus();
|
|
272
|
+
el.value = value;
|
|
273
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
274
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
if (el instanceof HTMLElement) {
|
|
278
|
+
el.focus();
|
|
279
|
+
if (el.isContentEditable) {
|
|
280
|
+
el.textContent = value;
|
|
281
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return false;
|
|
286
|
+
};
|
|
287
|
+
const clickIconHistoryButton = () => {
|
|
288
|
+
const iconTargets = asArray(document.querySelectorAll('svg, i, span, div'));
|
|
289
|
+
const patterns = ['history', 'clock', 'conversation', 'past'];
|
|
290
|
+
for (const icon of iconTargets) {
|
|
291
|
+
const descriptor = normalize([
|
|
292
|
+
icon.getAttribute?.('class') || '',
|
|
293
|
+
icon.getAttribute?.('data-testid') || '',
|
|
294
|
+
icon.getAttribute?.('data-icon') || '',
|
|
295
|
+
icon.getAttribute?.('aria-label') || '',
|
|
296
|
+
icon.getAttribute?.('title') || '',
|
|
297
|
+
icon.getAttribute?.('data-tooltip-id') || '',
|
|
298
|
+
].join(' '));
|
|
299
|
+
if (!descriptor) continue;
|
|
300
|
+
if (!patterns.some((p) => descriptor.includes(p))) continue;
|
|
301
|
+
const clickable = getClickable(icon);
|
|
302
|
+
if (clickable && isVisible(clickable)) {
|
|
303
|
+
clickable.click();
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return false;
|
|
308
|
+
};
|
|
309
|
+
const openMenuThenClickPast = async () => {
|
|
310
|
+
const openedMenu = clickByPatterns(
|
|
311
|
+
['more', 'options', 'menu', 'actions', '...', 'ellipsis', '設定', '操作'],
|
|
312
|
+
'button[aria-haspopup], [role="button"][aria-haspopup], button, [role="button"]',
|
|
313
|
+
);
|
|
314
|
+
if (!openedMenu) return false;
|
|
315
|
+
await wait(180);
|
|
316
|
+
return clickByPatterns([
|
|
317
|
+
'past conversations',
|
|
318
|
+
'past conversation',
|
|
319
|
+
'conversation history',
|
|
320
|
+
'past chats',
|
|
321
|
+
'過去の会話',
|
|
322
|
+
'chat history',
|
|
323
|
+
], '[role="menuitem"], [role="option"], button, [role="button"], li, div, span');
|
|
324
|
+
};
|
|
325
|
+
const pressEnter = (el) => {
|
|
326
|
+
if (!(el instanceof HTMLElement)) return;
|
|
327
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', bubbles: true }));
|
|
328
|
+
el.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', code: 'Enter', bubbles: true }));
|
|
329
|
+
};
|
|
330
|
+
const findSearchInput = () => {
|
|
331
|
+
const inputs = asArray(document.querySelectorAll('input, textarea, [role="combobox"], [role="searchbox"], [contenteditable="true"]'));
|
|
332
|
+
const strongPatterns = ['select a conversation', 'search conversation', 'search chats', 'search'];
|
|
333
|
+
const placeholders = [];
|
|
334
|
+
for (const el of inputs) {
|
|
335
|
+
if (!isVisible(el)) continue;
|
|
336
|
+
const placeholder = normalize(el.getAttribute('placeholder') || '');
|
|
337
|
+
const ariaLabel = normalize(el.getAttribute('aria-label') || '');
|
|
338
|
+
const text = normalize(getLabelText(el));
|
|
339
|
+
const combined = [placeholder, ariaLabel, text].filter(Boolean).join(' ');
|
|
340
|
+
placeholders.push({ el, combined });
|
|
341
|
+
}
|
|
342
|
+
for (const p of strongPatterns) {
|
|
343
|
+
const found = placeholders.find((x) => x.combined.includes(p));
|
|
344
|
+
if (found) return found.el;
|
|
345
|
+
}
|
|
346
|
+
return placeholders[0]?.el || null;
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
return (async () => {
|
|
350
|
+
// Primary: click via data-past-conversations-toggle attribute
|
|
351
|
+
let opened = false;
|
|
352
|
+
const toggleBtn = document.querySelector('[data-past-conversations-toggle]');
|
|
353
|
+
if (toggleBtn && isVisible(toggleBtn)) {
|
|
354
|
+
const clickable = getClickable(toggleBtn);
|
|
355
|
+
if (clickable) { clickable.click(); opened = true; }
|
|
356
|
+
}
|
|
357
|
+
if (!opened) {
|
|
358
|
+
// Fallback: data-tooltip-id containing "history"
|
|
359
|
+
const tooltipEls = asArray(document.querySelectorAll('[data-tooltip-id]'));
|
|
360
|
+
for (const el of tooltipEls) {
|
|
361
|
+
if (!isVisible(el)) continue;
|
|
362
|
+
const tid = normalize(el.getAttribute('data-tooltip-id') || '');
|
|
363
|
+
if (tid.includes('history') || tid.includes('past-conversations')) {
|
|
364
|
+
const cl = getClickable(el);
|
|
365
|
+
if (cl) { cl.click(); opened = true; break; }
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (!opened) {
|
|
370
|
+
opened = clickByPatterns([
|
|
371
|
+
'past conversations',
|
|
372
|
+
'past conversation',
|
|
373
|
+
'conversation history',
|
|
374
|
+
'past chats',
|
|
375
|
+
'過去の会話',
|
|
376
|
+
'chat history',
|
|
377
|
+
]);
|
|
378
|
+
}
|
|
379
|
+
if (!opened) {
|
|
380
|
+
opened = clickIconHistoryButton();
|
|
381
|
+
}
|
|
382
|
+
if (!opened) {
|
|
383
|
+
opened = await openMenuThenClickPast();
|
|
384
|
+
}
|
|
385
|
+
if (!opened) {
|
|
386
|
+
return { ok: false, error: 'Past Conversations button not found' };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
await wait(320);
|
|
390
|
+
|
|
391
|
+
// In some UI states "Select a conversation" itself is a trigger.
|
|
392
|
+
clickByPatterns(['select a conversation', 'select conversation', 'conversation'], '[role="button"], button, [aria-haspopup], [data-testid*="conversation"]');
|
|
393
|
+
await wait(220);
|
|
394
|
+
|
|
395
|
+
const input = findSearchInput();
|
|
396
|
+
if (input) {
|
|
397
|
+
setInputValue(input, wantedRaw);
|
|
398
|
+
await wait(260);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
let selected = clickByPatterns([wanted, wantedLoose], '[role="option"], li, button, [data-testid*="conversation"]');
|
|
402
|
+
if (!selected && input) {
|
|
403
|
+
pressEnter(input);
|
|
404
|
+
await wait(220);
|
|
405
|
+
selected = true;
|
|
406
|
+
}
|
|
407
|
+
if (!selected) {
|
|
408
|
+
return { ok: false, error: 'Conversation not found in Past Conversations' };
|
|
409
|
+
}
|
|
410
|
+
return { ok: true };
|
|
411
|
+
})();
|
|
412
|
+
})()`;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Service for managing chat sessions on Antigravity via CDP.
|
|
416
|
+
*
|
|
417
|
+
* CDP dependencies are received as method arguments (connection pool compatible).
|
|
418
|
+
*/
|
|
419
|
+
class ChatSessionService {
|
|
420
|
+
static ACTIVATE_SESSION_MAX_WAIT_MS = 30000;
|
|
421
|
+
static ACTIVATE_SESSION_RETRY_INTERVAL_MS = 800;
|
|
422
|
+
static LIST_SESSIONS_TARGET = 20;
|
|
423
|
+
/**
|
|
424
|
+
* List recent sessions by opening the Past Conversations panel.
|
|
425
|
+
*
|
|
426
|
+
* Flow (all clicks via CDP Input.dispatchMouseEvent for Electron compatibility):
|
|
427
|
+
* 1. Find Past Conversations button coordinates
|
|
428
|
+
* 2. Click it via CDP mouse events
|
|
429
|
+
* 3. Wait for panel to render
|
|
430
|
+
* 4. Scrape visible sessions
|
|
431
|
+
* 5. If < TARGET sessions, find & click "Show N more..."
|
|
432
|
+
* 6. Re-scrape
|
|
433
|
+
* 7. Close panel with Escape key
|
|
434
|
+
*
|
|
435
|
+
* @param cdpService CdpService instance to use
|
|
436
|
+
* @returns Array of session list items (empty array on failure)
|
|
437
|
+
*/
|
|
438
|
+
async listAllSessions(cdpService) {
|
|
439
|
+
try {
|
|
440
|
+
// Step 1: Find Past Conversations button
|
|
441
|
+
const btnState = await this.evaluateOnAnyContext(cdpService, FIND_PAST_CONVERSATIONS_BUTTON_SCRIPT, false);
|
|
442
|
+
if (!btnState?.found) {
|
|
443
|
+
return [];
|
|
444
|
+
}
|
|
445
|
+
// Step 2: Click via CDP mouse events (reliable in Electron)
|
|
446
|
+
await this.cdpMouseClick(cdpService, btnState.x, btnState.y);
|
|
447
|
+
// Step 3: Wait for panel to render
|
|
448
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
449
|
+
// Step 4: Scrape sessions
|
|
450
|
+
let scrapeResult = await this.evaluateOnAnyContext(cdpService, SCRAPE_PAST_CONVERSATIONS_SCRIPT, false);
|
|
451
|
+
let sessions = scrapeResult?.sessions ?? [];
|
|
452
|
+
// Step 5: If fewer than TARGET, click "Show N more..."
|
|
453
|
+
if (sessions.length < ChatSessionService.LIST_SESSIONS_TARGET) {
|
|
454
|
+
const showMoreState = await this.evaluateOnAnyContext(cdpService, FIND_SHOW_MORE_BUTTON_SCRIPT, false);
|
|
455
|
+
if (showMoreState?.found) {
|
|
456
|
+
await this.cdpMouseClick(cdpService, showMoreState.x, showMoreState.y);
|
|
457
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
458
|
+
// Step 6: Re-scrape
|
|
459
|
+
scrapeResult = await this.evaluateOnAnyContext(cdpService, SCRAPE_PAST_CONVERSATIONS_SCRIPT, false);
|
|
460
|
+
sessions = scrapeResult?.sessions ?? [];
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
// Step 7: Close panel with Escape
|
|
464
|
+
await cdpService.call('Input.dispatchKeyEvent', {
|
|
465
|
+
type: 'keyDown', key: 'Escape', code: 'Escape',
|
|
466
|
+
windowsVirtualKeyCode: 27, nativeVirtualKeyCode: 27,
|
|
467
|
+
});
|
|
468
|
+
await cdpService.call('Input.dispatchKeyEvent', {
|
|
469
|
+
type: 'keyUp', key: 'Escape', code: 'Escape',
|
|
470
|
+
windowsVirtualKeyCode: 27, nativeVirtualKeyCode: 27,
|
|
471
|
+
});
|
|
472
|
+
return sessions.slice(0, ChatSessionService.LIST_SESSIONS_TARGET);
|
|
473
|
+
}
|
|
474
|
+
catch (_) {
|
|
475
|
+
return [];
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Evaluate a script on the first context that returns a truthy value.
|
|
480
|
+
*/
|
|
481
|
+
async evaluateOnAnyContext(cdpService, expression, awaitPromise) {
|
|
482
|
+
const contexts = cdpService.getContexts();
|
|
483
|
+
for (const ctx of contexts) {
|
|
484
|
+
try {
|
|
485
|
+
const result = await cdpService.call('Runtime.evaluate', {
|
|
486
|
+
expression, returnByValue: true, awaitPromise, contextId: ctx.id,
|
|
487
|
+
});
|
|
488
|
+
const value = result?.result?.value;
|
|
489
|
+
if (value)
|
|
490
|
+
return value;
|
|
491
|
+
}
|
|
492
|
+
catch (_) { /* try next context */ }
|
|
493
|
+
}
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Click at coordinates via CDP Input.dispatchMouseEvent.
|
|
498
|
+
*/
|
|
499
|
+
async cdpMouseClick(cdpService, x, y) {
|
|
500
|
+
await cdpService.call('Input.dispatchMouseEvent', {
|
|
501
|
+
type: 'mouseMoved', x, y,
|
|
502
|
+
});
|
|
503
|
+
await cdpService.call('Input.dispatchMouseEvent', {
|
|
504
|
+
type: 'mousePressed', x, y, button: 'left', clickCount: 1,
|
|
505
|
+
});
|
|
506
|
+
await cdpService.call('Input.dispatchMouseEvent', {
|
|
507
|
+
type: 'mouseReleased', x, y, button: 'left', clickCount: 1,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Start a new chat session in the Antigravity UI.
|
|
512
|
+
*
|
|
513
|
+
* Strategy:
|
|
514
|
+
* 1. Check the state of the new chat button
|
|
515
|
+
* 2. cursor: not-allowed -> already an empty chat (do nothing)
|
|
516
|
+
* 3. cursor: pointer -> click via Input.dispatchMouseEvent coordinates
|
|
517
|
+
* 4. Button not found -> error
|
|
518
|
+
*
|
|
519
|
+
* @param cdpService CdpService instance to use
|
|
520
|
+
* @returns { ok: true } on success, { ok: false, error: string } on failure
|
|
521
|
+
*/
|
|
522
|
+
async startNewChat(cdpService) {
|
|
523
|
+
try {
|
|
524
|
+
// Contexts may be empty right after Antigravity starts.
|
|
525
|
+
// Wait up to 10 seconds for the cascade-panel to become ready.
|
|
526
|
+
let contexts = cdpService.getContexts();
|
|
527
|
+
if (contexts.length === 0) {
|
|
528
|
+
const ready = await cdpService.waitForCascadePanelReady(10000, 500);
|
|
529
|
+
if (!ready) {
|
|
530
|
+
return { ok: false, error: 'No contexts available (timed out)' };
|
|
531
|
+
}
|
|
532
|
+
contexts = cdpService.getContexts();
|
|
533
|
+
}
|
|
534
|
+
// Get button state (retry waiting for DOM load: up to 5 times, 1 second interval)
|
|
535
|
+
let btnState = await this.getNewChatButtonState(cdpService, contexts);
|
|
536
|
+
if (!btnState.found) {
|
|
537
|
+
const maxRetries = 5;
|
|
538
|
+
for (let i = 0; i < maxRetries && !btnState.found; i++) {
|
|
539
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
540
|
+
contexts = cdpService.getContexts();
|
|
541
|
+
btnState = await this.getNewChatButtonState(cdpService, contexts);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (!btnState.found) {
|
|
545
|
+
return { ok: false, error: 'New chat button not found' };
|
|
546
|
+
}
|
|
547
|
+
// cursor: not-allowed -> already an empty chat (no need to create new)
|
|
548
|
+
if (!btnState.enabled) {
|
|
549
|
+
return { ok: true };
|
|
550
|
+
}
|
|
551
|
+
// cursor: pointer -> click via CDP Input API coordinates
|
|
552
|
+
await cdpService.call('Input.dispatchMouseEvent', {
|
|
553
|
+
type: 'mouseMoved', x: btnState.x, y: btnState.y,
|
|
554
|
+
});
|
|
555
|
+
await cdpService.call('Input.dispatchMouseEvent', {
|
|
556
|
+
type: 'mousePressed', x: btnState.x, y: btnState.y,
|
|
557
|
+
button: 'left', clickCount: 1,
|
|
558
|
+
});
|
|
559
|
+
await cdpService.call('Input.dispatchMouseEvent', {
|
|
560
|
+
type: 'mouseReleased', x: btnState.x, y: btnState.y,
|
|
561
|
+
button: 'left', clickCount: 1,
|
|
562
|
+
});
|
|
563
|
+
// Wait for UI to update after click
|
|
564
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
565
|
+
// Check if button changed to not-allowed (evidence that a new chat was opened)
|
|
566
|
+
const afterState = await this.getNewChatButtonState(cdpService, contexts);
|
|
567
|
+
if (afterState.found && !afterState.enabled) {
|
|
568
|
+
return { ok: true };
|
|
569
|
+
}
|
|
570
|
+
// Button still enabled -> click may not have worked
|
|
571
|
+
return { ok: false, error: 'Clicked new chat button but state did not change' };
|
|
572
|
+
}
|
|
573
|
+
catch (error) {
|
|
574
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
575
|
+
return { ok: false, error: message };
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Get the current chat session information.
|
|
580
|
+
* @param cdpService CdpService instance to use
|
|
581
|
+
* @returns Chat session information
|
|
582
|
+
*/
|
|
583
|
+
async getCurrentSessionInfo(cdpService) {
|
|
584
|
+
try {
|
|
585
|
+
const contexts = cdpService.getContexts();
|
|
586
|
+
for (const ctx of contexts) {
|
|
587
|
+
try {
|
|
588
|
+
const result = await cdpService.call('Runtime.evaluate', {
|
|
589
|
+
expression: GET_CHAT_TITLE_SCRIPT,
|
|
590
|
+
returnByValue: true,
|
|
591
|
+
contextId: ctx.id,
|
|
592
|
+
});
|
|
593
|
+
const value = result?.result?.value;
|
|
594
|
+
if (value && value.title) {
|
|
595
|
+
return {
|
|
596
|
+
title: value.title,
|
|
597
|
+
hasActiveChat: value.hasActiveChat ?? false,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
catch (_) { /* try next context */ }
|
|
602
|
+
}
|
|
603
|
+
return { title: '(Failed to retrieve)', hasActiveChat: false };
|
|
604
|
+
}
|
|
605
|
+
catch (error) {
|
|
606
|
+
return { title: '(Failed to retrieve)', hasActiveChat: false };
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Activate an existing chat by title.
|
|
611
|
+
* Returns ok:false if the target chat cannot be located or verified.
|
|
612
|
+
*/
|
|
613
|
+
async activateSessionByTitle(cdpService, title, options) {
|
|
614
|
+
if (!title || title.trim().length === 0) {
|
|
615
|
+
return { ok: false, error: 'Session title is empty' };
|
|
616
|
+
}
|
|
617
|
+
const current = await this.getCurrentSessionInfo(cdpService);
|
|
618
|
+
if (current.title.trim().toLowerCase() === title.trim().toLowerCase()) {
|
|
619
|
+
return { ok: true };
|
|
620
|
+
}
|
|
621
|
+
const maxWaitMs = options?.maxWaitMs ?? ChatSessionService.ACTIVATE_SESSION_MAX_WAIT_MS;
|
|
622
|
+
const retryIntervalMs = options?.retryIntervalMs ?? ChatSessionService.ACTIVATE_SESSION_RETRY_INTERVAL_MS;
|
|
623
|
+
let usedPastConversations = false;
|
|
624
|
+
let directResult = { ok: false, error: 'not attempted' };
|
|
625
|
+
let pastResult = null;
|
|
626
|
+
let clicked = false;
|
|
627
|
+
const startedAt = Date.now();
|
|
628
|
+
let attempts = 0;
|
|
629
|
+
while (Date.now() - startedAt <= maxWaitMs) {
|
|
630
|
+
attempts += 1;
|
|
631
|
+
directResult = await this.tryActivateByDirectSidePanel(cdpService, title);
|
|
632
|
+
clicked = directResult.ok;
|
|
633
|
+
if (!clicked) {
|
|
634
|
+
pastResult = await this.tryActivateByPastConversations(cdpService, title);
|
|
635
|
+
clicked = pastResult.ok;
|
|
636
|
+
usedPastConversations = pastResult.ok;
|
|
637
|
+
}
|
|
638
|
+
if (clicked) {
|
|
639
|
+
break;
|
|
640
|
+
}
|
|
641
|
+
if (Date.now() - startedAt <= maxWaitMs) {
|
|
642
|
+
await new Promise((resolve) => setTimeout(resolve, retryIntervalMs));
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
if (!clicked) {
|
|
646
|
+
return {
|
|
647
|
+
ok: false,
|
|
648
|
+
error: `Failed to activate session "${title}" ` +
|
|
649
|
+
`after ${attempts} attempt(s) ` +
|
|
650
|
+
`(direct: ${directResult.error || 'direct search failed'}; ` +
|
|
651
|
+
`past: ${pastResult?.error || 'past conversations search failed'})`,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
// Wait briefly for DOM state transition and verify destination chat.
|
|
655
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
656
|
+
const after = await this.getCurrentSessionInfo(cdpService);
|
|
657
|
+
if (after.title.trim().toLowerCase() === title.trim().toLowerCase()) {
|
|
658
|
+
return { ok: true };
|
|
659
|
+
}
|
|
660
|
+
// If direct side-panel activation hit the wrong row, try the explicit Past Conversations flow.
|
|
661
|
+
if (!usedPastConversations) {
|
|
662
|
+
const viaPast = await this.tryActivateByPastConversations(cdpService, title);
|
|
663
|
+
if (viaPast.ok) {
|
|
664
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
665
|
+
const afterPast = await this.getCurrentSessionInfo(cdpService);
|
|
666
|
+
if (afterPast.title.trim().toLowerCase() === title.trim().toLowerCase()) {
|
|
667
|
+
return { ok: true };
|
|
668
|
+
}
|
|
669
|
+
return {
|
|
670
|
+
ok: false,
|
|
671
|
+
error: `Past Conversations selected a different chat (expected="${title}", actual="${afterPast.title}")`,
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
return {
|
|
675
|
+
ok: false,
|
|
676
|
+
error: `Activated chat did not match target title (expected="${title}", actual="${after.title}") ` +
|
|
677
|
+
`and Past Conversations fallback failed (${viaPast.error || 'unknown'})`,
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
return {
|
|
681
|
+
ok: false,
|
|
682
|
+
error: `Activated chat did not match target title (expected="${title}", actual="${after.title}")`,
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
async tryActivateByDirectSidePanel(cdpService, title) {
|
|
686
|
+
return this.tryActivateWithScript(cdpService, buildActivateChatByTitleScript(title), false);
|
|
687
|
+
}
|
|
688
|
+
async tryActivateByPastConversations(cdpService, title) {
|
|
689
|
+
return this.tryActivateWithScript(cdpService, buildActivateViaPastConversationsScript(title), true);
|
|
690
|
+
}
|
|
691
|
+
async tryActivateWithScript(cdpService, script, awaitPromise) {
|
|
692
|
+
const contexts = cdpService.getContexts();
|
|
693
|
+
let lastError = 'Activation script returned no match';
|
|
694
|
+
for (const ctx of contexts) {
|
|
695
|
+
try {
|
|
696
|
+
const result = await cdpService.call('Runtime.evaluate', {
|
|
697
|
+
expression: script,
|
|
698
|
+
returnByValue: true,
|
|
699
|
+
awaitPromise,
|
|
700
|
+
contextId: ctx.id,
|
|
701
|
+
});
|
|
702
|
+
const value = result?.result?.value;
|
|
703
|
+
if (value?.ok) {
|
|
704
|
+
return { ok: true };
|
|
705
|
+
}
|
|
706
|
+
if (value?.error && typeof value.error === 'string') {
|
|
707
|
+
lastError = value.error;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
catch (error) {
|
|
711
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return { ok: false, error: lastError };
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Get the state (enabled/disabled, coordinates) of the new chat button.
|
|
718
|
+
*/
|
|
719
|
+
async getNewChatButtonState(cdpService, contexts) {
|
|
720
|
+
for (const ctx of contexts) {
|
|
721
|
+
try {
|
|
722
|
+
const res = await cdpService.call('Runtime.evaluate', {
|
|
723
|
+
expression: GET_NEW_CHAT_BUTTON_SCRIPT,
|
|
724
|
+
returnByValue: true,
|
|
725
|
+
contextId: ctx.id,
|
|
726
|
+
});
|
|
727
|
+
const value = res?.result?.value;
|
|
728
|
+
if (value?.found) {
|
|
729
|
+
return { found: true, enabled: value.enabled, x: value.x, y: value.y };
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
catch (_) { /* try next context */ }
|
|
733
|
+
}
|
|
734
|
+
return { found: false, enabled: false, x: 0, y: 0 };
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
exports.ChatSessionService = ChatSessionService;
|
|
738
|
+
//# sourceMappingURL=chatSessionService.js.map
|