botmux 2.110.0 → 2.111.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 (221) hide show
  1. package/dist/adapters/backend/tmux-backend.d.ts +25 -5
  2. package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
  3. package/dist/adapters/backend/tmux-backend.js +59 -93
  4. package/dist/adapters/backend/tmux-backend.js.map +1 -1
  5. package/dist/adapters/backend/tmux-pipe-backend.d.ts.map +1 -1
  6. package/dist/adapters/backend/tmux-pipe-backend.js +5 -0
  7. package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
  8. package/dist/adapters/backend/zellij-backend.d.ts +30 -0
  9. package/dist/adapters/backend/zellij-backend.d.ts.map +1 -1
  10. package/dist/adapters/backend/zellij-backend.js +80 -13
  11. package/dist/adapters/backend/zellij-backend.js.map +1 -1
  12. package/dist/adapters/backend/zellij-socket-probe.d.ts +18 -0
  13. package/dist/adapters/backend/zellij-socket-probe.d.ts.map +1 -0
  14. package/dist/adapters/backend/zellij-socket-probe.js +146 -0
  15. package/dist/adapters/backend/zellij-socket-probe.js.map +1 -0
  16. package/dist/adapters/cli/claude-code.d.ts.map +1 -1
  17. package/dist/adapters/cli/claude-code.js +5 -0
  18. package/dist/adapters/cli/claude-code.js.map +1 -1
  19. package/dist/adapters/cli/codex-app-turn.d.ts +43 -0
  20. package/dist/adapters/cli/codex-app-turn.d.ts.map +1 -0
  21. package/dist/adapters/cli/codex-app-turn.js +110 -0
  22. package/dist/adapters/cli/codex-app-turn.js.map +1 -0
  23. package/dist/adapters/cli/codex-app.d.ts.map +1 -1
  24. package/dist/adapters/cli/codex-app.js +6 -0
  25. package/dist/adapters/cli/codex-app.js.map +1 -1
  26. package/dist/adapters/cli/codex.d.ts.map +1 -1
  27. package/dist/adapters/cli/codex.js +1 -0
  28. package/dist/adapters/cli/codex.js.map +1 -1
  29. package/dist/adapters/cli/runner-input.d.ts +3 -2
  30. package/dist/adapters/cli/runner-input.d.ts.map +1 -1
  31. package/dist/adapters/cli/runner-input.js +7 -4
  32. package/dist/adapters/cli/runner-input.js.map +1 -1
  33. package/dist/adapters/cli/types.d.ts +16 -0
  34. package/dist/adapters/cli/types.d.ts.map +1 -1
  35. package/dist/bot-registry.d.ts +5 -0
  36. package/dist/bot-registry.d.ts.map +1 -1
  37. package/dist/bot-registry.js +1 -0
  38. package/dist/bot-registry.js.map +1 -1
  39. package/dist/codex-app-runner.js +75 -12
  40. package/dist/codex-app-runner.js.map +1 -1
  41. package/dist/core/command-handler.d.ts +11 -0
  42. package/dist/core/command-handler.d.ts.map +1 -1
  43. package/dist/core/command-handler.js +101 -7
  44. package/dist/core/command-handler.js.map +1 -1
  45. package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
  46. package/dist/core/dashboard-ipc-server.js +69 -11
  47. package/dist/core/dashboard-ipc-server.js.map +1 -1
  48. package/dist/core/doc-comment-prompt.d.ts +57 -0
  49. package/dist/core/doc-comment-prompt.d.ts.map +1 -1
  50. package/dist/core/doc-comment-prompt.js +147 -2
  51. package/dist/core/doc-comment-prompt.js.map +1 -1
  52. package/dist/core/inflight-input-tracker.d.ts +2 -0
  53. package/dist/core/inflight-input-tracker.d.ts.map +1 -1
  54. package/dist/core/inflight-input-tracker.js.map +1 -1
  55. package/dist/core/passthrough-commands.d.ts.map +1 -1
  56. package/dist/core/passthrough-commands.js +1 -1
  57. package/dist/core/passthrough-commands.js.map +1 -1
  58. package/dist/core/persistent-backend.d.ts +4 -0
  59. package/dist/core/persistent-backend.d.ts.map +1 -1
  60. package/dist/core/persistent-backend.js +6 -0
  61. package/dist/core/persistent-backend.js.map +1 -1
  62. package/dist/core/plugins/cli-generation.d.ts +3 -0
  63. package/dist/core/plugins/cli-generation.d.ts.map +1 -1
  64. package/dist/core/plugins/cli-generation.js +1 -0
  65. package/dist/core/plugins/cli-generation.js.map +1 -1
  66. package/dist/core/session-board.d.ts +2 -1
  67. package/dist/core/session-board.d.ts.map +1 -1
  68. package/dist/core/session-board.js +3 -2
  69. package/dist/core/session-board.js.map +1 -1
  70. package/dist/core/session-create.d.ts +38 -0
  71. package/dist/core/session-create.d.ts.map +1 -1
  72. package/dist/core/session-create.js +43 -9
  73. package/dist/core/session-create.js.map +1 -1
  74. package/dist/core/session-manager.d.ts +65 -2
  75. package/dist/core/session-manager.d.ts.map +1 -1
  76. package/dist/core/session-manager.js +285 -68
  77. package/dist/core/session-manager.js.map +1 -1
  78. package/dist/core/session-rename.d.ts +25 -0
  79. package/dist/core/session-rename.d.ts.map +1 -0
  80. package/dist/core/session-rename.js +43 -0
  81. package/dist/core/session-rename.js.map +1 -0
  82. package/dist/core/session-title.d.ts +11 -0
  83. package/dist/core/session-title.d.ts.map +1 -0
  84. package/dist/core/session-title.js +17 -0
  85. package/dist/core/session-title.js.map +1 -0
  86. package/dist/core/trigger-session.d.ts +11 -0
  87. package/dist/core/trigger-session.d.ts.map +1 -1
  88. package/dist/core/trigger-session.js +61 -20
  89. package/dist/core/trigger-session.js.map +1 -1
  90. package/dist/core/types.d.ts +16 -1
  91. package/dist/core/types.d.ts.map +1 -1
  92. package/dist/core/types.js.map +1 -1
  93. package/dist/core/worker-pool.d.ts +6 -2
  94. package/dist/core/worker-pool.d.ts.map +1 -1
  95. package/dist/core/worker-pool.js +48 -15
  96. package/dist/core/worker-pool.js.map +1 -1
  97. package/dist/core/zellij-adopt-discovery.d.ts +17 -0
  98. package/dist/core/zellij-adopt-discovery.d.ts.map +1 -1
  99. package/dist/core/zellij-adopt-discovery.js +24 -4
  100. package/dist/core/zellij-adopt-discovery.js.map +1 -1
  101. package/dist/core/zellij-session-discovery.d.ts +3 -0
  102. package/dist/core/zellij-session-discovery.d.ts.map +1 -1
  103. package/dist/core/zellij-session-discovery.js +1 -0
  104. package/dist/core/zellij-session-discovery.js.map +1 -1
  105. package/dist/daemon.d.ts +3 -0
  106. package/dist/daemon.d.ts.map +1 -1
  107. package/dist/daemon.js +181 -81
  108. package/dist/daemon.js.map +1 -1
  109. package/dist/dashboard/bot-payload.d.ts +1 -0
  110. package/dist/dashboard/bot-payload.d.ts.map +1 -1
  111. package/dist/dashboard/bot-payload.js +1 -0
  112. package/dist/dashboard/bot-payload.js.map +1 -1
  113. package/dist/dashboard/federated-group-core.d.ts +29 -8
  114. package/dist/dashboard/federated-group-core.d.ts.map +1 -1
  115. package/dist/dashboard/federated-group-core.js +68 -6
  116. package/dist/dashboard/federated-group-core.js.map +1 -1
  117. package/dist/dashboard/federation-api.d.ts +10 -9
  118. package/dist/dashboard/federation-api.d.ts.map +1 -1
  119. package/dist/dashboard/federation-api.js +82 -3
  120. package/dist/dashboard/federation-api.js.map +1 -1
  121. package/dist/dashboard/federation-spoke-api.d.ts +8 -9
  122. package/dist/dashboard/federation-spoke-api.d.ts.map +1 -1
  123. package/dist/dashboard/federation-spoke-api.js +7 -2
  124. package/dist/dashboard/federation-spoke-api.js.map +1 -1
  125. package/dist/dashboard/team-group.d.ts +21 -0
  126. package/dist/dashboard/team-group.d.ts.map +1 -1
  127. package/dist/dashboard/team-group.js +16 -0
  128. package/dist/dashboard/team-group.js.map +1 -1
  129. package/dist/dashboard/web/bot-defaults-page.d.ts +10 -0
  130. package/dist/dashboard/web/bot-defaults-page.d.ts.map +1 -1
  131. package/dist/dashboard/web/bot-defaults-page.js +33 -1
  132. package/dist/dashboard/web/bot-defaults-page.js.map +1 -1
  133. package/dist/dashboard/web/bot-defaults.d.ts +1 -0
  134. package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
  135. package/dist/dashboard/web/bot-defaults.js.map +1 -1
  136. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  137. package/dist/dashboard/web/i18n.js +8 -0
  138. package/dist/dashboard/web/i18n.js.map +1 -1
  139. package/dist/dashboard/web/sessions.d.ts.map +1 -1
  140. package/dist/dashboard/web/sessions.js +5 -1
  141. package/dist/dashboard/web/sessions.js.map +1 -1
  142. package/dist/dashboard/web/ui.d.ts.map +1 -1
  143. package/dist/dashboard/web/ui.js +2 -4
  144. package/dist/dashboard/web/ui.js.map +1 -1
  145. package/dist/dashboard-web/app.js +1 -1
  146. package/dist/dashboard-web/chunks/bot-defaults-page-7T7DPOV6.js +4 -0
  147. package/dist/dashboard-web/chunks/{chunk-SHZ5PUYF.js → chunk-IGXYYRYJ.js} +1 -1
  148. package/dist/dashboard-web/chunks/{chunk-GF7DU2B5.js → chunk-ISOD7VDX.js} +1 -1
  149. package/dist/dashboard-web/chunks/{chunk-TORH53FI.js → chunk-OEJIT66L.js} +1 -1
  150. package/dist/dashboard-web/chunks/{chunk-7UY333A5.js → chunk-PNK3HQBI.js} +2 -2
  151. package/dist/dashboard-web/chunks/{chunk-IAES47WV.js → chunk-YQFXGRSD.js} +1 -1
  152. package/dist/dashboard-web/chunks/{connectors-page-N6GKR6JL.js → connectors-page-UQ4QBE45.js} +1 -1
  153. package/dist/dashboard-web/chunks/{groups-page-IK3GXSQU.js → groups-page-5AJPPJH2.js} +1 -1
  154. package/dist/dashboard-web/chunks/{insights-page-T2PDFPGO.js → insights-page-R3DFECX4.js} +1 -1
  155. package/dist/dashboard-web/chunks/{legacy-workflow-page-YQPLKH74.js → legacy-workflow-page-QMC7OZXH.js} +1 -1
  156. package/dist/dashboard-web/chunks/{monitor-room-7EL3LVJS.js → monitor-room-NLMKJLDY.js} +1 -1
  157. package/dist/dashboard-web/chunks/{monitoring-page-BCADF56L.js → monitoring-page-EHSOX4GV.js} +1 -1
  158. package/dist/dashboard-web/chunks/{office-page-KP7BDLAE.js → office-page-OXJKFJ4R.js} +1 -1
  159. package/dist/dashboard-web/chunks/{overview-page-6GWDHDTI.js → overview-page-GJHIVXJF.js} +1 -1
  160. package/dist/dashboard-web/chunks/{roles-page-2OFR2IBG.js → roles-page-6ZODQISW.js} +1 -1
  161. package/dist/dashboard-web/chunks/{schedules-page-TAWMTSNL.js → schedules-page-DAGMHVCH.js} +1 -1
  162. package/dist/dashboard-web/chunks/{sessions-page-42QZHPPP.js → sessions-page-NNZIDHZM.js} +1 -1
  163. package/dist/dashboard-web/chunks/{settings-page-OVKXUXSV.js → settings-page-LJIXX5LQ.js} +1 -1
  164. package/dist/dashboard-web/chunks/{skills-page-Y2EC3NBI.js → skills-page-J7EVUHXP.js} +1 -1
  165. package/dist/dashboard-web/chunks/{team-federation-page-QYMJUH5F.js → team-federation-page-YM5JBIII.js} +1 -1
  166. package/dist/dashboard-web/chunks/{v3-page-FZJCW5QG.js → v3-page-YLYVHGKD.js} +1 -1
  167. package/dist/dashboard-web/chunks/{whiteboards-page-FBHBBCAL.js → whiteboards-page-75N2U6W4.js} +1 -1
  168. package/dist/dashboard.js +46 -7
  169. package/dist/dashboard.js.map +1 -1
  170. package/dist/i18n/en.d.ts.map +1 -1
  171. package/dist/i18n/en.js +8 -0
  172. package/dist/i18n/en.js.map +1 -1
  173. package/dist/i18n/zh.d.ts.map +1 -1
  174. package/dist/i18n/zh.js +8 -0
  175. package/dist/i18n/zh.js.map +1 -1
  176. package/dist/im/lark/card-handler.d.ts.map +1 -1
  177. package/dist/im/lark/card-handler.js +96 -32
  178. package/dist/im/lark/card-handler.js.map +1 -1
  179. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  180. package/dist/im/lark/event-dispatcher.js +16 -5
  181. package/dist/im/lark/event-dispatcher.js.map +1 -1
  182. package/dist/services/bot-config-store.d.ts.map +1 -1
  183. package/dist/services/bot-config-store.js +1 -0
  184. package/dist/services/bot-config-store.js.map +1 -1
  185. package/dist/services/card-prefs-store.d.ts +4 -0
  186. package/dist/services/card-prefs-store.d.ts.map +1 -1
  187. package/dist/services/card-prefs-store.js +8 -0
  188. package/dist/services/card-prefs-store.js.map +1 -1
  189. package/dist/services/default-worktree.d.ts.map +1 -1
  190. package/dist/services/default-worktree.js +4 -1
  191. package/dist/services/default-worktree.js.map +1 -1
  192. package/dist/services/group-creator.d.ts +19 -0
  193. package/dist/services/group-creator.d.ts.map +1 -1
  194. package/dist/services/group-creator.js +62 -28
  195. package/dist/services/group-creator.js.map +1 -1
  196. package/dist/services/groups-store.d.ts +7 -5
  197. package/dist/services/groups-store.d.ts.map +1 -1
  198. package/dist/services/groups-store.js +10 -8
  199. package/dist/services/groups-store.js.map +1 -1
  200. package/dist/setup/ensure-tmux.d.ts +17 -0
  201. package/dist/setup/ensure-tmux.d.ts.map +1 -1
  202. package/dist/setup/ensure-tmux.js +72 -1
  203. package/dist/setup/ensure-tmux.js.map +1 -1
  204. package/dist/types.d.ts +56 -6
  205. package/dist/types.d.ts.map +1 -1
  206. package/dist/utils/child-env.d.ts +27 -0
  207. package/dist/utils/child-env.d.ts.map +1 -1
  208. package/dist/utils/child-env.js +59 -0
  209. package/dist/utils/child-env.js.map +1 -1
  210. package/dist/utils/codex-app-context.d.ts +9 -0
  211. package/dist/utils/codex-app-context.d.ts.map +1 -0
  212. package/dist/utils/codex-app-context.js +47 -0
  213. package/dist/utils/codex-app-context.js.map +1 -0
  214. package/dist/utils/pending-input-queue.d.ts +2 -0
  215. package/dist/utils/pending-input-queue.d.ts.map +1 -1
  216. package/dist/utils/pending-input-queue.js +6 -0
  217. package/dist/utils/pending-input-queue.js.map +1 -1
  218. package/dist/worker.js +197 -38
  219. package/dist/worker.js.map +1 -1
  220. package/package.json +1 -1
  221. package/dist/dashboard-web/chunks/bot-defaults-page-QXZ2VFJJ.js +0 -4
