aicodeman 1.28.2 → 1.29.1

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 (202) hide show
  1. package/README.md +58 -31
  2. package/README.zh-CN.md +207 -51
  3. package/dist/attachment-registry.d.ts +20 -0
  4. package/dist/attachment-registry.d.ts.map +1 -1
  5. package/dist/attachment-registry.js +75 -10
  6. package/dist/attachment-registry.js.map +1 -1
  7. package/dist/config/cli-registry/schema.d.ts +25 -0
  8. package/dist/config/cli-registry/schema.d.ts.map +1 -1
  9. package/dist/config/cli-registry/schema.js +43 -0
  10. package/dist/config/cli-registry/schema.js.map +1 -1
  11. package/dist/config/cli-registry/stock.d.ts.map +1 -1
  12. package/dist/config/cli-registry/stock.js +157 -2
  13. package/dist/config/cli-registry/stock.js.map +1 -1
  14. package/dist/config/cli-registry/types.d.ts +60 -0
  15. package/dist/config/cli-registry/types.d.ts.map +1 -1
  16. package/dist/custom-model-hosts.d.ts +39 -0
  17. package/dist/custom-model-hosts.d.ts.map +1 -0
  18. package/dist/custom-model-hosts.js +35 -0
  19. package/dist/custom-model-hosts.js.map +1 -0
  20. package/dist/custom-model-injection-apply.d.ts +49 -0
  21. package/dist/custom-model-injection-apply.d.ts.map +1 -0
  22. package/dist/custom-model-injection-apply.js +77 -0
  23. package/dist/custom-model-injection-apply.js.map +1 -0
  24. package/dist/custom-model-injection.d.ts +79 -0
  25. package/dist/custom-model-injection.d.ts.map +1 -0
  26. package/dist/custom-model-injection.js +169 -0
  27. package/dist/custom-model-injection.js.map +1 -0
  28. package/dist/docker-hosts.d.ts +42 -0
  29. package/dist/docker-hosts.d.ts.map +1 -1
  30. package/dist/docker-hosts.js +18 -0
  31. package/dist/docker-hosts.js.map +1 -1
  32. package/dist/generated-artifact-attachments.d.ts +3 -0
  33. package/dist/generated-artifact-attachments.d.ts.map +1 -1
  34. package/dist/generated-artifact-attachments.js +18 -8
  35. package/dist/generated-artifact-attachments.js.map +1 -1
  36. package/dist/mux-interface.d.ts +7 -0
  37. package/dist/mux-interface.d.ts.map +1 -1
  38. package/dist/remote-files.d.ts +145 -0
  39. package/dist/remote-files.d.ts.map +1 -0
  40. package/dist/remote-files.js +347 -0
  41. package/dist/remote-files.js.map +1 -0
  42. package/dist/remote-hosts.d.ts +12 -0
  43. package/dist/remote-hosts.d.ts.map +1 -1
  44. package/dist/remote-hosts.js +7 -1
  45. package/dist/remote-hosts.js.map +1 -1
  46. package/dist/remote-ssh-limiter.d.ts +42 -0
  47. package/dist/remote-ssh-limiter.d.ts.map +1 -0
  48. package/dist/remote-ssh-limiter.js +84 -0
  49. package/dist/remote-ssh-limiter.js.map +1 -0
  50. package/dist/session-auto-name.d.ts +74 -0
  51. package/dist/session-auto-name.d.ts.map +1 -0
  52. package/dist/session-auto-name.js +339 -0
  53. package/dist/session-auto-name.js.map +1 -0
  54. package/dist/session-manager.d.ts.map +1 -1
  55. package/dist/session-manager.js +8 -1
  56. package/dist/session-manager.js.map +1 -1
  57. package/dist/session.d.ts +123 -5
  58. package/dist/session.d.ts.map +1 -1
  59. package/dist/session.js +237 -9
  60. package/dist/session.js.map +1 -1
  61. package/dist/tmux-manager.d.ts.map +1 -1
  62. package/dist/tmux-manager.js +27 -13
  63. package/dist/tmux-manager.js.map +1 -1
  64. package/dist/types/api.d.ts +6 -0
  65. package/dist/types/api.d.ts.map +1 -1
  66. package/dist/types/api.js.map +1 -1
  67. package/dist/types/session.d.ts +48 -0
  68. package/dist/types/session.d.ts.map +1 -1
  69. package/dist/types/session.js.map +1 -1
  70. package/dist/web/middleware/auth.d.ts +11 -1
  71. package/dist/web/middleware/auth.d.ts.map +1 -1
  72. package/dist/web/middleware/auth.js +69 -1
  73. package/dist/web/middleware/auth.js.map +1 -1
  74. package/dist/web/public/admin-ui.js.gz +0 -0
  75. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  76. package/dist/web/public/{app.78bf0bf8.js → app.556be563.js} +7 -7
  77. package/dist/web/public/app.556be563.js.br +0 -0
  78. package/dist/web/public/app.556be563.js.gz +0 -0
  79. package/dist/web/public/approvals-ui.js.gz +0 -0
  80. package/dist/web/public/constants.258b140f.js.gz +0 -0
  81. package/dist/web/public/cron-ui.js.gz +0 -0
  82. package/dist/web/public/entrance-animations.js.gz +0 -0
  83. package/dist/web/public/home-sessions.js.gz +0 -0
  84. package/dist/web/public/i18n.5f897ed5.js +1 -0
  85. package/dist/web/public/i18n.5f897ed5.js.br +0 -0
  86. package/dist/web/public/i18n.5f897ed5.js.gz +0 -0
  87. package/dist/web/public/image-input.cd4b97c4.js.gz +0 -0
  88. package/dist/web/public/index.html +23 -9
  89. package/dist/web/public/index.html.br +0 -0
  90. package/dist/web/public/index.html.gz +0 -0
  91. package/dist/web/public/input-cjk.8bc46081.js +1 -0
  92. package/dist/web/public/input-cjk.8bc46081.js.br +0 -0
  93. package/dist/web/public/input-cjk.8bc46081.js.gz +0 -0
  94. package/dist/web/public/{keyboard-accessory.1d0dda9b.js → keyboard-accessory.2cf04f17.js} +38 -3
  95. package/dist/web/public/keyboard-accessory.2cf04f17.js.br +0 -0
  96. package/dist/web/public/keyboard-accessory.2cf04f17.js.gz +0 -0
  97. package/dist/web/public/mobile-handlers.6f354a87.js.gz +0 -0
  98. package/dist/web/public/mobile-overview.js.gz +0 -0
  99. package/dist/web/public/mobile.e9d0b53e.css.gz +0 -0
  100. package/dist/web/public/notification-manager.36ea4624.js.gz +0 -0
  101. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  102. package/dist/web/public/panels-ui.5b07ad14.js.gz +0 -0
  103. package/dist/web/public/ralph-panel.6de2d0f8.js.gz +0 -0
  104. package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
  105. package/dist/web/public/readmymind-ui.js.gz +0 -0
  106. package/dist/web/public/respawn-ui.ff0dae4c.js.gz +0 -0
  107. package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
  108. package/dist/web/public/session-lineage.js.gz +0 -0
  109. package/dist/web/public/{session-ui.5d0a7b15.js → session-ui.42b81477.js} +13 -13
  110. package/dist/web/public/session-ui.42b81477.js.br +0 -0
  111. package/dist/web/public/session-ui.42b81477.js.gz +0 -0
  112. package/dist/web/public/settings-ui.58f1d756.js +67 -0
  113. package/dist/web/public/settings-ui.58f1d756.js.br +0 -0
  114. package/dist/web/public/settings-ui.58f1d756.js.gz +0 -0
  115. package/dist/web/public/{styles.5d36726d.css → styles.6add175d.css} +1 -1
  116. package/dist/web/public/styles.6add175d.css.br +0 -0
  117. package/dist/web/public/{styles.5d36726d.css.gz → styles.6add175d.css.gz} +0 -0
  118. package/dist/web/public/subagent-windows.e6ca799f.js.gz +0 -0
  119. package/dist/web/public/sw.js.gz +0 -0
  120. package/dist/web/public/tab-rail-resize.42c24949.js.gz +0 -0
  121. package/dist/web/public/terminal-keycode229-recovery.6ea8fe37.js.gz +0 -0
  122. package/dist/web/public/terminal-ui.38c49244.js +2 -0
  123. package/dist/web/public/terminal-ui.38c49244.js.br +0 -0
  124. package/dist/web/public/terminal-ui.38c49244.js.gz +0 -0
  125. package/dist/web/public/ultracode-panel.js.gz +0 -0
  126. package/dist/web/public/ultracode-windows.js.gz +0 -0
  127. package/dist/web/public/upload.html.gz +0 -0
  128. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  129. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  130. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  131. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  132. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  133. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  134. package/dist/web/public/vendor/xterm-predictive-echo.bd6882b8.js.gz +0 -0
  135. package/dist/web/public/vendor/xterm-zerolag-input.6fee72f2.js.gz +0 -0
  136. package/dist/web/public/vendor/xterm.css.gz +0 -0
  137. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  138. package/dist/web/public/voice-input.c4b51eb6.js.gz +0 -0
  139. package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
  140. package/dist/web/public/webview-tabs.js +56 -4
  141. package/dist/web/public/webview-tabs.js.br +0 -0
  142. package/dist/web/public/webview-tabs.js.gz +0 -0
  143. package/dist/web/route-helpers.d.ts +20 -0
  144. package/dist/web/route-helpers.d.ts.map +1 -1
  145. package/dist/web/route-helpers.js +29 -2
  146. package/dist/web/route-helpers.js.map +1 -1
  147. package/dist/web/routes/case-routes.d.ts.map +1 -1
  148. package/dist/web/routes/case-routes.js +23 -6
  149. package/dist/web/routes/case-routes.js.map +1 -1
  150. package/dist/web/routes/custom-model-routes.d.ts +18 -0
  151. package/dist/web/routes/custom-model-routes.d.ts.map +1 -0
  152. package/dist/web/routes/custom-model-routes.js +136 -0
  153. package/dist/web/routes/custom-model-routes.js.map +1 -0
  154. package/dist/web/routes/file-routes.d.ts.map +1 -1
  155. package/dist/web/routes/file-routes.js +484 -100
  156. package/dist/web/routes/file-routes.js.map +1 -1
  157. package/dist/web/routes/index.d.ts +1 -0
  158. package/dist/web/routes/index.d.ts.map +1 -1
  159. package/dist/web/routes/index.js +1 -0
  160. package/dist/web/routes/index.js.map +1 -1
  161. package/dist/web/routes/session-routes.d.ts.map +1 -1
  162. package/dist/web/routes/session-routes.js +93 -7
  163. package/dist/web/routes/session-routes.js.map +1 -1
  164. package/dist/web/routes/ws-routes.d.ts.map +1 -1
  165. package/dist/web/routes/ws-routes.js +28 -3
  166. package/dist/web/routes/ws-routes.js.map +1 -1
  167. package/dist/web/schemas.d.ts +21 -0
  168. package/dist/web/schemas.d.ts.map +1 -1
  169. package/dist/web/schemas.js +38 -0
  170. package/dist/web/schemas.js.map +1 -1
  171. package/dist/web/server.d.ts +8 -0
  172. package/dist/web/server.d.ts.map +1 -1
  173. package/dist/web/server.js +70 -3
  174. package/dist/web/server.js.map +1 -1
  175. package/dist/web/session-listener-wiring.d.ts +6 -2
  176. package/dist/web/session-listener-wiring.d.ts.map +1 -1
  177. package/dist/web/session-listener-wiring.js +33 -2
  178. package/dist/web/session-listener-wiring.js.map +1 -1
  179. package/dist/web/webview-proxy.d.ts +41 -0
  180. package/dist/web/webview-proxy.d.ts.map +1 -1
  181. package/dist/web/webview-proxy.js +102 -1
  182. package/dist/web/webview-proxy.js.map +1 -1
  183. package/package.json +2 -2
  184. package/dist/web/public/app.78bf0bf8.js.br +0 -0
  185. package/dist/web/public/app.78bf0bf8.js.gz +0 -0
  186. package/dist/web/public/i18n.561ca09c.js +0 -1
  187. package/dist/web/public/i18n.561ca09c.js.br +0 -0
  188. package/dist/web/public/i18n.561ca09c.js.gz +0 -0
  189. package/dist/web/public/input-cjk.87f14251.js +0 -1
  190. package/dist/web/public/input-cjk.87f14251.js.br +0 -0
  191. package/dist/web/public/input-cjk.87f14251.js.gz +0 -0
  192. package/dist/web/public/keyboard-accessory.1d0dda9b.js.br +0 -0
  193. package/dist/web/public/keyboard-accessory.1d0dda9b.js.gz +0 -0
  194. package/dist/web/public/session-ui.5d0a7b15.js.br +0 -0
  195. package/dist/web/public/session-ui.5d0a7b15.js.gz +0 -0
  196. package/dist/web/public/settings-ui.e0c7f6b4.js +0 -67
  197. package/dist/web/public/settings-ui.e0c7f6b4.js.br +0 -0
  198. package/dist/web/public/settings-ui.e0c7f6b4.js.gz +0 -0
  199. package/dist/web/public/styles.5d36726d.css.br +0 -0
  200. package/dist/web/public/terminal-ui.0cbe8637.js +0 -2
  201. package/dist/web/public/terminal-ui.0cbe8637.js.br +0 -0
  202. package/dist/web/public/terminal-ui.0cbe8637.js.gz +0 -0
