@gajae-code/coding-agent 0.9.5 → 0.10.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 (194) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/types/cli/classify-install-target.d.ts +0 -10
  3. package/dist/types/cli/daemon-cli.d.ts +2 -0
  4. package/dist/types/cli/initial-message.d.ts +1 -1
  5. package/dist/types/cli/malloc-env-guard.d.ts +65 -0
  6. package/dist/types/cli/malloc-env-guard.test.d.ts +1 -0
  7. package/dist/types/cli/notify-cli.d.ts +3 -1
  8. package/dist/types/cli/session-picker.d.ts +4 -3
  9. package/dist/types/cli/update-cli.d.ts +22 -3
  10. package/dist/types/cli.d.ts +4 -0
  11. package/dist/types/commands/daemon.d.ts +6 -2
  12. package/dist/types/commands/launch.d.ts +1 -0
  13. package/dist/types/commands/notify.d.ts +6 -0
  14. package/dist/types/commit/analysis/conventional.d.ts +1 -1
  15. package/dist/types/commit/analysis/summary.d.ts +1 -1
  16. package/dist/types/commit/changelog/generate.d.ts +1 -1
  17. package/dist/types/commit/changelog/index.d.ts +2 -2
  18. package/dist/types/commit/map-reduce/map-phase.d.ts +1 -1
  19. package/dist/types/commit/map-reduce/reduce-phase.d.ts +1 -1
  20. package/dist/types/config/keybindings.d.ts +5 -0
  21. package/dist/types/config/model-profile-activation.d.ts +1 -0
  22. package/dist/types/config/settings-schema.d.ts +47 -7
  23. package/dist/types/coordinator-mcp/model-preset.d.ts +45 -0
  24. package/dist/types/coordinator-mcp/server.d.ts +67 -6
  25. package/dist/types/daemon/control-types.d.ts +10 -0
  26. package/dist/types/daemon/operator-contract.d.ts +47 -0
  27. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +4 -0
  28. package/dist/types/discovery/helpers.d.ts +1 -1
  29. package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
  30. package/dist/types/gjc-runtime/launch-tmux.d.ts +26 -7
  31. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +34 -3
  32. package/dist/types/gjc-runtime/tmux-common.d.ts +6 -0
  33. package/dist/types/gjc-runtime/tmux-owner-isolation-cli.d.ts +5 -0
  34. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +309 -0
  35. package/dist/types/gjc-runtime/tmux-sessions.d.ts +49 -17
  36. package/dist/types/internal-urls/local-protocol.d.ts +13 -9
  37. package/dist/types/main.d.ts +66 -2
  38. package/dist/types/modes/acp/acp-agent.d.ts +19 -2
  39. package/dist/types/modes/acp/acp-mode.d.ts +12 -0
  40. package/dist/types/modes/components/assistant-message.d.ts +1 -1
  41. package/dist/types/modes/components/custom-editor.d.ts +2 -0
  42. package/dist/types/modes/components/gajae-pet-widget.d.ts +59 -0
  43. package/dist/types/modes/components/hook-selector.d.ts +5 -0
  44. package/dist/types/modes/components/irc-sidebar.d.ts +32 -0
  45. package/dist/types/modes/components/pet-selector.d.ts +11 -0
  46. package/dist/types/modes/components/session-selector.d.ts +22 -9
  47. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  48. package/dist/types/modes/components/tool-execution.d.ts +1 -0
  49. package/dist/types/modes/components/user-message.d.ts +2 -1
  50. package/dist/types/modes/controllers/event-controller.d.ts +4 -0
  51. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  52. package/dist/types/modes/interactive-mode.d.ts +24 -2
  53. package/dist/types/modes/irc-observation-ledger.d.ts +22 -0
  54. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +8 -0
  55. package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +5 -0
  56. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +45 -0
  57. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +42 -2
  58. package/dist/types/modes/theme/defaults/index.d.ts +568 -568
  59. package/dist/types/modes/types.d.ts +28 -2
  60. package/dist/types/modes/utils/abort-message.d.ts +1 -1
  61. package/dist/types/modes/utils/irc-message.d.ts +38 -0
  62. package/dist/types/modes/utils/ui-helpers.d.ts +13 -4
  63. package/dist/types/notifications/config.d.ts +34 -0
  64. package/dist/types/notifications/daemon-paths.d.ts +7 -0
  65. package/dist/types/notifications/index.d.ts +27 -0
  66. package/dist/types/notifications/lifecycle-control-runtime.d.ts +12 -4
  67. package/dist/types/notifications/notification-service.d.ts +144 -0
  68. package/dist/types/notifications/rate-limit-pool.d.ts +27 -9
  69. package/dist/types/notifications/telegram-daemon-control.d.ts +6 -0
  70. package/dist/types/notifications/telegram-daemon.d.ts +130 -3
  71. package/dist/types/notifications/telegram-reference.d.ts +19 -2
  72. package/dist/types/notifications/topic-registry.d.ts +24 -4
  73. package/dist/types/registry/agent-registry.d.ts +2 -0
  74. package/dist/types/rlm/index.d.ts +1 -1
  75. package/dist/types/sdk.d.ts +2 -0
  76. package/dist/types/secrets/obfuscator.d.ts +1 -1
  77. package/dist/types/session/agent-session.d.ts +26 -2
  78. package/dist/types/session/cache-economics.d.ts +79 -0
  79. package/dist/types/session/provider-safety-stop.d.ts +5 -0
  80. package/dist/types/session/session-manager.d.ts +124 -4
  81. package/dist/types/session/session-storage.d.ts +155 -13
  82. package/dist/types/setup/credential-auto-import.d.ts +2 -2
  83. package/dist/types/task/executor.d.ts +3 -0
  84. package/dist/types/task/receipt.d.ts +3 -1
  85. package/dist/types/task/types.d.ts +8 -0
  86. package/dist/types/tools/ask-answer-registry.d.ts +5 -0
  87. package/dist/types/tools/ask.d.ts +25 -1
  88. package/dist/types/tools/bash.d.ts +1 -1
  89. package/dist/types/tools/browser/tab-supervisor.d.ts +6 -0
  90. package/dist/types/tools/index.d.ts +58 -6
  91. package/dist/types/tools/telegram-send.d.ts +7 -0
  92. package/dist/types/utils/external-editor.d.ts +2 -0
  93. package/package.json +9 -9
  94. package/scripts/compile-args.ts +8 -2
  95. package/scripts/g004-tmux-smoke.ts +196 -36
  96. package/src/cli/daemon-cli.ts +19 -29
  97. package/src/cli/malloc-env-guard.test.ts +104 -0
  98. package/src/cli/malloc-env-guard.ts +149 -0
  99. package/src/cli/notify-cli.ts +78 -14
  100. package/src/cli/session-picker.ts +23 -40
  101. package/src/cli/update-cli.ts +56 -37
  102. package/src/cli.ts +50 -10
  103. package/src/commands/daemon.ts +19 -5
  104. package/src/commands/harness.ts +515 -47
  105. package/src/commands/launch.ts +11 -3
  106. package/src/commands/notify.ts +5 -1
  107. package/src/commands/session.ts +6 -1
  108. package/src/config/keybindings.ts +5 -0
  109. package/src/config/model-profile-activation.ts +1 -1
  110. package/src/config/model-profiles.ts +23 -23
  111. package/src/config/settings-schema.ts +41 -1
  112. package/src/coordinator-mcp/model-preset.ts +100 -0
  113. package/src/coordinator-mcp/server.ts +2653 -425
  114. package/src/daemon/control-types.ts +11 -0
  115. package/src/daemon/operator-contract.ts +121 -0
  116. package/src/deep-interview/render-middleware.ts +67 -2
  117. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +57 -17
  118. package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +15 -2
  119. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +5 -1
  120. package/src/gjc-runtime/launch-tmux.ts +702 -135
  121. package/src/gjc-runtime/session-state-sidecar.ts +756 -135
  122. package/src/gjc-runtime/team-runtime.ts +6 -0
  123. package/src/gjc-runtime/tmux-common.ts +26 -7
  124. package/src/gjc-runtime/tmux-gc.ts +52 -9
  125. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +185 -0
  126. package/src/gjc-runtime/tmux-owner-isolation.ts +2060 -0
  127. package/src/gjc-runtime/tmux-sessions.ts +871 -68
  128. package/src/internal-urls/docs-index.generated.ts +9 -8
  129. package/src/internal-urls/local-protocol.ts +29 -10
  130. package/src/main.ts +298 -75
  131. package/src/modes/acp/acp-agent.ts +728 -98
  132. package/src/modes/acp/acp-mode.ts +29 -2
  133. package/src/modes/components/assistant-message.ts +32 -3
  134. package/src/modes/components/bash-execution.ts +43 -13
  135. package/src/modes/components/custom-editor.ts +5 -0
  136. package/src/modes/components/gajae-pet-widget.ts +360 -0
  137. package/src/modes/components/hook-selector.ts +12 -0
  138. package/src/modes/components/irc-sidebar.ts +206 -0
  139. package/src/modes/components/pet-selector.ts +55 -0
  140. package/src/modes/components/session-selector.ts +222 -221
  141. package/src/modes/components/settings-selector.ts +10 -0
  142. package/src/modes/components/tool-execution.ts +47 -6
  143. package/src/modes/components/user-message.ts +26 -9
  144. package/src/modes/controllers/command-controller.ts +28 -2
  145. package/src/modes/controllers/event-controller.ts +159 -36
  146. package/src/modes/controllers/extension-ui-controller.ts +36 -17
  147. package/src/modes/controllers/input-controller.ts +8 -2
  148. package/src/modes/controllers/selector-controller.ts +62 -15
  149. package/src/modes/interactive-mode.ts +183 -7
  150. package/src/modes/irc-observation-ledger.ts +127 -0
  151. package/src/modes/shared/agent-wire/deep-interview-gate.ts +45 -2
  152. package/src/modes/shared/agent-wire/unattended-session.ts +288 -1
  153. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +130 -20
  154. package/src/modes/types.ts +34 -2
  155. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  156. package/src/modes/utils/irc-message.ts +138 -0
  157. package/src/modes/utils/ui-helpers.ts +216 -50
  158. package/src/notifications/config.ts +44 -0
  159. package/src/notifications/daemon-paths.ts +8 -0
  160. package/src/notifications/index.ts +535 -75
  161. package/src/notifications/lifecycle-control-runtime.ts +930 -77
  162. package/src/notifications/notification-service.ts +710 -0
  163. package/src/notifications/rate-limit-pool.ts +46 -12
  164. package/src/notifications/telegram-daemon-cli.ts +2 -0
  165. package/src/notifications/telegram-daemon-control.ts +27 -2
  166. package/src/notifications/telegram-daemon.ts +635 -94
  167. package/src/notifications/telegram-reference.ts +73 -13
  168. package/src/notifications/topic-registry.ts +86 -9
  169. package/src/prompts/system/irc-peer-roster.md +1 -0
  170. package/src/prompts/system/subagent-system-prompt.md +1 -0
  171. package/src/prompts/system/system-prompt.md +1 -0
  172. package/src/prompts/tools/irc.md +1 -1
  173. package/src/registry/agent-registry.ts +3 -0
  174. package/src/sdk.ts +52 -1
  175. package/src/secrets/obfuscator.ts +4 -2
  176. package/src/session/agent-session.ts +396 -145
  177. package/src/session/cache-economics.ts +275 -0
  178. package/src/session/provider-safety-stop.ts +9 -0
  179. package/src/session/session-dump-format.ts +6 -0
  180. package/src/session/session-manager.ts +809 -39
  181. package/src/session/session-storage.ts +582 -38
  182. package/src/slash-commands/builtin-registry.ts +143 -9
  183. package/src/task/executor.ts +12 -3
  184. package/src/task/index.ts +8 -0
  185. package/src/task/receipt.ts +7 -2
  186. package/src/task/types.ts +26 -0
  187. package/src/tools/ask-answer-registry.ts +25 -0
  188. package/src/tools/ask.ts +355 -33
  189. package/src/tools/browser/tab-supervisor.ts +63 -6
  190. package/src/tools/browser.ts +8 -3
  191. package/src/tools/index.ts +81 -6
  192. package/src/tools/sqlite-reader.ts +8 -4
  193. package/src/tui/output-block.ts +18 -6
  194. package/src/utils/external-editor.ts +6 -1
