codex-chatgpt-control 0.2.0-alpha.1 → 0.5.0-alpha.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/CHANGELOG.md +15 -0
- package/README.md +42 -2
- package/contracts/v1/fixtures/backend-capabilities.json +11 -0
- package/contracts/v1/fixtures/command-descriptors.json +289 -7
- package/contracts/v1/fixtures/describe-runner-run.json +5 -2
- package/contracts/v1/fixtures/doctor-scenario-preflight.json +45 -2
- package/contracts/v1/fixtures/help-root.json +1 -1
- package/contracts/v1/fixtures/output-json-parse-success.json +9 -0
- package/contracts/v1/fixtures/reports-create-redacted.json +1 -1
- package/contracts/v1/fixtures/run-timeout-partial.json +15 -4
- package/contracts/v1/fixtures/stream-in-progress.ndjson +3 -0
- package/contracts/v1/fixtures/stream-submitted-completed.ndjson +1 -1
- package/contracts/v1/fixtures/surface-chat-legacy.json +35 -0
- package/contracts/v1/fixtures/surface-chat-simplified.json +37 -0
- package/contracts/v1/fixtures/surface-sidebar-false-positive.json +29 -0
- package/contracts/v1/fixtures/surface-work-advanced.json +45 -0
- package/contracts/v1/fixtures/surface-work-basic.json +38 -0
- package/contracts/v1/fixtures/workflow-ask-success.json +8 -0
- package/contracts/v1/manifest.json +32 -1
- package/contracts/v1/parity-suite.json +121 -1
- package/contracts/v1/schemas/backend-request.schema.json +15 -0
- package/contracts/v1/schemas/manifest.schema.json +6 -3
- package/contracts/v1/schemas/surface-profile.schema.json +166 -0
- package/dist/codex-chatgpt-control-backend.mjs +3198 -340
- package/dist/codex-chatgpt-control.bundle.mjs +4454 -1555
- package/dist/src/backend/client.d.ts +26 -1
- package/dist/src/backend/client.js +23 -1
- package/dist/src/backend/protocol.d.ts +1 -1
- package/dist/src/backend/protocol.js +11 -0
- package/dist/src/backend/session.js +22 -0
- package/dist/src/browser/attach.d.ts +1 -0
- package/dist/src/browser/attach.js +3 -3
- package/dist/src/browser/clipboard.d.ts +11 -0
- package/dist/src/browser/clipboard.js +34 -7
- package/dist/src/browser/page-state.js +17 -4
- package/dist/src/client.d.ts +32 -1
- package/dist/src/client.js +72 -16
- package/dist/src/commands/artifacts.js +1 -7
- package/dist/src/commands/configuration.d.ts +15 -0
- package/dist/src/commands/configuration.js +674 -0
- package/dist/src/commands/context.js +3 -1
- package/dist/src/commands/conversation.d.ts +15 -0
- package/dist/src/commands/conversation.js +44 -0
- package/dist/src/commands/deadline.d.ts +8 -0
- package/dist/src/commands/deadline.js +14 -0
- package/dist/src/commands/doctor.js +47 -3
- package/dist/src/commands/experience.d.ts +12 -0
- package/dist/src/commands/experience.js +288 -0
- package/dist/src/commands/files.js +61 -13
- package/dist/src/commands/messages.d.ts +2 -1
- package/dist/src/commands/messages.js +349 -87
- package/dist/src/commands/modes.d.ts +4 -1
- package/dist/src/commands/modes.js +243 -44
- package/dist/src/commands/probes.d.ts +15 -0
- package/dist/src/commands/probes.js +64 -0
- package/dist/src/commands/registry.js +102 -8
- package/dist/src/commands/reports.js +1 -1
- package/dist/src/commands/response-actions.js +1 -7
- package/dist/src/commands/sequence.js +24 -1
- package/dist/src/commands/session.d.ts +2 -0
- package/dist/src/commands/session.js +50 -1
- package/dist/src/commands/threads.js +3 -31
- package/dist/src/commands/work.d.ts +6 -0
- package/dist/src/commands/work.js +338 -0
- package/dist/src/dom/generation-state.d.ts +7 -0
- package/dist/src/dom/generation-state.js +7 -1
- package/dist/src/dom/label-match.d.ts +3 -0
- package/dist/src/dom/label-match.js +25 -0
- package/dist/src/dom/locale/am.d.ts +8 -1
- package/dist/src/dom/locale/am.js +8 -1
- package/dist/src/dom/locale/ar.d.ts +9 -1
- package/dist/src/dom/locale/ar.js +9 -1
- package/dist/src/dom/locale/bg.d.ts +9 -1
- package/dist/src/dom/locale/bg.js +9 -1
- package/dist/src/dom/locale/bn.d.ts +9 -1
- package/dist/src/dom/locale/bn.js +9 -1
- package/dist/src/dom/locale/bs.d.ts +8 -1
- package/dist/src/dom/locale/bs.js +8 -1
- package/dist/src/dom/locale/ca.d.ts +8 -1
- package/dist/src/dom/locale/ca.js +8 -1
- package/dist/src/dom/locale/cs.d.ts +8 -1
- package/dist/src/dom/locale/cs.js +8 -1
- package/dist/src/dom/locale/da.d.ts +7 -1
- package/dist/src/dom/locale/da.js +7 -1
- package/dist/src/dom/locale/de.d.ts +10 -3
- package/dist/src/dom/locale/de.js +10 -3
- package/dist/src/dom/locale/el.d.ts +8 -1
- package/dist/src/dom/locale/el.js +8 -1
- package/dist/src/dom/locale/en.d.ts +40 -1
- package/dist/src/dom/locale/en.js +42 -1
- package/dist/src/dom/locale/es-419.d.ts +8 -1
- package/dist/src/dom/locale/es-419.js +8 -1
- package/dist/src/dom/locale/es-ES.d.ts +8 -1
- package/dist/src/dom/locale/es-ES.js +8 -1
- package/dist/src/dom/locale/et.d.ts +8 -1
- package/dist/src/dom/locale/et.js +8 -1
- package/dist/src/dom/locale/fa.d.ts +9 -1
- package/dist/src/dom/locale/fa.js +9 -1
- package/dist/src/dom/locale/fi.d.ts +8 -1
- package/dist/src/dom/locale/fi.js +8 -1
- package/dist/src/dom/locale/fr-CA.d.ts +8 -1
- package/dist/src/dom/locale/fr-CA.js +8 -1
- package/dist/src/dom/locale/fr-FR.d.ts +8 -3
- package/dist/src/dom/locale/fr-FR.js +8 -3
- package/dist/src/dom/locale/gu.d.ts +8 -1
- package/dist/src/dom/locale/gu.js +8 -1
- package/dist/src/dom/locale/hi.d.ts +8 -1
- package/dist/src/dom/locale/hi.js +8 -1
- package/dist/src/dom/locale/hr.d.ts +7 -1
- package/dist/src/dom/locale/hr.js +7 -1
- package/dist/src/dom/locale/hu.d.ts +8 -1
- package/dist/src/dom/locale/hu.js +8 -1
- package/dist/src/dom/locale/hy.d.ts +9 -1
- package/dist/src/dom/locale/hy.js +9 -1
- package/dist/src/dom/locale/id.d.ts +8 -1
- package/dist/src/dom/locale/id.js +8 -1
- package/dist/src/dom/locale/index.d.ts +20 -1
- package/dist/src/dom/locale/index.js +102 -0
- package/dist/src/dom/locale/is.d.ts +8 -1
- package/dist/src/dom/locale/is.js +8 -1
- package/dist/src/dom/locale/it.d.ts +8 -1
- package/dist/src/dom/locale/it.js +8 -1
- package/dist/src/dom/locale/ja.d.ts +8 -1
- package/dist/src/dom/locale/ja.js +8 -1
- package/dist/src/dom/locale/ka.d.ts +8 -1
- package/dist/src/dom/locale/ka.js +8 -1
- package/dist/src/dom/locale/kk.d.ts +8 -1
- package/dist/src/dom/locale/kk.js +8 -1
- package/dist/src/dom/locale/kn.d.ts +9 -1
- package/dist/src/dom/locale/kn.js +9 -1
- package/dist/src/dom/locale/ko.d.ts +8 -1
- package/dist/src/dom/locale/ko.js +8 -1
- package/dist/src/dom/locale/lt.d.ts +9 -1
- package/dist/src/dom/locale/lt.js +9 -1
- package/dist/src/dom/locale/lv.d.ts +8 -1
- package/dist/src/dom/locale/lv.js +8 -1
- package/dist/src/dom/locale/mk.d.ts +7 -1
- package/dist/src/dom/locale/mk.js +7 -1
- package/dist/src/dom/locale/ml.d.ts +9 -1
- package/dist/src/dom/locale/ml.js +9 -1
- package/dist/src/dom/locale/mn.d.ts +9 -1
- package/dist/src/dom/locale/mn.js +9 -1
- package/dist/src/dom/locale/mr.d.ts +9 -1
- package/dist/src/dom/locale/mr.js +9 -1
- package/dist/src/dom/locale/ms.d.ts +8 -1
- package/dist/src/dom/locale/ms.js +8 -1
- package/dist/src/dom/locale/my.d.ts +8 -1
- package/dist/src/dom/locale/my.js +8 -1
- package/dist/src/dom/locale/nb.d.ts +8 -1
- package/dist/src/dom/locale/nb.js +8 -1
- package/dist/src/dom/locale/nl.d.ts +8 -1
- package/dist/src/dom/locale/nl.js +8 -1
- package/dist/src/dom/locale/pa.d.ts +9 -1
- package/dist/src/dom/locale/pa.js +9 -1
- package/dist/src/dom/locale/pl.d.ts +8 -1
- package/dist/src/dom/locale/pl.js +8 -1
- package/dist/src/dom/locale/pt-BR.d.ts +8 -1
- package/dist/src/dom/locale/pt-BR.js +8 -1
- package/dist/src/dom/locale/pt-PT.d.ts +8 -1
- package/dist/src/dom/locale/pt-PT.js +8 -1
- package/dist/src/dom/locale/ro.d.ts +7 -1
- package/dist/src/dom/locale/ro.js +7 -1
- package/dist/src/dom/locale/ru.d.ts +5 -1
- package/dist/src/dom/locale/ru.js +5 -1
- package/dist/src/dom/locale/sk.d.ts +8 -1
- package/dist/src/dom/locale/sk.js +8 -1
- package/dist/src/dom/locale/sl.d.ts +8 -1
- package/dist/src/dom/locale/sl.js +8 -1
- package/dist/src/dom/locale/so.d.ts +8 -1
- package/dist/src/dom/locale/so.js +8 -1
- package/dist/src/dom/locale/sq.d.ts +8 -1
- package/dist/src/dom/locale/sq.js +8 -1
- package/dist/src/dom/locale/sr.d.ts +5 -1
- package/dist/src/dom/locale/sr.js +5 -1
- package/dist/src/dom/locale/sv.d.ts +8 -1
- package/dist/src/dom/locale/sv.js +8 -1
- package/dist/src/dom/locale/sw.d.ts +8 -1
- package/dist/src/dom/locale/sw.js +8 -1
- package/dist/src/dom/locale/ta.d.ts +9 -1
- package/dist/src/dom/locale/ta.js +9 -1
- package/dist/src/dom/locale/te.d.ts +9 -1
- package/dist/src/dom/locale/te.js +9 -1
- package/dist/src/dom/locale/th.d.ts +8 -1
- package/dist/src/dom/locale/th.js +8 -1
- package/dist/src/dom/locale/tl.d.ts +4 -5
- package/dist/src/dom/locale/tl.js +4 -5
- package/dist/src/dom/locale/tr.d.ts +8 -1
- package/dist/src/dom/locale/tr.js +8 -1
- package/dist/src/dom/locale/types.d.ts +25 -1
- package/dist/src/dom/locale/uk.d.ts +8 -1
- package/dist/src/dom/locale/uk.js +8 -1
- package/dist/src/dom/locale/ur.d.ts +8 -1
- package/dist/src/dom/locale/ur.js +8 -1
- package/dist/src/dom/locale/vi.d.ts +8 -1
- package/dist/src/dom/locale/vi.js +8 -1
- package/dist/src/dom/locale/zh-HK.d.ts +9 -1
- package/dist/src/dom/locale/zh-HK.js +10 -2
- package/dist/src/dom/locale/zh-Hans.d.ts +9 -1
- package/dist/src/dom/locale/zh-Hans.js +9 -1
- package/dist/src/dom/locale/zh-TW.d.ts +9 -1
- package/dist/src/dom/locale/zh-TW.js +9 -1
- package/dist/src/dom/menus.js +13 -2
- package/dist/src/dom/selectors.js +6 -1
- package/dist/src/dom/wait-snapshot.d.ts +43 -0
- package/dist/src/dom/wait-snapshot.js +127 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +3 -0
- package/dist/src/runner/responses.d.ts +3 -1
- package/dist/src/runner/responses.js +16 -1
- package/dist/src/runner/result.js +129 -4
- package/dist/src/runner/stream.d.ts +1 -1
- package/dist/src/runner/stream.js +6 -0
- package/dist/src/runner/types.d.ts +35 -1
- package/dist/src/safety/risk.d.ts +11 -0
- package/dist/src/safety/risk.js +11 -0
- package/dist/src/scripts/apply-intelligence-locale-captures.d.ts +6 -0
- package/dist/src/scripts/apply-intelligence-locale-captures.js +147 -15
- package/dist/src/scripts/capture-intelligence-locales.d.ts +3 -0
- package/dist/src/scripts/capture-intelligence-locales.js +372 -3
- package/dist/src/scripts/capture-surface-profile.d.ts +23 -0
- package/dist/src/scripts/capture-surface-profile.js +304 -0
- package/dist/src/scripts/live-smoke/scenarios.js +34 -1
- package/dist/src/scripts/live-smoke.js +1 -0
- package/dist/src/types.d.ts +273 -2
- package/package.json +4 -3
- package/references/2026-07-16-chat-work-surfaces.md +87 -0
- package/references/agents-runner.md +13 -2
- package/references/backend-protocol.md +19 -7
- package/references/language-coverage.md +40 -3
- package/references/localization.md +48 -10
- package/references/python-parity.md +16 -4
- package/references/responses-adapter.md +12 -1
- package/references/troubleshooting.md +49 -6
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { PageLike } from "../types.js";
|
|
2
|
+
export type ConversationTarget = {
|
|
3
|
+
href?: string;
|
|
4
|
+
url: string;
|
|
5
|
+
};
|
|
6
|
+
export type EnsureConversationTargetOptions = {
|
|
7
|
+
timeoutMs: number;
|
|
8
|
+
};
|
|
9
|
+
export type EnsureConversationTargetResult = {
|
|
10
|
+
navigated: boolean;
|
|
11
|
+
targetUrl: string;
|
|
12
|
+
expectedConversationId?: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function ensureConversationTarget(page: PageLike, target: ConversationTarget, options: EnsureConversationTargetOptions): Promise<EnsureConversationTargetResult>;
|
|
15
|
+
export declare function waitForConversationHydrated(page: PageLike, timeoutMs: number, expectedConversationId?: string): Promise<void>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { parseConversationId } from "../browser/page-state.js";
|
|
2
|
+
import { countPageMessages, readLatestMessageText } from "../dom/messages.js";
|
|
3
|
+
const CHATGPT_HOME = "https://chatgpt.com/";
|
|
4
|
+
export async function ensureConversationTarget(page, target, options) {
|
|
5
|
+
const targetUrl = absoluteConversationUrl(target);
|
|
6
|
+
const expectedConversationId = parseConversationId(targetUrl);
|
|
7
|
+
const currentUrl = typeof page.url === "function" ? await Promise.resolve(page.url()).catch(() => "") : "";
|
|
8
|
+
if (expectedConversationId !== undefined
|
|
9
|
+
&& parseConversationId(typeof currentUrl === "string" ? currentUrl : "") === expectedConversationId) {
|
|
10
|
+
await waitForConversationHydrated(page, options.timeoutMs, expectedConversationId);
|
|
11
|
+
return ensureResult(false, targetUrl, expectedConversationId);
|
|
12
|
+
}
|
|
13
|
+
await page.goto?.(targetUrl, { waitUntil: "domcontentloaded", timeout: options.timeoutMs });
|
|
14
|
+
await waitForConversationHydrated(page, options.timeoutMs, expectedConversationId);
|
|
15
|
+
return ensureResult(true, targetUrl, expectedConversationId);
|
|
16
|
+
}
|
|
17
|
+
export async function waitForConversationHydrated(page, timeoutMs, expectedConversationId) {
|
|
18
|
+
const started = Date.now();
|
|
19
|
+
do {
|
|
20
|
+
const url = typeof page.url === "function" ? await Promise.resolve(page.url()).catch(() => "") : "";
|
|
21
|
+
const urlMatches = expectedConversationId === undefined || parseConversationId(typeof url === "string" ? url : "") === expectedConversationId;
|
|
22
|
+
const count = await countPageMessages(page).catch(() => 0);
|
|
23
|
+
const latestAssistantText = await readLatestMessageText(page, "assistant").catch(() => undefined);
|
|
24
|
+
const title = typeof page.title === "function" ? await page.title().catch(() => "") : "";
|
|
25
|
+
if (urlMatches && ((latestAssistantText?.trim().length ?? 0) > 0 || (count > 0 && title.length > 0 && title !== "ChatGPT"))) {
|
|
26
|
+
await page.waitForTimeout?.(250);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
await page.waitForTimeout?.(500);
|
|
30
|
+
} while (Date.now() - started < timeoutMs);
|
|
31
|
+
}
|
|
32
|
+
function absoluteConversationUrl(target) {
|
|
33
|
+
if (target.href !== undefined && target.href.startsWith("/")) {
|
|
34
|
+
return new URL(target.href, CHATGPT_HOME).toString();
|
|
35
|
+
}
|
|
36
|
+
return target.href ?? target.url;
|
|
37
|
+
}
|
|
38
|
+
function ensureResult(navigated, targetUrl, expectedConversationId) {
|
|
39
|
+
const result = { navigated, targetUrl };
|
|
40
|
+
if (expectedConversationId !== undefined) {
|
|
41
|
+
result.expectedConversationId = expectedConversationId;
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type Deadline = {
|
|
2
|
+
startedAtMs: number;
|
|
3
|
+
timeoutMs: number;
|
|
4
|
+
expiresAtMs: number;
|
|
5
|
+
};
|
|
6
|
+
export declare function createDeadline(timeoutMs: number, startedAtMs?: number): Deadline;
|
|
7
|
+
export declare function remainingMs(deadline: Deadline, nowMs?: number): number;
|
|
8
|
+
export declare function childTimeoutMs(deadline: Deadline, capMs: number, nowMs?: number): number;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function createDeadline(timeoutMs, startedAtMs = Date.now()) {
|
|
2
|
+
const safeTimeoutMs = Math.max(0, timeoutMs);
|
|
3
|
+
return {
|
|
4
|
+
startedAtMs,
|
|
5
|
+
timeoutMs: safeTimeoutMs,
|
|
6
|
+
expiresAtMs: startedAtMs + safeTimeoutMs,
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export function remainingMs(deadline, nowMs = Date.now()) {
|
|
10
|
+
return Math.max(0, deadline.expiresAtMs - nowMs);
|
|
11
|
+
}
|
|
12
|
+
export function childTimeoutMs(deadline, capMs, nowMs = Date.now()) {
|
|
13
|
+
return Math.max(0, Math.min(Math.max(0, capMs), remainingMs(deadline, nowMs)));
|
|
14
|
+
}
|
|
@@ -3,6 +3,7 @@ import { access, stat } from "node:fs/promises";
|
|
|
3
3
|
import { readSystemClipboard } from "../browser/clipboard.js";
|
|
4
4
|
import { readPageState } from "../browser/page-state.js";
|
|
5
5
|
import { localeLabels } from "../dom/locale-labels.js";
|
|
6
|
+
import { localeCoverageSummary } from "../dom/locale/index.js";
|
|
6
7
|
import { BROWSER_BRIDGE_REMEDIATION, resultOk } from "../errors.js";
|
|
7
8
|
import { explainCommandBlocker } from "../diagnostics/blockers.js";
|
|
8
9
|
import { preflightFiles } from "./files.js";
|
|
@@ -31,6 +32,7 @@ const REQUIRED_LOCALE_KEYS = [
|
|
|
31
32
|
"rateLimitBlocker"
|
|
32
33
|
];
|
|
33
34
|
const REQUIRED_TOOL_IDS = ["web_search", "deep_research", "create_image"];
|
|
35
|
+
const REQUIRED_MODE_OPTION_IDS = ["latest", "instant", "thinking", "extended", "medium", "high", "extraHigh", "pro"];
|
|
34
36
|
export async function doctor(env, args = {}) {
|
|
35
37
|
const wanted = args.check ?? DEFAULT_CHECKS;
|
|
36
38
|
const checks = {};
|
|
@@ -229,27 +231,69 @@ async function filePreflightCheck(env, args) {
|
|
|
229
231
|
function localizationCheck(env) {
|
|
230
232
|
const requiredKeysMissing = REQUIRED_LOCALE_KEYS.filter(key => localeLabels[key].length === 0);
|
|
231
233
|
const missingToolIds = REQUIRED_TOOL_IDS.filter(id => (localeLabels.tools[id]?.length ?? 0) === 0);
|
|
234
|
+
const missingModeOptionIds = REQUIRED_MODE_OPTION_IDS.filter(id => (localeLabels.modeOptions[id]?.length ?? 0) === 0);
|
|
232
235
|
const toolIds = Object.keys(localeLabels.tools);
|
|
236
|
+
const modeOptionIds = Object.keys(localeLabels.modeOptions);
|
|
237
|
+
const proAliases = [...localeLabels.modeOptions.pro];
|
|
238
|
+
const supportsProExtendedAlias = proAliases.some(alias => /pro\s*(?:•\s*)?extended/i.test(alias))
|
|
239
|
+
&& proAliases.some(alias => /extended\s+pro/i.test(alias));
|
|
240
|
+
const runningStateLocalizationSupport = runningStateSupport();
|
|
241
|
+
const runningStateLabelCoverage = {
|
|
242
|
+
support: runningStateLocalizationSupport,
|
|
243
|
+
stopControlCandidateCount: localeLabels.stopControl.length,
|
|
244
|
+
stoppedAssistantCandidateCount: localeLabels.stoppedAssistant.length,
|
|
245
|
+
...localeCoverageSummary.runningState,
|
|
246
|
+
registeredLocaleCount: localeCoverageSummary.registeredLocaleCount,
|
|
247
|
+
nonEnglishLocaleCount: localeCoverageSummary.nonEnglishLocaleCount
|
|
248
|
+
};
|
|
233
249
|
const englishCanonicalPresent = localeLabels.composerTextbox[0] === "Chat with ChatGPT"
|
|
234
250
|
&& localeLabels.sendButton[0] === "Send prompt"
|
|
235
251
|
&& localeLabels.modeLabels.includes("Thinking")
|
|
252
|
+
&& localeLabels.modeOptions.pro?.[0] === "Pro"
|
|
236
253
|
&& localeLabels.tools.web_search?.[0] === "Web search";
|
|
237
254
|
const labelCandidateCount = REQUIRED_LOCALE_KEYS.reduce((total, key) => total + localeLabels[key].length, 0)
|
|
238
|
-
+ Object.values(localeLabels.tools).reduce((total, values) => total + values.length, 0)
|
|
255
|
+
+ Object.values(localeLabels.tools).reduce((total, values) => total + values.length, 0)
|
|
256
|
+
+ Object.values(localeLabels.modeOptions).reduce((total, values) => total + values.length, 0);
|
|
239
257
|
const details = {
|
|
240
258
|
englishCanonicalPresent,
|
|
241
259
|
requiredKeysMissing,
|
|
242
260
|
missingToolIds,
|
|
261
|
+
missingModeOptionIds,
|
|
243
262
|
toolIds,
|
|
263
|
+
modeOptionIds,
|
|
264
|
+
proAliases,
|
|
265
|
+
supportsProExtendedAlias,
|
|
266
|
+
runningStateLabelCoverage,
|
|
244
267
|
labelCandidateCount,
|
|
245
268
|
pageAvailable: env.page !== undefined,
|
|
246
269
|
runtimeSelectorCoverage: "registry_only_stage_2"
|
|
247
270
|
};
|
|
248
|
-
if (englishCanonicalPresent && requiredKeysMissing.length === 0 && missingToolIds.length === 0) {
|
|
249
|
-
|
|
271
|
+
if (englishCanonicalPresent && requiredKeysMissing.length === 0 && missingToolIds.length === 0 && missingModeOptionIds.length === 0) {
|
|
272
|
+
const runningStateMessage = runningStateLocalizationSupport === "complete"
|
|
273
|
+
? "running-state labels are present for every registered non-English locale"
|
|
274
|
+
: runningStateLocalizationSupport === "partial"
|
|
275
|
+
? "running-state labels are partially localized"
|
|
276
|
+
: runningStateLocalizationSupport === "english_only"
|
|
277
|
+
? "running-state labels are English-only"
|
|
278
|
+
: "running-state labels are missing";
|
|
279
|
+
return unknown(`The locale registry is loaded; localized runtime selector coverage is registry-only in Stage 2 and ${runningStateMessage}.`, undefined, details);
|
|
250
280
|
}
|
|
251
281
|
return blocked("The locale registry is missing canonical labels required for selector fallback.", ["Update src/dom/locale-labels.ts or src/dom/locale/* with verified visible labels before relying on localized controls."], details, "selector_drift");
|
|
252
282
|
}
|
|
283
|
+
function runningStateSupport() {
|
|
284
|
+
const coverage = localeCoverageSummary.runningState;
|
|
285
|
+
const hasEnglishStop = coverage.stopControlLocaleCount > coverage.nonEnglishStopControlLocaleCount;
|
|
286
|
+
const hasEnglishStopped = coverage.stoppedAssistantLocaleCount > coverage.nonEnglishStoppedAssistantLocaleCount;
|
|
287
|
+
const hasEnglishFallback = hasEnglishStop && hasEnglishStopped;
|
|
288
|
+
const allNonEnglishCovered = localeCoverageSummary.nonEnglishLocaleCount > 0
|
|
289
|
+
&& coverage.nonEnglishStopControlLocaleCount >= localeCoverageSummary.nonEnglishLocaleCount
|
|
290
|
+
&& coverage.nonEnglishStoppedAssistantLocaleCount >= localeCoverageSummary.nonEnglishLocaleCount;
|
|
291
|
+
if (allNonEnglishCovered)
|
|
292
|
+
return "complete";
|
|
293
|
+
if (coverage.nonEnglishStopControlLocaleCount > 0 || coverage.nonEnglishStoppedAssistantLocaleCount > 0)
|
|
294
|
+
return "partial";
|
|
295
|
+
return hasEnglishFallback ? "english_only" : "missing";
|
|
296
|
+
}
|
|
253
297
|
async function reportsCheck(options) {
|
|
254
298
|
const destDir = options?.destDir ?? "reports/runs";
|
|
255
299
|
const includeContent = options?.includeContent === true;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CommandResult, DetectExperienceArgs, DetectExperienceData, OpenExperienceArgs, OpenExperienceData, PageLike, RuntimeEnv } from "../types.js";
|
|
2
|
+
type SurfaceSnapshot = {
|
|
3
|
+
url: string;
|
|
4
|
+
composerLabels: string[];
|
|
5
|
+
mainControls: string[];
|
|
6
|
+
mainText: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function detectExperience(env: RuntimeEnv, args?: DetectExperienceArgs): Promise<CommandResult<DetectExperienceData>>;
|
|
9
|
+
export declare function openExperience(env: RuntimeEnv, args: OpenExperienceArgs): Promise<CommandResult<OpenExperienceData>>;
|
|
10
|
+
export declare function detectExperienceFromSnapshot(snapshot: SurfaceSnapshot): DetectExperienceData;
|
|
11
|
+
export declare function readSurfaceSnapshot(page: PageLike): Promise<SurfaceSnapshot>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { resultError, resultOk } from "../errors.js";
|
|
2
|
+
import { localeLabels } from "../dom/locale-labels.js";
|
|
3
|
+
import { normalizeForLabelMatch, visibleLabelMatches } from "../dom/label-match.js";
|
|
4
|
+
import { contextFromPage } from "./context.js";
|
|
5
|
+
import { ensurePage } from "./session.js";
|
|
6
|
+
export async function detectExperience(env, args = {}) {
|
|
7
|
+
void args;
|
|
8
|
+
const boot = await ensurePage(env);
|
|
9
|
+
if (!boot.ok) {
|
|
10
|
+
return boot;
|
|
11
|
+
}
|
|
12
|
+
const page = env.page;
|
|
13
|
+
try {
|
|
14
|
+
const data = detectExperienceFromSnapshot(await readSurfaceSnapshot(page));
|
|
15
|
+
return resultOk(data, await contextFromPage(page, {
|
|
16
|
+
experience: data.experience,
|
|
17
|
+
selectorProfile: data.selectorProfile
|
|
18
|
+
}), data.experience === "unknown"
|
|
19
|
+
? ["The current ChatGPT surface could not be classified as Chat or Work from scoped composer evidence."]
|
|
20
|
+
: []);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function openExperience(env, args) {
|
|
27
|
+
const boot = await ensurePage(env);
|
|
28
|
+
if (!boot.ok) {
|
|
29
|
+
return boot;
|
|
30
|
+
}
|
|
31
|
+
const page = env.page;
|
|
32
|
+
try {
|
|
33
|
+
const before = detectExperienceFromSnapshot(await readSurfaceSnapshot(page));
|
|
34
|
+
if (before.experience === args.experience) {
|
|
35
|
+
return resultOk({
|
|
36
|
+
experience: args.experience,
|
|
37
|
+
previousExperience: before.experience,
|
|
38
|
+
changed: false,
|
|
39
|
+
selectorProfile: before.selectorProfile
|
|
40
|
+
}, await contextFromPage(page, {
|
|
41
|
+
experience: before.experience,
|
|
42
|
+
selectorProfile: before.selectorProfile
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
const labels = localeLabels.experienceOptions[args.experience];
|
|
46
|
+
if (!await clickUniqueExperienceControl(page, labels)) {
|
|
47
|
+
return experienceSelectorDrift(page, `No unique visible ChatGPT ${args.experience === "work" ? "Work" : "Chat"} surface control was found.`, before);
|
|
48
|
+
}
|
|
49
|
+
const timeoutMs = args.timeoutMs ?? 30000;
|
|
50
|
+
const started = Date.now();
|
|
51
|
+
let after = before;
|
|
52
|
+
while (Date.now() - started < timeoutMs) {
|
|
53
|
+
await page.waitForTimeout?.(250);
|
|
54
|
+
after = detectExperienceFromSnapshot(await readSurfaceSnapshot(page));
|
|
55
|
+
if (after.experience === args.experience) {
|
|
56
|
+
return resultOk({
|
|
57
|
+
experience: args.experience,
|
|
58
|
+
previousExperience: before.experience,
|
|
59
|
+
changed: true,
|
|
60
|
+
selectorProfile: after.selectorProfile
|
|
61
|
+
}, await contextFromPage(page, {
|
|
62
|
+
experience: after.experience,
|
|
63
|
+
selectorProfile: after.selectorProfile
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
status: "blocked",
|
|
70
|
+
warnings: [],
|
|
71
|
+
blocker: {
|
|
72
|
+
kind: "selector_drift",
|
|
73
|
+
code: "experience_postcondition_unverified",
|
|
74
|
+
fieldPath: "experience",
|
|
75
|
+
message: `The ${args.experience} surface control was clicked, but the composer did not verify that ChatGPT switched to ${args.experience}.`,
|
|
76
|
+
candidates: labels.map(label => ({ label })),
|
|
77
|
+
resumable: true
|
|
78
|
+
},
|
|
79
|
+
context: await contextFromPage(page, {
|
|
80
|
+
experience: after.experience,
|
|
81
|
+
selectorProfile: after.selectorProfile
|
|
82
|
+
})
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export function detectExperienceFromSnapshot(snapshot) {
|
|
90
|
+
const evidence = [];
|
|
91
|
+
const composerLabels = snapshot.composerLabels.map(normalizeForLabelMatch);
|
|
92
|
+
const controls = snapshot.mainControls.map(normalizeForLabelMatch);
|
|
93
|
+
const mainText = normalizeForLabelMatch(snapshot.mainText);
|
|
94
|
+
const url = snapshot.url.toLowerCase();
|
|
95
|
+
const workComposer = matchingLabels(composerLabels, localeLabels.workComposerTextbox);
|
|
96
|
+
for (const label of workComposer) {
|
|
97
|
+
evidence.push({ source: "composer", label });
|
|
98
|
+
}
|
|
99
|
+
const chatComposer = matchingLabels(composerLabels, localeLabels.composerTextbox);
|
|
100
|
+
for (const label of chatComposer) {
|
|
101
|
+
evidence.push({ source: "composer", label });
|
|
102
|
+
}
|
|
103
|
+
const workAxisCount = ["model", "effort", "speed"]
|
|
104
|
+
.filter(axis => hasAnyLabel(controls, localeLabels.configurationAxes[axis]))
|
|
105
|
+
.length;
|
|
106
|
+
if (workAxisCount >= 2) {
|
|
107
|
+
evidence.push({ source: "control", label: `Work configuration axes (${workAxisCount}/3)` });
|
|
108
|
+
}
|
|
109
|
+
if (/\/work(?:\/|$|\?)/.test(url)) {
|
|
110
|
+
evidence.push({ source: "url", label: snapshot.url });
|
|
111
|
+
}
|
|
112
|
+
if (containsAny(mainText, ["work on something else", "work on anything"])) {
|
|
113
|
+
evidence.push({ source: "heading", label: "Work composer copy" });
|
|
114
|
+
}
|
|
115
|
+
const workScore = workComposer.length * 4
|
|
116
|
+
+ (workAxisCount >= 2 ? 4 : 0)
|
|
117
|
+
+ (/\/work(?:\/|$|\?)/.test(url) ? 3 : 0)
|
|
118
|
+
+ (containsAny(mainText, ["work on something else", "work on anything"]) ? 2 : 0);
|
|
119
|
+
const chatScore = chatComposer.length * 4;
|
|
120
|
+
let experience = "unknown";
|
|
121
|
+
let confidence = "low";
|
|
122
|
+
if (workScore > chatScore && workScore >= 4) {
|
|
123
|
+
experience = "work";
|
|
124
|
+
confidence = workScore >= 7 ? "high" : "medium";
|
|
125
|
+
}
|
|
126
|
+
else if (chatScore > workScore && chatScore >= 4) {
|
|
127
|
+
experience = "chat";
|
|
128
|
+
confidence = "high";
|
|
129
|
+
}
|
|
130
|
+
const selectorProfile = profileFromSnapshot(snapshot, experience);
|
|
131
|
+
return { experience, selectorProfile, confidence, evidence };
|
|
132
|
+
}
|
|
133
|
+
export async function readSurfaceSnapshot(page) {
|
|
134
|
+
const url = typeof page.url === "function"
|
|
135
|
+
? await Promise.resolve(page.url()).catch(() => "")
|
|
136
|
+
: "";
|
|
137
|
+
if (typeof page.evaluate !== "function") {
|
|
138
|
+
return { url, composerLabels: [], mainControls: [], mainText: "" };
|
|
139
|
+
}
|
|
140
|
+
const snapshot = await page.evaluate(() => {
|
|
141
|
+
const visible = (element) => {
|
|
142
|
+
const html = element;
|
|
143
|
+
const rect = html.getBoundingClientRect?.();
|
|
144
|
+
const style = typeof window !== "undefined" ? window.getComputedStyle?.(html) : undefined;
|
|
145
|
+
return (rect === undefined || (rect.width > 0 && rect.height > 0))
|
|
146
|
+
&& style?.display !== "none"
|
|
147
|
+
&& style?.visibility !== "hidden"
|
|
148
|
+
&& style?.opacity !== "0";
|
|
149
|
+
};
|
|
150
|
+
const labelFor = (element) => {
|
|
151
|
+
const html = element;
|
|
152
|
+
return element.getAttribute("aria-label")
|
|
153
|
+
?? element.getAttribute("placeholder")
|
|
154
|
+
?? html.innerText
|
|
155
|
+
?? element.textContent
|
|
156
|
+
?? "";
|
|
157
|
+
};
|
|
158
|
+
const normalize = (value) => value.replace(/\s+/g, " ").trim();
|
|
159
|
+
const composerRoots = Array.from(document.querySelectorAll("main form, main [data-testid*='composer' i], main [class*='composer' i]"));
|
|
160
|
+
const composerNodes = composerRoots.flatMap(root => [
|
|
161
|
+
root,
|
|
162
|
+
...Array.from(root.querySelectorAll("textarea, [contenteditable='true'], [role='textbox'], input"))
|
|
163
|
+
]);
|
|
164
|
+
const composerLabels = Array.from(new Set(composerNodes
|
|
165
|
+
.filter(visible)
|
|
166
|
+
.map(labelFor)
|
|
167
|
+
.map(normalize)
|
|
168
|
+
.filter(Boolean)))
|
|
169
|
+
.slice(0, 16);
|
|
170
|
+
const main = document.querySelector("main");
|
|
171
|
+
const overlayRoots = Array.from(document.querySelectorAll("[role='menu'], [role='listbox'], [data-radix-popper-content-wrapper], [data-radix-menu-content]")).filter(visible);
|
|
172
|
+
const controlRoots = Array.from(new Set([...composerRoots, ...overlayRoots]));
|
|
173
|
+
const effectiveControlRoots = controlRoots.length > 0
|
|
174
|
+
? controlRoots
|
|
175
|
+
: main === null ? [] : [main];
|
|
176
|
+
const mainControls = Array.from(new Set(effectiveControlRoots.flatMap(root => Array.from(root.querySelectorAll("button, [role='button'], [role='menuitem'], [role='menuitemradio'], [role='option']")))
|
|
177
|
+
.filter(visible)
|
|
178
|
+
.map(labelFor)
|
|
179
|
+
.map(normalize)
|
|
180
|
+
.filter(Boolean)))
|
|
181
|
+
.slice(0, 120);
|
|
182
|
+
const surfaceTextNodes = main === null ? [] : Array.from(main.querySelectorAll("h1, h2, h3, form, [data-testid*='composer' i], [class*='composer' i]"))
|
|
183
|
+
.filter(visible)
|
|
184
|
+
.slice(0, 32);
|
|
185
|
+
const mainText = normalize(surfaceTextNodes.map(labelFor).join(" ")).slice(0, 2000);
|
|
186
|
+
return { composerLabels, mainControls, mainText };
|
|
187
|
+
}).catch(() => ({ composerLabels: [], mainControls: [], mainText: "" }));
|
|
188
|
+
return { url, ...snapshot };
|
|
189
|
+
}
|
|
190
|
+
function profileFromSnapshot(snapshot, experience) {
|
|
191
|
+
const controls = snapshot.mainControls.map(normalizeForLabelMatch);
|
|
192
|
+
const mainText = normalizeForLabelMatch(snapshot.mainText);
|
|
193
|
+
if (experience === "work") {
|
|
194
|
+
return hasAnyLabel(controls, localeLabels.configurationAxes.advanced)
|
|
195
|
+
|| containsAny(mainText, localeLabels.configurationAxes.advanced)
|
|
196
|
+
? "work_advanced_v1"
|
|
197
|
+
: "work_basic_v1";
|
|
198
|
+
}
|
|
199
|
+
if (experience !== "chat") {
|
|
200
|
+
return "unknown";
|
|
201
|
+
}
|
|
202
|
+
const simplifiedOptions = [
|
|
203
|
+
...localeLabels.configurationOptions.instant,
|
|
204
|
+
...localeLabels.configurationOptions.medium,
|
|
205
|
+
...localeLabels.configurationOptions.high,
|
|
206
|
+
...localeLabels.configurationOptions.extraHigh,
|
|
207
|
+
...localeLabels.configurationOptions.pro,
|
|
208
|
+
];
|
|
209
|
+
if (hasAnyLabel(controls, simplifiedOptions)) {
|
|
210
|
+
return "chat_simplified_v1";
|
|
211
|
+
}
|
|
212
|
+
const legacyOptions = [
|
|
213
|
+
...localeLabels.modeOptions.latest,
|
|
214
|
+
...localeLabels.modeOptions.thinking,
|
|
215
|
+
...localeLabels.modeOptions.extended,
|
|
216
|
+
];
|
|
217
|
+
return hasAnyLabel(controls, legacyOptions)
|
|
218
|
+
? "chat_legacy_v1"
|
|
219
|
+
: "chat_simplified_v1";
|
|
220
|
+
}
|
|
221
|
+
async function clickUniqueExperienceControl(page, labels) {
|
|
222
|
+
for (const label of labels) {
|
|
223
|
+
for (const role of ["button", "menuitem", "tab", "link"]) {
|
|
224
|
+
if (await clickIfUnique(page.getByRole?.(role, { name: label, exact: true }))) {
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (typeof page.evaluate !== "function") {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
return page.evaluate((wantedLabels) => {
|
|
233
|
+
const normalize = (value) => value.replace(/\s+/g, " ").trim().toLocaleLowerCase();
|
|
234
|
+
const wanted = new Set(wantedLabels.map(normalize));
|
|
235
|
+
const nodes = Array.from(document.querySelectorAll("header button, header [role='button'], header [role='tab'], main [role='menuitem'], main [role='option']"));
|
|
236
|
+
const matches = nodes.filter(node => {
|
|
237
|
+
const html = node;
|
|
238
|
+
const label = node.getAttribute("aria-label") ?? html.innerText ?? node.textContent ?? "";
|
|
239
|
+
return wanted.has(normalize(label));
|
|
240
|
+
});
|
|
241
|
+
if (matches.length !== 1)
|
|
242
|
+
return false;
|
|
243
|
+
matches[0].click();
|
|
244
|
+
return true;
|
|
245
|
+
}, labels).catch(() => false);
|
|
246
|
+
}
|
|
247
|
+
async function clickIfUnique(locator) {
|
|
248
|
+
if (locator?.count === undefined || locator.click === undefined) {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
if (await locator.count().catch(() => 0) !== 1) {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
await locator.click();
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
function matchingLabels(normalizedHaystack, candidates) {
|
|
258
|
+
const normalizedCandidates = candidates.map(normalizeForLabelMatch);
|
|
259
|
+
return normalizedHaystack
|
|
260
|
+
.filter(label => normalizedCandidates.some(candidate => label === candidate || visibleLabelMatches(label, candidate)))
|
|
261
|
+
.slice(0, 4);
|
|
262
|
+
}
|
|
263
|
+
function hasAnyLabel(normalizedHaystack, candidates) {
|
|
264
|
+
const normalizedCandidates = candidates.map(normalizeForLabelMatch);
|
|
265
|
+
return normalizedHaystack.some(label => normalizedCandidates.some(candidate => label === candidate || visibleLabelMatches(label, candidate)));
|
|
266
|
+
}
|
|
267
|
+
function containsAny(normalizedText, candidates) {
|
|
268
|
+
return candidates.map(normalizeForLabelMatch).some(candidate => normalizedText.includes(candidate));
|
|
269
|
+
}
|
|
270
|
+
async function experienceSelectorDrift(page, message, detected) {
|
|
271
|
+
return {
|
|
272
|
+
ok: false,
|
|
273
|
+
status: "unsupported",
|
|
274
|
+
warnings: [],
|
|
275
|
+
blocker: {
|
|
276
|
+
kind: "selector_drift",
|
|
277
|
+
code: "experience_control_not_found",
|
|
278
|
+
fieldPath: "experience",
|
|
279
|
+
message,
|
|
280
|
+
candidates: detected.evidence.map(item => ({ label: `${item.source}: ${item.label}` })),
|
|
281
|
+
resumable: true
|
|
282
|
+
},
|
|
283
|
+
context: await contextFromPage(page, {
|
|
284
|
+
experience: detected.experience,
|
|
285
|
+
selectorProfile: detected.selectorProfile
|
|
286
|
+
})
|
|
287
|
+
};
|
|
288
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { access, readFile, stat } from "node:fs/promises";
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { downloadLatestArtifact, locatorCountWithTimeout } from "./artifacts.js";
|
|
5
6
|
import { waitForDownloadFromClick } from "../browser/downloads.js";
|
|
@@ -8,7 +9,7 @@ import { addFilesButton, cssSelectors, requiredLocator } from "../dom/selectors.
|
|
|
8
9
|
import { localeLabels } from "../dom/locale-labels.js";
|
|
9
10
|
import { basenameForHostPath, isHostAbsolutePath, resolveForHostPath } from "../platform/local-paths.js";
|
|
10
11
|
import { contextFromPage } from "./context.js";
|
|
11
|
-
import {
|
|
12
|
+
import { ensurePage } from "./session.js";
|
|
12
13
|
import { localGuardTimeout } from "./timeouts.js";
|
|
13
14
|
const CODEX_UPLOAD_PERMISSION_FIX = "Codex Settings > Computer Use > Chrome > Permissions > Uploads: set to Always allow, or add chatgpt.com to the allowed upload domains.";
|
|
14
15
|
const CHROME_FILE_URL_PERMISSION_FIX = "Chrome chrome://extensions > Codex extension > Details: enable Allow access to file URLs.";
|
|
@@ -103,7 +104,17 @@ export async function preflightFiles(env, args) {
|
|
|
103
104
|
message: `File attachment exceeds the configured per-file preflight limit: ${absolute} (${fileStat.size}/${maxBytesPerFile} bytes)`
|
|
104
105
|
});
|
|
105
106
|
}
|
|
106
|
-
|
|
107
|
+
if (fileStat.size === 0) {
|
|
108
|
+
return filePreflightBlocker({
|
|
109
|
+
env,
|
|
110
|
+
status: "blocked",
|
|
111
|
+
kind: "upload_failed",
|
|
112
|
+
code: "file_empty",
|
|
113
|
+
fieldPath,
|
|
114
|
+
message: `File attachment path is zero bytes and ChatGPT rejects empty attachments: ${absolute}`
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
const metadata = await fileMetadata(absolute, fileStat.size, args.includeHashes === true);
|
|
107
118
|
files.push(metadata);
|
|
108
119
|
}
|
|
109
120
|
const totalBytes = files.reduce((sum, file) => sum + file.bytes, 0);
|
|
@@ -121,7 +132,11 @@ export async function preflightFiles(env, args) {
|
|
|
121
132
|
return resultOk({ files, totalBytes }, filePreflightContext(env), warnings);
|
|
122
133
|
}
|
|
123
134
|
export async function attachFiles(env, args) {
|
|
124
|
-
const
|
|
135
|
+
const preflightArgs = { paths: args.paths };
|
|
136
|
+
if (args.includeDiagnostics === true && args.includeHashes !== undefined) {
|
|
137
|
+
preflightArgs.includeHashes = args.includeHashes;
|
|
138
|
+
}
|
|
139
|
+
const preflight = await preflightFiles(env, preflightArgs);
|
|
125
140
|
if (!preflight.ok || preflight.data === undefined) {
|
|
126
141
|
return preflight;
|
|
127
142
|
}
|
|
@@ -137,6 +152,9 @@ export async function attachFiles(env, args) {
|
|
|
137
152
|
bytes: file.bytes
|
|
138
153
|
}));
|
|
139
154
|
await uploadFiles(page, files, args.timeoutMs ?? 30000);
|
|
155
|
+
const browserInput = args.includeDiagnostics === true
|
|
156
|
+
? await readBrowserInputDiagnostic(page).catch(() => undefined)
|
|
157
|
+
: undefined;
|
|
140
158
|
await page.waitForTimeout?.(args.timeoutMs === undefined ? 1000 : Math.min(args.timeoutMs, 3000));
|
|
141
159
|
const readiness = await waitForAttachedFilesReady(page, files, args.timeoutMs ?? 30000);
|
|
142
160
|
if (!readiness.ready) {
|
|
@@ -169,7 +187,14 @@ export async function attachFiles(env, args) {
|
|
|
169
187
|
context: await contextFromPage(page)
|
|
170
188
|
};
|
|
171
189
|
}
|
|
172
|
-
|
|
190
|
+
const data = { files };
|
|
191
|
+
if (args.includeDiagnostics === true) {
|
|
192
|
+
data.diagnostics = { preflight: preflight.data };
|
|
193
|
+
if (browserInput !== undefined) {
|
|
194
|
+
data.diagnostics.browserInput = browserInput;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return resultOk(data, await contextFromPage(page), preflight.warnings);
|
|
173
198
|
}
|
|
174
199
|
catch (error) {
|
|
175
200
|
if (isUploadBridgeBlocker(error)) {
|
|
@@ -209,10 +234,10 @@ function filePreflightBlocker(args) {
|
|
|
209
234
|
function filePreflightContext(env) {
|
|
210
235
|
return { timestamp: (env.now?.() ?? new Date()).toISOString() };
|
|
211
236
|
}
|
|
212
|
-
function fileMetadata(absolute, bytes) {
|
|
237
|
+
async function fileMetadata(absolute, bytes, includeHash = false) {
|
|
213
238
|
const extension = extensionForHostPath(absolute);
|
|
214
239
|
const { mimeType, category } = guessFileType(extension);
|
|
215
|
-
|
|
240
|
+
const metadata = {
|
|
216
241
|
path: absolute,
|
|
217
242
|
name: basenameForHostPath(absolute),
|
|
218
243
|
bytes,
|
|
@@ -220,6 +245,10 @@ function fileMetadata(absolute, bytes) {
|
|
|
220
245
|
mimeType,
|
|
221
246
|
category
|
|
222
247
|
};
|
|
248
|
+
if (includeHash) {
|
|
249
|
+
metadata.sha256 = createHash("sha256").update(await readFile(absolute)).digest("hex");
|
|
250
|
+
}
|
|
251
|
+
return metadata;
|
|
223
252
|
}
|
|
224
253
|
function extensionForHostPath(value) {
|
|
225
254
|
return process.platform === "win32"
|
|
@@ -230,9 +259,6 @@ function collectFilePreflightWarnings(files, warnings) {
|
|
|
230
259
|
const byPath = new Map();
|
|
231
260
|
const byName = new Map();
|
|
232
261
|
for (const file of files) {
|
|
233
|
-
if (file.bytes === 0) {
|
|
234
|
-
warnings.push(`Zero-byte file will be attached if ChatGPT accepts it: ${file.name}`);
|
|
235
|
-
}
|
|
236
262
|
const pathCount = (byPath.get(file.path) ?? 0) + 1;
|
|
237
263
|
byPath.set(file.path, pathCount);
|
|
238
264
|
if (pathCount === 2) {
|
|
@@ -556,11 +582,33 @@ async function setHiddenFileInput(page, files) {
|
|
|
556
582
|
}
|
|
557
583
|
await input.setInputFiles(files.map(file => file.path));
|
|
558
584
|
}
|
|
559
|
-
async function
|
|
560
|
-
if (
|
|
561
|
-
return
|
|
585
|
+
async function readBrowserInputDiagnostic(page) {
|
|
586
|
+
if (typeof page.evaluate !== "function") {
|
|
587
|
+
return undefined;
|
|
562
588
|
}
|
|
563
|
-
return
|
|
589
|
+
return page.evaluate(() => {
|
|
590
|
+
const input = (document.querySelector("#upload-files")
|
|
591
|
+
|| document.querySelector("input[type='file']:not([accept='image/*'])")
|
|
592
|
+
|| document.querySelector("input[type='file']"));
|
|
593
|
+
if (!input) {
|
|
594
|
+
return { files: [] };
|
|
595
|
+
}
|
|
596
|
+
return {
|
|
597
|
+
files: Array.from(input.files ?? []).map(file => {
|
|
598
|
+
const diagnostic = {
|
|
599
|
+
name: file.name,
|
|
600
|
+
size: file.size
|
|
601
|
+
};
|
|
602
|
+
if (file.type.length > 0) {
|
|
603
|
+
diagnostic.type = file.type;
|
|
604
|
+
}
|
|
605
|
+
if (file.lastModified !== 0) {
|
|
606
|
+
diagnostic.lastModified = file.lastModified;
|
|
607
|
+
}
|
|
608
|
+
return diagnostic;
|
|
609
|
+
})
|
|
610
|
+
};
|
|
611
|
+
});
|
|
564
612
|
}
|
|
565
613
|
async function setFilesViaDomDataTransfer(page, files) {
|
|
566
614
|
const totalBytes = files.reduce((sum, file) => sum + file.bytes, 0);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type AssistantGenerationState } from "../dom/generation-state.js";
|
|
2
|
-
import type { AskArgs, AskReadData, CommandResult, ComposeArgs, ComposeData, ReadLatestArgs, ReadLatestData, RuntimeEnv, SubmitArgs, SubmitData, WaitAndReadArgs, WaitArgs, WaitData } from "../types.js";
|
|
2
|
+
import type { AskArgs, AskReadData, CommandResult, ComposeArgs, ComposeData, MessageStatusArgs, MessageStatusData, ReadLatestArgs, ReadLatestData, RuntimeEnv, SubmitArgs, SubmitData, WaitAndReadArgs, WaitArgs, WaitData } from "../types.js";
|
|
3
3
|
export type CompletionSnapshot = {
|
|
4
4
|
textStableForMs: number;
|
|
5
5
|
stableMs: number;
|
|
@@ -12,6 +12,7 @@ export declare function composeMessage(env: RuntimeEnv, args: ComposeArgs): Prom
|
|
|
12
12
|
export declare function submitMessage(env: RuntimeEnv, args?: SubmitArgs): Promise<CommandResult<SubmitData>>;
|
|
13
13
|
export declare function waitForMessage(env: RuntimeEnv, args?: WaitArgs): Promise<CommandResult<WaitData>>;
|
|
14
14
|
export declare function readLatest(env: RuntimeEnv, args?: ReadLatestArgs): Promise<CommandResult<ReadLatestData>>;
|
|
15
|
+
export declare function messageStatus(env: RuntimeEnv, args?: MessageStatusArgs): Promise<CommandResult<MessageStatusData>>;
|
|
15
16
|
export declare function askMessage(env: RuntimeEnv, args: AskArgs): Promise<CommandResult<AskReadData>>;
|
|
16
17
|
export declare function waitAndRead(env: RuntimeEnv, args?: WaitAndReadArgs): Promise<CommandResult<AskReadData>>;
|
|
17
18
|
export declare function submittedUserTurnMatches(actual: string | undefined, wanted: string | undefined): boolean;
|