package/dist/session.js CHANGED
@@ -23,7 +23,7 @@
23
23
  * ralph-tracker (todo/completion parsing), bash-tool-parser (tool invocation tracking),
24
24
  * task-tracker (background tasks), mux-interface (tmux abstraction)
25
25
  * @consumedby session-manager, web/server, respawn-controller
26
- * @emits session:terminal, session:idle, session:working, session:completion, session:exit
26
+ * @emits session:terminal, session:idle, session:working, session:completion, session:promptSubmitted, session:exit
27
27
  *
28
28
  * @module session
29
29
  */
@@ -52,6 +52,7 @@ import { SessionAutoOps } from './session-auto-ops.js';
52
52
  import { detectUsageLimitPause } from './usage-limit-patterns.js';
53
53
  import { SessionTaskCache } from './session-task-cache.js';
54
54
  import { InteractivePtyExitBreaker } from './session-pty-exit-breaker.js';
55
+ import { isGeneratedSessionName, SubmittedPromptTracker } from './session-auto-name.js';
55
56
  import { parseTerminalAttachmentRequests } from './attachment-magic.js';
56
57
  import { sanitizeAttachmentHistory, upsertAttachmentHistory as upsertAttachmentHistoryList, } from './session-attachment-history.js';
57
58
  /** Line buffer flush interval (100ms) - forces processing of partial lines */