@@ -145,37 +145,56 @@ export class LocalProtocolHandler implements ProtocolHandler {
145
145
  readonly immutable = false;
146
146
 
147
147
  static #override: LocalProtocolOptions | undefined;
148
+ static #ownedOverrides: Array<{ options: LocalProtocolOptions }> = [];
148
149
 
149
150
  /**
150
- * Install a process-global override that wins over the AgentRegistry-based
151
- * derivation. Used by SDK consumers that wire `localProtocolOptions` on
152
- * `createAgentSession` and by subagents that share their parent's root.
151
+ * Install an explicit local-protocol mapping owned by the caller.
152
+ *
153
+ * The most recently installed live mapping wins. The returned disposer removes
154
+ * only this registration and is safe to call more than once.
155
+ */
156
+ static installOverride(value: LocalProtocolOptions): () => void {
157
+ const registration = { options: value };
158
+ LocalProtocolHandler.#ownedOverrides.push(registration);
159
+ let disposed = false;
160
+ return () => {
161
+ if (disposed) return;
162
+ disposed = true;
163
+ const index = LocalProtocolHandler.#ownedOverrides.indexOf(registration);
164
+ if (index !== -1) LocalProtocolHandler.#ownedOverrides.splice(index, 1);
165
+ };
166
+ }
167
+
168
+ /**
169
+ * Install a process-global test override that wins over owned and registry
170
+ * mappings. Prefer {@link installOverride} for lifecycle-bound production use.
153
171
  */
154
172
  static setOverride(value: LocalProtocolOptions | undefined): void {
155
173
  LocalProtocolHandler.#override = value;
156
174
  }
157
175
 
158
- /** Reset the process-global override. Test-only. */
176
+ /** Reset all process-global local-protocol overrides. Test-only. */
159
177
  static resetOverrideForTests(): void {
160
178
  LocalProtocolHandler.#override = undefined;
179
+ LocalProtocolHandler.#ownedOverrides = [];
161
180
  }
162
181
 
163
182
  /**
164
183
  * Returns the active local-protocol options.
165
184
  *
166
185
  * Resolution order:
167
- * 1. Explicit override installed via {@link setOverride} (used by subagents
168
- * that share their parent's root and by SDK consumers with a custom
169
- * artifacts/session id mapping).
170
- * 2. The main session in `AgentRegistry.global()`. Its `SessionManager`
171
- * supplies both `getArtifactsDir` and `getSessionId`.
186
+ * 1. Direct test override installed via {@link setOverride}.
187
+ * 2. The most recently installed live owned override.
188
+ * 3. A live main session in `AgentRegistry.global()`.
172
189
  */
173
190
  static resolveOptions(): LocalProtocolOptions | undefined {
174
191
  const override = LocalProtocolHandler.#override;
175
192
  if (override) return override;
193
+ const ownedOverride = LocalProtocolHandler.#ownedOverrides.at(-1)?.options;
194
+ if (ownedOverride) return ownedOverride;
176
195
  const main = AgentRegistry.global()
177
196
  .list()
178
- .find(ref => ref.kind === "main");
197
+ .find(ref => ref.kind === "main" && ref.session && (ref.status === "running" || ref.status === "idle"));
179
198
  const sessionManager = main?.session?.sessionManager;
180
199
  if (!sessionManager) return undefined;
181
200
  return {
package/src/main.ts CHANGED
@@ -34,7 +34,11 @@ import { BUNDLED_GROK_BUILD_EXTENSION_ID, getBundledGrokBuildExtensionFactory }
34
34
  import { initializeWithSettings } from "./discovery";
35
35
  import { exportFromFile } from "./export/html";
36
36
  import type { ExtensionUIContext } from "./extensibility/extensions/types";
37
+ import { persistCoordinatorRuntimeInputReady } from "./gjc-runtime/session-state-sidecar";
38
+ import { isTmuxOwnerIsolationCliArgv, runTmuxOwnerIsolationCliFromStdin } from "./gjc-runtime/tmux-owner-isolation-cli";
39
+ import type { SessionSelectionResult } from "./modes/components/session-selector";
37
40
  import type { InteractiveMode } from "./modes/interactive-mode";
41
+ import type { PrintModeOptions } from "./modes/print-mode";
38
42
  import { initTheme, stopThemeWatcher } from "./modes/theme/theme";
39
43
  import type { SubmittedUserInput } from "./modes/types";
40
44
  import { applyCliRuntimeApiKeyOverride } from "./runtime-api-key";
@@ -48,7 +52,13 @@ import {
48
52
  } from "./sdk";
49
53
  import type { AgentSession } from "./session/agent-session";
50
54
  import type { AuthStorage } from "./session/auth-storage";
51
- import { resolveResumableSession, type SessionInfo, SessionManager } from "./session/session-manager";
55
+ import {
56
+ type ResumeSessionIdentity,
57
+ resolveResumableSession,
58
+ type SessionInfo,
59
+ SessionManager,
60
+ type StrictSessionOpenResult,
61
+ } from "./session/session-manager";
52
62
  import { runStartupCredentialAutoImportIfNeeded } from "./setup/credential-auto-import";
53
63
  import { formatModelOnboardingGuidance } from "./setup/model-onboarding-guidance";
54
64
  import { executeBuiltinSlashCommand } from "./slash-commands/builtin-registry";
@@ -59,9 +69,6 @@ import { getDisplayChangelogEntries, getInstalledVersionChangelogEntry, getNewEn
59
69
  import type { EventBus } from "./utils/event-bus";
60
70
 
61
71
  async function checkForNewVersion(currentVersion: string): Promise<string | undefined> {
62
- if (!settings.get("startup.checkUpdate")) {
63
- return;
64
- }
65
72
  try {
66
73
  const response = await fetch("https://registry.npmjs.org/@gajae-code/coding-agent/latest");
67
74
  if (!response.ok) return undefined;
@@ -79,6 +86,69 @@ async function checkForNewVersion(currentVersion: string): Promise<string | unde
79
86
  }
80
87
  }
81
88
 
89
+ export type StartupUpdateRoute = "interactive" | "print" | "text" | "json" | "rpc" | "rpc-ui" | "acp" | "bridge";
90
+
91
+ export function classifyStartupUpdateRoute(
92
+ parsed: Pick<Args, "print" | "mode">,
93
+ autoPrint: boolean,
94
+ ): StartupUpdateRoute {
95
+ if (!parsed.print && !autoPrint && parsed.mode === undefined) {
96
+ return "interactive";
97
+ }
98
+ if (parsed.print) {
99
+ return "print";
100
+ }
101
+ return parsed.mode ?? "text";
102
+ }
103
+
104
+ /** Coordinates the non-blocking update check around the interactive UI lifecycle. */
105
+ export class StartupUpdateOrchestrator {
106
+ #versionCheckPromise: Promise<string | undefined> | undefined;
107
+ readonly #route: StartupUpdateRoute;
108
+ readonly #enabled: () => boolean;
109
+ readonly #check: () => Promise<string | undefined>;
110
+
111
+ constructor(route: StartupUpdateRoute, enabled: () => boolean, check: () => Promise<string | undefined>) {
112
+ this.#route = route;
113
+ this.#enabled = enabled;
114
+ this.#check = check;
115
+ }
116
+
117
+ startBeforeInteractiveInitialization(): void {
118
+ if (this.#route !== "interactive" || !this.#enabled() || this.#versionCheckPromise) {
119
+ return;
120
+ }
121
+ try {
122
+ this.#versionCheckPromise = this.#check().catch(() => undefined);
123
+ } catch {
124
+ this.#versionCheckPromise = Promise.resolve(undefined);
125
+ }
126
+ }
127
+
128
+ attachAfterInteractiveInitialization(notify: (version: string) => void): void {
129
+ this.#versionCheckPromise
130
+ ?.then(version => {
131
+ if (version && this.#enabled()) {
132
+ notify(version);
133
+ }
134
+ })
135
+ .catch(() => {});
136
+ }
137
+ }
138
+
139
+ export interface StartupUpdateInteractiveMode {
140
+ init: () => Promise<void>;
141
+ showNewVersionNotification: (version: string) => void;
142
+ }
143
+
144
+ export async function initializeInteractiveModeWithStartupUpdate(
145
+ mode: StartupUpdateInteractiveMode,
146
+ startupUpdate: StartupUpdateOrchestrator,
147
+ ): Promise<void> {
148
+ await mode.init();
149
+ startupUpdate.attachAfterInteractiveInitialization(version => mode.showNewVersionNotification(version));
150
+ }
151
+
82
152
  const RPC_DEFAULTED_SETTING_PATHS: SettingPath[] = [
83
153
  "todo.enabled",
84
154
  "todo.reminders",
@@ -313,12 +383,58 @@ export function createAcpSessionFactory(args: AcpSessionFactoryOptions): AcpSess
313
383
  };
314
384
  }
315
385
 
316
- async function runInteractiveMode(
386
+ interface InteractiveModeFactoryOptions {
387
+ session: AgentSession;
388
+ version: string;
389
+ changelogMarkdown: string | undefined;
390
+ setExtensionUIContext: (uiContext: ExtensionUIContext, hasUI: boolean) => void;
391
+ lspServers: LspStartupServerInfo[] | undefined;
392
+ mcpManager: MCPManager | undefined;
393
+ eventBus?: EventBus;
394
+ }
395
+
396
+ type CreateInteractiveMode = (options: InteractiveModeFactoryOptions) => InteractiveMode;
397
+
398
+ type ResumePickerTerminalCheck = () => boolean;
399
+ type ListForResumePickerReadOnly = (cwd: string, sessionDir?: string) => Promise<SessionInfo[]>;
400
+ type SelectResumeSession = (sessions: SessionInfo[]) => Promise<SessionSelectionResult>;
401
+ type OpenExistingSessionStrict = (
402
+ identity: ResumeSessionIdentity,
403
+ sessionDir?: string,
404
+ ) => Promise<StrictSessionOpenResult>;
405
+
406
+ export const BARE_RESUME_CONFLICT_ERROR =
407
+ "--resume without a session cannot be combined with --continue, --fork, or --no-session.";
408
+ export const BARE_RESUME_INTERACTIVE_ERROR = "--resume requires an interactive terminal; use --resume <id>.";
409
+ export const BARE_RESUME_OPEN_ERROR = "Could not open the selected session. Use --resume <id>.";
410
+
411
+ function isBareResume(parsed: Args): boolean {
412
+ return (
413
+ parsed.resume === true &&
414
+ parsed.version !== true &&
415
+ parsed.listModels === undefined &&
416
+ parsed.export === undefined
417
+ );
418
+ }
419
+
420
+ function hasBareResumeConflict(parsed: Args): boolean {
421
+ return parsed.continue === true || parsed.fork !== undefined || parsed.noSession === true;
422
+ }
423
+
424
+ function isNormalLocalInteractiveRoute(parsed: Args): boolean {
425
+ return parsed.mode === undefined && parsed.print !== true;
426
+ }
427
+
428
+ function hasResumePickerTerminal(): boolean {
429
+ return process.stdin.isTTY === true && process.stdout.isTTY === true;
430
+ }
431
+
432
+ export async function runInteractiveMode(
317
433
  session: AgentSession,
318
434
  version: string,
319
435
  changelogMarkdown: string | undefined,
320
436
  notifs: (InteractiveModeNotify | null)[],
321
- versionCheckPromise: Promise<string | undefined>,
437
+ startupUpdate: StartupUpdateOrchestrator,
322
438
  initialMessages: string[],
323
439
  setExtensionUIContext: (uiContext: ExtensionUIContext, hasUI: boolean) => void,
324
440
  lspServers: LspStartupServerInfo[] | undefined,
@@ -326,30 +442,36 @@ async function runInteractiveMode(
326
442
  eventBus?: EventBus,
327
443
  initialMessage?: string,
328
444
  initialImages?: ImageContent[],
445
+ createInteractiveMode?: CreateInteractiveMode,
446
+ resumeAction?: "continue-tail" | "open-idle",
329
447
  ): Promise<void> {
330
- const { InteractiveMode } = await import("./modes/interactive-mode");
331
- const mode = new InteractiveMode(
332
- session,
333
- version,
334
- changelogMarkdown,
335
- setExtensionUIContext,
336
- lspServers,
337
- mcpManager,
338
- eventBus,
339
- );
340
-
341
- await mode.init();
448
+ const mode = createInteractiveMode
449
+ ? createInteractiveMode({
450
+ session,
451
+ version,
452
+ changelogMarkdown,
453
+ setExtensionUIContext,
454
+ lspServers,
455
+ mcpManager,
456
+ eventBus,
457
+ })
458
+ : new (await import("./modes/interactive-mode")).InteractiveMode(
459
+ session,
460
+ version,
461
+ changelogMarkdown,
462
+ setExtensionUIContext,
463
+ lspServers,
464
+ mcpManager,
465
+ eventBus,
466
+ );
342
467
 
343
- versionCheckPromise
344
- .then(newVersion => {
345
- if (!settings.get("startup.checkUpdate")) {
346
- return;
347
- }
348
- if (newVersion) {
349
- mode.showNewVersionNotification(newVersion);
350
- }
351
- })
352
- .catch(() => {});
468
+ await initializeInteractiveModeWithStartupUpdate(mode, startupUpdate);
469
+ try {
470
+ await persistCoordinatorRuntimeInputReady();
471
+ } catch (error) {
472
+ logger.warn("Failed to persist coordinator runtime input readiness", { error: String(error) });
473
+ throw error;
474
+ }
353
475
 
354
476
  mode.renderInitialMessages(undefined, { preserveExistingChat: true });
355
477
 
@@ -366,6 +488,16 @@ async function runInteractiveMode(
366
488
  }
367
489
  }
368
490
 
491
+ const hasStartupInput = initialMessage !== undefined || initialMessages.length > 0;
492
+ if (!hasStartupInput && resumeAction === "continue-tail") {
493
+ try {
494
+ await session.continuePersistedHistory();
495
+ } catch (error: unknown) {
496
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
497
+ mode.showError(errorMessage);
498
+ }
499
+ }
500
+
369
501
  if (initialMessage !== undefined) {
370
502
  try {
371
503
  await session.prompt(initialMessage, { images: initialImages });
@@ -453,6 +585,9 @@ export async function createSessionManager(
453
585
  cwd: string,
454
586
  activeSettings: Settings = settings,
455
587
  ): Promise<SessionManager | undefined> {
588
+ if (parsed.resume === true) {
589
+ return undefined;
590
+ }
456
591
  if (parsed.fork) {
457
592
  if (parsed.noSession) {
458
593
  throw new Error("--fork requires session persistence");
@@ -746,13 +881,38 @@ export interface RlmPreset {
746
881
  onSessionCreated?: (session: AgentSession) => void | Promise<void>;
747
882
  }
748
883
 
749
- interface RunRootCommandDependencies {
884
+ type RunRpcMode = (
885
+ session: AgentSession,
886
+ setToolUIContext?: CreateAgentSessionResult["setToolUIContext"],
887
+ options?: { listen?: string },
888
+ ) => Promise<void>;
889
+ type RunBridgeMode = (
890
+ session: AgentSession,
891
+ setToolUIContext?: CreateAgentSessionResult["setToolUIContext"],
892
+ ) => Promise<void>;
893
+ type RunPrintMode = (session: AgentSession, options: PrintModeOptions) => Promise<void>;
894
+
895
+ export interface RunRootCommandDependencies {
750
896
  createAgentSession?: typeof createAgentSession;
751
897
  discoverAuthStorage?: typeof discoverAuthStorage;
752
898
  runAcpMode?: (createSession: AcpSessionFactory) => Promise<void>;
753
899
  settings?: Settings;
754
900
  rlmPreset?: RlmPreset;
755
901
  suppressProcessExit?: boolean;
902
+ startupUpdate?: { check: () => Promise<string | undefined> };
903
+ initTheme?: typeof initTheme;
904
+ readPipedInput?: typeof readPipedInput;
905
+ runStartupCredentialAutoImportIfNeeded?: typeof runStartupCredentialAutoImportIfNeeded;
906
+ getChangelogForDisplay?: typeof getChangelogForDisplay;
907
+ runRpcMode?: RunRpcMode;
908
+ runBridgeMode?: RunBridgeMode;
909
+ createInteractiveMode?: CreateInteractiveMode;
910
+ runPrintMode?: RunPrintMode;
911
+ isResumePickerTerminal?: ResumePickerTerminalCheck;
912
+ listForResumePickerReadOnly?: ListForResumePickerReadOnly;
913
+ selectResumeSession?: SelectResumeSession;
914
+ openExistingSessionStrict?: OpenExistingSessionStrict;
915
+ initializeSettings?: typeof Settings.init;
756
916
  }
757
917
 
758
918
  export async function runRootCommand(
@@ -760,14 +920,73 @@ export async function runRootCommand(
760
920
  rawArgs: string[],
761
921
  deps: RunRootCommandDependencies = {},
762
922
  ): Promise<void> {
763
- logger.startTiming();
923
+ const parsedArgs = parsed;
924
+ let initialThemeInitialized = false;
925
+ let autoChdirApplied = false;
926
+ let bareResumeSessionManager: SessionManager | undefined;
927
+ let bareResumeAction: "continue-tail" | "open-idle" | undefined;
928
+
929
+ if (isBareResume(parsedArgs)) {
930
+ if (hasBareResumeConflict(parsedArgs)) {
931
+ process.stderr.write(`${BARE_RESUME_CONFLICT_ERROR}\n`);
932
+ if (!deps.suppressProcessExit) process.exitCode = 1;
933
+ return;
934
+ }
935
+ if (!isNormalLocalInteractiveRoute(parsedArgs) || !(deps.isResumePickerTerminal ?? hasResumePickerTerminal)()) {
936
+ process.stderr.write(`${BARE_RESUME_INTERACTIVE_ERROR}\n`);
937
+ if (!deps.suppressProcessExit) process.exitCode = 1;
938
+ return;
939
+ }
764
940
 
765
- // Initialize theme early with defaults (CLI commands need symbols)
766
- // Will be re-initialized with user preferences later
767
- await logger.time("initTheme:initial", initTheme);
941
+ logger.startTiming();
942
+ await logger.time("initTheme:initial", deps.initTheme ?? initTheme);
943
+ initialThemeInitialized = true;
768
944
 
769
- const parsedArgs = parsed;
770
- await logger.time("maybeAutoChdir", maybeAutoChdir, parsedArgs);
945
+ await logger.time("maybeAutoChdir", maybeAutoChdir, parsedArgs);
946
+ autoChdirApplied = true;
947
+ const resumeCwd = getProjectDir();
948
+ const sessions = await (deps.listForResumePickerReadOnly ?? SessionManager.listForResumePickerReadOnly)(
949
+ resumeCwd,
950
+ parsedArgs.sessionDir,
951
+ );
952
+ if (sessions.length === 0) {
953
+ process.stdout.write(`${chalk.dim("No sessions found")}\n`);
954
+ return;
955
+ }
956
+ const selection = await (deps.selectResumeSession ?? selectSession)(sessions);
957
+ if (selection.kind === "cancelled") {
958
+ return;
959
+ }
960
+ let opened: StrictSessionOpenResult;
961
+ try {
962
+ opened = await (deps.openExistingSessionStrict ?? SessionManager.openExistingStrict)(
963
+ selection.identity,
964
+ parsedArgs.sessionDir,
965
+ );
966
+ } catch {
967
+ process.stderr.write(`${BARE_RESUME_OPEN_ERROR}\n`);
968
+ if (!deps.suppressProcessExit) process.exitCode = 1;
969
+ return;
970
+ }
971
+ if (opened.kind === "error") {
972
+ process.stderr.write(`${BARE_RESUME_OPEN_ERROR}\n`);
973
+ if (!deps.suppressProcessExit) process.exitCode = 1;
974
+ return;
975
+ }
976
+ bareResumeSessionManager = opened.manager;
977
+ bareResumeAction = selection.action;
978
+ }
979
+
980
+ if (!initialThemeInitialized) {
981
+ logger.startTiming();
982
+ // Initialize theme early with defaults (CLI commands need symbols).
983
+ // It is re-initialized with user preferences later.
984
+ await logger.time("initTheme:initial", deps.initTheme ?? initTheme);
985
+ }
986
+
987
+ if (!autoChdirApplied) {
988
+ await logger.time("maybeAutoChdir", maybeAutoChdir, parsedArgs);
989
+ }
771
990
 
772
991
  const notifs: (InteractiveModeNotify | null)[] = [];
773
992
 
@@ -820,7 +1039,8 @@ export async function runRootCommand(
820
1039
  }
821
1040
 
822
1041
  const cwd = getProjectDir();
823
- const settingsInstance = deps.settings ?? (await logger.time("settings:init", Settings.init, { cwd }));
1042
+ const settingsInstance =
1043
+ deps.settings ?? (await logger.time("settings:init", deps.initializeSettings ?? Settings.init, { cwd }));
824
1044
  if (
825
1045
  parsedArgs.mode === "rpc" ||
826
1046
  parsedArgs.mode === "rpc-ui" ||
@@ -843,7 +1063,7 @@ export async function runRootCommand(
843
1063
  Bun.env.PI_NO_TITLE = "1";
844
1064
  }
845
1065
  const { pipedInput, fileText, fileImages } = await logger.time("prepareInitialMessage", async () => {
846
- const pipedInput = await readPipedInput();
1066
+ const pipedInput = await (deps.readPipedInput ?? readPipedInput)();
847
1067
  if (parsedArgs.fileArgs.length === 0) {
848
1068
  return { pipedInput, fileText: undefined, fileImages: undefined };
849
1069
  }
@@ -859,7 +1079,13 @@ export async function runRootCommand(
859
1079
  stdinContent: pipedInput,
860
1080
  });
861
1081
  const autoPrint = pipedInput !== undefined && !parsedArgs.print && parsedArgs.mode === undefined;
862
- const isInteractive = !parsedArgs.print && !autoPrint && parsedArgs.mode === undefined;
1082
+ const startupUpdateRoute = classifyStartupUpdateRoute(parsedArgs, autoPrint);
1083
+ const startupUpdate = new StartupUpdateOrchestrator(
1084
+ startupUpdateRoute,
1085
+ () => settingsInstance.get("startup.checkUpdate"),
1086
+ deps.startupUpdate?.check ?? (() => checkForNewVersion(VERSION)),
1087
+ );
1088
+ const isInteractive = startupUpdateRoute === "interactive";
863
1089
  const mode = parsedArgs.mode || "text";
864
1090
 
865
1091
  // Initialize discovery system with settings for provider persistence
@@ -879,7 +1105,7 @@ export async function runRootCommand(
879
1105
 
880
1106
  await logger.time(
881
1107
  "initTheme:final",
882
- initTheme,
1108
+ deps.initTheme ?? initTheme,
883
1109
  isInteractive,
884
1110
  settingsInstance.get("symbolPreset"),
885
1111
  settingsInstance.get("colorBlindMode"),
@@ -888,11 +1114,15 @@ export async function runRootCommand(
888
1114
  );
889
1115
 
890
1116
  const credentialAutoImportNotice = isInteractive
891
- ? await logger.time("credentialAutoImport", runStartupCredentialAutoImportIfNeeded, {
892
- authStorage,
893
- modelRegistry,
894
- agentDir: settingsInstance.getAgentDir(),
895
- })
1117
+ ? await logger.time(
1118
+ "credentialAutoImport",
1119
+ deps.runStartupCredentialAutoImportIfNeeded ?? runStartupCredentialAutoImportIfNeeded,
1120
+ {
1121
+ authStorage,
1122
+ modelRegistry,
1123
+ agentDir: settingsInstance.getAgentDir(),
1124
+ },
1125
+ )
896
1126
  : undefined;
897
1127
 
898
1128
  let scopedModels: ScopedModel[] = [];
@@ -910,29 +1140,11 @@ export async function runRootCommand(
910
1140
  );
911
1141
  }
912
1142
 
913
- // Create session manager based on CLI flags
914
- let sessionManager = await logger.time(
915
- "createSessionManager",
916
- createSessionManager,
917
- parsedArgs,
918
- cwd,
919
- settingsInstance,
920
- );
921
-
922
- // Handle --resume (no value): show session picker
923
- if (parsedArgs.resume === true && !parsedArgs.fork) {
924
- const sessions = await logger.time("SessionManager.list", SessionManager.list, cwd, parsedArgs.sessionDir);
925
- if (sessions.length === 0) {
926
- process.stdout.write(`${chalk.dim("No sessions found")}\n`);
927
- return;
928
- }
929
- const selectedPath = await logger.time("selectSession", selectSession, sessions);
930
- if (!selectedPath) {
931
- process.stdout.write(`${chalk.dim("No session selected")}\n`);
932
- return;
933
- }
934
- sessionManager = await SessionManager.open(selectedPath);
935
- }
1143
+ // Create session manager based on CLI flags. A bare resume was strictly opened
1144
+ // before startup discovery, so it never reaches create-or-open behavior here.
1145
+ const sessionManager =
1146
+ bareResumeSessionManager ??
1147
+ (await logger.time("createSessionManager", createSessionManager, parsedArgs, cwd, settingsInstance));
936
1148
 
937
1149
  // Restore the resumed session's working directory so the HUD branch, the
938
1150
  // project path, and the agent's tools all match where the session was
@@ -1114,8 +1326,9 @@ export async function runRootCommand(
1114
1326
 
1115
1327
  if (mode === "rpc" || mode === "rpc-ui") {
1116
1328
  const { RpcListenRefusedError, runRpcMode } = await import("./modes/rpc/rpc-mode");
1329
+ const runRpc = deps.runRpcMode ?? runRpcMode;
1117
1330
  try {
1118
- await runRpcMode(session, mode === "rpc-ui" ? setToolUIContext : undefined, {
1331
+ await runRpc(session, mode === "rpc-ui" ? setToolUIContext : undefined, {
1119
1332
  listen: parsedArgs.rpcListen,
1120
1333
  });
1121
1334
  } catch (error) {
@@ -1128,11 +1341,15 @@ export async function runRootCommand(
1128
1341
  process.exit(1);
1129
1342
  }
1130
1343
  } else if (mode === "bridge") {
1131
- const { runBridgeMode } = await import("./modes/bridge/bridge-mode");
1132
- await runBridgeMode(session, setToolUIContext);
1344
+ const runBridge = deps.runBridgeMode ?? (await import("./modes/bridge/bridge-mode")).runBridgeMode;
1345
+ await runBridge(session, setToolUIContext);
1133
1346
  } else if (isInteractive) {
1134
- const versionCheckPromise = checkForNewVersion(VERSION).catch(() => undefined);
1135
- const changelogMarkdown = await logger.time("main:getChangelogForDisplay", getChangelogForDisplay, parsedArgs);
1347
+ startupUpdate.startBeforeInteractiveInitialization();
1348
+ const changelogMarkdown = await logger.time(
1349
+ "main:getChangelogForDisplay",
1350
+ deps.getChangelogForDisplay ?? getChangelogForDisplay,
1351
+ parsedArgs,
1352
+ );
1136
1353
 
1137
1354
  const scopedModelsForDisplay = sessionOptions.scopedModels ?? scopedModels;
1138
1355
  if (scopedModelsForDisplay.length > 0) {
@@ -1158,7 +1375,7 @@ export async function runRootCommand(
1158
1375
  VERSION,
1159
1376
  changelogMarkdown,
1160
1377
  notifs,
1161
- versionCheckPromise,
1378
+ startupUpdate,
1162
1379
  parsedArgs.messages,
1163
1380
  setToolUIContext,
1164
1381
  lspServers,
@@ -1166,10 +1383,12 @@ export async function runRootCommand(
1166
1383
  eventBus,
1167
1384
  initialMessage,
1168
1385
  initialImages,
1386
+ deps.createInteractiveMode,
1387
+ bareResumeAction,
1169
1388
  );
1170
1389
  } else {
1171
- const { runPrintMode } = await import("./modes/print-mode");
1172
- await runPrintMode(session, {
1390
+ const runPrint = deps.runPrintMode ?? (await import("./modes/print-mode")).runPrintMode;
1391
+ await runPrint(session, {
1173
1392
  mode,
1174
1393
  messages: parsedArgs.messages,
1175
1394
  initialMessage,
@@ -1191,6 +1410,10 @@ export async function runRootCommand(
1191
1410
  }
1192
1411
 
1193
1412
  export async function main(args: string[]): Promise<void> {
1413
+ if (isTmuxOwnerIsolationCliArgv(args)) {
1414
+ await runTmuxOwnerIsolationCliFromStdin();
1415
+ return;
1416
+ }
1194
1417
  const { runCli } = await import("./cli");
1195
1418
  await runCli(args.length === 0 ? ["launch"] : args);
1196
1419
  }