wtagent 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +2 -2
- package/src/browser/base-web-adapter.js +4 -34
- package/src/browser/chatgpt-web-adapter.js +4 -217
- package/src/browser/deepseek-web-adapter.js +0 -106
- package/src/browser/glm-web-adapter.js +2 -89
- package/src/browser/grok-web-adapter.js +145 -0
- package/src/browser/kimi-web-adapter.js +2 -74
- package/src/browser/provider-registry.js +8 -50
- package/src/cli/i18n.js +949 -0
- package/src/cli/main.js +125 -186
- package/src/cli/prompt-input.js +369 -155
- package/src/cli/render-events.js +45 -35
- package/src/cli/self-update.js +10 -3
- package/src/runtime/agent-runtime.js +14 -51
- package/src/shared/limits.js +2 -9
- package/src/browser/mode-selection.js +0 -214
- package/src/cli/mode-choice.js +0 -25
package/src/cli/render-events.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
// A single Renderer instance is shared across every turn of a conversation so
|
|
9
9
|
// spinner state and per-tool timing survive between the model's replies.
|
|
10
10
|
|
|
11
|
+
import { t } from "./i18n.js";
|
|
12
|
+
|
|
11
13
|
const DIM = "\x1b[2m";
|
|
12
14
|
const BOLD = "\x1b[1m";
|
|
13
15
|
const CYAN = "\x1b[36m";
|
|
@@ -109,7 +111,7 @@ export class Renderer {
|
|
|
109
111
|
}
|
|
110
112
|
this.spinner = {
|
|
111
113
|
kind: "processing",
|
|
112
|
-
label: "processing
|
|
114
|
+
label: t("renderer.processing"),
|
|
113
115
|
start: Date.now(),
|
|
114
116
|
bytes: 0,
|
|
115
117
|
};
|
|
@@ -119,7 +121,7 @@ export class Renderer {
|
|
|
119
121
|
beginRunning(name) {
|
|
120
122
|
this.spinner = {
|
|
121
123
|
kind: "running",
|
|
122
|
-
label:
|
|
124
|
+
label: t("renderer.running", { tool: name }),
|
|
123
125
|
start: Date.now(),
|
|
124
126
|
bytes: 0,
|
|
125
127
|
};
|
|
@@ -235,7 +237,7 @@ export class Renderer {
|
|
|
235
237
|
answer(message) {
|
|
236
238
|
this.stopSpinner();
|
|
237
239
|
this.println("");
|
|
238
|
-
this.println(`${GREEN}${BOLD}⏺ assistant${RESET}`);
|
|
240
|
+
this.println(`${GREEN}${BOLD}⏺ ${t("renderer.assistant")}${RESET}`);
|
|
239
241
|
for (const line of String(message ?? "").split(/\r?\n/)) {
|
|
240
242
|
this.println(line);
|
|
241
243
|
}
|
|
@@ -260,36 +262,25 @@ export class Renderer {
|
|
|
260
262
|
break;
|
|
261
263
|
}
|
|
262
264
|
this.lifecycleShown.add(type);
|
|
263
|
-
this.status("
|
|
265
|
+
this.status(t("renderer.chromeStarted"));
|
|
264
266
|
break;
|
|
265
267
|
case "browser.auth_required":
|
|
266
268
|
this.stopSpinner();
|
|
267
|
-
this.println(
|
|
269
|
+
this.println(
|
|
270
|
+
`${YELLOW}${t("renderer.loginRequired", { provider: this.providerLabel })}${RESET}`,
|
|
271
|
+
);
|
|
268
272
|
break;
|
|
269
273
|
case "browser.authenticated":
|
|
270
|
-
this.println(
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const { requested, status, selectedLabel } = payload;
|
|
274
|
-
if (status === "select" || status === "already") {
|
|
275
|
-
this.status(`Mode: ${selectedLabel ?? requested}.`);
|
|
276
|
-
} else if (status === "fallback") {
|
|
277
|
-
this.note(`Mode: ${requested} unavailable; using ${selectedLabel}.`);
|
|
278
|
-
} else {
|
|
279
|
-
this.note(`Mode: could not select ${requested}; continuing on current mode.`);
|
|
280
|
-
}
|
|
274
|
+
this.println(
|
|
275
|
+
`${GREEN}${t("renderer.loginDetected", { provider: this.providerLabel })}${RESET}`,
|
|
276
|
+
);
|
|
281
277
|
break;
|
|
282
|
-
}
|
|
283
278
|
case "conversation.started":
|
|
284
279
|
if (this.lifecycleShown.has(type)) {
|
|
285
280
|
break;
|
|
286
281
|
}
|
|
287
282
|
this.lifecycleShown.add(type);
|
|
288
|
-
|
|
289
|
-
this.status(`Conversation ready (${payload.mode}).`);
|
|
290
|
-
} else {
|
|
291
|
-
this.status("Conversation ready (current mode).");
|
|
292
|
-
}
|
|
283
|
+
this.status(t("renderer.conversationReady"));
|
|
293
284
|
break;
|
|
294
285
|
case "model.message_sent":
|
|
295
286
|
this.beginProcessing();
|
|
@@ -305,18 +296,29 @@ export class Renderer {
|
|
|
305
296
|
this.stopSpinner();
|
|
306
297
|
this.note(
|
|
307
298
|
payload.deadRequest
|
|
308
|
-
?
|
|
299
|
+
? t("renderer.noReply", {
|
|
300
|
+
provider: this.providerLabel,
|
|
301
|
+
retry: payload.retry,
|
|
302
|
+
maxRetries: payload.maxRetries,
|
|
303
|
+
})
|
|
309
304
|
: payload.generationFailed
|
|
310
|
-
?
|
|
311
|
-
|
|
305
|
+
? t("renderer.generationFailed", {
|
|
306
|
+
provider: this.providerLabel,
|
|
307
|
+
retry: payload.retry,
|
|
308
|
+
maxRetries: payload.maxRetries,
|
|
309
|
+
})
|
|
310
|
+
: t("renderer.emptyResponse", {
|
|
311
|
+
provider: this.providerLabel,
|
|
312
|
+
retry: payload.retry,
|
|
313
|
+
maxRetries: payload.maxRetries,
|
|
314
|
+
}),
|
|
312
315
|
);
|
|
313
316
|
break;
|
|
314
317
|
case "model.limit_reached":
|
|
315
318
|
this.stopSpinner();
|
|
316
|
-
this.note(
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
);
|
|
319
|
+
this.note(t("renderer.limitReached", {
|
|
320
|
+
provider: this.providerLabel,
|
|
321
|
+
}));
|
|
320
322
|
break;
|
|
321
323
|
case "model.progress":
|
|
322
324
|
this.stopSpinner();
|
|
@@ -329,7 +331,7 @@ export class Renderer {
|
|
|
329
331
|
break;
|
|
330
332
|
case "protocol.plain_answer":
|
|
331
333
|
this.stopSpinner();
|
|
332
|
-
this.note("
|
|
334
|
+
this.note(t("renderer.plainAnswer"));
|
|
333
335
|
break;
|
|
334
336
|
case "tool.proposed":
|
|
335
337
|
this.stopSpinner();
|
|
@@ -361,21 +363,25 @@ export class Renderer {
|
|
|
361
363
|
this.stopSpinner();
|
|
362
364
|
if (!payload.id || !this.handled.has(payload.id)) {
|
|
363
365
|
if (payload.id) this.handled.add(payload.id);
|
|
364
|
-
this.println(
|
|
366
|
+
this.println(
|
|
367
|
+
`${this.#hdr()} ${DIM}↺ ${t("renderer.reused", { tool: payload.name })}${RESET}`,
|
|
368
|
+
);
|
|
365
369
|
}
|
|
366
370
|
break;
|
|
367
371
|
case "tool.reused_unknown":
|
|
368
372
|
this.stopSpinner();
|
|
369
373
|
if (!payload.id || !this.handled.has(payload.id)) {
|
|
370
374
|
if (payload.id) this.handled.add(payload.id);
|
|
371
|
-
this.println(
|
|
375
|
+
this.println(
|
|
376
|
+
`${this.#hdr()} ${YELLOW}↺ ${t("renderer.reusedUnknown", { tool: payload.name })}${RESET}`,
|
|
377
|
+
);
|
|
372
378
|
}
|
|
373
379
|
break;
|
|
374
380
|
case "tool.conflict":
|
|
375
381
|
this.stopSpinner();
|
|
376
382
|
if (!payload.id || !this.handled.has(payload.id)) {
|
|
377
383
|
if (payload.id) this.handled.add(payload.id);
|
|
378
|
-
this.note(
|
|
384
|
+
this.note(t("renderer.conflictingReuse", { tool: payload.name }));
|
|
379
385
|
}
|
|
380
386
|
break;
|
|
381
387
|
case "run.completed":
|
|
@@ -383,12 +389,16 @@ export class Renderer {
|
|
|
383
389
|
break;
|
|
384
390
|
case "run.interrupted":
|
|
385
391
|
this.stopSpinner();
|
|
386
|
-
this.println(`${YELLOW}
|
|
392
|
+
this.println(`${YELLOW}${t("renderer.interrupted", {
|
|
393
|
+
message: truncate(payload.message, 160),
|
|
394
|
+
})}${RESET}`);
|
|
387
395
|
break;
|
|
388
396
|
case "run.recovery_required":
|
|
389
397
|
this.stopSpinner();
|
|
390
398
|
this.println(
|
|
391
|
-
`${YELLOW}${
|
|
399
|
+
`${YELLOW}${t("renderer.recoveryOpen", {
|
|
400
|
+
message: truncate(payload.message, 180),
|
|
401
|
+
})}${RESET}`,
|
|
392
402
|
);
|
|
393
403
|
break;
|
|
394
404
|
case "tool.result_sent":
|
package/src/cli/self-update.js
CHANGED
|
@@ -6,12 +6,14 @@ import { getPackageName, getPackageVersion } from "../shared/package-info.js";
|
|
|
6
6
|
export const UPDATE_CHECK_TIMEOUT_MS = NETWORK_TIMEOUT_MS;
|
|
7
7
|
export const UPDATE_COMMAND_TIMEOUT_MS = 10_000;
|
|
8
8
|
export const INSTALL_TIMEOUT_MS = 120_000;
|
|
9
|
-
export const
|
|
9
|
+
export const OFFICIAL_NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
10
|
+
export const MANUAL_INSTALL_COMMAND =
|
|
11
|
+
`npm install -g wtagent@latest --registry=${OFFICIAL_NPM_REGISTRY}`;
|
|
10
12
|
|
|
11
13
|
const VERSION_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
12
14
|
|
|
13
15
|
export function npmRegistryLatestUrl(name = getPackageName()) {
|
|
14
|
-
return
|
|
16
|
+
return `${OFFICIAL_NPM_REGISTRY}${encodeURIComponent(name)}/latest`;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export function parseVersion(value) {
|
|
@@ -76,7 +78,12 @@ export async function installLatest({
|
|
|
76
78
|
timeoutMs = INSTALL_TIMEOUT_MS,
|
|
77
79
|
stdio = "inherit",
|
|
78
80
|
} = {}) {
|
|
79
|
-
const plan = planCommandImpl("npm", [
|
|
81
|
+
const plan = planCommandImpl("npm", [
|
|
82
|
+
"install",
|
|
83
|
+
"-g",
|
|
84
|
+
"wtagent@latest",
|
|
85
|
+
`--registry=${OFFICIAL_NPM_REGISTRY}`,
|
|
86
|
+
]);
|
|
80
87
|
return await new Promise((resolve) => {
|
|
81
88
|
let settled = false;
|
|
82
89
|
let timer;
|
|
@@ -19,11 +19,7 @@ import {
|
|
|
19
19
|
toolResultOutput,
|
|
20
20
|
userMessage,
|
|
21
21
|
} from "../session/canonical-transcript.js";
|
|
22
|
-
import {
|
|
23
|
-
DEFAULT_LIMITS,
|
|
24
|
-
PRO_MODEL_TURN_TIMEOUT_MS,
|
|
25
|
-
isProMode,
|
|
26
|
-
} from "../shared/limits.js";
|
|
22
|
+
import { DEFAULT_LIMITS } from "../shared/limits.js";
|
|
27
23
|
import { utf8ByteLength } from "../shared/text-budget.js";
|
|
28
24
|
import {
|
|
29
25
|
BrowserAdapterError,
|
|
@@ -148,6 +144,7 @@ export class AgentRuntime {
|
|
|
148
144
|
session,
|
|
149
145
|
approval,
|
|
150
146
|
onEvent,
|
|
147
|
+
postAuthSetup = null,
|
|
151
148
|
limits = DEFAULT_LIMITS,
|
|
152
149
|
}) {
|
|
153
150
|
this.adapter = adapter;
|
|
@@ -156,6 +153,7 @@ export class AgentRuntime {
|
|
|
156
153
|
this.session = session;
|
|
157
154
|
this.approval = approval;
|
|
158
155
|
this.onEvent = onEvent;
|
|
156
|
+
this.postAuthSetup = postAuthSetup;
|
|
159
157
|
this.limits = limits;
|
|
160
158
|
}
|
|
161
159
|
|
|
@@ -239,12 +237,10 @@ export class AgentRuntime {
|
|
|
239
237
|
instruction = null,
|
|
240
238
|
files = [],
|
|
241
239
|
inPlaceRecovery = false,
|
|
242
|
-
mode = null,
|
|
243
240
|
} = {}) {
|
|
244
241
|
const {
|
|
245
242
|
task,
|
|
246
243
|
projectRoot,
|
|
247
|
-
mode: sessionMode,
|
|
248
244
|
} = this.session.state;
|
|
249
245
|
const previousConversationUrl = this.session.state.conversationUrl;
|
|
250
246
|
await this.session.recoverInterruptedSideEffects();
|
|
@@ -300,58 +296,25 @@ export class AgentRuntime {
|
|
|
300
296
|
: null,
|
|
301
297
|
},
|
|
302
298
|
);
|
|
303
|
-
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
: null;
|
|
312
|
-
const selectRequested = Boolean(
|
|
313
|
-
requestedMode && (!resume || mode != null),
|
|
314
|
-
);
|
|
315
|
-
if (selectRequested) {
|
|
316
|
-
const modeResult = await this.adapter.selectMode(requestedMode);
|
|
317
|
-
if (modeResult) {
|
|
318
|
-
await this.emit("conversation.mode_selected", {
|
|
319
|
-
requested: requestedMode,
|
|
320
|
-
status: modeResult.status,
|
|
321
|
-
selectedLabel: modeResult.selectedLabel ?? null,
|
|
322
|
-
attempts: modeResult.attempts ?? 0,
|
|
323
|
-
reason: modeResult.reason ?? null,
|
|
324
|
-
});
|
|
325
|
-
if (modeResult.status === "select" || modeResult.status === "already") {
|
|
326
|
-
activeMode = modeResult.selectedLabel ?? requestedMode;
|
|
327
|
-
} else if (modeResult.status === "fallback") {
|
|
328
|
-
activeMode = modeResult.selectedLabel ?? requestedMode;
|
|
329
|
-
} else {
|
|
330
|
-
// Pro not selected and no known fallback label — the real mode is
|
|
331
|
-
// whatever ChatGPT already had, which we cannot name reliably.
|
|
332
|
-
activeMode = null;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
299
|
+
|
|
300
|
+
// Optional setup after authentication and the target conversation are
|
|
301
|
+
// ready, but before the first message is sent. Interactive CLI runs use
|
|
302
|
+
// this to let the user choose a model directly on the provider website.
|
|
303
|
+
// WTAgent never names, selects, or overrides a provider-specific model.
|
|
304
|
+
// Resumes skip this setup so an existing conversation is not interrupted.
|
|
305
|
+
if (!resume && this.postAuthSetup) {
|
|
306
|
+
await this.postAuthSetup({ adapter: this.adapter });
|
|
335
307
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
// --model-turn-timeout-ms always wins (resolveLimits marks it). The
|
|
339
|
-
// active mode may differ from the requested one (Pro limited, fallback),
|
|
340
|
-
// so prefer the mode the conversation is actually on.
|
|
341
|
-
const modelTurnTimeoutMs = !this.limits.modelTurnTimeoutExplicit
|
|
342
|
-
&& (isProMode(activeMode) || isProMode(requestedMode))
|
|
343
|
-
? PRO_MODEL_TURN_TIMEOUT_MS
|
|
344
|
-
: this.limits.modelTurnTimeoutMs;
|
|
308
|
+
|
|
309
|
+
const modelTurnTimeoutMs = this.limits.modelTurnTimeoutMs;
|
|
345
310
|
|
|
346
311
|
await this.session.update({
|
|
347
312
|
phase: "running",
|
|
348
313
|
conversationUrl: await this.adapter.getConversationUrl(),
|
|
349
|
-
activeMode,
|
|
314
|
+
activeMode: null,
|
|
350
315
|
});
|
|
351
316
|
await this.emit("conversation.started", {
|
|
352
317
|
url: this.session.state.conversationUrl,
|
|
353
|
-
mode: activeMode,
|
|
354
|
-
requestedMode,
|
|
355
318
|
});
|
|
356
319
|
|
|
357
320
|
let initialMessage;
|
package/src/shared/limits.js
CHANGED
|
@@ -19,15 +19,8 @@ export const DEFAULT_LIMITS = Object.freeze({
|
|
|
19
19
|
maxSearchResults: 200,
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
export const PRO_MODEL_TURN_TIMEOUT_MS = 16 * 60_000;
|
|
25
|
-
|
|
26
|
-
// The CLI's --model-turn-timeout-ms always wins over the mode-aware default.
|
|
27
|
-
export function isProMode(mode) {
|
|
28
|
-
return String(mode ?? "") === "Pro";
|
|
29
|
-
}
|
|
30
|
-
|
|
22
|
+
// Model response timing is provider/model agnostic. Users who need a longer
|
|
23
|
+
// window can override it explicitly with --model-turn-timeout-ms.
|
|
31
24
|
export function resolveLimits({ modelTurnTimeoutMs } = {}) {
|
|
32
25
|
if (modelTurnTimeoutMs == null || modelTurnTimeoutMs === "") {
|
|
33
26
|
return DEFAULT_LIMITS;
|
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
// Language-independent model/mode selection logic for the ChatGPT web picker.
|
|
2
|
-
//
|
|
3
|
-
// Prefer language-independent option slugs derived from data-testid / data-* /
|
|
4
|
-
// id. Current ChatGPT menus sometimes omit every such attribute, including on
|
|
5
|
-
// the Pro item, so an exact normalized label match is kept as a conservative
|
|
6
|
-
// fallback. We never use fuzzy label matching.
|
|
7
|
-
//
|
|
8
|
-
// This module is pure and DOM-free so it can be unit-tested without a browser.
|
|
9
|
-
// The adapter supplies a small "port" of async DOM operations to
|
|
10
|
-
// `runModeSelection`; all the retry / fallback policy lives here.
|
|
11
|
-
|
|
12
|
-
export function slugTokens(slug) {
|
|
13
|
-
return String(slug ?? "").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function normalizeToken(value) {
|
|
17
|
-
return String(value ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Does the option's stable slug encode the requested mode token?
|
|
21
|
-
// `token` is a normalized (separator-free) string, e.g. "pro" or "extrahigh".
|
|
22
|
-
// Short tokens (< 4 chars, e.g. "pro") require an exact whole-segment match so
|
|
23
|
-
// "pro" does not match "provider". Longer tokens also allow a substring match
|
|
24
|
-
// against the separator-free slug, so a multi-word mode like "Extra high"
|
|
25
|
-
// (-> "extrahigh") matches a hyphenated slug like "o3-extra-high".
|
|
26
|
-
export function slugMatchesToken(slug, token) {
|
|
27
|
-
if (!token) {
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
const tokens = slugTokens(slug);
|
|
31
|
-
if (tokens.includes(token)) {
|
|
32
|
-
return true;
|
|
33
|
-
}
|
|
34
|
-
if (token.length >= 4) {
|
|
35
|
-
const joined = tokens.join("");
|
|
36
|
-
return joined.includes(token) || tokens.some((candidate) => candidate.includes(token));
|
|
37
|
-
}
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function labelMatchesToken(label, token) {
|
|
42
|
-
return String(label ?? "")
|
|
43
|
-
.split(/\r?\n/)
|
|
44
|
-
.some((line) => normalizeToken(line) === token);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Given the enumerated menu options and the requested mode, decide what to do.
|
|
48
|
-
// options: [{ index, slug, label, disabled, selected }] in DOM order.
|
|
49
|
-
// Returns { status, targetIndex, selectedLabel, reason }.
|
|
50
|
-
// status "select" -> requested mode is available; click it.
|
|
51
|
-
// status "already" -> requested mode is already selected.
|
|
52
|
-
// status "fallback" -> requested mode is limited/disabled; click
|
|
53
|
-
// the option immediately before it.
|
|
54
|
-
// status "unavailable" -> requested mode is not present in the menu.
|
|
55
|
-
// status "unavailable_disabled"-> requested mode is disabled and the previous
|
|
56
|
-
// option is not selectable either.
|
|
57
|
-
export function chooseModeOption(options, requested) {
|
|
58
|
-
const token = normalizeToken(requested);
|
|
59
|
-
let proIndex = options.findIndex(
|
|
60
|
-
(option) => slugMatchesToken(option.slug, token),
|
|
61
|
-
);
|
|
62
|
-
if (proIndex === -1) {
|
|
63
|
-
proIndex = options.findIndex(
|
|
64
|
-
(option) => labelMatchesToken(option.label, token),
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (proIndex === -1) {
|
|
69
|
-
return {
|
|
70
|
-
status: "unavailable",
|
|
71
|
-
targetIndex: null,
|
|
72
|
-
selectedLabel: null,
|
|
73
|
-
reason: `"${requested}" is not in the model menu.`,
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const pro = options[proIndex];
|
|
78
|
-
if (pro.selected) {
|
|
79
|
-
return {
|
|
80
|
-
status: "already",
|
|
81
|
-
targetIndex: null,
|
|
82
|
-
selectedLabel: pro.label || requested,
|
|
83
|
-
reason: `Already using ${requested}.`,
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
if (!pro.disabled) {
|
|
87
|
-
return {
|
|
88
|
-
status: "select",
|
|
89
|
-
targetIndex: proIndex,
|
|
90
|
-
selectedLabel: pro.label || requested,
|
|
91
|
-
reason: `Selecting ${requested}.`,
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Pro is present but limited. Fall back to the option immediately before it.
|
|
96
|
-
const previous = options[proIndex - 1];
|
|
97
|
-
if (previous && !previous.disabled) {
|
|
98
|
-
return {
|
|
99
|
-
status: "fallback",
|
|
100
|
-
targetIndex: proIndex - 1,
|
|
101
|
-
selectedLabel: previous.label || `option ${proIndex}`,
|
|
102
|
-
reason: `${requested} is limited; selecting the previous option `
|
|
103
|
-
+ `"${previous.label || proIndex - 1}" instead.`,
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
status: "unavailable_disabled",
|
|
109
|
-
targetIndex: null,
|
|
110
|
-
selectedLabel: null,
|
|
111
|
-
reason: previous
|
|
112
|
-
? `${requested} is limited and the previous option is also unavailable.`
|
|
113
|
-
: `${requested} is limited and has no previous option to fall back to.`,
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Orchestrates selection against a DOM "port". Never throws for mode issues:
|
|
118
|
-
// it always resolves to a structured result the runtime can surface to the CLI,
|
|
119
|
-
// so a limited/absent Pro degrades gracefully instead of aborting the run.
|
|
120
|
-
//
|
|
121
|
-
// port: {
|
|
122
|
-
// alreadyOnMode(requested) -> bool,
|
|
123
|
-
// hasSwitcher() -> bool,
|
|
124
|
-
// openMenu(requested) -> void,
|
|
125
|
-
// readOptions() -> [{index, slug, label, disabled, selected}],
|
|
126
|
-
// clickOption(index) -> bool (click landed),
|
|
127
|
-
// waitSelected(requested) -> bool (picker reflects the requested mode),
|
|
128
|
-
// waitClosed() -> bool (menu closed after click),
|
|
129
|
-
// closeMenu() -> void,
|
|
130
|
-
// writeDiagnostics(label) -> void,
|
|
131
|
-
// }
|
|
132
|
-
export async function runModeSelection(port, requested, { maxAttempts = 2 } = {}) {
|
|
133
|
-
if (!requested) {
|
|
134
|
-
return { status: "skipped", requested, attempts: 0, reason: "No mode requested." };
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (await port.alreadyOnMode(requested)) {
|
|
138
|
-
return {
|
|
139
|
-
status: "already",
|
|
140
|
-
requested,
|
|
141
|
-
selectedLabel: requested,
|
|
142
|
-
attempts: 0,
|
|
143
|
-
reason: `Already using ${requested}.`,
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (!await port.hasSwitcher()) {
|
|
148
|
-
await port.writeDiagnostics("mode-switcher-not-found");
|
|
149
|
-
return {
|
|
150
|
-
status: "switcher_not_found",
|
|
151
|
-
requested,
|
|
152
|
-
attempts: 0,
|
|
153
|
-
reason: "Model switcher was not found.",
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
let lastReason = "";
|
|
158
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
159
|
-
await port.openMenu(requested);
|
|
160
|
-
const options = await port.readOptions();
|
|
161
|
-
|
|
162
|
-
// The Radix menu populates asynchronously; an empty read is a race, retry.
|
|
163
|
-
if (!options || options.length === 0) {
|
|
164
|
-
lastReason = "Model menu did not populate in time.";
|
|
165
|
-
await port.closeMenu();
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const choice = chooseModeOption(options, requested);
|
|
170
|
-
|
|
171
|
-
if (choice.status === "already") {
|
|
172
|
-
await port.closeMenu();
|
|
173
|
-
return { ...choice, requested, attempts: attempt };
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (choice.status === "select" || choice.status === "fallback") {
|
|
177
|
-
const clicked = await port.clickOption(choice.targetIndex);
|
|
178
|
-
const expectedMode = choice.status === "fallback"
|
|
179
|
-
? choice.selectedLabel
|
|
180
|
-
: requested;
|
|
181
|
-
const selected = clicked && (
|
|
182
|
-
typeof port.waitSelected === "function"
|
|
183
|
-
? await port.waitSelected(expectedMode)
|
|
184
|
-
: await port.waitClosed()
|
|
185
|
-
);
|
|
186
|
-
if (selected) {
|
|
187
|
-
return { ...choice, requested, attempts: attempt };
|
|
188
|
-
}
|
|
189
|
-
lastReason = "The option click did not change the selected mode.";
|
|
190
|
-
await port.closeMenu();
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
await port.closeMenu();
|
|
195
|
-
|
|
196
|
-
// A genuinely absent Pro might still be populating on the first pass, so
|
|
197
|
-
// retry once; a disabled Pro will not change, so report immediately.
|
|
198
|
-
if (choice.status === "unavailable" && attempt < maxAttempts) {
|
|
199
|
-
lastReason = choice.reason;
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
await port.writeDiagnostics(`mode-${requested}-${choice.status}`);
|
|
204
|
-
return { ...choice, requested, attempts: attempt };
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
await port.writeDiagnostics(`mode-${requested}-unresolved`);
|
|
208
|
-
return {
|
|
209
|
-
status: "unresolved",
|
|
210
|
-
requested,
|
|
211
|
-
attempts: maxAttempts,
|
|
212
|
-
reason: lastReason || `Could not select ${requested}.`,
|
|
213
|
-
};
|
|
214
|
-
}
|
package/src/cli/mode-choice.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
export const CHATGPT_MODE_CHOICES = Object.freeze([
|
|
2
|
-
Object.freeze({
|
|
3
|
-
name: "Pro — select Pro for this conversation",
|
|
4
|
-
value: "pro",
|
|
5
|
-
}),
|
|
6
|
-
Object.freeze({
|
|
7
|
-
name: "Current — keep the current ChatGPT setting",
|
|
8
|
-
value: "current",
|
|
9
|
-
}),
|
|
10
|
-
]);
|
|
11
|
-
|
|
12
|
-
export function normalizeConfiguredMode(value) {
|
|
13
|
-
const configured = String(value ?? "").trim();
|
|
14
|
-
if (/^current$/i.test(configured)) {
|
|
15
|
-
return null;
|
|
16
|
-
}
|
|
17
|
-
if (/^pro$/i.test(configured)) {
|
|
18
|
-
return "Pro";
|
|
19
|
-
}
|
|
20
|
-
throw new Error('ChatGPT mode must be either "Pro" or "Current".');
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function modeFromPromptChoice(choice) {
|
|
24
|
-
return choice === "pro" ? "Pro" : null;
|
|
25
|
-
}
|