wtagent 0.4.0 → 1.0.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 +161 -7
- package/package.json +5 -4
- package/src/artifacts/artifact-store.js +154 -0
- package/src/audio/music-artifact.js +66 -0
- package/src/audio/native-music-receiver.js +91 -0
- package/src/browser/base-web-adapter.js +354 -50
- package/src/browser/chatgpt-dom.js +179 -0
- package/src/browser/chatgpt-web-adapter.js +89 -125
- package/src/browser/fake-web-model-adapter.js +16 -1
- package/src/browser/gemini-web-adapter.js +61 -20
- package/src/browser/grok-web-adapter.js +35 -3
- package/src/browser/rendered-text.js +11 -0
- package/src/cli/i18n.js +40 -10
- package/src/cli/main.js +32 -3
- package/src/cli/prompt-input.js +68 -0
- package/src/image/browser-image-driver.js +238 -0
- package/src/image/browser-image-session-pool.js +96 -0
- package/src/image/image-generation-service.js +101 -0
- package/src/image/image-provider-driver.js +22 -0
- package/src/image/native-image-receiver.js +66 -0
- package/src/image/provider-registry.js +23 -0
- package/src/image/providers/chatgpt-image-driver.js +211 -0
- package/src/image/providers/gemini-image-driver.js +100 -0
- package/src/image/providers/grok-image-driver.js +193 -0
- package/src/policy/policy-engine.js +10 -5
- package/src/protocol/envelope.js +74 -0
- package/src/protocol/markers.js +16 -0
- package/src/protocol/prompt-builder.js +12 -9
- package/src/protocol/xml-protocol.js +36 -18
- package/src/runtime/agent-runtime.js +345 -65
- package/src/session/agent-session.js +73 -2
- package/src/tools/image-tools.js +92 -0
- package/src/tools/registry.js +13 -1
- package/docs/technical-design.md +0 -873
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BaseWebAdapter, firstVisible } from "./base-web-adapter.js";
|
|
2
2
|
import { isUsageLimitNotice } from "../shared/usage-limit.js";
|
|
3
|
+
import { readRenderedBlocks } from "./rendered-text.js";
|
|
3
4
|
|
|
4
5
|
export { isConnectionLostError } from "./base-web-adapter.js";
|
|
5
6
|
|
|
@@ -20,7 +21,11 @@ export class GrokWebAdapter extends BaseWebAdapter {
|
|
|
20
21
|
|
|
21
22
|
composerLocators() {
|
|
22
23
|
return [
|
|
23
|
-
|
|
24
|
+
// Grok replaces its initial textarea with a role=textbox editor during
|
|
25
|
+
// hydration. Keep both variants in one live locator so a composer found
|
|
26
|
+
// just before that replacement does not become an empty stale selector
|
|
27
|
+
// by the time sendMessage fills it.
|
|
28
|
+
this.page.locator('main textarea, main [role="textbox"]').first(),
|
|
24
29
|
this.page.locator("textarea[aria-label]").first(),
|
|
25
30
|
this.page.locator('[role="textbox"]').first(),
|
|
26
31
|
];
|
|
@@ -84,13 +89,40 @@ export class GrokWebAdapter extends BaseWebAdapter {
|
|
|
84
89
|
);
|
|
85
90
|
}
|
|
86
91
|
|
|
92
|
+
async userMessageText(message) {
|
|
93
|
+
const markdown = message.locator(".response-content-markdown");
|
|
94
|
+
if (await markdown.count().catch(() => 0) > 0) {
|
|
95
|
+
const exact = await markdown.evaluateAll(readRenderedBlocks);
|
|
96
|
+
if (exact.trim()) return exact;
|
|
97
|
+
}
|
|
98
|
+
return await super.userMessageText(message);
|
|
99
|
+
}
|
|
100
|
+
|
|
87
101
|
async messageIdentity(message) {
|
|
88
102
|
const testId = await message.getAttribute("data-testid").catch(() => null);
|
|
89
103
|
const aria = await message.getAttribute("aria-label").catch(() => null);
|
|
90
104
|
const scope = testId === "user-message" || aria === "You"
|
|
91
105
|
? this.userMessages()
|
|
92
106
|
: this.assistantMessages();
|
|
93
|
-
|
|
107
|
+
// Grok wraps every visible user/assistant bubble in a stable response UUID:
|
|
108
|
+
//
|
|
109
|
+
// <div id="response-..." data-scroll-anchor-root>
|
|
110
|
+
// <div data-testid="user-message|assistant-message">...</div>
|
|
111
|
+
// </div>
|
|
112
|
+
//
|
|
113
|
+
// Keep the count fallback for older DOM variants, but prefer the wrapper
|
|
114
|
+
// identity whenever it is present. Besides making normal multi-turn
|
|
115
|
+
// correlation stronger, this lets a fresh chat safely follow Grok's late
|
|
116
|
+
// root -> /c/... navigation after the user bubble has already committed.
|
|
117
|
+
const id = await message.evaluate((element) => (
|
|
118
|
+
element.closest('[data-scroll-anchor-root][id^="response-"]')?.id
|
|
119
|
+
?? element.closest('[id^="response-"]')?.id
|
|
120
|
+
?? null
|
|
121
|
+
)).catch(() => null);
|
|
122
|
+
return {
|
|
123
|
+
id: id || null,
|
|
124
|
+
turn: await scope.count().catch(() => 0),
|
|
125
|
+
};
|
|
94
126
|
}
|
|
95
127
|
|
|
96
128
|
isNewAssistantIdentity({ turn, text }) {
|
|
@@ -104,7 +136,7 @@ export class GrokWebAdapter extends BaseWebAdapter {
|
|
|
104
136
|
async assistantText(message) {
|
|
105
137
|
const response = message.locator(".response-content-markdown");
|
|
106
138
|
if (await response.count().catch(() => 0) > 0) {
|
|
107
|
-
return await response.
|
|
139
|
+
return await response.evaluateAll(readRenderedBlocks);
|
|
108
140
|
}
|
|
109
141
|
return await message.innerText().catch(() => "");
|
|
110
142
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Passed directly to Locator.evaluateAll: keep this function self-contained.
|
|
2
|
+
// Read connected nodes so innerText keeps layout whitespace and omits hidden
|
|
3
|
+
// descendants. Cloning first changes innerText into textContent-like output.
|
|
4
|
+
export function readRenderedBlocks(elements) {
|
|
5
|
+
const roots = elements.filter((element) => (
|
|
6
|
+
!elements.some((other) => other !== element && other.contains(element))
|
|
7
|
+
));
|
|
8
|
+
return roots.filter((element) => element.getClientRects().length > 0)
|
|
9
|
+
.map((element) => element.innerText)
|
|
10
|
+
.join("\n");
|
|
11
|
+
}
|
package/src/cli/i18n.js
CHANGED
|
@@ -20,6 +20,7 @@ const MESSAGES = Object.freeze({
|
|
|
20
20
|
"option.project": "Project directory",
|
|
21
21
|
"option.model": "Web AI provider: {providers} (default: {defaultProvider})",
|
|
22
22
|
"option.once": "Run a single request and exit instead of a conversation",
|
|
23
|
+
"option.type": "Request type: agent (default) or music (Gemini only)",
|
|
23
24
|
"option.timeout": "Maximum wait for one model response (default: 10 minutes)",
|
|
24
25
|
"option.noMinimize": "Keep the Chrome window visible instead of minimizing it",
|
|
25
26
|
"option.debug": "Write browser diagnostics",
|
|
@@ -42,7 +43,9 @@ const MESSAGES = Object.freeze({
|
|
|
42
43
|
"login.savingProfile": "Closing native Chrome and saving the dedicated profile...",
|
|
43
44
|
"login.verified": "{provider} login verified through a fresh CDP connection.",
|
|
44
45
|
"login.stillGuest": "{provider} is still in guest mode. Reopening native Chrome; complete the final {provider} sign-in/continue step.",
|
|
45
|
-
"model.chooseInBrowser": "{provider} is signed in
|
|
46
|
+
"model.chooseInBrowser": "{provider} is signed in.\nYou can change the model in the browser.\nPress Enter to continue, or wait 5 seconds.",
|
|
47
|
+
"model.continueCountdown": "Continuing in {seconds}s…",
|
|
48
|
+
"model.continuing": "Continuing…",
|
|
46
49
|
"model.setupCancelled": "Model selection confirmation cancelled.",
|
|
47
50
|
"logout.none": "No Chrome profile found at {profileDir}; already logged out.",
|
|
48
51
|
"logout.refuse": "Refusing to delete {profileDir}: it does not look like a wtagent Chrome profile.",
|
|
@@ -106,6 +109,7 @@ const MESSAGES = Object.freeze({
|
|
|
106
109
|
"option.project": "项目目录",
|
|
107
110
|
"option.model": "网页 AI 提供商:{providers}(默认:{defaultProvider})",
|
|
108
111
|
"option.once": "只执行一次请求并退出,不进入连续对话",
|
|
112
|
+
"option.type": "请求类型:agent(默认)或 music(仅 Gemini)",
|
|
109
113
|
"option.timeout": "单次模型响应最大等待时间(默认:10 分钟)",
|
|
110
114
|
"option.noMinimize": "保持 Chrome 窗口可见,不自动最小化",
|
|
111
115
|
"option.debug": "写入浏览器诊断信息",
|
|
@@ -128,7 +132,9 @@ const MESSAGES = Object.freeze({
|
|
|
128
132
|
"login.savingProfile": "正在关闭原生 Chrome 并保存专用配置...",
|
|
129
133
|
"login.verified": "已通过新的 CDP 连接验证 {provider} 登录状态。",
|
|
130
134
|
"login.stillGuest": "{provider} 仍处于访客模式。正在重新打开 Chrome;请完成最后的登录/继续步骤。",
|
|
131
|
-
"model.chooseInBrowser": "{provider}
|
|
135
|
+
"model.chooseInBrowser": "{provider} 已登录。\n如需切换模型,请在浏览器中选择。\n按 Enter 立即继续,或等待 5 秒自动继续。",
|
|
136
|
+
"model.continueCountdown": "{seconds} 秒后自动继续…",
|
|
137
|
+
"model.continuing": "正在继续…",
|
|
132
138
|
"model.setupCancelled": "模型选择确认已取消。",
|
|
133
139
|
"logout.none": "在 {profileDir} 未找到 Chrome 配置;当前已是登出状态。",
|
|
134
140
|
"logout.refuse": "拒绝删除 {profileDir}:它看起来不是 wtagent 的 Chrome 配置目录。",
|
|
@@ -192,6 +198,7 @@ const MESSAGES = Object.freeze({
|
|
|
192
198
|
"option.project": "專案目錄",
|
|
193
199
|
"option.model": "網頁 AI 提供者:{providers}(預設:{defaultProvider})",
|
|
194
200
|
"option.once": "只執行一次請求並退出,不進入連續對話",
|
|
201
|
+
"option.type": "請求類型:agent(預設)或 music(僅 Gemini)",
|
|
195
202
|
"option.timeout": "單次模型回應最大等待時間(預設:10 分鐘)",
|
|
196
203
|
"option.noMinimize": "保持 Chrome 視窗可見,不自動最小化",
|
|
197
204
|
"option.debug": "寫入瀏覽器診斷資訊",
|
|
@@ -209,7 +216,9 @@ const MESSAGES = Object.freeze({
|
|
|
209
216
|
"login.savingProfile": "正在關閉原生 Chrome 並儲存專用設定檔...",
|
|
210
217
|
"login.verified": "已透過新的 CDP 連線驗證 {provider} 登入狀態。",
|
|
211
218
|
"login.stillGuest": "{provider} 仍處於訪客模式。正在重新開啟 Chrome;請完成最後的登入/繼續步驟。",
|
|
212
|
-
"model.chooseInBrowser": "{provider}
|
|
219
|
+
"model.chooseInBrowser": "{provider} 已登入。\n如需切換模型,請在瀏覽器中選擇。\n按 Enter 立即繼續,或等待 5 秒自動繼續。",
|
|
220
|
+
"model.continueCountdown": "{seconds} 秒後自動繼續…",
|
|
221
|
+
"model.continuing": "正在繼續…",
|
|
213
222
|
"model.setupCancelled": "模型選擇確認已取消。",
|
|
214
223
|
"task.youPrompt": "你 ›",
|
|
215
224
|
"option.yes": "略過確認提示",
|
|
@@ -278,6 +287,7 @@ const MESSAGES = Object.freeze({
|
|
|
278
287
|
"option.project": "プロジェクトディレクトリ",
|
|
279
288
|
"option.model": "Web AI プロバイダー: {providers}(既定: {defaultProvider})",
|
|
280
289
|
"option.once": "会話を継続せず、1 回のリクエストを実行して終了",
|
|
290
|
+
"option.type": "リクエストの種類: agent(既定)または music(Gemini のみ)",
|
|
281
291
|
"option.timeout": "1 回のモデル応答の最大待機時間(既定: 10 分)",
|
|
282
292
|
"option.noMinimize": "Chrome ウィンドウを最小化せず表示したままにする",
|
|
283
293
|
"option.debug": "ブラウザー診断を書き出す",
|
|
@@ -295,7 +305,9 @@ const MESSAGES = Object.freeze({
|
|
|
295
305
|
"login.savingProfile": "ネイティブ Chrome を閉じ、専用プロファイルを保存しています...",
|
|
296
306
|
"login.verified": "新しい CDP 接続で {provider} のログインを確認しました。",
|
|
297
307
|
"login.stillGuest": "{provider} はまだゲスト状態です。Chrome を再度開きますので、最後のサインイン/続行手順を完了してください。",
|
|
298
|
-
"model.chooseInBrowser": "{provider}
|
|
308
|
+
"model.chooseInBrowser": "{provider} にログイン済みです。\n必要ならブラウザーでモデルを変更できます。\nEnter で続行、または 5 秒後に自動で続行します。",
|
|
309
|
+
"model.continueCountdown": "{seconds} 秒後に続行…",
|
|
310
|
+
"model.continuing": "続行中…",
|
|
299
311
|
"model.setupCancelled": "モデル選択の確認をキャンセルしました。",
|
|
300
312
|
"task.youPrompt": "あなた ›",
|
|
301
313
|
"option.yes": "確認プロンプトを省略",
|
|
@@ -364,6 +376,7 @@ const MESSAGES = Object.freeze({
|
|
|
364
376
|
"option.project": "프로젝트 디렉터리",
|
|
365
377
|
"option.model": "웹 AI 제공자: {providers} (기본값: {defaultProvider})",
|
|
366
378
|
"option.once": "대화를 계속하지 않고 요청 한 번만 실행한 뒤 종료",
|
|
379
|
+
"option.type": "요청 유형: agent(기본값) 또는 music(Gemini 전용)",
|
|
367
380
|
"option.timeout": "모델 응답 1회의 최대 대기 시간 (기본값: 10분)",
|
|
368
381
|
"option.noMinimize": "Chrome 창을 최소화하지 않고 표시",
|
|
369
382
|
"option.debug": "브라우저 진단 정보 기록",
|
|
@@ -381,7 +394,9 @@ const MESSAGES = Object.freeze({
|
|
|
381
394
|
"login.savingProfile": "네이티브 Chrome을 닫고 전용 프로필을 저장하는 중...",
|
|
382
395
|
"login.verified": "새 CDP 연결을 통해 {provider} 로그인을 확인했습니다.",
|
|
383
396
|
"login.stillGuest": "{provider}가 아직 게스트 모드입니다. Chrome을 다시 열고 마지막 로그인/계속 단계를 완료하세요.",
|
|
384
|
-
"model.chooseInBrowser": "{provider}에
|
|
397
|
+
"model.chooseInBrowser": "{provider}에 로그인되었습니다.\n필요하면 브라우저에서 모델을 변경하세요.\nEnter를 누르거나 5초 후 자동으로 계속합니다.",
|
|
398
|
+
"model.continueCountdown": "{seconds}초 후 계속…",
|
|
399
|
+
"model.continuing": "계속하는 중…",
|
|
385
400
|
"model.setupCancelled": "모델 선택 확인이 취소되었습니다.",
|
|
386
401
|
"task.youPrompt": "나 ›",
|
|
387
402
|
"option.yes": "확인 프롬프트 건너뛰기",
|
|
@@ -450,6 +465,7 @@ const MESSAGES = Object.freeze({
|
|
|
450
465
|
"option.project": "Directorio del proyecto",
|
|
451
466
|
"option.model": "Proveedor de IA web: {providers} (predeterminado: {defaultProvider})",
|
|
452
467
|
"option.once": "Ejecutar una sola solicitud y salir en lugar de mantener una conversación",
|
|
468
|
+
"option.type": "Tipo de solicitud: agent (predeterminado) o music (solo Gemini)",
|
|
453
469
|
"option.timeout": "Espera máxima por respuesta del modelo (predeterminado: 10 minutos)",
|
|
454
470
|
"option.noMinimize": "Mantener visible la ventana de Chrome",
|
|
455
471
|
"option.debug": "Guardar diagnósticos del navegador",
|
|
@@ -467,7 +483,9 @@ const MESSAGES = Object.freeze({
|
|
|
467
483
|
"login.savingProfile": "Cerrando Chrome y guardando el perfil dedicado...",
|
|
468
484
|
"login.verified": "Inicio de sesión de {provider} verificado mediante una nueva conexión CDP.",
|
|
469
485
|
"login.stillGuest": "{provider} sigue en modo invitado. Se volverá a abrir Chrome; completa el último paso de inicio de sesión/continuación.",
|
|
470
|
-
"model.chooseInBrowser": "{provider} ha iniciado sesión
|
|
486
|
+
"model.chooseInBrowser": "{provider} ha iniciado sesión.\nPuedes cambiar el modelo en el navegador.\nPulsa Enter para continuar o espera 5 segundos.",
|
|
487
|
+
"model.continueCountdown": "Continuando en {seconds}s…",
|
|
488
|
+
"model.continuing": "Continuando…",
|
|
471
489
|
"model.setupCancelled": "Se canceló la confirmación de selección del modelo.",
|
|
472
490
|
"task.youPrompt": "tú ›",
|
|
473
491
|
"option.yes": "Omitir la confirmación",
|
|
@@ -536,6 +554,7 @@ const MESSAGES = Object.freeze({
|
|
|
536
554
|
"option.project": "Répertoire du projet",
|
|
537
555
|
"option.model": "Fournisseur d’IA web : {providers} (par défaut : {defaultProvider})",
|
|
538
556
|
"option.once": "Exécuter une seule requête puis quitter",
|
|
557
|
+
"option.type": "Type de requête : agent (par défaut) ou music (Gemini uniquement)",
|
|
539
558
|
"option.timeout": "Attente maximale d’une réponse du modèle (10 minutes par défaut)",
|
|
540
559
|
"option.noMinimize": "Garder la fenêtre Chrome visible",
|
|
541
560
|
"option.debug": "Écrire les diagnostics du navigateur",
|
|
@@ -553,7 +572,9 @@ const MESSAGES = Object.freeze({
|
|
|
553
572
|
"login.savingProfile": "Fermeture de Chrome et enregistrement du profil dédié...",
|
|
554
573
|
"login.verified": "Connexion à {provider} vérifiée via une nouvelle connexion CDP.",
|
|
555
574
|
"login.stillGuest": "{provider} est encore en mode invité. Chrome va être rouvert ; terminez la dernière étape de connexion/continuation.",
|
|
556
|
-
"model.chooseInBrowser": "{provider} est connect
|
|
575
|
+
"model.chooseInBrowser": "{provider} est connecté.\nVous pouvez changer de modèle dans le navigateur.\nAppuyez sur Entrée pour continuer ou attendez 5 secondes.",
|
|
576
|
+
"model.continueCountdown": "Reprise dans {seconds}s…",
|
|
577
|
+
"model.continuing": "Reprise…",
|
|
557
578
|
"model.setupCancelled": "La confirmation du choix du modèle a été annulée.",
|
|
558
579
|
"task.youPrompt": "vous ›",
|
|
559
580
|
"option.yes": "Ignorer la demande de confirmation",
|
|
@@ -622,6 +643,7 @@ const MESSAGES = Object.freeze({
|
|
|
622
643
|
"option.project": "Projektverzeichnis",
|
|
623
644
|
"option.model": "Web-AI-Anbieter: {providers} (Standard: {defaultProvider})",
|
|
624
645
|
"option.once": "Nur eine Anfrage ausführen und danach beenden",
|
|
646
|
+
"option.type": "Anfragetyp: agent (Standard) oder music (nur Gemini)",
|
|
625
647
|
"option.timeout": "Maximale Wartezeit pro Modellantwort (Standard: 10 Minuten)",
|
|
626
648
|
"option.noMinimize": "Chrome-Fenster sichtbar lassen",
|
|
627
649
|
"option.debug": "Browserdiagnose schreiben",
|
|
@@ -639,7 +661,9 @@ const MESSAGES = Object.freeze({
|
|
|
639
661
|
"login.savingProfile": "Chrome wird geschlossen und das dedizierte Profil gespeichert...",
|
|
640
662
|
"login.verified": "{provider}-Anmeldung über eine neue CDP-Verbindung bestätigt.",
|
|
641
663
|
"login.stillGuest": "{provider} befindet sich noch im Gastmodus. Chrome wird erneut geöffnet; schließe den letzten Anmelde-/Fortsetzen-Schritt ab.",
|
|
642
|
-
"model.chooseInBrowser": "{provider} ist angemeldet
|
|
664
|
+
"model.chooseInBrowser": "{provider} ist angemeldet.\nDu kannst das Modell im Browser ändern.\nDrücke Enter zum Fortfahren oder warte 5 Sekunden.",
|
|
665
|
+
"model.continueCountdown": "Weiter in {seconds}s…",
|
|
666
|
+
"model.continuing": "Weiter…",
|
|
643
667
|
"model.setupCancelled": "Bestätigung der Modellauswahl abgebrochen.",
|
|
644
668
|
"task.youPrompt": "du ›",
|
|
645
669
|
"option.yes": "Bestätigungsabfrage überspringen",
|
|
@@ -708,6 +732,7 @@ const MESSAGES = Object.freeze({
|
|
|
708
732
|
"option.project": "Diretório do projeto",
|
|
709
733
|
"option.model": "Provedor de IA web: {providers} (padrão: {defaultProvider})",
|
|
710
734
|
"option.once": "Executar apenas uma solicitação e sair",
|
|
735
|
+
"option.type": "Tipo de solicitação: agent (padrão) ou music (somente Gemini)",
|
|
711
736
|
"option.timeout": "Espera máxima por uma resposta do modelo (padrão: 10 minutos)",
|
|
712
737
|
"option.noMinimize": "Manter a janela do Chrome visível",
|
|
713
738
|
"option.debug": "Gravar diagnósticos do navegador",
|
|
@@ -725,7 +750,9 @@ const MESSAGES = Object.freeze({
|
|
|
725
750
|
"login.savingProfile": "Fechando o Chrome e salvando o perfil dedicado...",
|
|
726
751
|
"login.verified": "Login de {provider} verificado por uma nova conexão CDP.",
|
|
727
752
|
"login.stillGuest": "{provider} ainda está no modo visitante. O Chrome será reaberto; conclua a última etapa de login/continuação.",
|
|
728
|
-
"model.chooseInBrowser": "{provider} está conectado
|
|
753
|
+
"model.chooseInBrowser": "{provider} está conectado.\nVocê pode alterar o modelo no navegador.\nPressione Enter para continuar ou aguarde 5 segundos.",
|
|
754
|
+
"model.continueCountdown": "Continuando em {seconds}s…",
|
|
755
|
+
"model.continuing": "Continuando…",
|
|
729
756
|
"model.setupCancelled": "A confirmação da seleção do modelo foi cancelada.",
|
|
730
757
|
"task.youPrompt": "você ›",
|
|
731
758
|
"option.yes": "Ignorar a confirmação",
|
|
@@ -794,6 +821,7 @@ const MESSAGES = Object.freeze({
|
|
|
794
821
|
"option.project": "Каталог проекта",
|
|
795
822
|
"option.model": "Провайдер веб-ИИ: {providers} (по умолчанию: {defaultProvider})",
|
|
796
823
|
"option.once": "Выполнить один запрос и завершить работу",
|
|
824
|
+
"option.type": "Тип запроса: agent (по умолчанию) или music (только Gemini)",
|
|
797
825
|
"option.timeout": "Максимальное ожидание ответа модели (по умолчанию: 10 минут)",
|
|
798
826
|
"option.noMinimize": "Не сворачивать окно Chrome",
|
|
799
827
|
"option.debug": "Записывать диагностику браузера",
|
|
@@ -811,7 +839,9 @@ const MESSAGES = Object.freeze({
|
|
|
811
839
|
"login.savingProfile": "Chrome закрывается, отдельный профиль сохраняется...",
|
|
812
840
|
"login.verified": "Вход в {provider} подтверждён через новое соединение CDP.",
|
|
813
841
|
"login.stillGuest": "{provider} всё ещё работает в гостевом режиме. Chrome будет открыт снова; завершите последний шаг входа/продолжения.",
|
|
814
|
-
"model.chooseInBrowser": "{provider}: вход
|
|
842
|
+
"model.chooseInBrowser": "{provider}: вход выполнен.\nПри необходимости смените модель в браузере.\nНажмите Enter или подождите 5 секунд для продолжения.",
|
|
843
|
+
"model.continueCountdown": "Продолжение через {seconds} с…",
|
|
844
|
+
"model.continuing": "Продолжение…",
|
|
815
845
|
"model.setupCancelled": "Подтверждение выбора модели отменено.",
|
|
816
846
|
"task.youPrompt": "вы ›",
|
|
817
847
|
"option.yes": "Пропустить запрос подтверждения",
|
package/src/cli/main.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
import { confirm, select } from "@inquirer/prompts";
|
|
7
7
|
import { launchNativeLoginBrowser } from "../browser/native-login.js";
|
|
@@ -33,6 +33,8 @@ import { PolicyEngine } from "../policy/policy-engine.js";
|
|
|
33
33
|
import { ApprovalStore } from "../policy/approval-store.js";
|
|
34
34
|
import { createDefaultToolRegistry } from "../tools/default-tools.js";
|
|
35
35
|
import { ProcessManager } from "../tools/process-manager.js";
|
|
36
|
+
import { NativeImageReceiver } from "../image/native-image-receiver.js";
|
|
37
|
+
import { NativeMusicReceiver, validateGenerationType } from "../audio/native-music-receiver.js";
|
|
36
38
|
import { resolveLimits } from "../shared/limits.js";
|
|
37
39
|
import { BrowserAdapterError } from "../shared/errors.js";
|
|
38
40
|
import { EXPORTERS } from "../session/session-export.js";
|
|
@@ -41,6 +43,7 @@ import { extractAtMentions } from "./at-files.js";
|
|
|
41
43
|
import {
|
|
42
44
|
classifyChatInput,
|
|
43
45
|
promptForConfirm,
|
|
46
|
+
promptForContinue,
|
|
44
47
|
promptForText,
|
|
45
48
|
promptForSelect,
|
|
46
49
|
readChatMessage,
|
|
@@ -241,6 +244,9 @@ class ConversationRunner {
|
|
|
241
244
|
// interactive TTY sessions where stdin is available to listen on.
|
|
242
245
|
cancelOnEsc: interactive,
|
|
243
246
|
});
|
|
247
|
+
const generationType = validateGenerationType(session.state.generationType, provider.id);
|
|
248
|
+
this.nativeMusicReceiver = generationType === "music" ? new NativeMusicReceiver({ adapter: this.adapter }) : null;
|
|
249
|
+
this.nativeImageReceiver = this.nativeMusicReceiver ? null : NativeImageReceiver.forAdapter(provider.id, this.adapter);
|
|
244
250
|
this.interrupted = false;
|
|
245
251
|
this.closed = false;
|
|
246
252
|
}
|
|
@@ -248,6 +254,8 @@ class ConversationRunner {
|
|
|
248
254
|
#buildRuntime() {
|
|
249
255
|
return new AgentRuntime({
|
|
250
256
|
adapter: this.adapter,
|
|
257
|
+
nativeImageReceiver: this.nativeImageReceiver,
|
|
258
|
+
nativeMusicReceiver: this.nativeMusicReceiver,
|
|
251
259
|
registry: createDefaultToolRegistry({
|
|
252
260
|
processManager: this.processManager,
|
|
253
261
|
limits: this.limits,
|
|
@@ -261,10 +269,12 @@ class ConversationRunner {
|
|
|
261
269
|
this.renderer.stopSpinner();
|
|
262
270
|
await adapter.restoreWindow?.();
|
|
263
271
|
try {
|
|
264
|
-
const answer = await
|
|
272
|
+
const answer = await promptForContinue({
|
|
265
273
|
message: t("model.chooseInBrowser", {
|
|
266
274
|
provider: this.provider.label,
|
|
267
275
|
}),
|
|
276
|
+
countdown: (seconds) => t(seconds > 0
|
|
277
|
+
? "model.continueCountdown" : "model.continuing", { seconds }),
|
|
268
278
|
});
|
|
269
279
|
if (answer == null) {
|
|
270
280
|
throw new Error(t("model.setupCancelled"));
|
|
@@ -749,6 +759,8 @@ async function runAgent(taskParts, options) {
|
|
|
749
759
|
// Fail fast on an unknown/unsupported --model before any startup work.
|
|
750
760
|
const provider = resolveProvider(options.model ?? DEFAULT_PROVIDER);
|
|
751
761
|
|
|
762
|
+
const generationType = validateGenerationType(options.type, provider.id);
|
|
763
|
+
|
|
752
764
|
const startup = await maybeRunStartupChecks(options);
|
|
753
765
|
if (startup === "updated" || startup === "aborted") {
|
|
754
766
|
return null;
|
|
@@ -789,6 +801,7 @@ async function runAgent(taskParts, options) {
|
|
|
789
801
|
projectRoot,
|
|
790
802
|
provider: provider.id,
|
|
791
803
|
mode: null,
|
|
804
|
+
generationType,
|
|
792
805
|
});
|
|
793
806
|
|
|
794
807
|
// Resolve @file attachments in the opening task, if any. The task itself is
|
|
@@ -863,6 +876,9 @@ async function runResume(sessionId, instructionParts, options) {
|
|
|
863
876
|
}
|
|
864
877
|
|
|
865
878
|
const provider = resolveProvider(session.state.provider ?? DEFAULT_PROVIDER);
|
|
879
|
+
if (options.type != null && validateGenerationType(options.type, provider.id) !== (session.state.generationType ?? "agent")) {
|
|
880
|
+
throw new Error("--type cannot change the generation type of an existing session.");
|
|
881
|
+
}
|
|
866
882
|
// A conversation belongs to one provider. --model on resume is only allowed
|
|
867
883
|
// if it names the same provider; switching mid-conversation is rejected.
|
|
868
884
|
if (options.model != null && getProvider(options.model).id !== provider.id) {
|
|
@@ -988,6 +1004,7 @@ const program = new Command()
|
|
|
988
1004
|
}),
|
|
989
1005
|
)
|
|
990
1006
|
.option("--once", t("option.once"), false)
|
|
1007
|
+
.option("--type <type>", t("option.type"))
|
|
991
1008
|
.option("--model-turn-timeout-ms <milliseconds>", t("option.timeout"))
|
|
992
1009
|
.option("--no-minimize", t("option.noMinimize"))
|
|
993
1010
|
.option("--debug", t("option.debug"), false)
|
|
@@ -1055,7 +1072,13 @@ program
|
|
|
1055
1072
|
const entryPath = process.argv[1]
|
|
1056
1073
|
? await fs.realpath(process.argv[1]).catch(() => null)
|
|
1057
1074
|
: null;
|
|
1058
|
-
|
|
1075
|
+
const modulePath = await fs.realpath(fileURLToPath(import.meta.url)).catch(() => null);
|
|
1076
|
+
// npm's Windows shim can use a short path or differently cased path. Compare
|
|
1077
|
+
// canonical filesystem paths rather than independently encoded file URLs.
|
|
1078
|
+
const isEntryPoint = entryPath && modulePath && (process.platform === "win32"
|
|
1079
|
+
? entryPath.toLowerCase() === modulePath.toLowerCase()
|
|
1080
|
+
: entryPath === modulePath);
|
|
1081
|
+
if (isEntryPoint) {
|
|
1059
1082
|
await program.parseAsync().catch((error) => {
|
|
1060
1083
|
// Expected, actionable failures carry a plain message instead of a stack
|
|
1061
1084
|
// trace (e.g. the provider's Chrome profile is locked by another session).
|
|
@@ -1065,6 +1088,12 @@ if (entryPath && import.meta.url === pathToFileURL(entryPath).href) {
|
|
|
1065
1088
|
return;
|
|
1066
1089
|
}
|
|
1067
1090
|
console.error(error.stack ?? error.message);
|
|
1091
|
+
if (error.details?.diagnosticFile) {
|
|
1092
|
+
const dom = error.details.dom;
|
|
1093
|
+
const explanation = dom?.issues?.length ? dom.issues.join(", ") : dom?.status ?? "unavailable";
|
|
1094
|
+
console.error(`Page compatibility: ${explanation}`);
|
|
1095
|
+
console.error(`Diagnostic report: ${error.details.diagnosticFile}`);
|
|
1096
|
+
}
|
|
1068
1097
|
process.exitCode = 1;
|
|
1069
1098
|
}).finally(() => {
|
|
1070
1099
|
// By the time every command finishes, all per-command cleanup (browser
|
package/src/cli/prompt-input.js
CHANGED
|
@@ -849,6 +849,74 @@ export async function promptForText(config, options = {}) {
|
|
|
849
849
|
});
|
|
850
850
|
}
|
|
851
851
|
|
|
852
|
+
// A non-permission startup pause: Enter and expiry both continue. Keep this
|
|
853
|
+
// separate from text/approval prompts, which must never accept on a timer.
|
|
854
|
+
export async function promptForContinue({ message, countdown, timeoutMs = 5_000 }, {
|
|
855
|
+
inputStream = process.stdin,
|
|
856
|
+
outputStream = process.stdout,
|
|
857
|
+
} = {}) {
|
|
858
|
+
if (inputStream.readableEnded || inputStream.destroyed) return null;
|
|
859
|
+
return await new Promise((resolve, reject) => {
|
|
860
|
+
const previousRaw = Boolean(inputStream.isRaw);
|
|
861
|
+
const deadline = Date.now() + timeoutMs;
|
|
862
|
+
let timer;
|
|
863
|
+
let settled = false;
|
|
864
|
+
const render = (seconds) => {
|
|
865
|
+
const text = countdown(seconds);
|
|
866
|
+
if (outputStream.isTTY) {
|
|
867
|
+
// Keep the changing status on one physical terminal row.
|
|
868
|
+
let visible = "";
|
|
869
|
+
for (const ch of text) {
|
|
870
|
+
if (displayWidth(visible + ch) >= (outputStream.columns || 80)) break;
|
|
871
|
+
visible += ch;
|
|
872
|
+
}
|
|
873
|
+
outputStream.write(`\r\u001b[2K${visible}`);
|
|
874
|
+
} else {
|
|
875
|
+
outputStream.write(`${text}\n`);
|
|
876
|
+
}
|
|
877
|
+
};
|
|
878
|
+
const finish = (value, error) => {
|
|
879
|
+
if (settled) return;
|
|
880
|
+
settled = true;
|
|
881
|
+
clearInterval(timer);
|
|
882
|
+
inputStream.removeListener("data", onData);
|
|
883
|
+
inputStream.removeListener("end", onEnd);
|
|
884
|
+
inputStream.removeListener("close", onEnd);
|
|
885
|
+
inputStream.removeListener("error", onError);
|
|
886
|
+
inputStream.setRawMode?.(previousRaw);
|
|
887
|
+
inputStream.pause?.();
|
|
888
|
+
if (value) render(0);
|
|
889
|
+
if (outputStream.isTTY) outputStream.write("\n");
|
|
890
|
+
if (error) reject(error);
|
|
891
|
+
else resolve(value);
|
|
892
|
+
};
|
|
893
|
+
const onData = (chunk) => {
|
|
894
|
+
const text = String(chunk);
|
|
895
|
+
if (/[\u0003\u0004]/.test(text)) finish(null);
|
|
896
|
+
else if (/[\r\n]/.test(text)) finish(true);
|
|
897
|
+
};
|
|
898
|
+
const onEnd = () => finish(null);
|
|
899
|
+
const onError = (error) => finish(null, error);
|
|
900
|
+
inputStream.on("data", onData);
|
|
901
|
+
inputStream.once("end", onEnd);
|
|
902
|
+
inputStream.once("close", onEnd);
|
|
903
|
+
inputStream.once("error", onError);
|
|
904
|
+
try {
|
|
905
|
+
inputStream.setRawMode?.(true);
|
|
906
|
+
outputStream.write(`${message}\n`);
|
|
907
|
+
render(Math.ceil(timeoutMs / 1_000));
|
|
908
|
+
timer = setInterval(() => {
|
|
909
|
+
const seconds = Math.max(0, Math.ceil((deadline - Date.now()) / 1_000));
|
|
910
|
+
if (seconds === 0) finish(true);
|
|
911
|
+
else render(seconds);
|
|
912
|
+
}, 1_000);
|
|
913
|
+
inputStream.resume?.();
|
|
914
|
+
} catch (error) {
|
|
915
|
+
finish(null, error);
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
|
|
852
920
|
export async function promptForSelect(config, options = {}) {
|
|
853
921
|
return await promptWithCleanExit(config, {
|
|
854
922
|
...options,
|