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.
Files changed (34) hide show
  1. package/README.md +161 -7
  2. package/package.json +5 -4
  3. package/src/artifacts/artifact-store.js +154 -0
  4. package/src/audio/music-artifact.js +66 -0
  5. package/src/audio/native-music-receiver.js +91 -0
  6. package/src/browser/base-web-adapter.js +354 -50
  7. package/src/browser/chatgpt-dom.js +179 -0
  8. package/src/browser/chatgpt-web-adapter.js +89 -125
  9. package/src/browser/fake-web-model-adapter.js +16 -1
  10. package/src/browser/gemini-web-adapter.js +61 -20
  11. package/src/browser/grok-web-adapter.js +35 -3
  12. package/src/browser/rendered-text.js +11 -0
  13. package/src/cli/i18n.js +40 -10
  14. package/src/cli/main.js +32 -3
  15. package/src/cli/prompt-input.js +68 -0
  16. package/src/image/browser-image-driver.js +238 -0
  17. package/src/image/browser-image-session-pool.js +96 -0
  18. package/src/image/image-generation-service.js +101 -0
  19. package/src/image/image-provider-driver.js +22 -0
  20. package/src/image/native-image-receiver.js +66 -0
  21. package/src/image/provider-registry.js +23 -0
  22. package/src/image/providers/chatgpt-image-driver.js +211 -0
  23. package/src/image/providers/gemini-image-driver.js +100 -0
  24. package/src/image/providers/grok-image-driver.js +193 -0
  25. package/src/policy/policy-engine.js +10 -5
  26. package/src/protocol/envelope.js +74 -0
  27. package/src/protocol/markers.js +16 -0
  28. package/src/protocol/prompt-builder.js +12 -9
  29. package/src/protocol/xml-protocol.js +36 -18
  30. package/src/runtime/agent-runtime.js +345 -65
  31. package/src/session/agent-session.js +73 -2
  32. package/src/tools/image-tools.js +92 -0
  33. package/src/tools/registry.js +13 -1
  34. 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
- this.page.locator("main textarea").first(),
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
- return { id: null, turn: await scope.count().catch(() => 0) };
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.last().innerText().catch(() => "");
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. Choose the model you want to use in the browser if desired, then press Enter here. Press Enter immediately to keep the currently selected model.",
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} 已登录。如需切换模型,请直接在浏览器网页中人工选择;完成后在此按 Enter。若保持当前已选模型,直接按 Enter 即可。",
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} 已登入。如需切換模型,請直接在瀏覽器頁面中自行選擇;完成後在此按 Enter。若保持目前已選模型,直接按 Enter 即可。",
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} にログイン済みです。必要ならブラウザー上で使用するモデルを手動で選択し、ここで Enter を押してください。現在のモデルを使う場合は、そのまま Enter を押します。",
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}에 로그인되었습니다. 원하는 경우 브라우저에서 사용할 모델을 직접 선택한 뒤 여기서 Enter를 누르세요. 현재 선택된 모델을 유지하려면 바로 Enter를 누르면 됩니다.",
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. Si quieres, selecciona manualmente en el navegador el modelo que deseas usar y después pulsa Enter aquí. Pulsa Enter directamente para conservar el modelo actual.",
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é. Si nécessaire, choisissez manuellement dans le navigateur le modèle à utiliser, puis appuyez ici sur Entrée. Appuyez directement sur Entrée pour conserver le modèle actuel.",
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. Wähle bei Bedarf das gewünschte Modell manuell im Browser und drücke danach hier Enter. Drücke direkt Enter, um das aktuell ausgewählte Modell beizubehalten.",
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. Se quiser, escolha manualmente no navegador o modelo a usar e depois pressione Enter aqui. Pressione Enter imediatamente para manter o modelo atual.",
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}: вход выполнен. При необходимости вручную выберите нужную модель в браузере, затем нажмите Enter здесь. Чтобы оставить текущую модель, сразу нажмите Enter.",
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 { pathToFileURL } from "node:url";
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 promptForText({
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
- if (entryPath && import.meta.url === pathToFileURL(entryPath).href) {
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
@@ -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,