package/dist/worker.js CHANGED
@@ -98,6 +98,7 @@ import * as pty from 'node-pty';
98
98
  import { createHash } from 'node:crypto';
99
99
  import { installHook } from './adapters/hook-installer.js';
100
100
  import { hookCommandFor } from './adapters/hook-command.js';
101
+ import { withCodexAppContext } from './utils/codex-app-context.js';
101
102
  // ─── State ───────────────────────────────────────────────────────────────────
102
103
  let cliAdapter = null;
103
104
  let backend = null;
@@ -154,6 +155,9 @@ function refreshCliPluginGeneration(cfg, adapter) {
154
155
  throw new Error(t('worker.skill_delivery_failed', { reason }));
155
156
  }
156
157
  cfg.prompt = generation.prompt;
158
+ if (cfg.promptCodexAppInput && generation.skillCatalog) {
159
+ cfg.promptCodexAppInput = withCodexAppContext(cfg.promptCodexAppInput, 'botmux_plugin_skills', generation.skillCatalog, 'application');
160
+ }
157
161
  cfg.skillPluginDir = generation.skillPluginDir;
158
162
  cfg.skillReadonlyRoots = generation.skillReadonlyRoots;
159
163
  deferredPluginSkillCatalog = generation.deferredSkillCatalog ?? null;
