tide-commander 0.84.2 → 0.85.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.
@@ -1 +1 @@
1
- import{W as s}from"./main-BSaZHBzs.js";import"./modulepreload-polyfill-B5Qt9EMX.js";import"./vendor-react-uS-d4TUT.js";import"./vendor-three-DJ4p3FLF.js";class f extends s{constructor(){super(...arguments),this.pending=[],this.deliveredNotifications=[],this.hasNotificationSupport=()=>{if(!("Notification"in window)||!Notification.requestPermission)return!1;if(Notification.permission!=="granted")try{new Notification("")}catch(i){if(i instanceof Error&&i.name==="TypeError")return!1}return!0}}async getDeliveredNotifications(){const i=[];for(const t of this.deliveredNotifications){const e={title:t.title,id:parseInt(t.tag),body:t.body};i.push(e)}return{notifications:i}}async removeDeliveredNotifications(i){for(const t of i.notifications){const e=this.deliveredNotifications.find(n=>n.tag===String(t.id));e==null||e.close(),this.deliveredNotifications=this.deliveredNotifications.filter(()=>!e)}}async removeAllDeliveredNotifications(){for(const i of this.deliveredNotifications)i.close();this.deliveredNotifications=[]}async createChannel(){throw this.unimplemented("Not implemented on web.")}async deleteChannel(){throw this.unimplemented("Not implemented on web.")}async listChannels(){throw this.unimplemented("Not implemented on web.")}async schedule(i){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");for(const t of i.notifications)this.sendNotification(t);return{notifications:i.notifications.map(t=>({id:t.id}))}}async getPending(){return{notifications:this.pending}}async registerActionTypes(){throw this.unimplemented("Not implemented on web.")}async cancel(i){this.pending=this.pending.filter(t=>!i.notifications.find(e=>e.id===t.id))}async areEnabled(){const{display:i}=await this.checkPermissions();return{value:i==="granted"}}async changeExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async checkExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async requestPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(await Notification.requestPermission())}}async checkPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(Notification.permission)}}transformNotificationPermission(i){switch(i){case"granted":return"granted";case"denied":return"denied";default:return"prompt"}}sendPending(){var i;const t=[],e=new Date().getTime();for(const n of this.pending)!((i=n.schedule)===null||i===void 0)&&i.at&&n.schedule.at.getTime()<=e&&(this.buildNotification(n),t.push(n));this.pending=this.pending.filter(n=>!t.find(o=>o===n))}sendNotification(i){var t;if(!((t=i.schedule)===null||t===void 0)&&t.at){const e=i.schedule.at.getTime()-new Date().getTime();this.pending.push(i),setTimeout(()=>{this.sendPending()},e);return}this.buildNotification(i)}buildNotification(i){const t=new Notification(i.title,{body:i.body,tag:String(i.id)});return t.addEventListener("click",this.onClick.bind(this,i),!1),t.addEventListener("show",this.onShow.bind(this,i),!1),t.addEventListener("close",()=>{this.deliveredNotifications=this.deliveredNotifications.filter(()=>!this)},!1),this.deliveredNotifications.push(t),t}onClick(i){const t={actionId:"tap",notification:i};this.notifyListeners("localNotificationActionPerformed",t)}onShow(i){this.notifyListeners("localNotificationReceived",i)}}export{f as LocalNotificationsWeb};
1
+ import{W as s}from"./main-hADDQllW.js";import"./modulepreload-polyfill-B5Qt9EMX.js";import"./vendor-react-uS-d4TUT.js";import"./vendor-three-DJ4p3FLF.js";class f extends s{constructor(){super(...arguments),this.pending=[],this.deliveredNotifications=[],this.hasNotificationSupport=()=>{if(!("Notification"in window)||!Notification.requestPermission)return!1;if(Notification.permission!=="granted")try{new Notification("")}catch(i){if(i instanceof Error&&i.name==="TypeError")return!1}return!0}}async getDeliveredNotifications(){const i=[];for(const t of this.deliveredNotifications){const e={title:t.title,id:parseInt(t.tag),body:t.body};i.push(e)}return{notifications:i}}async removeDeliveredNotifications(i){for(const t of i.notifications){const e=this.deliveredNotifications.find(n=>n.tag===String(t.id));e==null||e.close(),this.deliveredNotifications=this.deliveredNotifications.filter(()=>!e)}}async removeAllDeliveredNotifications(){for(const i of this.deliveredNotifications)i.close();this.deliveredNotifications=[]}async createChannel(){throw this.unimplemented("Not implemented on web.")}async deleteChannel(){throw this.unimplemented("Not implemented on web.")}async listChannels(){throw this.unimplemented("Not implemented on web.")}async schedule(i){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");for(const t of i.notifications)this.sendNotification(t);return{notifications:i.notifications.map(t=>({id:t.id}))}}async getPending(){return{notifications:this.pending}}async registerActionTypes(){throw this.unimplemented("Not implemented on web.")}async cancel(i){this.pending=this.pending.filter(t=>!i.notifications.find(e=>e.id===t.id))}async areEnabled(){const{display:i}=await this.checkPermissions();return{value:i==="granted"}}async changeExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async checkExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async requestPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(await Notification.requestPermission())}}async checkPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(Notification.permission)}}transformNotificationPermission(i){switch(i){case"granted":return"granted";case"denied":return"denied";default:return"prompt"}}sendPending(){var i;const t=[],e=new Date().getTime();for(const n of this.pending)!((i=n.schedule)===null||i===void 0)&&i.at&&n.schedule.at.getTime()<=e&&(this.buildNotification(n),t.push(n));this.pending=this.pending.filter(n=>!t.find(o=>o===n))}sendNotification(i){var t;if(!((t=i.schedule)===null||t===void 0)&&t.at){const e=i.schedule.at.getTime()-new Date().getTime();this.pending.push(i),setTimeout(()=>{this.sendPending()},e);return}this.buildNotification(i)}buildNotification(i){const t=new Notification(i.title,{body:i.body,tag:String(i.id)});return t.addEventListener("click",this.onClick.bind(this,i),!1),t.addEventListener("show",this.onShow.bind(this,i),!1),t.addEventListener("close",()=>{this.deliveredNotifications=this.deliveredNotifications.filter(()=>!this)},!1),this.deliveredNotifications.push(t),t}onClick(i){const t={actionId:"tap",notification:i};this.notifyListeners("localNotificationActionPerformed",t)}onShow(i){this.notifyListeners("localNotificationReceived",i)}}export{f as LocalNotificationsWeb};
package/dist/index.html CHANGED
@@ -22,11 +22,11 @@
22
22
  <link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png" />
23
23
  <link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png" />
24
24
  <title>Tide Commander</title>
25
- <script type="module" crossorigin src="/assets/main-BSaZHBzs.js"></script>
25
+ <script type="module" crossorigin src="/assets/main-hADDQllW.js"></script>
26
26
  <link rel="modulepreload" crossorigin href="/assets/modulepreload-polyfill-B5Qt9EMX.js">
27
27
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-uS-d4TUT.js">
28
28
  <link rel="modulepreload" crossorigin href="/assets/vendor-three-DJ4p3FLF.js">
29
- <link rel="stylesheet" crossorigin href="/assets/main-C6IAMrFB.css">
29
+ <link rel="stylesheet" crossorigin href="/assets/main-BB4dFHqm.css">
30
30
  </head>
31
31
  <body>
32
32
  <div id="app"></div>
@@ -53,7 +53,10 @@
53
53
  "tokenRequired": "Erforderlich wenn der Server AUTH_TOKEN gesetzt hat",
54
54
  "saveAndReconnect": "Speichern und neu verbinden",
55
55
  "showToken": "Token anzeigen",
56
- "hideToken": "Token ausblenden"
56
+ "hideToken": "Token ausblenden",
57
+ "codexBinaryPath": "Codex-Binärpfad",
58
+ "codexBinaryPathPlaceholder": "Leer lassen für automatische Erkennung",
59
+ "codexBinaryPathHint": "Pfad zur nativen Codex-Binärdatei. Umgeht den Node.js-Wrapper für bessere Sitzungspersistenz."
57
60
  },
58
61
  "scene": {
59
62
  "characterSize": "Charaktergröße",
@@ -53,7 +53,10 @@
53
53
  "tokenRequired": "Required if server has AUTH_TOKEN set",
54
54
  "saveAndReconnect": "Save and reconnect",
55
55
  "showToken": "Show token",
56
- "hideToken": "Hide token"
56
+ "hideToken": "Hide token",
57
+ "codexBinaryPath": "Codex Binary Path",
58
+ "codexBinaryPathPlaceholder": "Leave empty for auto-detect",
59
+ "codexBinaryPathHint": "Path to the native Codex binary. Bypasses the Node.js wrapper for better session persistence."
57
60
  },
58
61
  "scene": {
59
62
  "characterSize": "Character Size",
@@ -53,7 +53,10 @@
53
53
  "tokenRequired": "Requerido si el servidor tiene AUTH_TOKEN configurado",
54
54
  "saveAndReconnect": "Guardar y reconectar",
55
55
  "showToken": "Mostrar token",
56
- "hideToken": "Ocultar token"
56
+ "hideToken": "Ocultar token",
57
+ "codexBinaryPath": "Ruta del binario Codex",
58
+ "codexBinaryPathPlaceholder": "Dejar vacío para detección automática",
59
+ "codexBinaryPathHint": "Ruta al binario nativo de Codex. Omite el wrapper de Node.js para mejor persistencia de sesión."
57
60
  },
58
61
  "scene": {
59
62
  "characterSize": "Tamano del personaje",
@@ -53,7 +53,10 @@
53
53
  "tokenRequired": "Requis si le serveur a AUTH_TOKEN défini",
54
54
  "saveAndReconnect": "Enregistrer et reconnecter",
55
55
  "showToken": "Afficher le jeton",
56
- "hideToken": "Masquer le jeton"
56
+ "hideToken": "Masquer le jeton",
57
+ "codexBinaryPath": "Chemin du binaire Codex",
58
+ "codexBinaryPathPlaceholder": "Laisser vide pour la détection automatique",
59
+ "codexBinaryPathHint": "Chemin vers le binaire natif Codex. Contourne le wrapper Node.js pour une meilleure persistance de session."
57
60
  },
58
61
  "scene": {
59
62
  "characterSize": "Taille du personnage",
@@ -53,7 +53,10 @@
53
53
  "tokenRequired": "यदि सर्वर पर AUTH_TOKEN सेट है तो आवश्यक",
54
54
  "saveAndReconnect": "सहेजें और पुनः कनेक्ट करें",
55
55
  "showToken": "टोकन दिखाएं",
56
- "hideToken": "टोकन छुपाएं"
56
+ "hideToken": "टोकन छुपाएं",
57
+ "codexBinaryPath": "Codex बाइनरी पथ",
58
+ "codexBinaryPathPlaceholder": "स्वतः पहचान के लिए खाली छोड़ें",
59
+ "codexBinaryPathHint": "नेटिव Codex बाइनरी का पथ। बेहतर सत्र स्थिरता के लिए Node.js रैपर को बायपास करता है।"
57
60
  },
58
61
  "scene": {
59
62
  "characterSize": "कैरेक्टर आकार",
@@ -53,7 +53,10 @@
53
53
  "tokenRequired": "Richiesto se il server ha AUTH_TOKEN impostato",
54
54
  "saveAndReconnect": "Salva e riconnetti",
55
55
  "showToken": "Mostra token",
56
- "hideToken": "Nascondi token"
56
+ "hideToken": "Nascondi token",
57
+ "codexBinaryPath": "Percorso binario Codex",
58
+ "codexBinaryPathPlaceholder": "Lasciare vuoto per il rilevamento automatico",
59
+ "codexBinaryPathHint": "Percorso al binario nativo Codex. Bypassa il wrapper Node.js per una migliore persistenza della sessione."
57
60
  },
58
61
  "scene": {
59
62
  "characterSize": "Dimensione personaggio",
@@ -53,7 +53,10 @@
53
53
  "tokenRequired": "サーバーにAUTH_TOKENが設定されている場合は必須",
54
54
  "saveAndReconnect": "保存して再接続",
55
55
  "showToken": "トークンを表示",
56
- "hideToken": "トークンを非表示"
56
+ "hideToken": "トークンを非表示",
57
+ "codexBinaryPath": "Codexバイナリパス",
58
+ "codexBinaryPathPlaceholder": "空欄で自動検出",
59
+ "codexBinaryPathHint": "ネイティブCodexバイナリへのパス。Node.jsラッパーをバイパスしてセッションの永続性を向上させます。"
57
60
  },
58
61
  "scene": {
59
62
  "characterSize": "キャラクターサイズ",
@@ -53,7 +53,10 @@
53
53
  "tokenRequired": "Obrigatorio se o servidor tiver AUTH_TOKEN definido",
54
54
  "saveAndReconnect": "Salvar e reconectar",
55
55
  "showToken": "Mostrar token",
56
- "hideToken": "Ocultar token"
56
+ "hideToken": "Ocultar token",
57
+ "codexBinaryPath": "Caminho do binário Codex",
58
+ "codexBinaryPathPlaceholder": "Deixe vazio para detecção automática",
59
+ "codexBinaryPathHint": "Caminho para o binário nativo do Codex. Ignora o wrapper Node.js para melhor persistência de sessão."
57
60
  },
58
61
  "scene": {
59
62
  "characterSize": "Tamanho do Personagem",
@@ -53,7 +53,10 @@
53
53
  "tokenRequired": "Требуется, если на сервере установлен AUTH_TOKEN",
54
54
  "saveAndReconnect": "Сохранить и переподключиться",
55
55
  "showToken": "Показать токен",
56
- "hideToken": "Скрыть токен"
56
+ "hideToken": "Скрыть токен",
57
+ "codexBinaryPath": "Путь к бинарному файлу Codex",
58
+ "codexBinaryPathPlaceholder": "Оставьте пустым для автоопределения",
59
+ "codexBinaryPathHint": "Путь к нативному бинарному файлу Codex. Обходит обёртку Node.js для лучшей сохранности сессий."
57
60
  },
58
61
  "scene": {
59
62
  "characterSize": "Размер персонажа",
@@ -53,7 +53,10 @@
53
53
  "tokenRequired": "如果服务器设置了 AUTH_TOKEN 则必填",
54
54
  "saveAndReconnect": "保存并重新连接",
55
55
  "showToken": "显示令牌",
56
- "hideToken": "隐藏令牌"
56
+ "hideToken": "隐藏令牌",
57
+ "codexBinaryPath": "Codex 二进制路径",
58
+ "codexBinaryPathPlaceholder": "留空以自动检测",
59
+ "codexBinaryPathHint": "原生 Codex 二进制文件的路径。绕过 Node.js 包装器以获得更好的会话持久性。"
57
60
  },
58
61
  "scene": {
59
62
  "characterSize": "角色大小",
@@ -42,6 +42,7 @@ export class RunnerProcessLifecycle {
42
42
  const executable = this.backend.getExecutablePath();
43
43
  log.log(`🚀 Spawning: ${executable} ${args.join(' ')}`);
44
44
  const isWindows = process.platform === 'win32';
45
+ const extraEnv = this.backend.getExtraEnv?.() ?? {};
45
46
  const childProcess = spawn(executable, args, {
46
47
  cwd: workingDir,
47
48
  env: {
@@ -49,6 +50,7 @@ export class RunnerProcessLifecycle {
49
50
  LANG: 'en_US.UTF-8',
50
51
  LC_ALL: 'en_US.UTF-8',
51
52
  TIDE_SERVER: `http://localhost:${process.env.TIDE_PORT || process.env.PORT || 5174}`,
53
+ ...extraEnv,
52
54
  },
53
55
  shell: isWindows ? true : false,
54
56
  detached: isWindows ? false : true,
@@ -155,6 +155,34 @@ function normalizeCodexFunctionToolCall(rawToolName, rawToolInput) {
155
155
  toolInput: rawToolInput,
156
156
  };
157
157
  }
158
+ function normalizeCodexWebSearchToolInput(payload) {
159
+ const action = isObject(payload.action) ? payload.action : {};
160
+ const actionQueriesRaw = action.queries;
161
+ const actionQueries = Array.isArray(actionQueriesRaw) && actionQueriesRaw.every((q) => typeof q === 'string')
162
+ ? actionQueriesRaw
163
+ : undefined;
164
+ return {
165
+ actionType: typeof action.type === 'string' ? action.type : undefined,
166
+ actionQuery: typeof action.query === 'string' ? action.query : undefined,
167
+ actionQueries,
168
+ actionUrl: typeof action.url === 'string' ? action.url : undefined,
169
+ status: typeof payload.status === 'string' ? payload.status : undefined,
170
+ };
171
+ }
172
+ function normalizeCodexEventFallbackText(eventType, payload) {
173
+ let serialized = '';
174
+ try {
175
+ serialized = JSON.stringify(payload, null, 2);
176
+ }
177
+ catch {
178
+ serialized = String(payload);
179
+ }
180
+ const MAX_LEN = 4000;
181
+ if (serialized.length > MAX_LEN) {
182
+ serialized = `${serialized.slice(0, MAX_LEN)}...`;
183
+ }
184
+ return `[codex-event] ${eventType}\n${serialized}`;
185
+ }
158
186
  function findCodexSessionFile(sessionId) {
159
187
  const cached = codexSessionFileById.get(sessionId);
160
188
  if (cached && fs.existsSync(cached)) {
@@ -445,6 +473,37 @@ function parseCodexEntryMessages(entry, messages, toolUseIdToName) {
445
473
  });
446
474
  return;
447
475
  }
476
+ // token_count: Skip in history (token accounting shown via turn.completed)
477
+ if (payload.type === 'token_count') {
478
+ return;
479
+ }
480
+ // agent_reasoning: Skip in history (consistent with Claude thinking behavior)
481
+ if (payload.type === 'agent_reasoning') {
482
+ return;
483
+ }
484
+ // turn_aborted: Skip in history (interruption is visible from agent stopping)
485
+ if (payload.type === 'turn_aborted') {
486
+ return;
487
+ }
488
+ // task_complete: Show the final agent message as formatted text
489
+ if (payload.type === 'task_complete' && typeof payload.last_agent_message === 'string') {
490
+ messages.push({
491
+ type: 'assistant',
492
+ content: payload.last_agent_message,
493
+ timestamp: entry.timestamp,
494
+ uuid: `${entry.timestamp}-assistant-task-complete`,
495
+ });
496
+ return;
497
+ }
498
+ // Unknown event_msg types: keep fallback
499
+ const payloadType = typeof payload.type === 'string' ? payload.type : 'unknown';
500
+ messages.push({
501
+ type: 'assistant',
502
+ content: normalizeCodexEventFallbackText(`event_msg.${payloadType}`, payload),
503
+ timestamp: entry.timestamp,
504
+ uuid: `${entry.timestamp}-assistant-codex-event`,
505
+ });
506
+ return;
448
507
  }
449
508
  if (entry.type !== 'response_item' || !isObject(entry.payload)) {
450
509
  return;
@@ -494,6 +553,32 @@ function parseCodexEntryMessages(entry, messages, toolUseIdToName) {
494
553
  });
495
554
  return;
496
555
  }
556
+ if (payloadType === 'web_search_call') {
557
+ const toolName = 'web_search';
558
+ const toolInput = normalizeCodexWebSearchToolInput(payload);
559
+ const status = typeof payload.status === 'string' ? payload.status : undefined;
560
+ const toolUseId = typeof payload.call_id === 'string' ? payload.call_id : `${entry.timestamp}-web-search`;
561
+ messages.push({
562
+ type: 'tool_use',
563
+ content: JSON.stringify(toolInput, null, 2),
564
+ timestamp: entry.timestamp,
565
+ uuid: `${entry.timestamp}-tool-use-web-search`,
566
+ toolName,
567
+ toolInput,
568
+ toolUseId,
569
+ });
570
+ if (status === 'completed') {
571
+ messages.push({
572
+ type: 'tool_result',
573
+ content: JSON.stringify(toolInput, null, 2),
574
+ timestamp: entry.timestamp,
575
+ uuid: `${entry.timestamp}-tool-result-web-search`,
576
+ toolName,
577
+ toolUseId,
578
+ });
579
+ }
580
+ return;
581
+ }
497
582
  if (payloadType === 'function_call_output') {
498
583
  const toolUseId = typeof payload.call_id === 'string' ? payload.call_id : undefined;
499
584
  const toolName = toolUseId ? (toolUseIdToName.get(toolUseId) || 'unknown') : 'unknown';
@@ -506,7 +591,80 @@ function parseCodexEntryMessages(entry, messages, toolUseIdToName) {
506
591
  toolName,
507
592
  toolUseId,
508
593
  });
594
+ return;
595
+ }
596
+ // reasoning: Skip in history (consistent with Claude thinking behavior)
597
+ if (payloadType === 'reasoning') {
598
+ return;
599
+ }
600
+ // custom_tool_call: Map to tool_use (normalize apply_patch -> Edit)
601
+ if (payloadType === 'custom_tool_call') {
602
+ const rawToolName = typeof payload.name === 'string' ? payload.name : 'unknown';
603
+ const callId = typeof payload.call_id === 'string' ? payload.call_id : undefined;
604
+ const rawInput = typeof payload.input === 'string' ? payload.input : '';
605
+ // Normalize: apply_patch -> Edit for consistent UI rendering
606
+ const toolName = rawToolName === 'apply_patch' ? 'Edit' : rawToolName;
607
+ const toolInput = {};
608
+ if (rawToolName === 'apply_patch' && rawInput) {
609
+ // Extract file path from patch for clickable display
610
+ const fileMatch = rawInput.match(/\*\*\* (?:Update|Add|Delete) File: (.+)/);
611
+ if (fileMatch) {
612
+ toolInput.file_path = fileMatch[1].trim();
613
+ }
614
+ toolInput.old_string = '';
615
+ toolInput.new_string = rawInput;
616
+ }
617
+ else {
618
+ toolInput.input = rawInput;
619
+ }
620
+ if (callId) {
621
+ toolUseIdToName.set(callId, toolName);
622
+ }
623
+ messages.push({
624
+ type: 'tool_use',
625
+ content: JSON.stringify(toolInput, null, 2),
626
+ timestamp: entry.timestamp,
627
+ uuid: `${entry.timestamp}-tool-use-custom`,
628
+ toolName,
629
+ toolInput,
630
+ toolUseId: callId,
631
+ });
632
+ return;
633
+ }
634
+ // custom_tool_call_output: Map to tool_result
635
+ if (payloadType === 'custom_tool_call_output') {
636
+ const callId = typeof payload.call_id === 'string' ? payload.call_id : undefined;
637
+ const toolName = callId ? (toolUseIdToName.get(callId) || 'unknown') : 'unknown';
638
+ let content = normalizeTextContent(payload.output);
639
+ // Parse apply_patch JSON output wrapper to extract meaningful text
640
+ if (content) {
641
+ try {
642
+ const parsed = JSON.parse(content);
643
+ if (isObject(parsed) && typeof parsed.output === 'string') {
644
+ content = parsed.output;
645
+ }
646
+ }
647
+ catch {
648
+ // Use raw content as-is
649
+ }
650
+ }
651
+ messages.push({
652
+ type: 'tool_result',
653
+ content,
654
+ timestamp: entry.timestamp,
655
+ uuid: `${entry.timestamp}-tool-result-custom`,
656
+ toolName,
657
+ toolUseId: callId,
658
+ });
659
+ return;
509
660
  }
661
+ const unknownPayloadType = typeof payloadType === 'string' ? payloadType : 'unknown';
662
+ messages.push({
663
+ type: 'assistant',
664
+ content: normalizeCodexEventFallbackText(`response_item.${unknownPayloadType}`, payload),
665
+ timestamp: entry.timestamp,
666
+ uuid: `${entry.timestamp}-assistant-codex-response-item`,
667
+ });
510
668
  }
511
669
  async function parseSessionMessages(resolved) {
512
670
  const messages = [];
@@ -251,12 +251,18 @@ function installLocalCert(host) {
251
251
  fs.mkdirSync(TLS_DIR, { recursive: true });
252
252
  const mkcertBin = findSystemMkcert();
253
253
  if (!mkcertBin) {
254
- throw new Error('mkcert (Go binary) is required for --install-local-cert but was not found in PATH.\n'
254
+ throw new Error('mkcert (Go binary) is required for HTTPS but was not found in PATH.\n'
255
255
  + 'Install it from: https://github.com/FiloSottile/mkcert\n'
256
256
  + 'Note: the npm "mkcert" package is not the same tool.');
257
257
  }
258
+ const dim = '\x1b[2m';
259
+ const cyan = '\x1b[36m';
260
+ const reset = '\x1b[0m';
261
+ console.log(`\n${cyan}Installing local CA with mkcert...${reset}`);
262
+ console.log(`${dim}Running: ${mkcertBin} -install${reset}`);
263
+ console.log(`${dim}This may require your password to trust the local CA.${reset}\n`);
258
264
  try {
259
- spawnSyncOrThrow(`"${mkcertBin}" -install`);
265
+ execSync(`"${mkcertBin}" -install`, { stdio: 'inherit' });
260
266
  }
261
267
  catch {
262
268
  throw new Error(`mkcert -install failed. You may need to run: sudo ${mkcertBin} -install`);
@@ -266,14 +272,18 @@ function installLocalCert(host) {
266
272
  hostArgs.push(host);
267
273
  }
268
274
  const mkcertGenCmd = `"${mkcertBin}" -cert-file "${DEFAULT_TLS_CERT_FILE}" -key-file "${DEFAULT_TLS_KEY_FILE}" ${hostArgs.join(' ')}`;
269
- spawnSyncOrThrow(mkcertGenCmd);
275
+ console.log(`${dim}Running: ${mkcertBin} ${hostArgs.join(' ')}${reset}`);
276
+ try {
277
+ execSync(mkcertGenCmd, { stdio: 'inherit' });
278
+ }
279
+ catch {
280
+ throw new Error(`Failed to generate TLS certificates with mkcert`);
281
+ }
270
282
  ensureFileExists(DEFAULT_TLS_CERT_FILE, 'TLS cert');
271
283
  ensureFileExists(DEFAULT_TLS_KEY_FILE, 'TLS key');
284
+ console.log(`${cyan}Local certificates generated at ${TLS_DIR}${reset}\n`);
272
285
  return { keyPath: DEFAULT_TLS_KEY_FILE, certPath: DEFAULT_TLS_CERT_FILE };
273
286
  }
274
- function spawnSyncOrThrow(command) {
275
- execSync(command, { stdio: 'ignore' });
276
- }
277
287
  function readPidFile() {
278
288
  try {
279
289
  const raw = fs.readFileSync(PID_FILE, 'utf8').trim();
@@ -585,15 +595,25 @@ async function main() {
585
595
  if (shouldEnableHttps) {
586
596
  process.env.HTTPS = '1';
587
597
  }
598
+ if (options.tlsKey && options.tlsCert) {
599
+ process.env.TLS_KEY_PATH = resolveFromCwd(options.tlsKey);
600
+ process.env.TLS_CERT_PATH = resolveFromCwd(options.tlsCert);
601
+ }
588
602
  if (options.installLocalCert) {
589
603
  const host = process.env.HOST || 'localhost';
590
604
  const generated = installLocalCert(host);
591
605
  process.env.TLS_KEY_PATH = generated.keyPath;
592
606
  process.env.TLS_CERT_PATH = generated.certPath;
593
607
  }
594
- if (options.tlsKey && options.tlsCert) {
595
- process.env.TLS_KEY_PATH = resolveFromCwd(options.tlsKey);
596
- process.env.TLS_CERT_PATH = resolveFromCwd(options.tlsCert);
608
+ if (process.env.HTTPS === '1' && !process.env.TLS_KEY_PATH && !process.env.TLS_CERT_PATH) {
609
+ const defaultKeyExists = fs.existsSync(DEFAULT_TLS_KEY_FILE);
610
+ const defaultCertExists = fs.existsSync(DEFAULT_TLS_CERT_FILE);
611
+ if (!defaultKeyExists || !defaultCertExists) {
612
+ const host = process.env.HOST || 'localhost';
613
+ const generated = installLocalCert(host);
614
+ process.env.TLS_KEY_PATH = generated.keyPath;
615
+ process.env.TLS_CERT_PATH = generated.certPath;
616
+ }
597
617
  }
598
618
  if (process.env.HTTPS === '1') {
599
619
  const tlsKeyPath = resolveFromCwd(process.env.TLS_KEY_PATH || DEFAULT_TLS_KEY_FILE);
@@ -3,7 +3,7 @@ import * as os from 'os';
3
3
  import * as path from 'path';
4
4
  import { CodexJsonEventParser } from './json-event-parser.js';
5
5
  import { TIDE_COMMANDER_APPENDED_PROMPT } from '../prompts/tide-commander.js';
6
- import { isEchoPromptEnabled } from '../services/system-prompt-service.js';
6
+ import { isEchoPromptEnabled, getCodexBinaryPath } from '../services/system-prompt-service.js';
7
7
  function shouldPassCodexModel(model) {
8
8
  if (!model)
9
9
  return false;
@@ -44,7 +44,7 @@ export class CodexBackend {
44
44
  buildArgs(config) {
45
45
  this.parser.setWorkingDirectory(config.workingDir);
46
46
  const prompt = buildCodexPrompt(config);
47
- const args = ['exec', '--json'];
47
+ const args = ['exec', '--experimental-json'];
48
48
  const codexConfig = config.codexConfig;
49
49
  const fullAuto = codexConfig?.fullAuto !== false;
50
50
  if (fullAuto) {
@@ -94,6 +94,15 @@ export class CodexBackend {
94
94
  return null;
95
95
  }
96
96
  getExecutablePath() {
97
+ // Priority: 1) CODEX_BINARY env var 2) Settings UI 3) auto-detect
98
+ const envBinary = process.env.CODEX_BINARY;
99
+ if (envBinary && fs.existsSync(envBinary)) {
100
+ return envBinary;
101
+ }
102
+ const settingsBinary = getCodexBinaryPath();
103
+ if (settingsBinary && fs.existsSync(settingsBinary)) {
104
+ return settingsBinary;
105
+ }
97
106
  return this.detectInstallation() || 'codex';
98
107
  }
99
108
  detectInstallation() {
@@ -116,6 +125,21 @@ export class CodexBackend {
116
125
  }
117
126
  return null;
118
127
  }
128
+ getExtraEnv() {
129
+ // When using the native binary directly, we need to add
130
+ // the vendor path directory to PATH so codex can find bundled tools like rg.
131
+ const envBinary = process.env.CODEX_BINARY || getCodexBinaryPath();
132
+ if (!envBinary)
133
+ return {};
134
+ const codexDir = path.dirname(envBinary); // .../codex/
135
+ const archRoot = path.dirname(codexDir); // .../x86_64-unknown-linux-musl/
136
+ const pathDir = path.join(archRoot, 'path');
137
+ if (fs.existsSync(pathDir)) {
138
+ const sep = process.platform === 'win32' ? ';' : ':';
139
+ return { PATH: pathDir + sep + (process.env.PATH || '') };
140
+ }
141
+ return {};
142
+ }
119
143
  requiresStdinInput() {
120
144
  return false;
121
145
  }