pi-oracle 0.7.7 → 0.7.9

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.
@@ -5,7 +5,7 @@
5
5
  // Invariants/Assumptions: Poller scans are serialized per session key, wake-up delivery is best-effort, and terminal-job notifications always re-read durable job state before send.
6
6
  import { existsSync } from "node:fs";
7
7
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
8
- import { buildOracleStatusText, buildOracleWakeupNotificationContent } from "../shared/job-observability-helpers.mjs";
8
+ import { buildOracleStatusText, buildOracleWakeupNotificationContent, type OracleReadinessStatus } from "../shared/job-observability-helpers.mjs";
9
9
  import { isProcessAlive, readProcessStartedAt } from "../shared/process-helpers.mjs";
10
10
  import { isLockTimeoutError, listLeaseMetadata, releaseLease, withGlobalReconcileLock, writeLeaseMetadata } from "./locks.js";
11
11
  import {
@@ -46,6 +46,7 @@ interface OraclePollerLifecycle {
46
46
  }
47
47
 
48
48
  const activePollers = new Map<string, OracleActivePoller>();
49
+ const readinessBySession = new Map<string, OracleReadinessStatus>();
49
50
  const scansInFlight = new Set<string>();
50
51
  const POLLER_LOCK_TIMEOUT_MS = 50;
51
52
  const WAKEUP_TARGET_LEASE_KIND = "wakeup-target";
@@ -167,19 +168,31 @@ function getJobCountsForSession(sessionFile: string | undefined, cwd: string): {
167
168
  function refreshOracleStatusSnapshot(snapshot: OraclePollerContextSnapshot): void {
168
169
  if (!snapshot.hasUI) return;
169
170
  if (!snapshot.sessionFile) {
170
- snapshot.ui.setStatus("oracle", snapshot.ui.theme.fg("accent", "oracle: unavailable"));
171
+ snapshot.ui.setStatus("oracle", snapshot.ui.theme.fg("error", "oracle: unavailable"));
171
172
  return;
172
173
  }
173
174
  const counts = getJobCountsForSession(snapshot.sessionFile, snapshot.cwd);
174
- const statusText = buildOracleStatusText(counts);
175
- const tone = counts.active > 0 ? "success" : "accent";
176
- snapshot.ui.setStatus("oracle", snapshot.ui.theme.fg(tone, statusText));
175
+ const readiness = readinessBySession.get(getPollerSessionKey(snapshot.sessionFile, snapshot.cwd)) ?? "loaded";
176
+ const statusText = buildOracleStatusText(counts, readiness);
177
+ if (counts.active > 0) {
178
+ snapshot.ui.setStatus("oracle", snapshot.ui.theme.fg("success", statusText));
179
+ } else if (readiness === "auth_needed" || readiness === "config_error") {
180
+ snapshot.ui.setStatus("oracle", snapshot.ui.theme.fg("error", statusText));
181
+ } else {
182
+ snapshot.ui.setStatus("oracle", statusText);
183
+ }
177
184
  }
178
185
 
179
186
  export function refreshOracleStatus(ctx: ExtensionContext): void {
180
187
  refreshOracleStatusSnapshot(snapshotPollerContext(ctx));
181
188
  }
182
189
 
190
+ export function setOracleReadiness(ctx: ExtensionContext, readiness: OracleReadinessStatus): void {
191
+ const snapshot = snapshotPollerContext(ctx);
192
+ if (snapshot.sessionFile) readinessBySession.set(getPollerSessionKey(snapshot.sessionFile, snapshot.cwd), readiness);
193
+ refreshOracleStatusSnapshot(snapshot);
194
+ }
195
+
183
196
  function requestWakeupTurn(pi: ExtensionAPI, job: OraclePollerJob): void {
184
197
  pi.sendMessage(
185
198
  {
@@ -412,6 +425,7 @@ export function stopPollerForSession(sessionFile: string | undefined, cwd: strin
412
425
  if (handle.timer) clearInterval(handle.timer);
413
426
  activePollers.delete(sessionKey);
414
427
  }
428
+ readinessBySession.delete(sessionKey);
415
429
  const wakeupTargetLeaseKey = getWakeupTargetLeaseKey(sessionKey);
416
430
  void releaseLease(WAKEUP_TARGET_LEASE_KIND, wakeupTargetLeaseKey).catch(() => undefined);
417
431
  }
@@ -423,6 +437,7 @@ export async function stopAllPollers(): Promise<void> {
423
437
  if (handle.timer) clearInterval(handle.timer);
424
438
  }
425
439
  activePollers.clear();
440
+ readinessBySession.clear();
426
441
  await Promise.all(handles.map(async (handle) => {
427
442
  const wakeupTargetLeaseKey = getWakeupTargetLeaseKey(handle.sessionKey);
428
443
  await releaseLease(WAKEUP_TARGET_LEASE_KIND, wakeupTargetLeaseKey).catch(() => undefined);
@@ -169,6 +169,10 @@ function unreadableAuthSeedProfileMessage(seedDir: string): string {
169
169
  return `Oracle auth seed profile is not readable: ${seedDir}. Fix its permissions or rerun /oracle-auth.`;
170
170
  }
171
171
 
172
+ function unauthenticatedAuthSeedProfileMessage(seedDir: string): string {
173
+ return `Oracle auth seed profile exists but is not authenticated: ${seedDir}. Run /oracle-auth to create a verified auth seed before submitting oracle jobs.`;
174
+ }
175
+
172
176
  function missingBrowserExecutableMessage(executablePath: string): string {
173
177
  return `Configured oracle browser executable does not exist: ${executablePath}. Fix browser.executablePath or install Chrome there.`;
174
178
  }
@@ -315,6 +319,10 @@ export async function assertOracleAuthSeedProfileReady(config: OracleConfig): Pr
315
319
  } catch {
316
320
  throw new Error(unreadableAuthSeedProfileMessage(seedDir));
317
321
  }
322
+
323
+ if (!getSeedGeneration(config)) {
324
+ throw new Error(unauthenticatedAuthSeedProfileMessage(seedDir));
325
+ }
318
326
  }
319
327
 
320
328
  export async function assertOracleSubmitPrerequisites(config: OracleConfig): Promise<void> {
@@ -914,6 +914,15 @@ function buildOracleToolErrorDetails(toolName: OracleToolErrorSource, error: unk
914
914
  };
915
915
  }
916
916
 
917
+ if (message.startsWith("Oracle auth seed profile exists but is not authenticated: ")) {
918
+ return {
919
+ code: "auth_seed_profile_unauthenticated",
920
+ message,
921
+ rejectedValue: message.replace(/^Oracle auth seed profile exists but is not authenticated: /, "").replace(/\. Run \/oracle-auth.*$/, ""),
922
+ suggestedNextStep: "Call oracle_auth or run /oracle-auth once to create a verified auth seed, then retry the oracle tool call.",
923
+ };
924
+ }
925
+
917
926
  if (message.startsWith("Failed to parse oracle config ") || message.startsWith("Invalid oracle config:") || message.startsWith("Invalid oracle project config:")) {
918
927
  return {
919
928
  code: "oracle_config_invalid",
@@ -1151,6 +1160,10 @@ function formatOraclePreflightResponse(details: OraclePreflightDetails): string
1151
1160
  ].filter(Boolean).join("\n");
1152
1161
  }
1153
1162
 
1163
+ function isProjectTrusted(ctx: ExtensionContext): boolean {
1164
+ return (ctx as { isProjectTrusted?: () => boolean }).isProjectTrusted?.() ?? true;
1165
+ }
1166
+
1154
1167
  async function runOraclePreflight(ctx: ExtensionContext, params: { provider?: unknown; followUpJobId?: unknown } = {}): Promise<OraclePreflightDetails> {
1155
1168
  const sessionFile = getSessionFile(ctx);
1156
1169
  if (!hasPersistedSessionFile(sessionFile)) {
@@ -1174,7 +1187,7 @@ async function runOraclePreflight(ctx: ExtensionContext, params: { provider?: un
1174
1187
  if (followUpJobId !== undefined && typeof followUpJobId !== "string") {
1175
1188
  throw new Error("oracle_preflight followUpJobId must be a string");
1176
1189
  }
1177
- const baseConfig = loadOracleConfig(ctx.cwd);
1190
+ const baseConfig = loadOracleConfig(ctx.cwd, { projectConfigTrusted: isProjectTrusted(ctx) });
1178
1191
  const followUp = resolveFollowUp(followUpJobId, ctx.cwd);
1179
1192
  provider = normalizeOracleProvider(params.provider, followUp.provider ?? baseConfig.defaults.provider, "oracle_preflight");
1180
1193
  if (followUp.provider && provider !== followUp.provider) {
@@ -1202,7 +1215,7 @@ async function runOraclePreflight(ctx: ExtensionContext, params: { provider?: un
1202
1215
  session: { persisted: true, sessionFile },
1203
1216
  config: { ready: true },
1204
1217
  auth: {
1205
- ready: !["auth_seed_profile_missing", "auth_seed_profile_unreadable", "auth_seed_profile_invalid_type"].includes(errorDetails.code),
1218
+ ready: !["auth_seed_profile_missing", "auth_seed_profile_unreadable", "auth_seed_profile_invalid_type", "auth_seed_profile_unauthenticated"].includes(errorDetails.code),
1206
1219
  seedProfileDir: config.browser.authSeedProfileDir,
1207
1220
  },
1208
1221
  error: errorDetails,
@@ -1264,9 +1277,9 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
1264
1277
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1265
1278
  try {
1266
1279
  const projectCwd = getProjectId(ctx.cwd);
1267
- const baseConfig = loadOracleConfig(projectCwd, { projectConfigTrustCwd: ctx.cwd });
1280
+ const baseConfig = loadOracleConfig(projectCwd, { projectConfigTrustCwd: ctx.cwd, projectConfigTrusted: isProjectTrusted(ctx) });
1268
1281
  const provider = normalizeOracleProvider(params.provider, baseConfig.defaults.provider, "oracle_auth");
1269
- const message = await runOracleAuthBootstrap(authWorkerPath, projectCwd, provider);
1282
+ const message = await runOracleAuthBootstrap(authWorkerPath, projectCwd, provider, { projectConfigTrustCwd: ctx.cwd, projectConfigTrusted: isProjectTrusted(ctx) });
1270
1283
  return {
1271
1284
  content: [{ type: "text" as const, text: message }],
1272
1285
  details: {
@@ -1311,7 +1324,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
1311
1324
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1312
1325
  try {
1313
1326
  const projectCwd = getProjectId(ctx.cwd);
1314
- const baseConfig = loadOracleConfig(projectCwd, { projectConfigTrustCwd: ctx.cwd });
1327
+ const baseConfig = loadOracleConfig(projectCwd, { projectConfigTrustCwd: ctx.cwd, projectConfigTrusted: isProjectTrusted(ctx) });
1315
1328
  const originSessionFile = requirePersistedSessionFile(getSessionFile(ctx), "submit oracle jobs");
1316
1329
  const projectId = getProjectId(projectCwd);
1317
1330
  const sessionId = getSessionId(originSessionFile, projectId);
@@ -10,6 +10,12 @@ export interface ExecutableSearchOptions {
10
10
  pathDelimiter?: string;
11
11
  }
12
12
 
13
+ export interface KnownBrowserUserDataPathMatchDetails {
14
+ root: string;
15
+ source: "knownBrowserUserDataDir" | "auth.chromeCookiePath" | "auth.chromeProfile" | "extraProtectedPath";
16
+ configuredPath?: string;
17
+ }
18
+
13
19
  export const SWEET_COOKIE_SAFE_STORAGE_PASSWORD_ENV_NAMES: readonly [
14
20
  "SWEET_COOKIE_CHROME_SAFE_STORAGE_PASSWORD",
15
21
  "SWEET_COOKIE_BRAVE_SAFE_STORAGE_PASSWORD",
@@ -40,6 +46,15 @@ export function knownBrowserUserDataPathMatch(
40
46
  cookieSources?: { chromeProfile?: string; chromeCookiePath?: string };
41
47
  },
42
48
  ): string | undefined;
49
+ export function knownBrowserUserDataPathMatchDetails(
50
+ pathValue: string,
51
+ options?: BrowserPathOptions & {
52
+ platform?: OraclePlatform;
53
+ includeUnsupported?: boolean;
54
+ extraProtectedPaths?: string[];
55
+ cookieSources?: { chromeProfile?: string; chromeCookiePath?: string };
56
+ },
57
+ ): KnownBrowserUserDataPathMatchDetails | undefined;
43
58
  export function assertNotKnownBrowserUserDataPath(
44
59
  pathValue: string,
45
60
  label: string,
@@ -224,23 +224,40 @@ function protectedPathsForCookieDb(cookiePath) {
224
224
  * @returns {string[]}
225
225
  */
226
226
  export function protectedCookieSourcePaths(cookieSources) {
227
+ return protectedCookieSourcePathEntries(cookieSources).map((entry) => entry.path);
228
+ }
229
+
230
+ function protectedCookieSourcePathEntries(cookieSources) {
227
231
  if (!cookieSources) return [];
228
232
  const roots = [];
229
233
  const cookiePath = typeof cookieSources.chromeCookiePath === "string" && cookieSources.chromeCookiePath.trim()
230
234
  ? cookieSources.chromeCookiePath.trim()
231
235
  : undefined;
232
- if (cookiePath) roots.push(...protectedPathsForCookieDb(cookiePath));
236
+ if (cookiePath) {
237
+ roots.push(...protectedPathsForCookieDb(cookiePath).map((path) => ({ path, source: "auth.chromeCookiePath", configuredPath: cookiePath })));
238
+ }
233
239
 
234
240
  const profile = typeof cookieSources.chromeProfile === "string" && cookieSources.chromeProfile.trim()
235
241
  ? cookieSources.chromeProfile.trim()
236
242
  : undefined;
237
243
  if (profile && looksLikeFilesystemPath(profile)) {
238
244
  const normalizedProfile = normalizedAbsolutePath(profile);
239
- if (isCookiesDbPath(normalizedProfile)) roots.push(...protectedPathsForCookieDb(normalizedProfile));
240
- else roots.push(normalizedProfile, dirname(normalizedProfile));
245
+ if (isCookiesDbPath(normalizedProfile)) {
246
+ roots.push(...protectedPathsForCookieDb(normalizedProfile).map((path) => ({ path, source: "auth.chromeProfile", configuredPath: profile })));
247
+ } else {
248
+ roots.push({ path: normalizedProfile, source: "auth.chromeProfile", configuredPath: profile }, { path: dirname(normalizedProfile), source: "auth.chromeProfile", configuredPath: profile });
249
+ }
241
250
  }
242
251
 
243
- return [...new Set(roots.map((root) => normalize(root)))];
252
+ const seen = new Set();
253
+ return roots
254
+ .map((root) => ({ ...root, path: normalize(root.path) }))
255
+ .filter((root) => {
256
+ const key = `${root.path}\0${root.source}\0${root.configuredPath}`;
257
+ if (seen.has(key)) return false;
258
+ seen.add(key);
259
+ return true;
260
+ });
244
261
  }
245
262
 
246
263
  /**
@@ -249,19 +266,24 @@ export function protectedCookieSourcePaths(cookieSources) {
249
266
  * @returns {string | undefined}
250
267
  */
251
268
  export function knownBrowserUserDataPathMatch(pathValue, options = {}) {
269
+ return knownBrowserUserDataPathMatchDetails(pathValue, options)?.root;
270
+ }
271
+
272
+ export function knownBrowserUserDataPathMatchDetails(pathValue, options = {}) {
252
273
  const platform = options.platform ?? process.platform;
253
274
  const normalizedPath = normalizedAbsolutePath(pathValue, options);
254
275
  const resolvedPath = resolvePathThroughExistingAncestorsSync(normalizedPath);
255
276
  const roots = [
256
- ...browserUserDataDirsForPlatform(platform, { ...options, includeUnsupported: options.includeUnsupported ?? true }),
257
- ...protectedCookieSourcePaths(options.cookieSources),
258
- ...((options.extraProtectedPaths ?? [])),
277
+ ...browserUserDataDirsForPlatform(platform, { ...options, includeUnsupported: options.includeUnsupported ?? true })
278
+ .map((path) => ({ path, source: "knownBrowserUserDataDir" })),
279
+ ...protectedCookieSourcePathEntries(options.cookieSources),
280
+ ...((options.extraProtectedPaths ?? [])).map((path) => ({ path, source: "extraProtectedPath" })),
259
281
  ];
260
282
  for (const root of roots) {
261
- const normalizedRoot = normalizedAbsolutePath(root, options);
262
- if (pathInsideOrEqual(normalizedPath, normalizedRoot)) return normalizedRoot;
283
+ const normalizedRoot = normalizedAbsolutePath(root.path, options);
284
+ if (pathInsideOrEqual(normalizedPath, normalizedRoot)) return { root: normalizedRoot, source: root.source, configuredPath: root.configuredPath };
263
285
  const resolvedRoot = resolvePathThroughExistingAncestorsSync(normalizedRoot) ?? normalizedRoot;
264
- if (resolvedPath && pathInsideOrEqual(resolvedPath, resolvedRoot)) return resolvedRoot;
286
+ if (resolvedPath && pathInsideOrEqual(resolvedPath, resolvedRoot)) return { root: resolvedRoot, source: root.source, configuredPath: root.configuredPath };
265
287
  }
266
288
  return undefined;
267
289
  }
@@ -273,10 +295,12 @@ export function knownBrowserUserDataPathMatch(pathValue, options = {}) {
273
295
  * @returns {void}
274
296
  */
275
297
  export function assertNotKnownBrowserUserDataPath(pathValue, label, options = {}) {
276
- const match = knownBrowserUserDataPathMatch(pathValue, options);
277
- if (match) {
278
- throw new Error(`${label} must not point into a real browser user-data directory (${match}): ${pathValue}`);
298
+ const match = knownBrowserUserDataPathMatchDetails(pathValue, options);
299
+ if (!match) return;
300
+ if (match.source === "auth.chromeCookiePath" || match.source === "auth.chromeProfile") {
301
+ throw new Error(`${label} is inside the browser profile root inferred from ${match.source} (${match.configuredPath} -> ${match.root}): ${pathValue}`);
279
302
  }
303
+ throw new Error(`${label} must not point into a real browser user-data directory (${match.root}): ${pathValue}`);
280
304
  }
281
305
 
282
306
  /**
@@ -62,6 +62,8 @@ export interface OracleStatusCounts {
62
62
  queued: number;
63
63
  }
64
64
 
65
+ export type OracleReadinessStatus = "unavailable" | "loaded" | "auth_needed" | "config_error" | "ready";
66
+
65
67
  export declare function formatBytes(bytes: number): string;
66
68
  export declare function formatOracleLifecycleEvent(event: OracleJobLifecycleEvent | undefined): string | undefined;
67
69
  export declare function formatOracleCancelOutcome(job: { id: string; status: string }): string;
@@ -71,4 +73,4 @@ export declare function buildOracleWakeupNotificationContent(
71
73
  options?: { responsePath?: string; responseAvailable?: boolean; artifactsPath?: string },
72
74
  ): string;
73
75
  export declare function formatOracleSubmitResponse(job: OracleJobSummaryLike & { promptPath: string; archivePath: string }, options: OracleSubmitResponseOptions): string;
74
- export declare function buildOracleStatusText(counts: OracleStatusCounts): string;
76
+ export declare function buildOracleStatusText(counts: OracleStatusCounts, readiness?: OracleReadinessStatus): string;
@@ -254,19 +254,28 @@ export function formatOracleSubmitResponse(job, options) {
254
254
 
255
255
  /**
256
256
  * @param {OracleStatusCounts} counts
257
+ * @param {"unavailable" | "loaded" | "auth_needed" | "config_error" | "ready"} [readiness]
257
258
  * @returns {string}
258
259
  */
259
- export function buildOracleStatusText(counts) {
260
+ export function buildOracleStatusText(counts, readiness = "loaded") {
261
+ const readinessText = {
262
+ unavailable: "unavailable",
263
+ loaded: "loaded",
264
+ auth_needed: "auth needed",
265
+ config_error: "config error",
266
+ ready: "ready",
267
+ }[readiness] ?? "loaded";
268
+ const readinessSuffix = readiness === "ready" ? "" : `, ${readinessText}`;
260
269
  if (counts.active > 0 && counts.queued > 0) {
261
- return `oracle: running (${counts.active}), queued (${counts.queued})`;
270
+ return `oracle: running (${counts.active}), queued (${counts.queued})${readinessSuffix}`;
262
271
  }
263
272
  if (counts.active > 0) {
264
273
  const suffix = counts.active > 1 ? ` (${counts.active})` : "";
265
- return `oracle: running${suffix}`;
274
+ return `oracle: running${suffix}${readinessSuffix}`;
266
275
  }
267
276
  if (counts.queued > 0) {
268
277
  const suffix = counts.queued > 1 ? ` (${counts.queued})` : "";
269
- return `oracle: queued${suffix}`;
278
+ return `oracle: queued${suffix}${readinessSuffix}`;
270
279
  }
271
- return "oracle: ready";
280
+ return `oracle: ${readinessText}`;
272
281
  }
@@ -3,9 +3,18 @@ export interface OracleStableValueState {
3
3
  stableCount: number;
4
4
  }
5
5
 
6
+ export interface OracleSendAcceptanceState {
7
+ url?: string;
8
+ urlKnown?: boolean;
9
+ assistantCount?: number;
10
+ stopStreaming?: boolean;
11
+ }
12
+
6
13
  export declare function assistantSnapshotSlice(snapshot: string, composerLabel: string, responseIndex: number): string | undefined;
7
14
  export declare function stripUrlQueryAndHash(url: string | undefined): string;
8
15
  export declare function isConversationPathUrl(url: string): boolean;
16
+ export declare function conversationIdFromUrl(url: string | undefined): string | undefined;
17
+ export declare function providerSendAccepted(before: OracleSendAcceptanceState, after: OracleSendAcceptanceState): boolean;
9
18
  export declare function resolveStableConversationUrlCandidate(url: string, previousChatUrl?: string): string | undefined;
10
19
  export declare function nextStableValueState(
11
20
  state: Partial<OracleStableValueState> | undefined,
@@ -5,6 +5,7 @@
5
5
  // Invariants/Assumptions: Snapshot text comes from agent-browser `snapshot -i`; URL inputs may be malformed and must fail safely.
6
6
 
7
7
  /** @typedef {import("./chatgpt-flow-helpers.d.mts").OracleStableValueState} OracleStableValueState */
8
+ /** @typedef {import("./chatgpt-flow-helpers.d.mts").OracleSendAcceptanceState} OracleSendAcceptanceState */
8
9
 
9
10
  /**
10
11
  * @param {string} snapshot
@@ -52,13 +53,39 @@ export function stripUrlQueryAndHash(url) {
52
53
  * @returns {boolean}
53
54
  */
54
55
  export function isConversationPathUrl(url) {
56
+ return Boolean(conversationIdFromUrl(url));
57
+ }
58
+
59
+ /**
60
+ * @param {string | undefined} url
61
+ * @returns {string | undefined}
62
+ */
63
+ export function conversationIdFromUrl(url) {
64
+ if (typeof url !== "string" || !url.trim()) return undefined;
55
65
  try {
56
- return /\/(?:c|chat)\/[A-Za-z0-9-]+$/i.test(new URL(url).pathname);
66
+ const match = new URL(url).pathname.match(/\/(?:c|chat)\/([A-Za-z0-9-]+)$/i);
67
+ return match?.[1];
57
68
  } catch {
58
- return false;
69
+ return undefined;
59
70
  }
60
71
  }
61
72
 
73
+ /**
74
+ * @param {OracleSendAcceptanceState} before
75
+ * @param {OracleSendAcceptanceState} after
76
+ * @returns {boolean}
77
+ */
78
+ export function providerSendAccepted(before, after) {
79
+ const beforeUrlKnown = before.urlKnown !== false;
80
+ const afterUrlKnown = after.urlKnown !== false;
81
+ const beforeConversationId = beforeUrlKnown ? conversationIdFromUrl(before.url) : undefined;
82
+ const afterConversationId = afterUrlKnown ? conversationIdFromUrl(after.url) : undefined;
83
+ if (beforeUrlKnown && afterUrlKnown && afterConversationId && afterConversationId !== beforeConversationId) return true;
84
+ if ((after.assistantCount ?? 0) > (before.assistantCount ?? 0)) return true;
85
+ if (after.stopStreaming === true && before.stopStreaming !== true) return true;
86
+ return false;
87
+ }
88
+
62
89
  /**
63
90
  * @param {string} url
64
91
  * @param {string | undefined} previousChatUrl
@@ -10,6 +10,7 @@ export interface OracleUiSelection {
10
10
  export declare const CHATGPT_CANONICAL_APP_ORIGINS: readonly string[];
11
11
 
12
12
  export declare function buildAllowedChatGptOrigins(chatUrl: string, authUrl?: string): string[];
13
+ export declare function stripChatGptResponseChrome(value: string | undefined): string;
13
14
  export declare function matchesModelFamilyLabel(label: string | undefined, family: OracleUiModelFamily): boolean;
14
15
  export declare function matchesRequestedModelControlLabel(label: string | undefined, selection: OracleUiSelection): boolean;
15
16
  export declare function matchesCompactIntelligenceOpenerLabel(label: string | undefined): boolean;
@@ -29,15 +29,19 @@ const AUTO_SWITCH_LABEL = "Auto-switch to Thinking";
29
29
  const THINKING_EFFORT_COMBOBOX_LABEL = "Thinking effort";
30
30
  const PRO_THINKING_EFFORT_COMBOBOX_LABEL = "Pro thinking effort";
31
31
  const EFFORT_LABELS = new Set(["Light", "Standard", "Extended", "Heavy"]);
32
- const COMPACT_INTELLIGENCE_MENU_PATTERN = /Intelligence.*Instant.*Medium.*High.*Pro/i;
33
- const COMPACT_INTELLIGENCE_CONTROL_PATTERN = /^(?:Instant(?:\s+5s)?|Medium(?:\s+5\s*[–-]\s*30s)?|High(?:\s+15\s*[–-]\s*60s)?|Pro(?:\s+5\+\s*min)?)$/i;
34
- const COMPACT_INTELLIGENCE_OPENER_PATTERN = /^(?:Instant|Medium|High|Pro)$/i;
32
+ const COMPACT_INTELLIGENCE_MENU_PATTERN = /(?:Intelligence.*Instant.*Medium.*High.*Pro|^(?:Instant|Medium|High|Extra High|Pro Standard|Pro Extended)$)/i;
33
+ const COMPACT_INTELLIGENCE_CONTROL_PATTERN = /^(?:Instant(?:\s+5s)?|Medium(?:\s+5\s*[–-]\s*30s)?|High(?:\s+15\s*[–-]\s*60s)?|Extra High|Pro\s+5\+\s*min|Pro Standard|Pro Extended)$/i;
34
+ const COMPACT_INTELLIGENCE_OPENER_PATTERN = /^(?:Instant|Medium|High|Extra High|Pro|Pro Standard|Pro Extended)$/i;
35
35
  const BARE_EFFORT_PATTERN = /^(light|standard|extended|heavy)(?:, click to remove)?$/i;
36
36
  const INSTANT_CHIP_PATTERN = /^instant(?:, click to remove)?$/i;
37
37
  const THINKING_CHIP_PATTERN = /^(?:(light|standard|extended|heavy)\s+)?thinking(?:, click to remove)?$/i;
38
38
  const PRO_CHIP_PATTERN = /^(?:(light|standard|extended|heavy)\s+)?pro(?:, click to remove)?$/i;
39
39
  const MODEL_FAMILY_CONTROL_KINDS = new Set(["button", "radio", "menuitemradio"]);
40
40
  const COMPACT_INTELLIGENCE_CONTROL_KINDS = new Set(["menuitemradio"]);
41
+ const CHATGPT_RESPONSE_CHROME_LINE_PATTERNS = Object.freeze([
42
+ /^Stopped thinking$/i,
43
+ /^Do you like this personality\?$/i,
44
+ ]);
41
45
 
42
46
  /**
43
47
  * @param {string | undefined} url
@@ -90,6 +94,18 @@ export function buildAllowedChatGptOrigins(chatUrl, authUrl) {
90
94
  ]);
91
95
  }
92
96
 
97
+ /**
98
+ * @param {string | undefined} value
99
+ * @returns {string}
100
+ */
101
+ export function stripChatGptResponseChrome(value) {
102
+ return String(value || "")
103
+ .split("\n")
104
+ .filter((line) => !CHATGPT_RESPONSE_CHROME_LINE_PATTERNS.some((pattern) => pattern.test(line.trim())))
105
+ .join("\n")
106
+ .trim();
107
+ }
108
+
93
109
  /**
94
110
  * @param {string | undefined} label
95
111
  * @param {OracleUiModelFamily} family
@@ -144,6 +160,14 @@ function parseComposerChipSelection(label) {
144
160
  };
145
161
  }
146
162
 
163
+ const proPrefixedEffortMatch = normalized.match(/^pro\s+(standard|extended)$/i);
164
+ if (proPrefixedEffortMatch) {
165
+ return {
166
+ modelFamily: /** @type {OracleUiModelFamily} */ ("pro"),
167
+ effort: /** @type {import("./chatgpt-ui-helpers.d.mts").OracleUiEffort} */ (proPrefixedEffortMatch[1].toLowerCase()),
168
+ };
169
+ }
170
+
147
171
  const proMatch = normalized.match(PRO_CHIP_PATTERN);
148
172
  if (proMatch) {
149
173
  return {
@@ -180,7 +204,22 @@ function parseCompactIntelligenceSelection(label) {
180
204
  compactTier: "high",
181
205
  };
182
206
  }
183
- if (/^Pro(?:\s+5\+\s*min)?$/i.test(normalized)) {
207
+ if (/^Extra High$/i.test(normalized)) {
208
+ return {
209
+ modelFamily: /** @type {OracleUiModelFamily} */ ("thinking"),
210
+ effort: /** @type {import("./chatgpt-ui-helpers.d.mts").OracleUiEffort} */ ("heavy"),
211
+ compactTier: "extra-high",
212
+ };
213
+ }
214
+ const proEffortMatch = normalized.match(/^Pro\s+(Standard|Extended)$/i);
215
+ if (proEffortMatch) {
216
+ return {
217
+ modelFamily: /** @type {OracleUiModelFamily} */ ("pro"),
218
+ effort: /** @type {import("./chatgpt-ui-helpers.d.mts").OracleUiEffort} */ (proEffortMatch[1].toLowerCase()),
219
+ compactTier: "pro",
220
+ };
221
+ }
222
+ if (/^Pro\s+5\+\s*min$/i.test(normalized)) {
184
223
  return {
185
224
  modelFamily: /** @type {OracleUiModelFamily} */ ("pro"),
186
225
  compactTier: "pro",
@@ -198,7 +237,7 @@ function hasRemovableComposerModelChip(entries) {
198
237
 
199
238
  function hasCompactIntelligenceMenuContext(entries) {
200
239
  return entries.some((entry) => !entry.disabled && entry.kind === "menu" && COMPACT_INTELLIGENCE_MENU_PATTERN.test(normalizeText(entry.label)))
201
- || entries.some((entry) => !entry.disabled && entry.kind === "menuitemradio" && checkedState(entry) === true && /\d/.test(String(entry.label || "")) && parseCompactIntelligenceSelection(entry.label));
240
+ || entries.some((entry) => !entry.disabled && entry.kind === "menuitemradio" && checkedState(entry) === true && parseCompactIntelligenceSelection(entry.label));
202
241
  }
203
242
 
204
243
  function hasLegacyEffortCombobox(entries) {
@@ -211,7 +250,6 @@ function hasLegacyEffortCombobox(entries) {
211
250
 
212
251
  function compactSelectionFromEntry(entry, _entries, _options = {}) {
213
252
  if (entry.disabled || !COMPACT_INTELLIGENCE_CONTROL_KINDS.has(entry.kind || "")) return undefined;
214
- if (!/\d/.test(String(entry.label || ""))) return undefined;
215
253
  return parseCompactIntelligenceSelection(entry.label);
216
254
  }
217
255
 
@@ -225,14 +263,16 @@ function compactSelectionMatchesRequested(selection, compactSelection) {
225
263
  }
226
264
 
227
265
  if (selection.modelFamily === "pro") {
228
- // The compact picker exposes one Pro tier instead of separate Pro efforts.
229
- return compactSelection.compactTier === "pro";
266
+ if (compactSelection.compactTier !== "pro") return false;
267
+ if (!compactSelection.effort) return true;
268
+ return compactSelection.effort === (selection.effort || "standard");
230
269
  }
231
270
 
232
271
  if (selection.modelFamily === "thinking") {
233
272
  const requestedEffort = selection.effort || "standard";
234
273
  if (compactSelection.compactTier === "medium") return requestedEffort === "light" || requestedEffort === "standard";
235
- if (compactSelection.compactTier === "high") return requestedEffort === "extended" || requestedEffort === "heavy";
274
+ if (compactSelection.compactTier === "high") return requestedEffort === "extended";
275
+ if (compactSelection.compactTier === "extra-high") return requestedEffort === "heavy";
236
276
  }
237
277
 
238
278
  return false;
@@ -353,7 +393,7 @@ export function effortSelectionVisible(snapshot, effortLabel) {
353
393
  if (compactSelection?.modelFamily === "thinking") {
354
394
  return compactSelectionMatchesRequested({ modelFamily: "thinking", effort: /** @type {import("./chatgpt-ui-helpers.d.mts").OracleUiEffort} */ (normalizedEffort), autoSwitchToThinking: false }, compactSelection);
355
395
  }
356
- if (compactSelection?.modelFamily === "pro") return true;
396
+ if (compactSelection?.modelFamily === "pro") return !compactSelection.effort || compactSelection.effort === normalizedEffort;
357
397
  if (entry.kind === "combobox" && normalizeText(entry.value).toLowerCase() === normalizedEffort) return true;
358
398
  const chipSelection = entry.kind === "button" ? parseComposerChipSelection(entry.label) : undefined;
359
399
  if (chipSelection?.effort === normalizedEffort) return true;
@@ -395,17 +435,16 @@ export function snapshotHasModelConfigurationUi(snapshot) {
395
435
  ),
396
436
  );
397
437
  const visibleCompactControls = entries.filter(
398
- (entry) => !entry.disabled && entry.kind === "menuitemradio" && /\d/.test(String(entry.label || "")) && parseCompactIntelligenceSelection(entry.label),
438
+ (entry) => !entry.disabled && entry.kind === "menuitemradio" && parseCompactIntelligenceSelection(entry.label),
399
439
  );
400
440
  const hasCompactIntelligenceMenu = entries.some(
401
441
  (entry) => !entry.disabled && entry.kind === "menu" && COMPACT_INTELLIGENCE_MENU_PATTERN.test(normalizeText(entry.label)),
402
442
  );
403
- const hasCloseButton = entries.some((entry) => entry.kind === "button" && entry.label === "Close" && !entry.disabled);
404
443
  const hasIntelligenceHeading = entries.some((entry) => entry.kind === "heading" && normalizeText(entry.label) === "Intelligence" && !entry.disabled);
405
444
  const hasEffortCombobox = entries.some(
406
445
  (entry) => entry.kind === "combobox" && EFFORT_LABELS.has(entry.value || "") && !entry.disabled,
407
446
  );
408
- return visibleFamilies.size >= 2 || visibleRadioFamilies.size >= 2 || visibleCompactControls.length >= 2 || hasCompactIntelligenceMenu || hasCloseButton || hasIntelligenceHeading || hasEffortCombobox;
447
+ return visibleFamilies.size >= 2 || visibleRadioFamilies.size >= 2 || visibleCompactControls.length >= 2 || hasCompactIntelligenceMenu || hasIntelligenceHeading || hasEffortCombobox;
409
448
  }
410
449
 
411
450
  /**