@@ -554,6 +558,26 @@ async function sendRawCommandLine(be, content) {
554
558
  be.write('\r');
555
559
  }
556
560
  }
561
+ /** Serialize only the literal command-line write window (text -> beat -> Enter).
562
+ * raw_input deliberately keeps its legacy "send while busy" behaviour; this
563
+ * narrow mutex merely prevents concurrent IPC handlers (or native /rename)
564
+ * from splicing keystrokes into one another. */
565
+ let commandLineWriteTail = Promise.resolve();
566
+ let commandLineWritesPending = 0;
567
+ async function sendRawCommandLineSerially(be, content) {
568
+ const previous = commandLineWriteTail;
569
+ let release;
570
+ commandLineWriteTail = new Promise(resolve => { release = resolve; });
571
+ commandLineWritesPending += 1;
572
+ await previous;
573
+ try {
574
+ await sendRawCommandLine(be, content);
575
+ }
576
+ finally {
577
+ commandLineWritesPending -= 1;
578
+ release();
579
+ }
580
+ }
557
581
  /** Resolve once the PTY has been quiet for `quietMs`, or after `capMs` total.
558
582
  * Spaces out startup commands so each is processed before the next is typed. */
559
583
  function awaitPtyQuiescence(quietMs, capMs) {
@@ -597,7 +621,7 @@ async function runStartupCommands() {
597
621
  if (!backend)
598
622
  break;
599
623
  try {
600
- await sendRawCommandLine(backend, cmd);
624
+ await sendRawCommandLineSerially(backend, cmd);
601
625
  await awaitPtyQuiescence(STARTUP_CMD_QUIET_MS, STARTUP_CMD_CAP_MS);
602
626
  log(`Startup command sent: ${cmd}`);
603
627
  }
@@ -610,6 +634,85 @@ async function runStartupCommands() {
610
634
  idleDetector?.reset();
611
635
  }
612
636
  const pendingMessages = [];
637
+ /** Literal commands that arrived while native /rename owned the TUI. Normal
638
+ * raw_input commands are still delivered immediately (including while busy). */
639
+ const pendingRawInputs = [];
640
+ /** Latest requested canonical session title. Unlike a normal prompt this is an
641
+ * administrative TUI command: never type-ahead while the agent is busy, never
642
+ * open a model turn, and latest-wins if several renames arrive before idle. */
643
+ let pendingSessionRename = null;
644
+ /** True after the rename command's Enter lands and until the TUI returns to its
645
+ * prompt. Blocks type-ahead user messages from racing into the command UI. */
646
+ let sessionRenameInFlight = false;
647
+ const SESSION_RENAME_IDLE_TIMEOUT_MS = 5_000;
648
+ let sessionRenameIdleTimer = null;
649
+ function clearSessionRenameInFlight() {
650
+ if (sessionRenameIdleTimer) {
651
+ clearTimeout(sessionRenameIdleTimer);
652
+ sessionRenameIdleTimer = null;
653
+ }
654
+ sessionRenameInFlight = false;
655
+ }
656
+ /** Fail open if a CLI executes /rename without emitting enough redraw output
657
+ * for IdleDetector to rediscover the prompt. The verified Codex/Claude paths
658
+ * normally settle immediately; this cap prevents one administrative command
659
+ * from wedging all later Lark messages forever. */
660
+ function armSessionRenameIdleTimeout() {
661
+ if (!sessionRenameInFlight)
662
+ return;
663
+ if (sessionRenameIdleTimer)
664
+ clearTimeout(sessionRenameIdleTimer);
665
+ sessionRenameIdleTimer = setTimeout(() => {
666
+ sessionRenameIdleTimer = null;
667
+ if (!sessionRenameInFlight)
668
+ return;
669
+ sessionRenameInFlight = false;
670
+ // The timeout specifically means prompt redraw detection failed. Fail open
671
+ // by restoring the gate explicitly; otherwise a deferred raw_input is the
672
+ // only queued work and flushPending would keep waiting for the very signal
673
+ // this timer exists to replace.
674
+ isPromptReady = true;
675
+ log(`Native session rename idle timeout after ${SESSION_RENAME_IDLE_TIMEOUT_MS}ms; releasing queued input`);
676
+ void flushPending();
677
+ }, SESSION_RENAME_IDLE_TIMEOUT_MS);
678
+ sessionRenameIdleTimer.unref?.();
679
+ }
680
+ /** Deliver passthrough exactly as before: it may be injected while the CLI is
681
+ * busy (notably Codex /btw). Only the short keystroke sequence is serialized.
682
+ * Commands received while /rename owns the TUI are deferred by the IPC handler
683
+ * and come through this same function after the prompt returns. */
684
+ async function deliverRawInput(msg) {
685
+ renderer?.markNewTurn();
686
+ usageLimitTracker.beginTurn(currentUsageLimitSnapshot());
687
+ if (tmuxScrolledHalfPages > 0)
688
+ exitTmuxScrollMode();
689
+ const targetBackend = backend;
690
+ if (!targetBackend)
691
+ return;
692
+ let sent = false;
693
+ try {
694
+ await sendRawCommandLineSerially(targetBackend, msg.content);
695
+ sent = true;
696
+ isPromptReady = false;
697
+ idleDetector?.reset();
698
+ log(`Passthrough slash command: ${msg.content}`);
699
+ }
700
+ catch (err) {
701
+ // Do not send another queued command against a backend whose write failed.
702
+ isPromptReady = false;
703
+ log(`Passthrough slash command failed (${msg.content}): ${err?.message ?? err}`);
704
+ }
705
+ // Follow-up rides on the same IPC and is enqueued only after this command's
706
+ // Enter lands. sendToPty also observes commandLineWritesPending, so another
707
+ // raw command's text -> Enter window cannot be interrupted by the follow-up.
708
+ if (sent && msg.followUpContent) {
709
+ sendToPty(msg.followUpContent, undefined, msg.followUpCodexAppInput);
710
+ log(`Enqueued follow-up after raw input (${msg.followUpContent.length} chars)`);
711
+ }
712
+ // A pending /rename may have been held by the command-write mutex. It still
713
+ // waits for a genuine prompt because isPromptReady was cleared above.
714
+ void flushPending();
715
+ }
613
716
  /** Inputs written to the CLI whose turn hasn't completed — re-queued across a
614
717
  * CLI crash so a submit-time death can't silently eat user messages. */
615
718
  const inflightInputs = new InflightInputTracker();
@@ -3500,6 +3603,7 @@ function markPromptReady() {
3500
3603
  return;
3501
3604
  }
3502
3605
  isPromptReady = true;
3606
+ clearSessionRenameInFlight();
3503
3607
  // CLI 实际启动成功(回到 prompt):复位连续重启计数。
3504
3608
  // 任何能到这一步的 spawn 都算"成功"——后续即便再崩溃(不是 resume 目标不存在
3505
3609
  // 的问题),下一轮也该有新的 2 次重试预算,而不是被历史重启计数卡住。
@@ -3522,7 +3626,7 @@ function markPromptReady() {
3522
3626
  // make the CLI busy, so the idle state is transient and shouldn't appear
3523
3627
  // in the card. This avoids a false "就绪" flash on daemon restart
3524
3628
  // (where the initial prompt is queued before the CLI becomes idle).
3525
- if (renderer && pendingMessages.length === 0 && !isFlushing) {
3629
+ if (renderer && pendingMessages.length === 0 && pendingRawInputs.length === 0 && pendingSessionRename === null && !isFlushing) {
3526
3630
  const { content } = renderer.snapshot();
3527
3631
  send({ type: 'screen_update', content, ...usageLimitTracker.classify(content, 'idle'), turnId: currentBotmuxTurnId });
3528
3632
  }
@@ -3766,8 +3870,12 @@ async function flushPending() {
3766
3870
  return; // while loop in active flush will pick up new messages
3767
3871
  if (!backend || !cliAdapter)
3768
3872
  return;
3769
- if (pendingMessages.length === 0)
3873
+ if (pendingMessages.length === 0 && pendingRawInputs.length === 0 && pendingSessionRename === null)
3770
3874
  return; // nothing to flush — keep isPromptReady
3875
+ if (sessionRenameInFlight)
3876
+ return; // wait for /rename to finish before any user input
3877
+ if (commandLineWritesPending > 0)
3878
+ return; // do not splice into text -> Enter
3771
3879
  if (bareShellLaunchBlocked)
3772
3880
  return; // launch failed into a bare shell — don't type prompts into it
3773
3881
  // Ready-gate: hold the FIRST prompt until the SessionStart hook fires a true-
@@ -3811,6 +3919,21 @@ async function flushPending() {
3811
3919
  const claudeBridgeActive = !!bridgeJsonlPath && !lastInitConfig?.adoptMode;
3812
3920
  const codexBridgeActive = codexBridgeFallbackActive();
3813
3921
  const typeAheadAllowed = cliAdapter.supportsTypeAhead;
3922
+ // Native /rename is an administrative command, not a steer/queued model
3923
+ // message. It must wait for a real prompt even on type-ahead CLIs. Normal
3924
+ // pending messages can still drain while busy; the rename stays queued.
3925
+ const sessionRenameReady = isPromptReady && pendingSessionRename !== null;
3926
+ const rawInputReady = isPromptReady && pendingRawInputs.length > 0;
3927
+ let supportedSessionRenameReady = sessionRenameReady;
3928
+ if (sessionRenameReady && (!cliAdapter.buildSessionRenameCommand || effectiveBackendType === 'riff')) {
3929
+ pendingSessionRename = null;
3930
+ supportedSessionRenameReady = false;
3931
+ log(`Ignoring native session rename — unsupported by ${cliName()}${effectiveBackendType === 'riff' ? ' on riff backend' : ''}`);
3932
+ if (pendingMessages.length === 0 && pendingRawInputs.length === 0)
3933
+ return;
3934
+ }
3935
+ if (!isPromptReady && pendingMessages.length === 0)
3936
+ return;
3814
3937
  if (!isPromptReady && !typeAheadAllowed)
3815
3938
  return;
3816
3939
  isFlushing = true;
@@ -3844,6 +3967,38 @@ async function flushPending() {
3844
3967
  hasRunStartupCommands = true;
3845
3968
  await runStartupCommands();
3846
3969
  }
3970
+ // Commands deferred behind a previous rename run before the latest pending
3971
+ // rename. Some passthroughs (/clear, /new) can rotate the native session;
3972
+ // applying the canonical title last keeps the resume-picker label aligned.
3973
+ if (rawInputReady && pendingRawInputs.length > 0 && backend) {
3974
+ const raw = pendingRawInputs.shift();
3975
+ await deliverRawInput(raw);
3976
+ return;
3977
+ }
3978
+ if (supportedSessionRenameReady && pendingSessionRename !== null && backend && cliAdapter) {
3979
+ const title = pendingSessionRename;
3980
+ const buildRename = cliAdapter.buildSessionRenameCommand;
3981
+ pendingSessionRename = null;
3982
+ sessionRenameInFlight = true;
3983
+ try {
3984
+ await sendRawCommandLineSerially(backend, buildRename(title));
3985
+ armSessionRenameIdleTimeout();
3986
+ idleDetector?.reset();
3987
+ log(`Native session rename command sent (${cliName()}): ${title}`);
3988
+ }
3989
+ catch (err) {
3990
+ // Local title persistence is authoritative; native sync is best-effort.
3991
+ // Do not blindly replay a partially typed command after a backend error.
3992
+ // Keep the rename gate for the same bounded fail-open window: the write
3993
+ // may have stopped after typing only part of the command, so immediately
3994
+ // appending a deferred raw_input could corrupt both commands.
3995
+ armSessionRenameIdleTimeout();
3996
+ log(`Native session rename command failed (${cliName()}): ${err?.message ?? err}; waiting for prompt or fail-open timeout`);
3997
+ }
3998
+ // Wait for the command to finish and the TUI to become idle again before
3999
+ // sending queued user prompts; otherwise they can land in its picker.
4000
+ return;
4001
+ }
3847
4002
  while (pendingMessages.length > 0 && backend && cliAdapter) {
3848
4003
  const item = pendingMessages.shift();
3849
4004
  // Track as in-flight until the CLI returns to idle (markPromptReady).
@@ -3884,7 +4039,9 @@ async function flushPending() {
3884
4039
  // worker — exactly the failure mode this change is closing. Contain it.
3885
4040
  let result;
3886
4041
  try {
3887
- result = await cliAdapter.writeInput(backend, msg);
4042
+ result = item.codexAppInput && cliAdapter.writeStructuredInput
4043
+ ? await cliAdapter.writeStructuredInput(backend, msg, item.codexAppInput)
4044
+ : await cliAdapter.writeInput(backend, msg);
3888
4045
  scheduleBusyPatternIdleProbe(`${cliName()} post-submit`);
3889
4046
  }
3890
4047
  catch (err) {
@@ -3929,10 +4086,10 @@ async function flushPending() {
3929
4086
  isFlushing = false;
3930
4087
  }
3931
4088
  }
3932
- function sendToPty(content, turnId) {
4089
+ function sendToPty(content, turnId, codexAppInput) {
3933
4090
  if (!backend || !cliAdapter)
3934
4091
  return;
3935
- const next = { content, turnId };
4092
+ const next = { content, turnId, codexAppInput };
3936
4093
  const shouldMergeQueued = !isFlushing && !shouldWriteNow({
3937
4094
  isPromptReady,
3938
4095
  isFlushing,
@@ -3969,7 +4126,7 @@ function sendToPty(content, turnId) {
3969
4126
  // type-ahead write is dropped (no input box yet) — markPromptReady()'s flush
3970
4127
  // delivers queued messages instead. See input-gate.ts; this fixes dispatch's
3971
4128
  // brief reaching Codex before its first idle and never landing.
3972
- if (shouldWriteNow({ isPromptReady, isFlushing, supportsTypeAhead: cliAdapter.supportsTypeAhead === true, awaitingFirstPrompt })) {
4129
+ if (!sessionRenameInFlight && commandLineWritesPending === 0 && shouldWriteNow({ isPromptReady, isFlushing, supportsTypeAhead: cliAdapter.supportsTypeAhead === true, awaitingFirstPrompt })) {
3973
4130
  if (!mergedQueued)
3974
4131
  log(`Writing to PTY: "${content.substring(0, 80)}"`);
3975
4132
  flushPending(); // fire-and-forget async; no-op if already flushing
@@ -4345,6 +4502,7 @@ function scheduleBusyPatternIdleProbe(source) {
4345
4502
  busyPatternIdleProbeTimer.unref?.();
4346
4503
  }
4347
4504
  function spawnCli(cfg) {
4505
+ clearSessionRenameInFlight();
4348
4506
  // (startupCommands one-shot is re-armed below, AFTER the reattach-vs-fresh
4349
4507
  // prediction — only a genuinely fresh CLI process replays them; see
4350
4508
  // willReattachPersistent.)
@@ -5850,7 +6008,11 @@ function killCli() {
5850
6008
  sandboxCleanup = null;
5851
6009
  }
5852
6010
  isPromptReady = false;
6011
+ clearSessionRenameInFlight();
5853
6012
  pendingMessages.length = 0;
6013
+ // pendingRawInputs contains only commands that were accepted but never typed
6014
+ // because /rename owned the TUI. Preserve them across restart; unlike an
6015
+ // in-flight raw command, replaying these cannot duplicate a side effect.
5854
6016
  scrollback = '';
5855
6017
  altBufferActive = false;
5856
6018
  trustHandled = false;
@@ -6960,7 +7122,11 @@ process.on('message', async (raw) => {
6960
7122
  codexBridgeMarkPendingTurn(msg.prompt, msg.turnId);
6961
7123
  }
6962
7124
  if (msg.prompt && (!cliAdapter?.passesInitialPromptViaArgs || deferInitialPrompt)) {
6963
- pendingMessages.push({ content: msg.prompt, turnId: msg.turnId });
7125
+ pendingMessages.push({
7126
+ content: msg.prompt,
7127
+ turnId: msg.turnId,
7128
+ codexAppInput: msg.promptCodexAppInput,
7129
+ });
6964
7130
  }
6965
7131
  // Riff (remote HTTP backends): spawnCli already marked the prompt ready
6966
7132
  // (no local boot → isPromptReady=true immediately), but the initial prompt
@@ -6986,8 +7152,12 @@ process.on('message', async (raw) => {
6986
7152
  if (tmuxScrolledHalfPages > 0)
6987
7153
  exitTmuxScrollMode();
6988
7154
  let content = msg.content;
7155
+ let codexAppInput = msg.codexAppInput;
6989
7156
  if (deferredPluginSkillCatalog && !lastInitConfig?.adoptMode) {
6990
7157
  content = `${content}\n\n${deferredPluginSkillCatalog}`;
7158
+ if (codexAppInput) {
7159
+ codexAppInput = withCodexAppContext(codexAppInput, 'botmux_plugin_skills', deferredPluginSkillCatalog, 'application');
7160
+ }
6991
7161
  deferredPluginSkillCatalog = null;
6992
7162
  log('Attached refreshed plugin Skill catalog to the first turn of this CLI generation');
6993
7163
  }
@@ -7103,43 +7273,32 @@ process.on('message', async (raw) => {
7103
7273
  // arrival. Marking now would race with a still-running previous
7104
7274
  // turn whose `botmux send` could sneak its sentAtMs past this
7105
7275
  // turn's markTimeMs and falsely suppress its fallback.
7106
- sendToPty(content, msg.turnId);
7276
+ sendToPty(content, msg.turnId, codexAppInput);
7107
7277
  }
7108
7278
  break;
7109
7279
  }
7110
7280
  case 'raw_input': {
7111
- // Slash-command passthrough (e.g. /compact, /model, /usage). Write the
7112
- // literal string + Enter without bracketed paste otherwise Claude Code
7113
- // treats `/…` as pasted prompt text and the slash-command parser never
7114
- // fires. Also skip adapter.writeInput() / pendingMessages queueing so
7115
- // the prompt wrapping (Session ID, mention hints) is not prepended.
7116
- renderer?.markNewTurn();
7117
- usageLimitTracker.beginTurn(currentUsageLimitSnapshot());
7118
- if (tmuxScrolledHalfPages > 0)
7119
- exitTmuxScrollMode();
7120
- if (backend) {
7121
- // sendRawCommandLine: literal text → 200ms beat (so the CLI's slash-
7122
- // command picker registers the match before submit; without it Codex /
7123
- // other Ink TUIs fire Enter while the picker is still building, dismiss
7124
- // the match, and submit the literal `/clear` as a regular user prompt —
7125
- // visible to the user as "/clear + 换行" stuck in history; the 200ms
7126
- // mirrors the codex adapter's own writeInput paste-detection delay) →
7127
- // Enter. Shared with runStartupCommands so both stay in lockstep.
7128
- await sendRawCommandLine(backend, msg.content);
7129
- isPromptReady = false;
7130
- idleDetector?.reset();
7131
- log(`Passthrough slash command: ${msg.content}`);
7132
- // Follow-up rides on the SAME IPC (see DaemonToWorker.raw_input) so it
7133
- // cannot race the 200ms text→Enter window above. Enqueue only after the
7134
- // Enter landed: sendToPty queues it as the next turn (type-ahead /
7135
- // pendingMessages), exactly like a Lark message arriving while busy.
7136
- if (msg.followUpContent) {
7137
- sendToPty(msg.followUpContent);
7138
- log(`Enqueued follow-up after raw input (${msg.followUpContent.length} chars)`);
7139
- }
7281
+ // Preserve legacy busy delivery (/btw and other steering commands). A
7282
+ // native /rename already submitted to the TUI is the sole exception: its
7283
+ // command UI must return to the prompt before another slash command lands.
7284
+ if (sessionRenameInFlight) {
7285
+ pendingRawInputs.push(msg);
7286
+ log(`Deferred passthrough slash command until native rename settles: ${msg.content}`);
7287
+ }
7288
+ else {
7289
+ await deliverRawInput(msg);
7140
7290
  }
7141
7291
  break;
7142
7292
  }
7293
+ case 'rename_session': {
7294
+ // IPC handlers are concurrent with async init, so queue first even when
7295
+ // the adapter/backend has not finished initializing. flushPending will
7296
+ // capability-check and deliver the latest title once a real prompt is idle.
7297
+ pendingSessionRename = msg.title;
7298
+ log(`Queued native session rename: ${msg.title}`);
7299
+ void flushPending();
7300
+ break;
7301
+ }
7143
7302
  case 'park_diagnostic': {
7144
7303
  // The daemon gave up auto-restarting (crash loop) and wants the last
7145
7304
  // terminal output preserved. Park the diagnostic shell now — deferred from