@@ -288,6 +289,15 @@ export class Session extends EventEmitter {
288
289
  // Task description cache (extracted to SessionTaskCache)
289
290
  _taskCache = new SessionTaskCache();
290
291
  _name;
292
+ _nameSource;
293
+ /**
294
+ * Reconstructs the composer draft from USER keystrokes so the first real
295
+ * prompt can name the tab. Fed only when a write says `fromUser`, and never
296
+ * for a CLI whose Enter runs a command rather than submitting a prompt
297
+ * (`startMode: 'shell'`), so a shell tab is not renamed after every `ls`.
298
+ */
299
+ _submittedPromptTracker = new SubmittedPromptTracker();
300
+ _acceptsPrompts;
291
301
  ptyProcess = null;
292
302
  _pid = null;
293
303
  _status = 'idle';
@@ -423,6 +433,18 @@ export class Session extends EventEmitter {
423
433
  // user can still switch in-session via /effort (incl. ultracode). Never carried as
424
434
  // the CLAUDE_CODE_EFFORT_LEVEL env var, which would hard-lock the session.
425
435
  _effort;
436
+ // Custom Model Endpoint Profiles (docs/custom-model-endpoints-plan.md). `envKeys`,
437
+ // `configDir` and `launchModel` are internal bookkeeping ONLY (never surfaced via
438
+ // toState()/the customModel getter): they are what setCustomModel() needs to undo a
439
+ // previous injection (remove exactly the env keys it added, delete a previous isolated
440
+ // config dir) without guessing what it once wrote. Persisted disk-only (`__customModel`).
441
+ _customModel;
442
+ // Env keys a retired custom-model selection injected that the NEXT respawn must
443
+ // `tmux setenv -u`. Deleting a key from `_envOverrides` alone does nothing to the
444
+ // tmux session, which keeps every `setenv` and hands it to `respawn-pane`, so the
445
+ // relaunched CLI would come back still pointed at the old endpoint (measured, see
446
+ // TmuxManager.applyEnvOverrides). Drained after a successful respawn.
447
+ _pendingEnvUnsets = new Set();
426
448
  // tmux history-limit (scrollback lines) allocated when this session's pane is created.
427
449
  _tmuxHistoryLimit;
428
450
  // Remote execution metadata, present when this session runs over SSH through local tmux.
@@ -465,6 +487,9 @@ export class Session extends EventEmitter {
465
487
  this.createdAt = config.createdAt || Date.now();
466
488
  this.mode = config.mode || 'claude';
467
489
  this._name = config.name || '';
490
+ this._nameSource =
491
+ config.nameSource ?? (!this._name || isGeneratedSessionName(this._name) ? 'placeholder' : 'manual');
492
+ this._acceptsPrompts = getCli(this.mode)?.capabilities.startMode !== 'shell';
468
493
  this._resumeSessionId = config.resumeSessionId;
469
494
  // NOW, not `createdAt`: recovery passes the ORIGINAL creation time of a
470
495
  // days-old tmux session, and seeding last-activity from it would report a
@@ -936,6 +961,62 @@ export class Session extends EventEmitter {
936
961
  this._color = color;
937
962
  }
938
963
  }
964
+ // Custom Model Endpoint Profiles (docs/custom-model-endpoints-plan.md) — public-safe
965
+ // subset only (never envKeys/configDir/launchModel, the bookkeeping for setCustomModel).
966
+ get customModel() {
967
+ if (!this._customModel)
968
+ return undefined;
969
+ const { endpointId, modelId, label } = this._customModel;
970
+ return { endpointId, modelId, label };
971
+ }
972
+ /**
973
+ * The full selection incl. bookkeeping, for state.json ONLY (`__customModel`, the
974
+ * same disk-only convention as `getEnvOverridesForPersist()`). Carries no env values,
975
+ * so nothing secret lands on disk; recovery re-derives them from the endpoint store.
976
+ * Without this a Codeman restart left the pane on the custom endpoint (tmux keeps
977
+ * its `setenv`s) while `customModel` came back undefined, so the state was wrong and
978
+ * clearing had nothing to unset. Must NOT be included in any API-bound serializer.
979
+ */
980
+ getCustomModelForPersist() {
981
+ return this._customModel ? { ...this._customModel, envKeys: [...this._customModel.envKeys] } : undefined;
982
+ }
983
+ /**
984
+ * Update this session's custom-model selection and merge the endpoint's injected env
985
+ * vars into `_envOverrides` — first UNDOING whatever the previous selection injected
986
+ * (removing exactly those env keys), so switching endpoints, or clearing back to the
987
+ * harness's native cloud default, never leaves a stale key behind. Synchronous and
988
+ * side-effect-free beyond mutating state, matching `setNice`/`setColor` above — this
989
+ * class does no file IO, so it reports the PREVIOUS `configDir` (if any) for the
990
+ * caller to clean up on disk (custom-model-injection.ts's configDir kind).
991
+ *
992
+ * Keys the previous selection injected that the new one does not re-set are queued
993
+ * for `tmux setenv -u` on the next respawn (`_pendingEnvUnsets`, threaded through
994
+ * `_buildRespawnPaneOptions().unsetEnvKeys`): the tmux session inherits every
995
+ * `setenv` into `respawn-pane`, so dropping them from the map alone would relaunch
996
+ * the CLI still pointed at the old endpoint — and for the `configDir` kinds, at a
997
+ * `HOME`/`CODEX_HOME`/`GROK_HOME` the caller has just deleted.
998
+ */
999
+ setCustomModel(next, envOverrides) {
1000
+ const previousConfigDir = this._customModel?.configDir;
1001
+ const removedEnvKeys = [];
1002
+ if (this._customModel) {
1003
+ for (const key of this._customModel.envKeys) {
1004
+ if (this._envOverrides)
1005
+ delete this._envOverrides[key];
1006
+ removedEnvKeys.push(key);
1007
+ this._pendingEnvUnsets.add(key);
1008
+ }
1009
+ }
1010
+ this._customModel = next ? { ...next, envKeys: [...next.envKeys] } : undefined;
1011
+ if (envOverrides && Object.keys(envOverrides).length > 0) {
1012
+ this._envOverrides = { ...(this._envOverrides ?? {}), ...envOverrides };
1013
+ // A key the new selection sets again does not need an unset (applyEnvOverrides
1014
+ // would set it right back anyway); keep the list to what actually goes away.
1015
+ for (const key of Object.keys(envOverrides))
1016
+ this._pendingEnvUnsets.delete(key);
1017
+ }
1018
+ return { removedEnvKeys, previousConfigDir };
1019
+ }
939
1020
  // Token tracking getters and setters
940
1021
  get totalTokens() {
941
1022
  return this._totalInputTokens + this._totalOutputTokens;
@@ -979,8 +1060,32 @@ export class Session extends EventEmitter {
979
1060
  get name() {
980
1061
  return this._name;
981
1062
  }
1063
+ /** An explicit rename: the name is the user's from here on and auto-naming never touches it. */
982
1064
  set name(value) {
983
1065
  this._name = value;
1066
+ this._nameSource = 'manual';
1067
+ }
1068
+ /**
1069
+ * Names the tab after its first prompt. Only a placeholder is eligible, and
1070
+ * the session stops being one whether or not the string changed: "first
1071
+ * prompt" means the first, not "every prompt until a rename". Returns
1072
+ * whether the name changed, so the caller knows whether to persist and
1073
+ * broadcast.
1074
+ */
1075
+ applyAutoName(value) {
1076
+ if (this._nameSource !== 'placeholder')
1077
+ return false;
1078
+ const name = value.trim();
1079
+ if (!name)
1080
+ return false;
1081
+ this._nameSource = 'auto';
1082
+ if (this._name === name)
1083
+ return false;
1084
+ this._name = name;
1085
+ return true;
1086
+ }
1087
+ get nameSource() {
1088
+ return this._nameSource;
984
1089
  }
985
1090
  setAutoClear(enabled, threshold) {
986
1091
  this._autoOps.setAutoClear(enabled, threshold);
@@ -1100,6 +1205,7 @@ export class Session extends EventEmitter {
1100
1205
  // attach repaint, so the home screens' quiet ordering survives a restart.
1101
1206
  lastActivityAt: this._wireActivityAt,
1102
1207
  name: this._name,
1208
+ nameSource: this._nameSource,
1103
1209
  mode: this.mode,
1104
1210
  autoClearEnabled: this._autoOps.autoClearEnabled,
1105
1211
  autoClearThreshold: this._autoOps.autoClearThreshold,
@@ -1138,6 +1244,7 @@ export class Session extends EventEmitter {
1138
1244
  ompConfig: this._ompConfig,
1139
1245
  resumeSessionId: this._resumeSessionId,
1140
1246
  effort: this._effort,
1247
+ customModel: this.customModel,
1141
1248
  // COD-118: runtime-only — surfaced so the frontend can require explicit user
1142
1249
  // intent before restarting a crash-looped session. Deliberately NOT restored
1143
1250
  // by the constructor: a Codeman restart starts with a fresh breaker so boot
@@ -1270,6 +1377,7 @@ export class Session extends EventEmitter {
1270
1377
  needsNewSession = true;
1271
1378
  }
1272
1379
  else {
1380
+ this._pendingEnvUnsets.clear();
1273
1381
  // Wait a moment for the respawned process to fully start
1274
1382
  await new Promise((resolve) => setTimeout(resolve, MUX_STARTUP_DELAY_MS));
1275
1383
  }
@@ -1358,14 +1466,66 @@ export class Session extends EventEmitter {
1358
1466
  console.log('[Session] reattachRemote: reattached remote session', this._muxSession.muxName, 'pid', newPid);
1359
1467
  return true;
1360
1468
  }
1469
+ /**
1470
+ * Kill and relaunch this session's CLI process IN PLACE — same pane, same tmux
1471
+ * session, fresh env/args from current state. Custom Model Endpoint Profiles
1472
+ * (docs/custom-model-endpoints-plan.md) is the first caller: after `setCustomModel()` merges new
1473
+ * env vars into `_envOverrides`, the running CLI process still has the OLD env
1474
+ * (inherited at its own process start, not live-reloaded), so switching a
1475
+ * session's model/endpoint requires this restart to actually take effect.
1476
+ *
1477
+ * A GENERALIZED {@link reattachRemote} with the `!this._remote` guard dropped —
1478
+ * `_buildRespawnPaneOptions()` already passes `remote: this._remote` through
1479
+ * unconditionally, so `mux.respawnPane()` builds the right command either way
1480
+ * (a local session gets `respawn-pane -k` + the real launch line, which is the
1481
+ * kill-and-relaunch this method exists for; a remote session gets the existing
1482
+ * reattach-to-durable-tmux behavior). Deliberately does NOT check `isBusy()` —
1483
+ * that's the caller's job (mirrors `/interactive`'s guard), since a raw restart
1484
+ * primitive shouldn't itself decide when it's safe to use.
1485
+ *
1486
+ * @returns true if the pane was respawned, false otherwise (no mux session, or
1487
+ * the mux session is gone — see {@link reattachRemote} for that reasoning).
1488
+ */
1489
+ async restartCli() {
1490
+ if (!this._useMux || !this._mux || !this._muxSession)
1491
+ return false;
1492
+ const mux = this._mux;
1493
+ if (!mux.muxSessionExists(this._muxSession.muxName)) {
1494
+ console.log('[Session] restartCli: mux session gone, skipping:', this._muxSession.muxName);
1495
+ return false;
1496
+ }
1497
+ this._pinOmpRespawnId();
1498
+ const options = this._buildRespawnPaneOptions();
1499
+ // Unlike the dead-pane respawn, this one kills a WORKING pane whose conversation
1500
+ // already has a transcript, and a CLI that launches with `--session-id <id>` refuses
1501
+ // an id that is already in use (claude: `Error: Session ID ... is already in use.`),
1502
+ // which turned an endpoint switch into a dead pane and a lost session. A launch that
1503
+ // declares a `fallback` chain renders `resume || new` once a resume id is set, the
1504
+ // same `--resume <id> || --session-id <id>` shape the docker and remote pane commands
1505
+ // already use, so pin the live conversation id for THIS respawn only. The registry
1506
+ // shape is the gate, not the CLI's name: an entry whose resume id is minted by the
1507
+ // CLI itself (codex/pi/omp/grok) never declares that chain, and its resume field is
1508
+ // read from its own `<Mode>Config` rather than this top-level one anyway.
1509
+ if (!options.resumeSessionId && getCli(this.mode)?.launch.chain === 'fallback') {
1510
+ options.resumeSessionId = this._claudeSessionId ?? this.id;
1511
+ }
1512
+ const newPid = await mux.respawnPane(options);
1513
+ if (!newPid) {
1514
+ console.error('[Session] restartCli: respawnPane failed for', this._muxSession.muxName);
1515
+ return false;
1516
+ }
1517
+ this._pendingEnvUnsets.clear();
1518
+ console.log('[Session] restartCli: restarted CLI for', this._muxSession.muxName, 'pid', newPid);
1519
+ return true;
1520
+ }
1361
1521
  /**
1362
1522
  * Assemble the {@link RespawnPaneOptions} for this session. Single source of
1363
1523
  * truth shared by interactive start, shell start (via their inline copies),
1364
- * and {@link reattachRemote} so the remote reattach path can never drift from
1524
+ * {@link reattachRemote}, and {@link restartCli} so no respawn path can drift from
1365
1525
  * the spawn path.
1366
1526
  */
1367
1527
  _buildRespawnPaneOptions() {
1368
- return {
1528
+ const options = {
1369
1529
  sessionId: this.id,
1370
1530
  workingDir: this.workingDir,
1371
1531
  mode: this.mode,
@@ -1393,12 +1553,37 @@ export class Session extends EventEmitter {
1393
1553
  ompConfig: this._ompConfig,
1394
1554
  resumeSessionId: this._resumeSessionId,
1395
1555
  envOverrides: this._envOverrides,
1556
+ unsetEnvKeys: this._pendingEnvUnsets.size > 0 ? [...this._pendingEnvUnsets] : undefined,
1396
1557
  effort: this._effort,
1397
1558
  historyLimit: this._tmuxHistoryLimit,
1398
1559
  remote: this._remote,
1399
1560
  docker: this._docker,
1400
1561
  owner: this._owner,
1401
1562
  };
1563
+ return this._withCustomModelLaunchModel(options);
1564
+ }
1565
+ /**
1566
+ * Force the custom-model selection's `launchModel` (pi/omp `custom/<id>`, grok's
1567
+ * `[model.<name>]` block name) onto the CLI's `model` launch param. Where that param
1568
+ * lives is registry DATA — the entry's `legacyConfigField` (`piConfig`, `grokConfig`,
1569
+ * ...) or the top-level `model` for an entry that declares none — so this stays a
1570
+ * generic reader rather than a branch per CLI. Applied on the OPTIONS only: the stored
1571
+ * `<Mode>Config` keeps whatever model the user chose at create, which is exactly what a
1572
+ * later clear must fall back to.
1573
+ */
1574
+ _withCustomModelLaunchModel(options) {
1575
+ const launchModel = this._customModel?.launchModel;
1576
+ if (!launchModel)
1577
+ return options;
1578
+ const entry = getCli(this.mode);
1579
+ if (!entry)
1580
+ return options;
1581
+ const field = entry.launch.legacyConfigField;
1582
+ if (!field)
1583
+ return { ...options, model: launchModel };
1584
+ const bag = options;
1585
+ const existing = (bag[field] ?? {});
1586
+ return { ...options, [field]: { ...existing, model: launchModel } };
1402
1587
  }
1403
1588
  /**
1404
1589
  * OMP-only: resolve and PIN the exact conversation to continue when
@@ -2879,11 +3064,12 @@ export class Session extends EventEmitter {
2879
3064
  * discards the data, but it used to do so with no signal at all — which is how
2880
3065
  * input could disappear while the caller believed it had been delivered.
2881
3066
  */
2882
- write(data) {
2883
- this._trackSubmit(data);
3067
+ write(data, options = {}) {
3068
+ const submittedPrompt = this._trackSubmit(data, options);
2884
3069
  if (!this.ptyProcess)
2885
3070
  return false;
2886
3071
  this.ptyProcess.write(data);
3072
+ this._emitSubmittedPrompt(submittedPrompt);
2887
3073
  return true;
2888
3074
  }
2889
3075
  // ── Conversation tracking ─────────────────────────────────────────────
@@ -2897,10 +3083,34 @@ export class Session extends EventEmitter {
2897
3083
  get lastSubmitAt() {
2898
3084
  return this._lastSubmitAt;
2899
3085
  }
2900
- _trackSubmit(data) {
3086
+ /**
3087
+ * Stamps the pane's last Enter for EVERY write, and feeds the auto-name
3088
+ * tracker only for user-originated input on a prompt-taking CLI. Ralph
3089
+ * kick-starts, respawn `/clear`s, cron launches, approval answers and the
3090
+ * trust-dialog keys all arrive without `fromUser` and so can never name a tab.
3091
+ */
3092
+ _trackSubmit(data, options) {
3093
+ const submitted = options.fromUser && this._acceptsPrompts ? this._submittedPromptTracker.feed(data) : [];
2901
3094
  if (data.includes('\r') || data.includes('\n')) {
2902
3095
  this._lastSubmitAt = Date.now();
2903
3096
  }
3097
+ return submitted;
3098
+ }
3099
+ /**
3100
+ * Feeds user input that reaches the pane AROUND the write paths: the
3101
+ * send-key route injects Shift+Enter's line feed through `tmux send-keys -H`
3102
+ * directly, and without this the two lines of a prompt joined with no
3103
+ * separator. Reports submissions like a write would (a line feed never is one).
3104
+ */
3105
+ trackUserInput(data) {
3106
+ if (!this._acceptsPrompts)
3107
+ return;
3108
+ this._emitSubmittedPrompt(this._submittedPromptTracker.feed(data));
3109
+ }
3110
+ _emitSubmittedPrompt(prompts) {
3111
+ for (const prompt of prompts) {
3112
+ this.emit('promptSubmitted', prompt);
3113
+ }
2904
3114
  }
2905
3115
  /**
2906
3116
  * A prompt was submitted, reported by the CLI's own UserPromptSubmit hook.
@@ -2933,6 +3143,20 @@ export class Session extends EventEmitter {
2933
3143
  * half-open socket silently drops frames with no error) would type a prompt
2934
3144
  * twice whenever an ACK is lost after the write landed.
2935
3145
  */
3146
+ /**
3147
+ * The highest input seq recorded for `clientId`, or 0 when this session has
3148
+ * never seen it.
3149
+ *
3150
+ * Reported back on a REJECTED (duplicate) frame so the client can lift its own
3151
+ * counter above this watermark. Without that number a client whose persisted
3152
+ * counter fell behind ours has no way to find its way out: every fresh
3153
+ * keystroke it sends lands at or below the watermark, is dropped as a
3154
+ * duplicate, and is ACKed anyway — so the UI looks healthy while nothing is
3155
+ * delivered, and a reload restores the same stale counter from localStorage.
3156
+ */
3157
+ lastInputSeq(clientId) {
3158
+ return this._appliedInputSeq.get(clientId) ?? 0;
3159
+ }
2936
3160
  shouldApplyInput(clientId, seq) {
2937
3161
  const last = this._appliedInputSeq.get(clientId);
2938
3162
  if (last !== undefined && seq <= last)
@@ -2980,14 +3204,18 @@ export class Session extends EventEmitter {
2980
3204
  * session.writeViaMux('/init\r'); // Send /init command
2981
3205
  * ```
2982
3206
  */
2983
- async writeViaMux(data) {
2984
- this._trackSubmit(data);
3207
+ async writeViaMux(data, options = {}) {
3208
+ const submittedPrompt = this._trackSubmit(data, options);
2985
3209
  if (this._mux && this._muxSession) {
2986
- return this._mux.sendInput(this.id, data);
3210
+ const sent = await this._mux.sendInput(this.id, data);
3211
+ if (sent)
3212
+ this._emitSubmittedPrompt(submittedPrompt);
3213
+ return sent;
2987
3214
  }
2988
3215
  // Fallback to PTY write
2989
3216
  if (this.ptyProcess) {
2990
3217
  this.ptyProcess.write(data);
3218
+ this._emitSubmittedPrompt(submittedPrompt);
2991
3219
  return true;
2992
3220
  }
2993
3221
  return false;