codex-chatgpt-control 0.2.0-alpha.1 → 0.3.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 +8 -0
- package/contracts/v1/fixtures/backend-capabilities.json +1 -0
- package/contracts/v1/fixtures/command-descriptors.json +33 -2
- package/contracts/v1/fixtures/doctor-scenario-preflight.json +12 -1
- package/contracts/v1/fixtures/help-root.json +1 -1
- package/contracts/v1/fixtures/reports-create-redacted.json +1 -1
- package/contracts/v1/parity-suite.json +8 -1
- package/dist/codex-chatgpt-control-backend.mjs +1199 -159
- package/dist/codex-chatgpt-control.bundle.mjs +1198 -156
- package/dist/src/backend/protocol.d.ts +1 -1
- package/dist/src/backend/protocol.js +1 -0
- package/dist/src/backend/session.js +2 -0
- package/dist/src/browser/clipboard.d.ts +11 -0
- package/dist/src/browser/clipboard.js +34 -7
- package/dist/src/browser/page-state.js +12 -2
- package/dist/src/client.d.ts +2 -1
- package/dist/src/client.js +3 -2
- 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 +9 -2
- package/dist/src/commands/files.js +62 -8
- package/dist/src/commands/messages.js +212 -72
- package/dist/src/commands/modes.d.ts +4 -1
- package/dist/src/commands/modes.js +215 -30
- package/dist/src/commands/probes.d.ts +15 -0
- package/dist/src/commands/probes.js +64 -0
- package/dist/src/commands/registry.js +34 -2
- package/dist/src/commands/reports.js +1 -1
- package/dist/src/commands/threads.js +2 -24
- 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 +6 -0
- package/dist/src/dom/locale/am.js +6 -0
- package/dist/src/dom/locale/ar.d.ts +7 -0
- package/dist/src/dom/locale/ar.js +7 -0
- package/dist/src/dom/locale/bg.d.ts +7 -0
- package/dist/src/dom/locale/bg.js +7 -0
- package/dist/src/dom/locale/bn.d.ts +7 -0
- package/dist/src/dom/locale/bn.js +7 -0
- package/dist/src/dom/locale/bs.d.ts +6 -0
- package/dist/src/dom/locale/bs.js +6 -0
- package/dist/src/dom/locale/ca.d.ts +6 -0
- package/dist/src/dom/locale/ca.js +6 -0
- package/dist/src/dom/locale/cs.d.ts +6 -0
- package/dist/src/dom/locale/cs.js +6 -0
- package/dist/src/dom/locale/da.d.ts +5 -0
- package/dist/src/dom/locale/da.js +5 -0
- package/dist/src/dom/locale/de.d.ts +6 -0
- package/dist/src/dom/locale/de.js +6 -0
- package/dist/src/dom/locale/el.d.ts +6 -0
- package/dist/src/dom/locale/el.js +6 -0
- package/dist/src/dom/locale/en.d.ts +14 -0
- package/dist/src/dom/locale/en.js +15 -0
- package/dist/src/dom/locale/es-419.d.ts +6 -0
- package/dist/src/dom/locale/es-419.js +6 -0
- package/dist/src/dom/locale/es-ES.d.ts +6 -0
- package/dist/src/dom/locale/es-ES.js +6 -0
- package/dist/src/dom/locale/et.d.ts +6 -0
- package/dist/src/dom/locale/et.js +6 -0
- package/dist/src/dom/locale/fa.d.ts +7 -0
- package/dist/src/dom/locale/fa.js +7 -0
- package/dist/src/dom/locale/fi.d.ts +6 -0
- package/dist/src/dom/locale/fi.js +6 -0
- package/dist/src/dom/locale/fr-CA.d.ts +6 -0
- package/dist/src/dom/locale/fr-CA.js +6 -0
- package/dist/src/dom/locale/fr-FR.d.ts +5 -0
- package/dist/src/dom/locale/fr-FR.js +5 -0
- package/dist/src/dom/locale/gu.d.ts +6 -0
- package/dist/src/dom/locale/gu.js +6 -0
- package/dist/src/dom/locale/hi.d.ts +6 -0
- package/dist/src/dom/locale/hi.js +6 -0
- package/dist/src/dom/locale/hr.d.ts +5 -0
- package/dist/src/dom/locale/hr.js +5 -0
- package/dist/src/dom/locale/hu.d.ts +6 -0
- package/dist/src/dom/locale/hu.js +6 -0
- package/dist/src/dom/locale/hy.d.ts +7 -0
- package/dist/src/dom/locale/hy.js +7 -0
- package/dist/src/dom/locale/id.d.ts +6 -0
- package/dist/src/dom/locale/id.js +6 -0
- package/dist/src/dom/locale/index.d.ts +5 -1
- package/dist/src/dom/locale/index.js +37 -0
- package/dist/src/dom/locale/is.d.ts +6 -0
- package/dist/src/dom/locale/is.js +6 -0
- package/dist/src/dom/locale/it.d.ts +6 -0
- package/dist/src/dom/locale/it.js +6 -0
- package/dist/src/dom/locale/ja.d.ts +6 -0
- package/dist/src/dom/locale/ja.js +6 -0
- package/dist/src/dom/locale/ka.d.ts +6 -0
- package/dist/src/dom/locale/ka.js +6 -0
- package/dist/src/dom/locale/kk.d.ts +6 -0
- package/dist/src/dom/locale/kk.js +6 -0
- package/dist/src/dom/locale/kn.d.ts +7 -0
- package/dist/src/dom/locale/kn.js +7 -0
- package/dist/src/dom/locale/ko.d.ts +6 -0
- package/dist/src/dom/locale/ko.js +6 -0
- package/dist/src/dom/locale/lt.d.ts +7 -0
- package/dist/src/dom/locale/lt.js +7 -0
- package/dist/src/dom/locale/lv.d.ts +6 -0
- package/dist/src/dom/locale/lv.js +6 -0
- package/dist/src/dom/locale/mk.d.ts +5 -0
- package/dist/src/dom/locale/mk.js +5 -0
- package/dist/src/dom/locale/ml.d.ts +7 -0
- package/dist/src/dom/locale/ml.js +7 -0
- package/dist/src/dom/locale/mn.d.ts +7 -0
- package/dist/src/dom/locale/mn.js +7 -0
- package/dist/src/dom/locale/mr.d.ts +7 -0
- package/dist/src/dom/locale/mr.js +7 -0
- package/dist/src/dom/locale/ms.d.ts +6 -0
- package/dist/src/dom/locale/ms.js +6 -0
- package/dist/src/dom/locale/my.d.ts +6 -0
- package/dist/src/dom/locale/my.js +6 -0
- package/dist/src/dom/locale/nb.d.ts +6 -0
- package/dist/src/dom/locale/nb.js +6 -0
- package/dist/src/dom/locale/nl.d.ts +6 -0
- package/dist/src/dom/locale/nl.js +6 -0
- package/dist/src/dom/locale/pa.d.ts +7 -0
- package/dist/src/dom/locale/pa.js +7 -0
- package/dist/src/dom/locale/pl.d.ts +6 -0
- package/dist/src/dom/locale/pl.js +6 -0
- package/dist/src/dom/locale/pt-BR.d.ts +6 -0
- package/dist/src/dom/locale/pt-BR.js +6 -0
- package/dist/src/dom/locale/pt-PT.d.ts +6 -0
- package/dist/src/dom/locale/pt-PT.js +6 -0
- package/dist/src/dom/locale/ro.d.ts +5 -0
- package/dist/src/dom/locale/ro.js +5 -0
- package/dist/src/dom/locale/ru.d.ts +3 -0
- package/dist/src/dom/locale/ru.js +3 -0
- package/dist/src/dom/locale/sk.d.ts +6 -0
- package/dist/src/dom/locale/sk.js +6 -0
- package/dist/src/dom/locale/sl.d.ts +6 -0
- package/dist/src/dom/locale/sl.js +6 -0
- package/dist/src/dom/locale/so.d.ts +6 -0
- package/dist/src/dom/locale/so.js +6 -0
- package/dist/src/dom/locale/sq.d.ts +6 -0
- package/dist/src/dom/locale/sq.js +6 -0
- package/dist/src/dom/locale/sr.d.ts +3 -0
- package/dist/src/dom/locale/sr.js +3 -0
- package/dist/src/dom/locale/sv.d.ts +6 -0
- package/dist/src/dom/locale/sv.js +6 -0
- package/dist/src/dom/locale/sw.d.ts +6 -0
- package/dist/src/dom/locale/sw.js +6 -0
- package/dist/src/dom/locale/ta.d.ts +7 -0
- package/dist/src/dom/locale/ta.js +7 -0
- package/dist/src/dom/locale/te.d.ts +7 -0
- package/dist/src/dom/locale/te.js +7 -0
- package/dist/src/dom/locale/th.d.ts +6 -0
- package/dist/src/dom/locale/th.js +6 -0
- package/dist/src/dom/locale/tr.d.ts +6 -0
- package/dist/src/dom/locale/tr.js +6 -0
- package/dist/src/dom/locale/types.d.ts +8 -1
- package/dist/src/dom/locale/uk.d.ts +6 -0
- package/dist/src/dom/locale/uk.js +6 -0
- package/dist/src/dom/locale/ur.d.ts +6 -0
- package/dist/src/dom/locale/ur.js +6 -0
- package/dist/src/dom/locale/vi.d.ts +6 -0
- package/dist/src/dom/locale/vi.js +6 -0
- package/dist/src/dom/locale/zh-HK.d.ts +7 -0
- package/dist/src/dom/locale/zh-HK.js +7 -0
- package/dist/src/dom/locale/zh-Hans.d.ts +7 -0
- package/dist/src/dom/locale/zh-Hans.js +7 -0
- package/dist/src/dom/locale/zh-TW.d.ts +7 -0
- package/dist/src/dom/locale/zh-TW.js +7 -0
- package/dist/src/dom/menus.js +13 -2
- package/dist/src/dom/wait-snapshot.d.ts +43 -0
- package/dist/src/dom/wait-snapshot.js +127 -0
- package/dist/src/safety/risk.d.ts +1 -0
- package/dist/src/safety/risk.js +1 -0
- package/dist/src/scripts/apply-intelligence-locale-captures.js +99 -13
- package/dist/src/types.d.ts +28 -0
- package/package.json +2 -2
- package/references/backend-protocol.md +6 -4
- package/references/localization.md +14 -4
- package/references/python-parity.md +5 -3
- package/references/troubleshooting.md +28 -5
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { resultError, resultOk } from "../errors.js";
|
|
2
2
|
import { enumerateVisibleMenuItems, findUniqueMenuItem } from "../dom/menus.js";
|
|
3
3
|
import { localeLabels } from "../dom/locale-labels.js";
|
|
4
|
+
import { isShortLatinToken, normalizeForLabelMatch, visibleLabelMatches } from "../dom/label-match.js";
|
|
4
5
|
import { normalizeLabel, normalizeWhitespace } from "../dom/visible-text.js";
|
|
5
6
|
import { contextFromPage } from "./context.js";
|
|
6
7
|
import { bootstrap } from "./session.js";
|
|
7
8
|
const DEFAULT_MODE_EFFORT = "Thinking";
|
|
8
|
-
const CURRENT_MODE_LABELS = [
|
|
9
|
+
const CURRENT_MODE_LABELS = dedupeLabels([
|
|
10
|
+
...localeLabels.modeLabels,
|
|
11
|
+
...Object.values(localeLabels.modeOptions).flat(),
|
|
12
|
+
]);
|
|
9
13
|
const MODE_OPENER_LABELS = [...CURRENT_MODE_LABELS.filter(label => label !== "Pro"), ...localeLabels.modeOpenerExtra];
|
|
10
14
|
const MODEL_VERSION_FAMILY_PATTERN = /^gpt[\s-]/i;
|
|
11
15
|
const MODEL_VERSION_LABEL_PATTERN = /^(?:o\d+|\d+(?:\.\d+)?)$/i;
|
|
@@ -13,9 +17,33 @@ const CANONICAL_INTELLIGENCE_ORDER = new Map([
|
|
|
13
17
|
["instant", 0],
|
|
14
18
|
["medium", 1],
|
|
15
19
|
["high", 2],
|
|
16
|
-
["
|
|
20
|
+
["extraHigh", 3],
|
|
17
21
|
["pro", 4],
|
|
18
22
|
]);
|
|
23
|
+
const MODE_OPTION_IDS = [
|
|
24
|
+
"latest",
|
|
25
|
+
"instant",
|
|
26
|
+
"thinking",
|
|
27
|
+
"extended",
|
|
28
|
+
"medium",
|
|
29
|
+
"high",
|
|
30
|
+
"extraHigh",
|
|
31
|
+
"pro",
|
|
32
|
+
];
|
|
33
|
+
const MODE_ID_ALIASES = {
|
|
34
|
+
latest: ["latest"],
|
|
35
|
+
instant: ["instant"],
|
|
36
|
+
thinking: ["thinking"],
|
|
37
|
+
extended: ["extended"],
|
|
38
|
+
medium: ["medium"],
|
|
39
|
+
high: ["high"],
|
|
40
|
+
extraHigh: ["extra high", "extra-high", "extra_high", "extrahigh"],
|
|
41
|
+
pro: ["pro"],
|
|
42
|
+
};
|
|
43
|
+
const THREAD_ACTION_MENU_LABELS = new Set(localeLabels.threadActionMenuItems.map(normalizeForLabelMatch));
|
|
44
|
+
const THREAD_ACTION_PREFIXES = localeLabels.threadActionPrefixes
|
|
45
|
+
.map(normalizeForLabelMatch)
|
|
46
|
+
.filter(prefix => prefix.length > 0);
|
|
19
47
|
export async function setMode(env, args) {
|
|
20
48
|
const boot = await ensurePage(env);
|
|
21
49
|
if (!boot.ok) {
|
|
@@ -23,9 +51,9 @@ export async function setMode(env, args) {
|
|
|
23
51
|
}
|
|
24
52
|
const page = env.page;
|
|
25
53
|
try {
|
|
26
|
-
const requested =
|
|
54
|
+
const requested = requestedModeSelections(args);
|
|
27
55
|
const requestedVersion = requestedModelVersion(args);
|
|
28
|
-
const requestedForOpening = requestedVersion === undefined ? requested : [...requested, requestedVersion];
|
|
56
|
+
const requestedForOpening = requestedVersion === undefined ? requested : [...requested, requestedModeSelection(requestedVersion)];
|
|
29
57
|
const opened = await waitForModeMenu(page, requestedForOpening, args.timeoutMs ?? 30000);
|
|
30
58
|
if (requestedVersion === undefined && opened.alreadySelected.length === requested.length) {
|
|
31
59
|
return resultOk({ selected: opened.alreadySelected, candidates: opened.modeButtons }, await contextFromPage(page));
|
|
@@ -36,15 +64,25 @@ export async function setMode(env, args) {
|
|
|
36
64
|
await page.waitForTimeout?.(250);
|
|
37
65
|
const candidates = await enumerateVisibleMenuItems(page);
|
|
38
66
|
const selected = [];
|
|
39
|
-
|
|
40
|
-
const
|
|
67
|
+
if (requested.length > 0 && shouldRejectAsWrongModeMenu(candidates)) {
|
|
68
|
+
const candidateLabels = candidates.map(candidate => candidate.label);
|
|
69
|
+
return {
|
|
70
|
+
ok: false,
|
|
71
|
+
status: "unsupported",
|
|
72
|
+
warnings: [],
|
|
73
|
+
blocker: selectorDriftBlocker("Visible menu appears to be a thread/action menu, not the ChatGPT mode menu.", candidateLabels),
|
|
74
|
+
context: await contextFromPage(page)
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
for (const request of requested) {
|
|
78
|
+
const match = findModeMenuItem(candidates, request);
|
|
41
79
|
if (match === undefined) {
|
|
42
80
|
const candidateLabels = candidates.map(candidate => candidate.label);
|
|
43
81
|
return {
|
|
44
82
|
ok: false,
|
|
45
83
|
status: "unsupported",
|
|
46
84
|
warnings: [],
|
|
47
|
-
blocker: selectorDriftBlocker(`Mode option "${
|
|
85
|
+
blocker: selectorDriftBlocker(`Mode option "${request.requested}" was not found or was ambiguous.`, candidateLabels),
|
|
48
86
|
context: await contextFromPage(page)
|
|
49
87
|
};
|
|
50
88
|
}
|
|
@@ -68,7 +106,53 @@ export async function setMode(env, args) {
|
|
|
68
106
|
}
|
|
69
107
|
selected.push(versionResult.selected);
|
|
70
108
|
}
|
|
71
|
-
|
|
109
|
+
const verificationWarnings = await modeVerificationWarnings(page, requested, selected);
|
|
110
|
+
return resultOk({ selected, candidates: candidateLabels }, await contextFromPage(page), verificationWarnings);
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Post-condition check: after clicking mode rows, the composer's mode-labelled controls
|
|
118
|
+
* should reflect the requested selection. A mismatch does not fail the command — some
|
|
119
|
+
* ChatGPT surfaces do not echo every mode — but it must be visible to callers so a
|
|
120
|
+
* modes.set "ok" is never silently treated as a verified mode (the "Pin ... Pro ..."
|
|
121
|
+
* incident shape). Model-version selections are not verified because versions are
|
|
122
|
+
* usually not echoed on the composer.
|
|
123
|
+
*/
|
|
124
|
+
async function modeVerificationWarnings(page, requested, selected) {
|
|
125
|
+
if (requested.length === 0) {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
await page.waitForTimeout?.(250);
|
|
129
|
+
const visibleButtons = await visibleModeButtonLabelList(page);
|
|
130
|
+
if (visibleButtons.length === 0) {
|
|
131
|
+
return [
|
|
132
|
+
`Mode selection is unverified: no mode-labelled composer control was visible after selecting ${selected.map(label => JSON.stringify(label)).join(", ")}. Use modes.get or inspect modeStep before treating this as a verified mode.`
|
|
133
|
+
];
|
|
134
|
+
}
|
|
135
|
+
const unverified = requested.filter(request => findUniqueVisibleLabelForRequest(visibleButtons, request) === undefined);
|
|
136
|
+
if (unverified.length === 0) {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
return [
|
|
140
|
+
`Mode selection is unverified: requested ${unverified.map(request => JSON.stringify(request.requested)).join(", ")} is not reflected by the visible mode controls (${visibleButtons.join(", ")}). Use modes.get or inspect modeStep before treating this as a verified mode.`
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
export async function getMode(env, args = {}) {
|
|
144
|
+
void args;
|
|
145
|
+
const boot = await ensurePage(env);
|
|
146
|
+
if (!boot.ok) {
|
|
147
|
+
return boot;
|
|
148
|
+
}
|
|
149
|
+
const page = env.page;
|
|
150
|
+
try {
|
|
151
|
+
const modes = await visibleModeButtonLabelList(page);
|
|
152
|
+
const warnings = modes.length === 0
|
|
153
|
+
? ["No mode-labelled composer control is currently visible, so the active ChatGPT mode could not be read."]
|
|
154
|
+
: [];
|
|
155
|
+
return resultOk({ modes }, await contextFromPage(page), warnings);
|
|
72
156
|
}
|
|
73
157
|
catch (error) {
|
|
74
158
|
return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
|
|
@@ -84,7 +168,7 @@ async function waitForModeMenu(page, requested, timeoutMs) {
|
|
|
84
168
|
return { opened: false, alreadySelected, modeButtons };
|
|
85
169
|
}
|
|
86
170
|
const openMenuItems = await enumerateVisibleMenuItems(page);
|
|
87
|
-
if (looksLikeModeMenu(openMenuItems
|
|
171
|
+
if (looksLikeModeMenu(openMenuItems)) {
|
|
88
172
|
return { opened: true, alreadySelected: [], modeButtons };
|
|
89
173
|
}
|
|
90
174
|
if (await clickModeOpener(page, modeButtons)) {
|
|
@@ -165,12 +249,57 @@ async function clickModeOpener(page, modeButtons) {
|
|
|
165
249
|
}
|
|
166
250
|
return clickFirstUniqueButton(page, MODE_OPENER_LABELS);
|
|
167
251
|
}
|
|
168
|
-
function
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return
|
|
252
|
+
function isThreadActionLabel(label) {
|
|
253
|
+
const normalized = normalizeForLabelMatch(label);
|
|
254
|
+
if (THREAD_ACTION_MENU_LABELS.has(normalized)) {
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
return THREAD_ACTION_PREFIXES.some(prefix => normalized.startsWith(`${prefix} `));
|
|
258
|
+
}
|
|
259
|
+
function hasStructuralModeEvidence(item) {
|
|
260
|
+
if (item.testId?.startsWith("model-switcher-") === true) {
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
return MODEL_VERSION_FAMILY_PATTERN.test(item.label) || MODEL_VERSION_LABEL_PATTERN.test(item.label);
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Whether a single menu item is evidence that the visible menu is the ChatGPT mode menu.
|
|
267
|
+
*
|
|
268
|
+
* A fuzzy token hit on a short mode word such as "Pro" is NOT evidence: thread titles
|
|
269
|
+
* inside sidebar action menus can contain it ("Pin CopyBench Pro Consultation"), which is
|
|
270
|
+
* exactly the false positive that let modes.set select a pin action as the Pro mode.
|
|
271
|
+
* Thread-action rows can never be evidence, even when their title embeds a full mode word.
|
|
272
|
+
* Exact matching uses normalizeForLabelMatch (NFKC) so the evidence set and the veto set
|
|
273
|
+
* share identical Unicode folding — fullwidth/compatibility-form labels match both sides.
|
|
274
|
+
*/
|
|
275
|
+
function isModeMenuEvidence(item) {
|
|
276
|
+
if (hasStructuralModeEvidence(item)) {
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
if (isThreadActionLabel(item.label)) {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
const normalized = normalizeForLabelMatch(item.label);
|
|
283
|
+
return CURRENT_MODE_LABELS.some(modeLabel => {
|
|
284
|
+
const normalizedMode = normalizeForLabelMatch(modeLabel);
|
|
285
|
+
if (normalized === normalizedMode) {
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
return !isShortLatinToken(normalizedMode) && visibleLabelMatches(normalized, normalizedMode);
|
|
172
289
|
});
|
|
173
290
|
}
|
|
291
|
+
function looksLikeModeMenu(items) {
|
|
292
|
+
return items.some(item => isModeMenuEvidence(item));
|
|
293
|
+
}
|
|
294
|
+
function shouldRejectAsWrongModeMenu(items) {
|
|
295
|
+
if (items.length === 0) {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
if (items.some(item => isModeMenuEvidence(item))) {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
return items.some(item => isThreadActionLabel(item.label));
|
|
302
|
+
}
|
|
174
303
|
async function clickMenuItem(page, label) {
|
|
175
304
|
if (await clickModelSwitcherMenuItem(page, label)) {
|
|
176
305
|
return true;
|
|
@@ -262,28 +391,81 @@ function toolLabels(tool) {
|
|
|
262
391
|
const known = localeLabels.tools[tool];
|
|
263
392
|
return known !== undefined ? [...known] : [tool];
|
|
264
393
|
}
|
|
265
|
-
function findModeMenuItem(candidates,
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
394
|
+
function findModeMenuItem(candidates, request) {
|
|
395
|
+
const selectable = candidates.filter(candidate => !isThreadActionLabel(candidate.label));
|
|
396
|
+
for (const wanted of request.labels) {
|
|
397
|
+
const match = findUniqueModeMenuItem(selectable, wanted);
|
|
398
|
+
if (match !== undefined) {
|
|
399
|
+
return match;
|
|
400
|
+
}
|
|
269
401
|
}
|
|
270
|
-
const wantedIndex = CANONICAL_INTELLIGENCE_ORDER.get(
|
|
402
|
+
const wantedIndex = request.modeId === undefined ? undefined : CANONICAL_INTELLIGENCE_ORDER.get(request.modeId);
|
|
271
403
|
if (wantedIndex === undefined) {
|
|
272
404
|
return undefined;
|
|
273
405
|
}
|
|
274
|
-
const intelligenceItems =
|
|
406
|
+
const intelligenceItems = selectable.filter(candidate => candidate.role === "menuitemradio"
|
|
275
407
|
&& !MODEL_VERSION_LABEL_PATTERN.test(candidate.label)
|
|
276
408
|
&& !MODEL_VERSION_FAMILY_PATTERN.test(candidate.label));
|
|
277
409
|
return intelligenceItems.length >= CANONICAL_INTELLIGENCE_ORDER.size
|
|
278
410
|
? intelligenceItems[wantedIndex]
|
|
279
411
|
: undefined;
|
|
280
412
|
}
|
|
281
|
-
|
|
413
|
+
/**
|
|
414
|
+
* Unique-match a wanted mode label. Exact normalized equality is always accepted; a
|
|
415
|
+
* fuzzy token hit on a short mode word such as "Pro" additionally requires structural
|
|
416
|
+
* mode-row evidence (a model-switcher test id or a menuitemradio role), so an arbitrary
|
|
417
|
+
* unique menu row whose text merely contains "Pro" cannot be selected as the mode.
|
|
418
|
+
* Exact matching uses normalizeForLabelMatch (NFKC), consistent with isModeMenuEvidence.
|
|
419
|
+
*/
|
|
420
|
+
function findUniqueModeMenuItem(items, wanted) {
|
|
421
|
+
const normalizedWanted = normalizeForLabelMatch(wanted);
|
|
422
|
+
const exact = items.filter(item => normalizeForLabelMatch(item.label) === normalizedWanted);
|
|
423
|
+
if (exact.length === 1) {
|
|
424
|
+
return exact[0];
|
|
425
|
+
}
|
|
426
|
+
const fuzzy = items.filter(item => visibleLabelMatches(item.label, wanted));
|
|
427
|
+
if (fuzzy.length !== 1) {
|
|
428
|
+
return undefined;
|
|
429
|
+
}
|
|
430
|
+
const match = fuzzy[0];
|
|
431
|
+
if (!isShortLatinToken(normalizedWanted)) {
|
|
432
|
+
return match;
|
|
433
|
+
}
|
|
434
|
+
return hasStructuralModeEvidence(match) || match.role === "menuitemradio" ? match : undefined;
|
|
435
|
+
}
|
|
436
|
+
function requestedModeSelections(args) {
|
|
282
437
|
const requested = [args.model, args.intelligence, args.effort].filter((value) => value !== undefined);
|
|
283
438
|
if (requestedModelVersion(args) !== undefined && requested.length === 0) {
|
|
284
439
|
return [];
|
|
285
440
|
}
|
|
286
|
-
return requested.length > 0 ? requested : [DEFAULT_MODE_EFFORT];
|
|
441
|
+
return (requested.length > 0 ? requested : [DEFAULT_MODE_EFFORT]).map(requestedModeSelection);
|
|
442
|
+
}
|
|
443
|
+
function requestedModeSelection(requested) {
|
|
444
|
+
const modeId = modeOptionIdFor(requested);
|
|
445
|
+
const labels = modeId === undefined ? [requested] : localeLabels.modeOptions[modeId];
|
|
446
|
+
const request = {
|
|
447
|
+
requested,
|
|
448
|
+
labels: labels.length > 0 ? [...labels] : [requested],
|
|
449
|
+
};
|
|
450
|
+
if (modeId !== undefined) {
|
|
451
|
+
request.modeId = modeId;
|
|
452
|
+
}
|
|
453
|
+
return request;
|
|
454
|
+
}
|
|
455
|
+
function modeOptionIdFor(value) {
|
|
456
|
+
const normalized = normalizeModeLookupKey(value);
|
|
457
|
+
for (const id of MODE_OPTION_IDS) {
|
|
458
|
+
if (MODE_ID_ALIASES[id].some(alias => normalizeModeLookupKey(alias) === normalized)) {
|
|
459
|
+
return id;
|
|
460
|
+
}
|
|
461
|
+
if (localeLabels.modeOptions[id].some(label => normalizeModeLookupKey(label) === normalized)) {
|
|
462
|
+
return id;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return undefined;
|
|
466
|
+
}
|
|
467
|
+
function normalizeModeLookupKey(value) {
|
|
468
|
+
return normalizeForLabelMatch(value).replace(/[_-]+/g, " ");
|
|
287
469
|
}
|
|
288
470
|
function requestedModelVersion(args) {
|
|
289
471
|
return args.modelVersion ?? args.version;
|
|
@@ -297,12 +479,6 @@ function findUniqueVisibleLabel(labels, wanted) {
|
|
|
297
479
|
const fuzzy = labels.filter(label => visibleLabelMatches(normalizeLabel(label), normalized));
|
|
298
480
|
return fuzzy.length === 1 ? fuzzy[0] : undefined;
|
|
299
481
|
}
|
|
300
|
-
function visibleLabelMatches(label, wanted) {
|
|
301
|
-
if (wanted.length <= 3) {
|
|
302
|
-
return new RegExp(`(^|[^a-z0-9])${escapeRegExp(wanted)}([^a-z0-9]|$)`, "i").test(label);
|
|
303
|
-
}
|
|
304
|
-
return label.includes(wanted);
|
|
305
|
-
}
|
|
306
482
|
function escapeRegExp(value) {
|
|
307
483
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
308
484
|
}
|
|
@@ -311,13 +487,22 @@ function escapeAttributeValue(value) {
|
|
|
311
487
|
}
|
|
312
488
|
function findAlreadySelectedModes(visibleButtons, requested) {
|
|
313
489
|
return requested
|
|
314
|
-
.map(
|
|
490
|
+
.map(request => findUniqueVisibleLabelForRequest(visibleButtons, request))
|
|
315
491
|
.filter((label) => label !== undefined);
|
|
316
492
|
}
|
|
493
|
+
function findUniqueVisibleLabelForRequest(labels, request) {
|
|
494
|
+
for (const label of request.labels) {
|
|
495
|
+
const found = findUniqueVisibleLabel(labels, label);
|
|
496
|
+
if (found !== undefined) {
|
|
497
|
+
return found;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return undefined;
|
|
501
|
+
}
|
|
317
502
|
async function selectModelVersion(page, requestedVersion, currentCandidates, timeoutMs) {
|
|
318
503
|
let candidates = await enumerateVisibleMenuItems(page);
|
|
319
|
-
if (!looksLikeModeMenu(candidates
|
|
320
|
-
const opened = await waitForModeMenu(page, [requestedVersion], timeoutMs);
|
|
504
|
+
if (!looksLikeModeMenu(candidates)) {
|
|
505
|
+
const opened = await waitForModeMenu(page, [{ requested: requestedVersion, labels: [requestedVersion] }], timeoutMs);
|
|
321
506
|
if (opened.opened) {
|
|
322
507
|
await page.waitForTimeout?.(250);
|
|
323
508
|
candidates = await enumerateVisibleMenuItems(page);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type Deadline } from "./deadline.js";
|
|
2
|
+
export type ProbeResult<T> = {
|
|
3
|
+
ok: true;
|
|
4
|
+
value: T;
|
|
5
|
+
warnings: string[];
|
|
6
|
+
} | {
|
|
7
|
+
ok: false;
|
|
8
|
+
timedOut?: boolean;
|
|
9
|
+
skipped?: boolean;
|
|
10
|
+
warnings: string[];
|
|
11
|
+
};
|
|
12
|
+
export type ProbeOptions = {
|
|
13
|
+
timeoutMs: number;
|
|
14
|
+
};
|
|
15
|
+
export declare function createSingleFlightProbe<A, T>(name: string, probe: (arg: A) => Promise<T>): (arg: A, deadline: Deadline, options: ProbeOptions) => Promise<ProbeResult<T>>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { childTimeoutMs } from "./deadline.js";
|
|
2
|
+
export function createSingleFlightProbe(name, probe) {
|
|
3
|
+
let inFlight;
|
|
4
|
+
return async (arg, deadline, options) => {
|
|
5
|
+
if (inFlight !== undefined) {
|
|
6
|
+
return {
|
|
7
|
+
ok: false,
|
|
8
|
+
skipped: true,
|
|
9
|
+
warnings: [`Skipped ${name} DOM probe because previous browser-side work is still in flight.`]
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
const timeoutMs = childTimeoutMs(deadline, options.timeoutMs);
|
|
13
|
+
if (timeoutMs <= 0) {
|
|
14
|
+
return {
|
|
15
|
+
ok: false,
|
|
16
|
+
timedOut: true,
|
|
17
|
+
warnings: [`Skipped ${name} DOM probe because no deadline budget remained.`]
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const current = probe(arg);
|
|
21
|
+
inFlight = current;
|
|
22
|
+
current.finally(() => {
|
|
23
|
+
if (inFlight === current) {
|
|
24
|
+
inFlight = undefined;
|
|
25
|
+
}
|
|
26
|
+
}).catch(() => undefined);
|
|
27
|
+
let timeout;
|
|
28
|
+
try {
|
|
29
|
+
const value = await Promise.race([
|
|
30
|
+
current,
|
|
31
|
+
new Promise((_resolve, reject) => {
|
|
32
|
+
timeout = setTimeout(() => reject(new ProbeTimeoutError(timeoutMs)), timeoutMs);
|
|
33
|
+
})
|
|
34
|
+
]);
|
|
35
|
+
return { ok: true, value, warnings: [] };
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (error instanceof ProbeTimeoutError) {
|
|
39
|
+
return {
|
|
40
|
+
ok: false,
|
|
41
|
+
timedOut: true,
|
|
42
|
+
warnings: [`Timed out waiting ${timeoutMs}ms for ${name} DOM probe; this stopped SDK waiting but did not cancel browser-side work.`]
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
warnings: [`${name} DOM probe failed: ${error instanceof Error ? error.message : String(error)}`]
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
if (timeout !== undefined) {
|
|
52
|
+
clearTimeout(timeout);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
class ProbeTimeoutError extends Error {
|
|
58
|
+
timeoutMs;
|
|
59
|
+
constructor(timeoutMs) {
|
|
60
|
+
super(`Probe timed out after ${timeoutMs}ms.`);
|
|
61
|
+
this.timeoutMs = timeoutMs;
|
|
62
|
+
this.name = "ProbeTimeoutError";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -83,6 +83,7 @@ const descriptors = [
|
|
|
83
83
|
primitive("projects.sources.add", "Append explicit local files to a visible ChatGPT Project Sources list after confirmMutation: true.", 180000),
|
|
84
84
|
primitive("response.copy", "Click Copy response and return clipboard Markdown, with DOM fallback.", 5000),
|
|
85
85
|
primitive("modes.set", "Select a visible model, intelligence, effort, or nested model-version candidate when unambiguous.", 30000),
|
|
86
|
+
primitive("modes.get", "Read the mode labels shown on the visible composer controls without changing them.", 30000),
|
|
86
87
|
primitive("tools.select", "Select a visible ChatGPT tool when unambiguous.", 30000)
|
|
87
88
|
];
|
|
88
89
|
export function commandDescriptors() {
|
|
@@ -232,6 +233,14 @@ function reportArgs(name) {
|
|
|
232
233
|
return { result: "CommandResult to persist", destDir: "optional report directory" };
|
|
233
234
|
}
|
|
234
235
|
function primitiveArgs(name) {
|
|
236
|
+
if (name === "messages.wait")
|
|
237
|
+
return {
|
|
238
|
+
timeoutMs: "optional wait timeout",
|
|
239
|
+
stableMs: "optional stability window before completion",
|
|
240
|
+
pollMs: "optional polling interval",
|
|
241
|
+
mode: "normal or deep_research",
|
|
242
|
+
responseContent: "include for current behavior, metadata to omit assistant text and return chars/hash only"
|
|
243
|
+
};
|
|
235
244
|
if (name === "messages.readLatest")
|
|
236
245
|
return { role: "assistant or user", format: "markdown, normalized_text, visible_text, html, blocks, or all" };
|
|
237
246
|
if (name === "artifacts.listLatest")
|
|
@@ -245,7 +254,18 @@ function primitiveArgs(name) {
|
|
|
245
254
|
if (name.startsWith("threads.search"))
|
|
246
255
|
return { query: "history search query" };
|
|
247
256
|
if (name === "files.preflight")
|
|
248
|
-
return {
|
|
257
|
+
return {
|
|
258
|
+
paths: "absolute local file paths",
|
|
259
|
+
maxBytesPerFile: "optional local per-file byte limit",
|
|
260
|
+
maxTotalBytes: "optional local total byte limit",
|
|
261
|
+
includeHashes: "optional SHA-256 metadata for diagnostics; file contents are not returned"
|
|
262
|
+
};
|
|
263
|
+
if (name === "files.attach")
|
|
264
|
+
return {
|
|
265
|
+
paths: "absolute local file paths",
|
|
266
|
+
includeDiagnostics: "optional preflight and browser input file-size metadata",
|
|
267
|
+
includeHashes: "optional SHA-256 metadata inside diagnostics; file contents are not returned"
|
|
268
|
+
};
|
|
249
269
|
if (name.startsWith("files.attach"))
|
|
250
270
|
return { paths: "absolute local file paths" };
|
|
251
271
|
if (name === "projects.sources.list")
|
|
@@ -264,6 +284,11 @@ function primitiveArgs(name) {
|
|
|
264
284
|
timeoutMs: "optional timeout for opening and selecting the visible mode menu"
|
|
265
285
|
};
|
|
266
286
|
}
|
|
287
|
+
if (name === "modes.get") {
|
|
288
|
+
return {
|
|
289
|
+
timeoutMs: "optional timeout for attaching to the ChatGPT page"
|
|
290
|
+
};
|
|
291
|
+
}
|
|
267
292
|
return {};
|
|
268
293
|
}
|
|
269
294
|
function primitiveExamples(name) {
|
|
@@ -275,9 +300,16 @@ function primitiveExamples(name) {
|
|
|
275
300
|
`await chatgpt.askWithFiles({ mode: { model: "Pro" }, files: ["/absolute/host/path.jpg"], prompt: "Describe this image.", wait: true });`
|
|
276
301
|
];
|
|
277
302
|
}
|
|
303
|
+
if (name === "modes.get") {
|
|
304
|
+
return [
|
|
305
|
+
`await chatgpt.modes.get();`,
|
|
306
|
+
`// Verify an expensive Pro consult before submitting: const current = await chatgpt.modes.get();`
|
|
307
|
+
];
|
|
308
|
+
}
|
|
278
309
|
if (name === "files.preflight") {
|
|
279
310
|
return [
|
|
280
|
-
`await chatgpt.files.preflight({ paths: ["/absolute/host/path.md"] })
|
|
311
|
+
`await chatgpt.files.preflight({ paths: ["/absolute/host/path.md"] });`,
|
|
312
|
+
`await chatgpt.files.preflight({ paths: ["/absolute/host/path.md"], includeHashes: true });`
|
|
281
313
|
];
|
|
282
314
|
}
|
|
283
315
|
if (name === "files.attach") {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { parseConversationId, readPageState } from "../browser/page-state.js";
|
|
2
2
|
import { resultError, resultOk } from "../errors.js";
|
|
3
|
-
import { countPageMessages, readLatestMessageText } from "../dom/messages.js";
|
|
4
3
|
import { requiredLocator, searchChatsButton, searchChatsInput, newChatButton } from "../dom/selectors.js";
|
|
5
4
|
import { anyLabelPattern, localeLabels } from "../dom/locale-labels.js";
|
|
6
5
|
import { normalizeWhitespace, stripTags } from "../dom/visible-text.js";
|
|
7
6
|
import { contextFromPage } from "./context.js";
|
|
7
|
+
import { ensureConversationTarget } from "./conversation.js";
|
|
8
8
|
import { bootstrap } from "./session.js";
|
|
9
9
|
const CHATGPT_HOME = "https://chatgpt.com/";
|
|
10
10
|
export function extractThreadSearchResultsFromHtml(html) {
|
|
@@ -100,13 +100,7 @@ export async function openThread(env, args, previousResults) {
|
|
|
100
100
|
context: await contextFromPage(page)
|
|
101
101
|
};
|
|
102
102
|
}
|
|
103
|
-
|
|
104
|
-
await page.goto?.(new URL(target.href, CHATGPT_HOME).toString(), { waitUntil: "domcontentloaded", timeout: args.timeoutMs ?? 30000 });
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
await page.goto?.(target.href ?? target.url, { waitUntil: "domcontentloaded", timeout: args.timeoutMs ?? 30000 });
|
|
108
|
-
}
|
|
109
|
-
await waitForThreadHydrated(page, args.timeoutMs ?? 30000, parseConversationId(target.url));
|
|
103
|
+
await ensureConversationTarget(page, target, { timeoutMs: args.timeoutMs ?? 30000 });
|
|
110
104
|
const state = await readPageState(page);
|
|
111
105
|
return resultOk(openThreadData(state.url, state.conversationId, state.title ?? target.title), await contextFromPage(page));
|
|
112
106
|
}
|
|
@@ -283,19 +277,3 @@ function filterResultsByQuery(results, query) {
|
|
|
283
277
|
return haystack.includes(wanted) || wanted.includes(normalizeForMatch(result.title));
|
|
284
278
|
});
|
|
285
279
|
}
|
|
286
|
-
async function waitForThreadHydrated(page, timeoutMs, expectedConversationId) {
|
|
287
|
-
const started = Date.now();
|
|
288
|
-
await page.waitForTimeout?.(1000);
|
|
289
|
-
while (Date.now() - started < timeoutMs) {
|
|
290
|
-
const url = typeof page.url === "function" ? await Promise.resolve(page.url()).catch(() => "") : "";
|
|
291
|
-
const urlMatches = expectedConversationId === undefined || url.includes(expectedConversationId);
|
|
292
|
-
const count = await countPageMessages(page).catch(() => 0);
|
|
293
|
-
const latestAssistantText = await readLatestMessageText(page, "assistant").catch(() => undefined);
|
|
294
|
-
const title = typeof page.title === "function" ? await page.title().catch(() => "") : "";
|
|
295
|
-
if (urlMatches && ((latestAssistantText?.trim().length ?? 0) > 0 || (count > 0 && title.length > 0 && title !== "ChatGPT"))) {
|
|
296
|
-
await page.waitForTimeout?.(250);
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
await page.waitForTimeout?.(500);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
@@ -4,5 +4,12 @@ export type AssistantGenerationState = {
|
|
|
4
4
|
stopped: boolean;
|
|
5
5
|
signals: string[];
|
|
6
6
|
};
|
|
7
|
+
/**
|
|
8
|
+
* Neutral fallback used when generation state cannot be inspected.
|
|
9
|
+
*
|
|
10
|
+
* This means "no active/stopped signal observed"; it is not evidence that a
|
|
11
|
+
* response is complete.
|
|
12
|
+
*/
|
|
13
|
+
export declare const EMPTY_GENERATION_STATE: AssistantGenerationState;
|
|
7
14
|
export declare function readAssistantGenerationState(page: PageLike): Promise<AssistantGenerationState>;
|
|
8
15
|
export declare function latestAssistantTurnHasResponseActions(page: PageLike): Promise<boolean>;
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { copyResponseButtons } from "./selectors.js";
|
|
2
2
|
import { localeLabels } from "./locale-labels.js";
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Neutral fallback used when generation state cannot be inspected.
|
|
5
|
+
*
|
|
6
|
+
* This means "no active/stopped signal observed"; it is not evidence that a
|
|
7
|
+
* response is complete.
|
|
8
|
+
*/
|
|
9
|
+
export const EMPTY_GENERATION_STATE = {
|
|
4
10
|
active: false,
|
|
5
11
|
stopped: false,
|
|
6
12
|
signals: []
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { normalizeWhitespace } from "./visible-text.js";
|
|
2
|
+
export function normalizeForLabelMatch(text) {
|
|
3
|
+
return normalizeWhitespace(text.normalize("NFKC")).toLocaleLowerCase();
|
|
4
|
+
}
|
|
5
|
+
export function visibleLabelMatches(label, wanted) {
|
|
6
|
+
const normalizedLabel = normalizeForLabelMatch(label);
|
|
7
|
+
const normalizedWanted = normalizeForLabelMatch(wanted);
|
|
8
|
+
if (normalizedWanted.length === 0) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
if (normalizedLabel === normalizedWanted) {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
if (isShortLatinToken(normalizedWanted)) {
|
|
15
|
+
return new RegExp(`(^|[^\\p{L}\\p{N}])${escapeRegExp(normalizedWanted)}([^\\p{L}\\p{N}]|$)`, "iu")
|
|
16
|
+
.test(normalizedLabel);
|
|
17
|
+
}
|
|
18
|
+
return normalizedLabel.includes(normalizedWanted);
|
|
19
|
+
}
|
|
20
|
+
export function isShortLatinToken(value) {
|
|
21
|
+
return value.length <= 3 && /^[a-z0-9]+$/i.test(value);
|
|
22
|
+
}
|
|
23
|
+
function escapeRegExp(value) {
|
|
24
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
25
|
+
}
|
|
@@ -17,6 +17,12 @@ export declare const am: {
|
|
|
17
17
|
addPhotosFilesMenuItem: string[];
|
|
18
18
|
copyResponse: string[];
|
|
19
19
|
modeLabels: string[];
|
|
20
|
+
modeOptions: {
|
|
21
|
+
instant: string[];
|
|
22
|
+
medium: string[];
|
|
23
|
+
high: string[];
|
|
24
|
+
extraHigh: string[];
|
|
25
|
+
};
|
|
20
26
|
modeOpenerExtra: string[];
|
|
21
27
|
tools: {
|
|
22
28
|
web_search: string[];
|
|
@@ -17,6 +17,12 @@ export const am = {
|
|
|
17
17
|
addPhotosFilesMenuItem: ["ፎቶዎችን እና ፋይሎችን ያክሉ"],
|
|
18
18
|
copyResponse: ["ምላሹን ይቅዱ"],
|
|
19
19
|
modeLabels: ["ፈጣን", "መካከለኛ", "ከፍተኛ", "እጅግ ከፍተኛ"],
|
|
20
|
+
modeOptions: {
|
|
21
|
+
instant: ["ፈጣን"],
|
|
22
|
+
medium: ["መካከለኛ"],
|
|
23
|
+
high: ["ከፍተኛ"],
|
|
24
|
+
extraHigh: ["እጅግ ከፍተኛ"],
|
|
25
|
+
},
|
|
20
26
|
modeOpenerExtra: ["ያዋቅሩ"],
|
|
21
27
|
tools: {
|
|
22
28
|
web_search: ["የድር ፍለጋ"],
|
|
@@ -17,6 +17,13 @@ export declare const ar: {
|
|
|
17
17
|
addPhotosFilesMenuItem: string[];
|
|
18
18
|
copyResponse: string[];
|
|
19
19
|
modeLabels: string[];
|
|
20
|
+
modeOptions: {
|
|
21
|
+
instant: string[];
|
|
22
|
+
medium: string[];
|
|
23
|
+
high: string[];
|
|
24
|
+
extraHigh: string[];
|
|
25
|
+
pro: string[];
|
|
26
|
+
};
|
|
20
27
|
modeOpenerExtra: string[];
|
|
21
28
|
tools: {
|
|
22
29
|
web_search: string[];
|