@steipete/oracle 0.10.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -11
- package/dist/bin/oracle-cli.js +104 -16
- package/dist/src/browser/actions/archiveConversation.js +236 -0
- package/dist/src/browser/actions/assistantResponse.js +26 -0
- package/dist/src/browser/actions/deepResearch.js +662 -0
- package/dist/src/browser/actions/modelSelection.js +86 -16
- package/dist/src/browser/actions/navigation.js +22 -0
- package/dist/src/browser/actions/projectSources.js +491 -0
- package/dist/src/browser/actions/promptComposer.js +52 -27
- package/dist/src/browser/actions/thinkingStatus.js +391 -0
- package/dist/src/browser/artifacts.js +150 -0
- package/dist/src/browser/attachRunning.js +31 -0
- package/dist/src/browser/chatgptImages.js +315 -0
- package/dist/src/browser/chromeLifecycle.js +214 -3
- package/dist/src/browser/config.js +27 -9
- package/dist/src/browser/constants.js +8 -0
- package/dist/src/browser/controlPlan.js +81 -0
- package/dist/src/browser/detect.js +206 -33
- package/dist/src/browser/domDebug.js +49 -0
- package/dist/src/browser/index.js +1234 -479
- package/dist/src/browser/liveTabs.js +434 -0
- package/dist/src/browser/profileState.js +83 -3
- package/dist/src/browser/projectSourcesRunner.js +366 -0
- package/dist/src/browser/reattach.js +117 -45
- package/dist/src/browser/reattachHelpers.js +1 -1
- package/dist/src/browser/sessionRunner.js +53 -1
- package/dist/src/browser/tabLeaseRegistry.js +182 -0
- package/dist/src/cli/bridge/claudeConfig.js +12 -8
- package/dist/src/cli/bridge/codexConfig.js +2 -2
- package/dist/src/cli/browserConfig.js +41 -8
- package/dist/src/cli/browserDefaults.js +31 -7
- package/dist/src/cli/browserTabs.js +228 -0
- package/dist/src/cli/dryRun.js +33 -1
- package/dist/src/cli/duplicatePromptGuard.js +10 -2
- package/dist/src/cli/help.js +1 -1
- package/dist/src/cli/options.js +4 -0
- package/dist/src/cli/projectSources.js +116 -0
- package/dist/src/cli/sessionCommand.js +51 -0
- package/dist/src/cli/sessionDisplay.js +121 -9
- package/dist/src/cli/sessionRunner.js +51 -7
- package/dist/src/mcp/consultPresets.js +19 -0
- package/dist/src/mcp/server.js +2 -0
- package/dist/src/mcp/tools/consult.js +201 -26
- package/dist/src/mcp/tools/projectSources.js +123 -0
- package/dist/src/mcp/types.js +11 -2
- package/dist/src/mcp/utils.js +6 -1
- package/dist/src/oracle/run.js +4 -1
- package/dist/src/projectSources/plan.js +27 -0
- package/dist/src/projectSources/types.js +1 -0
- package/dist/src/projectSources/url.js +23 -0
- package/dist/src/sessionManager.js +1 -0
- package/package.json +7 -6
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { formatElapsed } from "../../oracle/format.js";
|
|
2
|
+
import { ASSISTANT_ROLE_SELECTOR, CONVERSATION_TURN_SELECTOR } from "../constants.js";
|
|
3
|
+
const THINKING_STALE_HINT_MS = 10 * 60_000;
|
|
4
|
+
export function startThinkingStatusMonitor(Runtime, logger, options = {}) {
|
|
5
|
+
const intervalMs = resolveThinkingStatusInterval(options.intervalMs);
|
|
6
|
+
if (!intervalMs) {
|
|
7
|
+
return () => { };
|
|
8
|
+
}
|
|
9
|
+
const now = options.now ?? Date.now;
|
|
10
|
+
let stopped = false;
|
|
11
|
+
let pending = false;
|
|
12
|
+
let lastFingerprint = null;
|
|
13
|
+
let lastChangedAt = now();
|
|
14
|
+
const startedAt = now();
|
|
15
|
+
const interval = setInterval(async () => {
|
|
16
|
+
// stop flag flips asynchronously
|
|
17
|
+
if (stopped || pending) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
pending = true;
|
|
21
|
+
try {
|
|
22
|
+
const snapshot = await readThinkingStatus(Runtime);
|
|
23
|
+
if (stopped) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const tickAt = now();
|
|
27
|
+
if (snapshot) {
|
|
28
|
+
const fingerprint = buildThinkingStatusFingerprint(snapshot);
|
|
29
|
+
if (fingerprint !== lastFingerprint) {
|
|
30
|
+
lastFingerprint = fingerprint;
|
|
31
|
+
lastChangedAt = tickAt;
|
|
32
|
+
}
|
|
33
|
+
if (stopped) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
logger(formatThinkingLog(startedAt, tickAt, snapshot, "", tickAt - lastChangedAt));
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
logger(formatThinkingWaitingLog(startedAt, tickAt));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// ignore DOM polling errors
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
pending = false;
|
|
47
|
+
}
|
|
48
|
+
}, intervalMs);
|
|
49
|
+
interval.unref?.();
|
|
50
|
+
return () => {
|
|
51
|
+
// multiple callers may race to stop
|
|
52
|
+
if (stopped) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
stopped = true;
|
|
56
|
+
clearInterval(interval);
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export function formatThinkingLog(startedAt, now, status, locatorSuffix, unchangedMs = 0) {
|
|
60
|
+
const elapsedMs = now - startedAt;
|
|
61
|
+
const elapsedText = formatElapsed(elapsedMs);
|
|
62
|
+
const snapshot = typeof status === "string"
|
|
63
|
+
? { message: sanitizeThinkingText(status) || "active", source: "inline" }
|
|
64
|
+
: status;
|
|
65
|
+
const progress = typeof snapshot.progressPercent === "number" && Number.isFinite(snapshot.progressPercent)
|
|
66
|
+
? `${Math.max(0, Math.min(100, Math.round(snapshot.progressPercent)))}% UI progress`
|
|
67
|
+
: null;
|
|
68
|
+
const prefix = progress
|
|
69
|
+
? `[browser] ChatGPT thinking - ${progress}, ${elapsedText} elapsed`
|
|
70
|
+
: `[browser] ChatGPT thinking - ${elapsedText} elapsed`;
|
|
71
|
+
const statusLabel = snapshot.message ? `; status=${snapshot.message}` : "";
|
|
72
|
+
const changeLabel = unchangedMs > 0 ? `; last change ${formatElapsed(unchangedMs)} ago` : "";
|
|
73
|
+
const staleLabel = unchangedMs >= THINKING_STALE_HINT_MS ? "; stale-hint=no UI progress change" : "";
|
|
74
|
+
const sourceLabel = snapshot.source ? `; source=${snapshot.source}` : "";
|
|
75
|
+
return `${prefix}${statusLabel}${changeLabel}${staleLabel}${sourceLabel}${locatorSuffix}`;
|
|
76
|
+
}
|
|
77
|
+
export function formatThinkingWaitingLog(startedAt, now) {
|
|
78
|
+
return `[browser] Waiting for ChatGPT response - ${formatElapsed(now - startedAt)} elapsed; no thinking status detected yet.`;
|
|
79
|
+
}
|
|
80
|
+
function resolveThinkingStatusInterval(intervalMs) {
|
|
81
|
+
if (intervalMs === 0) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
if (typeof intervalMs === "number" && Number.isFinite(intervalMs) && intervalMs > 0) {
|
|
85
|
+
return Math.max(1000, Math.floor(intervalMs));
|
|
86
|
+
}
|
|
87
|
+
return 30_000;
|
|
88
|
+
}
|
|
89
|
+
function buildThinkingStatusFingerprint(snapshot) {
|
|
90
|
+
return [
|
|
91
|
+
snapshot.source,
|
|
92
|
+
snapshot.message,
|
|
93
|
+
snapshot.progressPercent == null ? "" : Math.round(snapshot.progressPercent),
|
|
94
|
+
snapshot.panelVisible ? "panel" : "",
|
|
95
|
+
].join(":");
|
|
96
|
+
}
|
|
97
|
+
async function readThinkingStatus(Runtime) {
|
|
98
|
+
const expression = buildThinkingStatusExpression();
|
|
99
|
+
const { result } = await Runtime.evaluate({
|
|
100
|
+
expression,
|
|
101
|
+
awaitPromise: true,
|
|
102
|
+
returnByValue: true,
|
|
103
|
+
});
|
|
104
|
+
const value = result.value;
|
|
105
|
+
if (typeof value === "string") {
|
|
106
|
+
const sanitized = sanitizeThinkingText(value);
|
|
107
|
+
return sanitized ? { message: sanitized, source: "inline" } : null;
|
|
108
|
+
}
|
|
109
|
+
if (!value || typeof value !== "object") {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
const source = value.source === "sidecar" ? "sidecar" : "inline";
|
|
113
|
+
const message = sanitizeThinkingText(value.message ?? "");
|
|
114
|
+
const progressPercent = typeof value.progressPercent === "number" && Number.isFinite(value.progressPercent)
|
|
115
|
+
? Math.max(0, Math.min(100, value.progressPercent))
|
|
116
|
+
: undefined;
|
|
117
|
+
if (!message && progressPercent == null) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
message: message || "active",
|
|
122
|
+
source,
|
|
123
|
+
progressPercent,
|
|
124
|
+
panelOpened: value.panelOpened === true,
|
|
125
|
+
panelVisible: value.panelVisible === true,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const SAFE_THINKING_STATUS_MESSAGES = new Set([
|
|
129
|
+
"active",
|
|
130
|
+
"thinking sidecar active",
|
|
131
|
+
"thinking sidecar opened",
|
|
132
|
+
]);
|
|
133
|
+
export function sanitizeThinkingText(raw) {
|
|
134
|
+
if (!raw) {
|
|
135
|
+
return "";
|
|
136
|
+
}
|
|
137
|
+
const trimmed = raw.replace(/\s+/g, " ").trim();
|
|
138
|
+
const prefixPattern = /^(pro thinking)\s*[•:\-–—]*\s*/i;
|
|
139
|
+
const normalized = prefixPattern.test(trimmed)
|
|
140
|
+
? trimmed.replace(prefixPattern, "").trim()
|
|
141
|
+
: trimmed;
|
|
142
|
+
if (!normalized) {
|
|
143
|
+
return "";
|
|
144
|
+
}
|
|
145
|
+
const normalizedKey = normalized.toLowerCase();
|
|
146
|
+
return SAFE_THINKING_STATUS_MESSAGES.has(normalizedKey) ? normalizedKey : "active";
|
|
147
|
+
}
|
|
148
|
+
function buildThinkingStatusExpression() {
|
|
149
|
+
const conversationLiteral = JSON.stringify(CONVERSATION_TURN_SELECTOR);
|
|
150
|
+
const assistantLiteral = JSON.stringify(ASSISTANT_ROLE_SELECTOR);
|
|
151
|
+
const selectors = [
|
|
152
|
+
"span.loading-shimmer",
|
|
153
|
+
"span.flex.items-center.gap-1.truncate.text-start.align-middle.text-token-text-tertiary",
|
|
154
|
+
'[data-testid*="thinking"]',
|
|
155
|
+
'[data-testid*="reasoning"]',
|
|
156
|
+
'[role="status"]',
|
|
157
|
+
'[aria-live="polite"]',
|
|
158
|
+
];
|
|
159
|
+
const keywords = ["pro thinking", "thinking", "reasoning"];
|
|
160
|
+
const selectorLiteral = JSON.stringify(selectors);
|
|
161
|
+
const keywordsLiteral = JSON.stringify(keywords);
|
|
162
|
+
return `(async () => {
|
|
163
|
+
const CONVERSATION_SELECTOR = ${conversationLiteral};
|
|
164
|
+
const ASSISTANT_SELECTOR = ${assistantLiteral};
|
|
165
|
+
const selectors = ${selectorLiteral};
|
|
166
|
+
const keywords = ${keywordsLiteral};
|
|
167
|
+
const normalize = (value) =>
|
|
168
|
+
String(value || '')
|
|
169
|
+
.normalize('NFD')
|
|
170
|
+
.replace(/[\\u0300-\\u036f]/g, '')
|
|
171
|
+
.toLowerCase()
|
|
172
|
+
.replace(/\\s+/g, ' ')
|
|
173
|
+
.trim();
|
|
174
|
+
const isVisible = (node) => {
|
|
175
|
+
if (!(node instanceof HTMLElement)) return false;
|
|
176
|
+
const rect = node.getBoundingClientRect();
|
|
177
|
+
if (!rect || rect.width <= 0 || rect.height <= 0) return false;
|
|
178
|
+
const style = window.getComputedStyle(node);
|
|
179
|
+
if (
|
|
180
|
+
style.display === 'none' ||
|
|
181
|
+
style.visibility === 'hidden' ||
|
|
182
|
+
(style.opacity !== '' && Number(style.opacity) === 0)
|
|
183
|
+
) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
return rect.bottom >= 0 && rect.right >= 0 && rect.top <= window.innerHeight && rect.left <= window.innerWidth;
|
|
187
|
+
};
|
|
188
|
+
const labelFor = (node) =>
|
|
189
|
+
normalize([
|
|
190
|
+
node.textContent,
|
|
191
|
+
node.getAttribute?.('aria-label'),
|
|
192
|
+
node.getAttribute?.('title'),
|
|
193
|
+
node.getAttribute?.('data-testid'),
|
|
194
|
+
].filter(Boolean).join(' '));
|
|
195
|
+
const looksLikeThinking = (node) => {
|
|
196
|
+
const label = labelFor(node);
|
|
197
|
+
return (
|
|
198
|
+
label.includes('thinking') ||
|
|
199
|
+
label.includes('reasoning') ||
|
|
200
|
+
label.includes('pro thinking') ||
|
|
201
|
+
label.includes('myslen') ||
|
|
202
|
+
label.includes('mysl') ||
|
|
203
|
+
label.includes('rozumow')
|
|
204
|
+
);
|
|
205
|
+
};
|
|
206
|
+
const isComposerAdjacent = (node) =>
|
|
207
|
+
Boolean(node.closest?.('[contenteditable="true"], textarea, [data-testid*="composer"], [id*="composer"]'));
|
|
208
|
+
const isAssistantTurn = (node) => {
|
|
209
|
+
if (!(node instanceof HTMLElement)) return false;
|
|
210
|
+
const turnAttr = (node.getAttribute('data-turn') || node.dataset?.turn || '').toLowerCase();
|
|
211
|
+
if (turnAttr === 'assistant') return true;
|
|
212
|
+
const role = (node.getAttribute('data-message-author-role') || node.dataset?.messageAuthorRole || '').toLowerCase();
|
|
213
|
+
if (role === 'assistant') return true;
|
|
214
|
+
const testId = (node.getAttribute('data-testid') || '').toLowerCase();
|
|
215
|
+
if (testId.includes('assistant')) return true;
|
|
216
|
+
return Boolean(node.querySelector(ASSISTANT_SELECTOR) || node.querySelector('[data-testid*="assistant"]'));
|
|
217
|
+
};
|
|
218
|
+
const latestAssistantTurn = () => {
|
|
219
|
+
const turns = Array.from(document.querySelectorAll(CONVERSATION_SELECTOR));
|
|
220
|
+
for (let index = turns.length - 1; index >= 0; index -= 1) {
|
|
221
|
+
if (isAssistantTurn(turns[index])) {
|
|
222
|
+
return turns[index];
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
};
|
|
227
|
+
const findThinkingDisclosure = (scope) => {
|
|
228
|
+
const candidates = Array.from(
|
|
229
|
+
scope.querySelectorAll(
|
|
230
|
+
[
|
|
231
|
+
'button',
|
|
232
|
+
'[role="button"]',
|
|
233
|
+
'[aria-expanded]',
|
|
234
|
+
'[data-testid*="thinking"]',
|
|
235
|
+
'[data-testid*="reasoning"]',
|
|
236
|
+
].join(','),
|
|
237
|
+
),
|
|
238
|
+
);
|
|
239
|
+
for (const node of candidates) {
|
|
240
|
+
if (!(node instanceof HTMLElement)) continue;
|
|
241
|
+
if (!isVisible(node) || isComposerAdjacent(node) || !looksLikeThinking(node)) continue;
|
|
242
|
+
if (node.getAttribute('aria-haspopup') === 'menu') continue;
|
|
243
|
+
if (node.dataset?.oracleThinkingProbed === 'true') continue;
|
|
244
|
+
const expanded = normalize(node.getAttribute('aria-expanded'));
|
|
245
|
+
if (expanded !== 'false') {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
return node;
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
};
|
|
252
|
+
const findProgressPercent = (scope) => {
|
|
253
|
+
const progressNodes = Array.from(
|
|
254
|
+
scope.querySelectorAll('progress, [role="progressbar"], [aria-valuenow], [data-testid*="progress"], [class*="progress"]'),
|
|
255
|
+
);
|
|
256
|
+
const readNumeric = (raw) => {
|
|
257
|
+
if (raw == null || raw === '') return null;
|
|
258
|
+
const value = Number(String(raw).replace('%', '').trim());
|
|
259
|
+
return Number.isFinite(value) ? value : null;
|
|
260
|
+
};
|
|
261
|
+
const readStylePercent = (node) => {
|
|
262
|
+
const style = node instanceof HTMLElement ? window.getComputedStyle(node) : null;
|
|
263
|
+
if (!style) return null;
|
|
264
|
+
const widthMatch = String(node.style?.width || style.width || '').match(
|
|
265
|
+
/([0-9]+(?:\\.[0-9]+)?)%/,
|
|
266
|
+
);
|
|
267
|
+
if (widthMatch) return readNumeric(widthMatch[1]);
|
|
268
|
+
const transform = String(style.transform || '');
|
|
269
|
+
const scaleMatch = transform.match(/scaleX\\(([0-9.]+)\\)/);
|
|
270
|
+
if (scaleMatch) {
|
|
271
|
+
const scale = readNumeric(scaleMatch[1]);
|
|
272
|
+
return scale == null ? null : scale * 100;
|
|
273
|
+
}
|
|
274
|
+
const matrixMatch = transform.match(/matrix\\(([0-9.\\-]+),/);
|
|
275
|
+
if (matrixMatch) {
|
|
276
|
+
const scale = readNumeric(matrixMatch[1]);
|
|
277
|
+
return scale == null ? null : scale * 100;
|
|
278
|
+
}
|
|
279
|
+
return null;
|
|
280
|
+
};
|
|
281
|
+
for (const node of progressNodes) {
|
|
282
|
+
if (!(node instanceof HTMLElement) || !isVisible(node)) continue;
|
|
283
|
+
const ariaNow = readNumeric(node.getAttribute('aria-valuenow'));
|
|
284
|
+
if (ariaNow != null) {
|
|
285
|
+
const ariaMin = readNumeric(node.getAttribute('aria-valuemin')) ?? 0;
|
|
286
|
+
const ariaMax = readNumeric(node.getAttribute('aria-valuemax')) ?? 100;
|
|
287
|
+
const span = Math.max(ariaMax - ariaMin, 1);
|
|
288
|
+
return Math.max(0, Math.min(100, ((ariaNow - ariaMin) / span) * 100));
|
|
289
|
+
}
|
|
290
|
+
if (node instanceof HTMLProgressElement && Number.isFinite(node.value) && Number.isFinite(node.max) && node.max > 0) {
|
|
291
|
+
return Math.max(0, Math.min(100, (node.value / node.max) * 100));
|
|
292
|
+
}
|
|
293
|
+
const stylePercent = readStylePercent(node);
|
|
294
|
+
if (stylePercent != null) {
|
|
295
|
+
return Math.max(0, Math.min(100, stylePercent));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return null;
|
|
299
|
+
};
|
|
300
|
+
const findThinkingPanel = () => {
|
|
301
|
+
const candidates = Array.from(
|
|
302
|
+
document.querySelectorAll(
|
|
303
|
+
[
|
|
304
|
+
'aside',
|
|
305
|
+
'[role="complementary"]',
|
|
306
|
+
'[role="dialog"]',
|
|
307
|
+
'[data-testid*="thinking"]',
|
|
308
|
+
'[data-testid*="reasoning"]',
|
|
309
|
+
'[data-testid*="sidebar"]',
|
|
310
|
+
'[class*="sidecar"]',
|
|
311
|
+
'[class*="sidebar"]',
|
|
312
|
+
].join(','),
|
|
313
|
+
),
|
|
314
|
+
);
|
|
315
|
+
for (const node of candidates) {
|
|
316
|
+
if (!(node instanceof HTMLElement) || !isVisible(node) || isComposerAdjacent(node)) continue;
|
|
317
|
+
const rect = node.getBoundingClientRect();
|
|
318
|
+
const rightSidePanel = rect.left >= window.innerWidth * 0.35 && rect.width >= 180 && rect.height >= 120;
|
|
319
|
+
const hasProgress = findProgressPercent(node) != null;
|
|
320
|
+
if (hasProgress || (rightSidePanel && looksLikeThinking(node))) {
|
|
321
|
+
return node;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return null;
|
|
325
|
+
};
|
|
326
|
+
const existingPanel = findThinkingPanel();
|
|
327
|
+
if (existingPanel) {
|
|
328
|
+
return {
|
|
329
|
+
message: 'thinking sidecar active',
|
|
330
|
+
source: 'sidecar',
|
|
331
|
+
progressPercent: findProgressPercent(existingPanel),
|
|
332
|
+
panelOpened: false,
|
|
333
|
+
panelVisible: true,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
let panelOpened = false;
|
|
337
|
+
const currentTurn = latestAssistantTurn();
|
|
338
|
+
const disclosure = currentTurn ? findThinkingDisclosure(currentTurn) : null;
|
|
339
|
+
if (disclosure) {
|
|
340
|
+
try {
|
|
341
|
+
disclosure.dataset.oracleThinkingProbed = 'true';
|
|
342
|
+
disclosure.click();
|
|
343
|
+
panelOpened = true;
|
|
344
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
345
|
+
} catch {
|
|
346
|
+
// non-fatal; fall through to passive status detection
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
const panel = findThinkingPanel();
|
|
350
|
+
if (panel) {
|
|
351
|
+
const progressPercent = findProgressPercent(panel);
|
|
352
|
+
return {
|
|
353
|
+
message: panelOpened ? 'thinking sidecar opened' : 'thinking sidecar active',
|
|
354
|
+
source: 'sidecar',
|
|
355
|
+
progressPercent,
|
|
356
|
+
panelOpened,
|
|
357
|
+
panelVisible: true,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
const nodes = new Set();
|
|
361
|
+
for (const selector of selectors) {
|
|
362
|
+
document.querySelectorAll(selector).forEach((node) => nodes.add(node));
|
|
363
|
+
}
|
|
364
|
+
for (const node of nodes) {
|
|
365
|
+
if (!(node instanceof HTMLElement)) {
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
const text = node.textContent?.trim();
|
|
369
|
+
if (!text) {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
const classLabel = String(node.className || '').toLowerCase();
|
|
373
|
+
const dataLabel = ((node.getAttribute('data-testid') || '') + ' ' + (node.getAttribute('aria-label') || ''))
|
|
374
|
+
.toLowerCase();
|
|
375
|
+
const normalizedText = text.toLowerCase();
|
|
376
|
+
const matches = keywords.some((keyword) =>
|
|
377
|
+
normalizedText.includes(keyword) || classLabel.includes(keyword) || dataLabel.includes(keyword)
|
|
378
|
+
);
|
|
379
|
+
if (matches) {
|
|
380
|
+
return {
|
|
381
|
+
message: 'active',
|
|
382
|
+
source: 'inline',
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return null;
|
|
387
|
+
})()`;
|
|
388
|
+
}
|
|
389
|
+
export const startThinkingStatusMonitorForTest = startThinkingStatusMonitor;
|
|
390
|
+
export const readThinkingStatusForTest = readThinkingStatus;
|
|
391
|
+
export const buildThinkingStatusExpressionForTest = buildThinkingStatusExpression;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getOracleHomeDir } from "../oracleHome.js";
|
|
4
|
+
const ARTIFACTS_DIRNAME = "artifacts";
|
|
5
|
+
function sanitizePathSegment(value, fallback) {
|
|
6
|
+
const sanitized = value
|
|
7
|
+
.toLowerCase()
|
|
8
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
9
|
+
.replace(/-+/g, "-")
|
|
10
|
+
.replace(/^-|-$/g, "")
|
|
11
|
+
.slice(0, 80);
|
|
12
|
+
return sanitized || fallback;
|
|
13
|
+
}
|
|
14
|
+
function normalizeSessionId(sessionId) {
|
|
15
|
+
return sanitizePathSegment(path.basename(sessionId), "session");
|
|
16
|
+
}
|
|
17
|
+
export function resolveSessionArtifactsDir(sessionId) {
|
|
18
|
+
return path.join(getOracleHomeDir(), "sessions", normalizeSessionId(sessionId), ARTIFACTS_DIRNAME);
|
|
19
|
+
}
|
|
20
|
+
async function pathExists(targetPath) {
|
|
21
|
+
try {
|
|
22
|
+
await fs.access(targetPath);
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function resolveUniquePath(basePath) {
|
|
30
|
+
const ext = path.extname(basePath);
|
|
31
|
+
const stem = ext ? path.basename(basePath, ext) : path.basename(basePath);
|
|
32
|
+
const dir = path.dirname(basePath);
|
|
33
|
+
let candidate = basePath;
|
|
34
|
+
let suffix = 2;
|
|
35
|
+
while (await pathExists(candidate)) {
|
|
36
|
+
candidate = path.join(dir, `${stem}-${suffix}${ext}`);
|
|
37
|
+
suffix += 1;
|
|
38
|
+
}
|
|
39
|
+
return candidate;
|
|
40
|
+
}
|
|
41
|
+
async function readSizeBytes(targetPath) {
|
|
42
|
+
try {
|
|
43
|
+
return (await fs.stat(targetPath)).size;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export async function writeTextBrowserArtifact(params) {
|
|
50
|
+
const text = params.contents.trim();
|
|
51
|
+
if (!params.sessionId || text.length === 0) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const dir = resolveSessionArtifactsDir(params.sessionId);
|
|
55
|
+
await fs.mkdir(dir, { recursive: true });
|
|
56
|
+
const filename = sanitizePathSegment(params.filename, "artifact.md");
|
|
57
|
+
const targetPath = await resolveUniquePath(path.join(dir, filename));
|
|
58
|
+
await fs.writeFile(targetPath, `${text}\n`, "utf8");
|
|
59
|
+
params.logger?.(`[browser] Saved ${params.kind} artifact to ${targetPath}`);
|
|
60
|
+
return {
|
|
61
|
+
kind: params.kind,
|
|
62
|
+
path: targetPath,
|
|
63
|
+
label: params.label,
|
|
64
|
+
mimeType: params.mimeType ?? "text/markdown",
|
|
65
|
+
sizeBytes: await readSizeBytes(targetPath),
|
|
66
|
+
sourceUrl: params.sourceUrl,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function isToolOnlyPlaceholder(text) {
|
|
70
|
+
const normalized = text.toLowerCase().replace(/\s+/g, " ").trim();
|
|
71
|
+
return (normalized === "called tool" ||
|
|
72
|
+
normalized === "used tool" ||
|
|
73
|
+
normalized === "użyto narzędzia" ||
|
|
74
|
+
normalized === "narzędzie wywołane");
|
|
75
|
+
}
|
|
76
|
+
export async function saveDeepResearchReportArtifact(params) {
|
|
77
|
+
const report = params.reportMarkdown.trim();
|
|
78
|
+
if (report.length < 40 || isToolOnlyPlaceholder(report)) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
return writeTextBrowserArtifact({
|
|
82
|
+
sessionId: params.sessionId,
|
|
83
|
+
kind: "deep-research-report",
|
|
84
|
+
filename: "deep-research-report.md",
|
|
85
|
+
contents: report,
|
|
86
|
+
label: "Deep Research report",
|
|
87
|
+
mimeType: "text/markdown",
|
|
88
|
+
sourceUrl: params.conversationUrl,
|
|
89
|
+
logger: params.logger,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
export async function saveBrowserTranscriptArtifact(params) {
|
|
93
|
+
const answer = params.answerMarkdown.trim();
|
|
94
|
+
if (!answer) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const artifactLines = params.artifacts && params.artifacts.length > 0
|
|
98
|
+
? [
|
|
99
|
+
"",
|
|
100
|
+
"## Artifacts",
|
|
101
|
+
"",
|
|
102
|
+
...params.artifacts.map((artifact) => {
|
|
103
|
+
const label = artifact.label ?? artifact.kind;
|
|
104
|
+
return `- ${label}: ${artifact.path}`;
|
|
105
|
+
}),
|
|
106
|
+
]
|
|
107
|
+
: [];
|
|
108
|
+
const conversationLines = params.conversationUrl
|
|
109
|
+
? ["", `Conversation: ${params.conversationUrl}`, ""]
|
|
110
|
+
: ["", ""];
|
|
111
|
+
const body = [
|
|
112
|
+
"# Oracle Browser Transcript",
|
|
113
|
+
...conversationLines,
|
|
114
|
+
"## Prompt",
|
|
115
|
+
"",
|
|
116
|
+
params.prompt.trim(),
|
|
117
|
+
"",
|
|
118
|
+
"## Answer",
|
|
119
|
+
"",
|
|
120
|
+
answer,
|
|
121
|
+
...artifactLines,
|
|
122
|
+
].join("\n");
|
|
123
|
+
return writeTextBrowserArtifact({
|
|
124
|
+
sessionId: params.sessionId,
|
|
125
|
+
kind: "transcript",
|
|
126
|
+
filename: "transcript.md",
|
|
127
|
+
contents: body,
|
|
128
|
+
label: "Browser transcript",
|
|
129
|
+
mimeType: "text/markdown",
|
|
130
|
+
sourceUrl: params.conversationUrl,
|
|
131
|
+
logger: params.logger,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
export function appendArtifacts(existing, additions) {
|
|
135
|
+
const merged = new Map();
|
|
136
|
+
for (const artifact of existing ?? []) {
|
|
137
|
+
merged.set(`${artifact.kind}:${artifact.path}`, artifact);
|
|
138
|
+
}
|
|
139
|
+
for (const artifact of additions) {
|
|
140
|
+
if (artifact) {
|
|
141
|
+
merged.set(`${artifact.kind}:${artifact.path}`, artifact);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const values = Array.from(merged.values());
|
|
145
|
+
return values.length > 0 ? values : undefined;
|
|
146
|
+
}
|
|
147
|
+
export const __test__ = {
|
|
148
|
+
normalizeSessionId,
|
|
149
|
+
sanitizePathSegment,
|
|
150
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { discoverDevToolsActivePortCandidates, } from "./detect.js";
|
|
2
|
+
export async function resolveAttachRunningConnection(config, logger) {
|
|
3
|
+
const host = config.remoteChrome?.host ?? "127.0.0.1";
|
|
4
|
+
const port = config.remoteChrome?.port ?? 9222;
|
|
5
|
+
if (config.chromePath) {
|
|
6
|
+
logger("Note: --browser-chrome-path is ignored when --browser-attach-running is enabled.");
|
|
7
|
+
}
|
|
8
|
+
logger(config.remoteChrome
|
|
9
|
+
? `Using explicit attach-running target ${host}:${port}.`
|
|
10
|
+
: `Using default attach-running target ${host}:${port}.`);
|
|
11
|
+
const candidates = (await discoverDevToolsActivePortCandidates({ host }))
|
|
12
|
+
.filter((candidate) => candidate.port === port)
|
|
13
|
+
.sort(compareDevToolsCandidates);
|
|
14
|
+
if (candidates.length === 0) {
|
|
15
|
+
throw new Error(`No running browser with attach metadata matched ${host}:${port}. Enable remote debugging in chrome://inspect/#remote-debugging first.`);
|
|
16
|
+
}
|
|
17
|
+
const candidate = candidates[0];
|
|
18
|
+
logger(`Selected attach-running browser metadata from ${candidate.path}`);
|
|
19
|
+
return {
|
|
20
|
+
host,
|
|
21
|
+
port: candidate.port,
|
|
22
|
+
browserWSEndpoint: candidate.browserWSEndpoint,
|
|
23
|
+
profileRoot: candidate.profileRoot,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function compareDevToolsCandidates(left, right) {
|
|
27
|
+
if (right.mtimeMs !== left.mtimeMs) {
|
|
28
|
+
return right.mtimeMs - left.mtimeMs;
|
|
29
|
+
}
|
|
30
|
+
return left.path.localeCompare(right.path);
|
|
31
|
+
}
|