pi-ui-extend 1.0.41 → 1.0.45

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 (236) hide show
  1. package/README.md +50 -13
  2. package/dist/app/app.d.ts +13 -0
  3. package/dist/app/app.js +158 -8
  4. package/dist/app/cli/install.js +2 -2
  5. package/dist/app/commands/command-controller.d.ts +4 -0
  6. package/dist/app/commands/command-controller.js +11 -0
  7. package/dist/app/commands/command-git-actions.d.ts +25 -0
  8. package/dist/app/commands/command-git-actions.js +381 -0
  9. package/dist/app/commands/command-host.d.ts +4 -0
  10. package/dist/app/commands/command-host.js +5 -2
  11. package/dist/app/commands/command-model-actions.d.ts +1 -0
  12. package/dist/app/commands/command-model-actions.js +57 -35
  13. package/dist/app/commands/command-navigation-actions.d.ts +1 -0
  14. package/dist/app/commands/command-navigation-actions.js +16 -3
  15. package/dist/app/commands/command-registry.d.ts +2 -0
  16. package/dist/app/commands/command-registry.js +15 -1
  17. package/dist/app/commands/command-session-actions.js +9 -3
  18. package/dist/app/commands/reload-context-inventory.d.ts +11 -0
  19. package/dist/app/commands/reload-context-inventory.js +70 -0
  20. package/dist/app/extensions/extension-actions-controller.d.ts +1 -0
  21. package/dist/app/extensions/extension-actions-controller.js +6 -0
  22. package/dist/app/extensions/subagent-catalog-state.d.ts +9 -0
  23. package/dist/app/extensions/subagent-catalog-state.js +23 -0
  24. package/dist/app/input/autocomplete-controller.js +61 -34
  25. package/dist/app/input/input-action-controller.d.ts +2 -0
  26. package/dist/app/input/input-action-controller.js +8 -1
  27. package/dist/app/input/input-controller.d.ts +2 -1
  28. package/dist/app/input/input-controller.js +7 -2
  29. package/dist/app/input/prompt-enhancer-controller.js +53 -35
  30. package/dist/app/input/voice-controller.d.ts +41 -45
  31. package/dist/app/input/voice-controller.js +351 -419
  32. package/dist/app/model/model-usage-controller.js +17 -7
  33. package/dist/app/model/model-usage-status.d.ts +4 -1
  34. package/dist/app/model/model-usage-status.js +270 -46
  35. package/dist/app/popup/menu-items-controller.d.ts +13 -3
  36. package/dist/app/popup/menu-items-controller.js +37 -21
  37. package/dist/app/popup/popup-action-controller.d.ts +12 -2
  38. package/dist/app/popup/popup-action-controller.js +77 -24
  39. package/dist/app/popup/popup-menu-controller.d.ts +36 -14
  40. package/dist/app/popup/popup-menu-controller.js +239 -69
  41. package/dist/app/rendering/dcp-stats.d.ts +9 -4
  42. package/dist/app/rendering/dcp-stats.js +40 -425
  43. package/dist/app/rendering/editor-panels.js +10 -4
  44. package/dist/app/rendering/popup-menu-renderer.d.ts +3 -5
  45. package/dist/app/rendering/popup-menu-renderer.js +42 -37
  46. package/dist/app/rendering/render-controller.js +23 -2
  47. package/dist/app/rendering/status-line-renderer.d.ts +5 -0
  48. package/dist/app/rendering/status-line-renderer.js +41 -5
  49. package/dist/app/rendering/tab-line-renderer.js +26 -20
  50. package/dist/app/runtime.d.ts +12 -1
  51. package/dist/app/runtime.js +120 -12
  52. package/dist/app/screen/mouse-controller.d.ts +2 -0
  53. package/dist/app/screen/mouse-controller.js +17 -7
  54. package/dist/app/screen/status-controller.d.ts +4 -0
  55. package/dist/app/screen/status-controller.js +5 -0
  56. package/dist/app/session/lazy-session-manager.js +34 -0
  57. package/dist/app/session/session-event-controller.d.ts +1 -0
  58. package/dist/app/session/session-event-controller.js +10 -1
  59. package/dist/app/session/session-history.d.ts +1 -0
  60. package/dist/app/session/session-history.js +12 -1
  61. package/dist/app/session/session-lifecycle-controller.d.ts +7 -1
  62. package/dist/app/session/session-lifecycle-controller.js +16 -1
  63. package/dist/app/session/tabs-controller.d.ts +26 -1
  64. package/dist/app/session/tabs-controller.js +454 -147
  65. package/dist/app/subagents/subagents-files.js +60 -1
  66. package/dist/app/subagents/subagents-model.d.ts +1 -0
  67. package/dist/app/subagents/subagents-model.js +18 -2
  68. package/dist/app/subagents/subagents-widget-controller.d.ts +1 -0
  69. package/dist/app/subagents/subagents-widget-controller.js +6 -0
  70. package/dist/app/types.d.ts +16 -1
  71. package/dist/app/workspace/workspace-actions-controller.js +10 -2
  72. package/dist/app/workspace/workspace-undo.d.ts +1 -0
  73. package/dist/app/workspace/workspace-undo.js +1 -0
  74. package/dist/bundled-extensions/question/index.js +9 -1
  75. package/dist/bundled-extensions/question/remote.d.ts +4 -0
  76. package/dist/bundled-extensions/question/remote.js +33 -0
  77. package/dist/bundled-extensions/telegram-connector/bot.d.ts +43 -0
  78. package/dist/bundled-extensions/telegram-connector/bot.js +166 -0
  79. package/dist/bundled-extensions/telegram-connector/config.d.ts +8 -0
  80. package/dist/bundled-extensions/telegram-connector/config.js +87 -0
  81. package/dist/bundled-extensions/telegram-connector/coordinator.d.ts +66 -0
  82. package/dist/bundled-extensions/telegram-connector/coordinator.js +413 -0
  83. package/dist/bundled-extensions/telegram-connector/index.d.ts +3 -0
  84. package/dist/bundled-extensions/telegram-connector/index.js +195 -0
  85. package/dist/bundled-extensions/terminal-bell/index.d.ts +0 -8
  86. package/dist/bundled-extensions/terminal-bell/index.js +0 -76
  87. package/dist/bundled-extensions/workspace-undo/index.d.ts +26 -0
  88. package/dist/bundled-extensions/workspace-undo/index.js +191 -0
  89. package/dist/config.d.ts +21 -2
  90. package/dist/config.js +159 -32
  91. package/dist/default-pix-config.js +25 -5
  92. package/dist/schemas/index.d.ts +1 -0
  93. package/dist/schemas/index.js +1 -0
  94. package/dist/schemas/pi-tools-suite-schema.d.ts +88 -62
  95. package/dist/schemas/pi-tools-suite-schema.js +54 -83
  96. package/dist/schemas/pix-schema.d.ts +19 -2
  97. package/dist/schemas/pix-schema.js +47 -5
  98. package/dist/schemas/tasks-schema.d.ts +18 -0
  99. package/dist/schemas/tasks-schema.js +41 -0
  100. package/docs/concurrency.md +9 -1
  101. package/docs/desktop-mvp.md +22 -6
  102. package/docs/desktop-task-manager.md +148 -84
  103. package/docs/release.md +30 -6
  104. package/external/pi-tools-suite/README.md +197 -84
  105. package/external/pi-tools-suite/docs/evals.md +1 -1
  106. package/external/pi-tools-suite/docs/session-recovery.md +47 -14
  107. package/external/pi-tools-suite/docs/subagent-model-pools.md +48 -30
  108. package/external/pi-tools-suite/docs/ui-qa-subagent.md +440 -0
  109. package/external/pi-tools-suite/package.json +6 -1
  110. package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +2 -1
  111. package/external/pi-tools-suite/src/antigravity-auth/constants.ts +9 -3
  112. package/external/pi-tools-suite/src/antigravity-auth/headers.ts +41 -3
  113. package/external/pi-tools-suite/src/antigravity-auth/models.ts +94 -21
  114. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +5 -17
  115. package/external/pi-tools-suite/src/antigravity-auth/payload.ts +72 -7
  116. package/external/pi-tools-suite/src/antigravity-auth/stream.ts +13 -1
  117. package/external/pi-tools-suite/src/async-subagents/agents/frontier-review.md +23 -0
  118. package/external/pi-tools-suite/src/async-subagents/agents/implement.md +1 -1
  119. package/external/pi-tools-suite/src/async-subagents/agents/presets.jsonc +16 -0
  120. package/external/pi-tools-suite/src/async-subagents/agents/research.md +5 -3
  121. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/browser.mjs +346 -0
  122. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/desktop.mjs +1038 -0
  123. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/tui.mjs +759 -0
  124. package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/scripts/browser-qa-runner.mjs +32 -23
  125. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/chrome-devtools/chrome-devtools-provider.mjs +895 -0
  126. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/linux/linux-atspi.py +494 -0
  127. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/macos/macos-accessibility.swift +1085 -0
  128. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/native-terminal/bridge-client.mjs +50 -0
  129. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/native-terminal/native-terminal-host.mjs +501 -0
  130. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/windows/windows-uia.ps1 +454 -0
  131. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-auth.md +114 -0
  132. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-chrome-devtools.md +91 -0
  133. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-playwright.md +145 -0
  134. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser.md +82 -0
  135. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-linux.md +26 -0
  136. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-macos.md +29 -0
  137. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-windows.md +23 -0
  138. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop.md +54 -0
  139. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui-native-terminal.md +59 -0
  140. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui-pty.md +37 -0
  141. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui.md +60 -0
  142. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/scripts/ui-qa-runner.mjs +535 -0
  143. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa.md +75 -0
  144. package/external/pi-tools-suite/src/async-subagents/commands.ts +15 -71
  145. package/external/pi-tools-suite/src/async-subagents/core/activity.ts +33 -0
  146. package/external/pi-tools-suite/src/async-subagents/core/agent-catalog.ts +5 -4
  147. package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +1 -1
  148. package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +8 -5
  149. package/external/pi-tools-suite/src/async-subagents/core/browser-qa.ts +16 -2
  150. package/external/pi-tools-suite/src/async-subagents/core/config.ts +160 -327
  151. package/external/pi-tools-suite/src/async-subagents/core/model-selection.ts +2 -1
  152. package/external/pi-tools-suite/src/async-subagents/core/registry.ts +158 -33
  153. package/external/pi-tools-suite/src/async-subagents/core/routing.ts +21 -5
  154. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +40 -26
  155. package/external/pi-tools-suite/src/async-subagents/core/state.ts +40 -0
  156. package/external/pi-tools-suite/src/async-subagents/core/types.ts +7 -0
  157. package/external/pi-tools-suite/src/async-subagents/core/ultrawork-auto.ts +51 -45
  158. package/external/pi-tools-suite/src/async-subagents/index.ts +69 -5
  159. package/external/pi-tools-suite/src/async-subagents/lib.ts +16 -9
  160. package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +8 -5
  161. package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +5 -4
  162. package/external/pi-tools-suite/src/async-subagents/types.ts +1 -0
  163. package/external/pi-tools-suite/src/coding-discipline/index.ts +90 -59
  164. package/external/pi-tools-suite/src/config.ts +55 -1
  165. package/external/pi-tools-suite/src/context-gateway/accounting-log.ts +282 -0
  166. package/external/pi-tools-suite/src/context-gateway/config.ts +77 -3
  167. package/external/pi-tools-suite/src/context-gateway/efficiency.ts +414 -0
  168. package/external/pi-tools-suite/src/context-gateway/enforcement.ts +222 -0
  169. package/external/pi-tools-suite/src/context-gateway/index.ts +206 -19
  170. package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +7 -7
  171. package/external/pi-tools-suite/src/context-gateway/telemetry.ts +101 -28
  172. package/external/pi-tools-suite/src/context-gateway/types.ts +16 -3
  173. package/external/pi-tools-suite/src/context-inventory.ts +99 -0
  174. package/external/pi-tools-suite/src/dcp/auto-compress-budget.ts +39 -4
  175. package/external/pi-tools-suite/src/dcp/auto-compress.ts +128 -49
  176. package/external/pi-tools-suite/src/dcp/commands.ts +32 -156
  177. package/external/pi-tools-suite/src/dcp/compress-tool.ts +177 -46
  178. package/external/pi-tools-suite/src/dcp/compression-blocks.ts +6 -58
  179. package/external/pi-tools-suite/src/dcp/compression-preview.ts +9 -0
  180. package/external/pi-tools-suite/src/dcp/compression-progress.ts +22 -0
  181. package/external/pi-tools-suite/src/dcp/config.ts +114 -17
  182. package/external/pi-tools-suite/src/dcp/conversation-index.ts +36 -9
  183. package/external/pi-tools-suite/src/dcp/diagnostics.ts +41 -0
  184. package/external/pi-tools-suite/src/dcp/fresh-tool-results.ts +38 -0
  185. package/external/pi-tools-suite/src/dcp/index.ts +338 -66
  186. package/external/pi-tools-suite/src/dcp/journal.ts +126 -4
  187. package/external/pi-tools-suite/src/dcp/progress-controller.ts +2 -1
  188. package/external/pi-tools-suite/src/dcp/prompts.ts +83 -192
  189. package/external/pi-tools-suite/src/dcp/protected-continuity.ts +175 -0
  190. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +93 -3
  191. package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +6 -10
  192. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +36 -33
  193. package/external/pi-tools-suite/src/dcp/pruner-tools.ts +6 -3
  194. package/external/pi-tools-suite/src/dcp/pruner.ts +1 -0
  195. package/external/pi-tools-suite/src/dcp/routine-pressure.ts +86 -0
  196. package/external/pi-tools-suite/src/dcp/state.ts +9 -0
  197. package/external/pi-tools-suite/src/dcp/statistics.d.ts +8 -0
  198. package/external/pi-tools-suite/src/dcp/statistics.js +156 -0
  199. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +78 -22
  200. package/external/pi-tools-suite/src/index.ts +7 -1
  201. package/external/pi-tools-suite/src/lib/project.ts +36 -1
  202. package/external/pi-tools-suite/src/model-tools/index.ts +10 -7
  203. package/external/pi-tools-suite/src/repo-discovery/index.ts +304 -4
  204. package/external/pi-tools-suite/src/resource-registry/index.ts +2551 -0
  205. package/external/pi-tools-suite/src/session-recovery/index.ts +17 -0
  206. package/external/pi-tools-suite/src/shell-command-policy.ts +219 -0
  207. package/external/pi-tools-suite/src/todo/index.ts +21 -0
  208. package/external/pi-tools-suite/src/todo/todo.ts +19 -1
  209. package/external/pi-tools-suite/src/tool-descriptions.ts +47 -19
  210. package/package.json +10 -9
  211. package/schemas/pi-tools-suite.json +466 -287
  212. package/schemas/pix.json +129 -11
  213. package/schemas/tasks.json +131 -0
  214. package/skills/simplify/SKILL.md +33 -5
  215. package/docs/desktop-markdown-media.md +0 -77
  216. package/external/pi-tools-suite/docs/browser-qa-subagent.md +0 -177
  217. package/external/pi-tools-suite/docs/dcp-emergency-current-turn.md +0 -102
  218. package/external/pi-tools-suite/src/async-subagents/agents/browser-qa.md +0 -598
  219. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +0 -54
  220. package/external/pi-tools-suite/src/skill-installer/index.ts +0 -333
  221. package/skills/playwright-cli/SKILL.md +0 -420
  222. package/skills/playwright-cli/references/element-attributes.md +0 -23
  223. package/skills/playwright-cli/references/playwright-tests.md +0 -50
  224. package/skills/playwright-cli/references/request-mocking.md +0 -87
  225. package/skills/playwright-cli/references/running-code.md +0 -241
  226. package/skills/playwright-cli/references/session-management.md +0 -273
  227. package/skills/playwright-cli/references/spec-driven-testing.md +0 -311
  228. package/skills/playwright-cli/references/storage-state.md +0 -290
  229. package/skills/playwright-cli/references/test-generation.md +0 -142
  230. package/skills/playwright-cli/references/tracing.md +0 -154
  231. package/skills/playwright-cli/references/video-recording.md +0 -147
  232. package/skills/spec-lite/SKILL.md +0 -140
  233. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/examples/qa-auth.example.jsonc +0 -0
  234. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/examples/qa-flow.example.jsonc +0 -0
  235. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/vendor/fflate.LICENSE +0 -0
  236. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/vendor/fflate.mjs +0 -0
@@ -32,6 +32,7 @@ export class AppTabsController {
32
32
  tabIdsNeedingHistoryReload = new Set();
33
33
  historyInvalidationGenerationByTabId = new Map();
34
34
  runtimeOwnershipGenerationByTabId = new Map();
35
+ touchedDraftTabIds = new Set();
35
36
  orphanRuntimeDisposals = new WeakSet();
36
37
  lifecycleMutationQueue = [];
37
38
  activeTabId;
@@ -42,6 +43,9 @@ export class AppTabsController {
42
43
  lifecycleGeneration = 0;
43
44
  lifecycleMutationRunning = false;
44
45
  saveTabsWriteTail = Promise.resolve();
46
+ startupSavedTabs;
47
+ startupPrepared = false;
48
+ startupRuntimePlan;
45
49
  restored = false;
46
50
  retentionCleanupRunning = false;
47
51
  retentionCleanupScheduled = false;
@@ -127,6 +131,22 @@ export class AppTabsController {
127
131
  this.syncActiveTabFromRuntime({ save: false });
128
132
  return this.activeTabId;
129
133
  }
134
+ isDraftTabActive() {
135
+ return this.activeTab()?.draft === true;
136
+ }
137
+ openSessionPaths() {
138
+ return this.tabItems.flatMap((tab) => tab.sessionPath ? [resolve(tab.sessionPath)] : []);
139
+ }
140
+ noteActiveDraftInputChanged() {
141
+ const tab = this.activeTab();
142
+ if (!tab?.draft)
143
+ return;
144
+ const input = this.host.captureInputState();
145
+ if (input.text.length === 0 && (input.attachments?.length ?? 0) === 0)
146
+ return;
147
+ this.touchedDraftTabIds.add(tab.id);
148
+ this.host.closeDraftSessionSelector?.();
149
+ }
130
150
  inputStateForTab(tabId) {
131
151
  if (!tabId)
132
152
  return this.host.captureInputState();
@@ -233,6 +253,26 @@ export class AppTabsController {
233
253
  if (options.save !== false)
234
254
  void this.saveTabs();
235
255
  }
256
+ async prepareStartupRuntime() {
257
+ if (this.startupPrepared)
258
+ return this.startupRuntimePlan ?? null;
259
+ this.startupPrepared = true;
260
+ if (this.host.options.noSession) {
261
+ this.startupRuntimePlan = {};
262
+ return this.startupRuntimePlan;
263
+ }
264
+ if (this.host.options.sessionPath) {
265
+ this.startupRuntimePlan = { sessionPath: resolve(this.host.options.sessionPath) };
266
+ return this.startupRuntimePlan;
267
+ }
268
+ const saved = await this.loadTabs();
269
+ this.startupSavedTabs = saved ?? null;
270
+ const paths = saved?.tabs.map((tab) => resolve(tab.path)).filter((path) => existsSync(path)) ?? [];
271
+ const activePath = saved?.activePath ? resolve(saved.activePath) : undefined;
272
+ const sessionPath = activePath && paths.includes(activePath) ? activePath : paths[0];
273
+ this.startupRuntimePlan = sessionPath ? { sessionPath } : null;
274
+ return this.startupRuntimePlan;
275
+ }
236
276
  restoreAfterStartup() {
237
277
  return this.runLifecycleMutation((generation) => this.restoreAfterStartupMutation(generation));
238
278
  }
@@ -241,15 +281,24 @@ export class AppTabsController {
241
281
  return;
242
282
  this.restored = true;
243
283
  const runtime = this.host.runtime();
244
- if (!runtime)
284
+ const saved = this.startupSavedTabs === undefined
285
+ ? await this.loadTabs()
286
+ : this.startupSavedTabs ?? undefined;
287
+ if (!runtime) {
288
+ if (this.host.options.noSession)
289
+ return;
290
+ const draft = this.createDraftTab();
291
+ this.tabItems.splice(0, this.tabItems.length, draft);
292
+ this.activateDraftTab(draft, { resetInput: true, openSelector: true });
293
+ await this.saveTabs();
245
294
  return;
295
+ }
246
296
  const startupSession = runtime.session;
247
297
  this.syncActiveTabFromRuntime({ save: false });
248
298
  if (this.host.options.noSession) {
249
299
  this.clearStartupTabPlaceholders();
250
300
  return;
251
301
  }
252
- const saved = await this.loadTabs();
253
302
  if (!this.isLifecycleOwner(generation, runtime, startupSession)) {
254
303
  this.syncActiveTabFromRuntime({ save: false, force: true });
255
304
  return;
@@ -350,6 +399,12 @@ export class AppTabsController {
350
399
  this.scheduleProjectSessionRetention();
351
400
  this.scheduleTabPrewarm();
352
401
  await this.loadActiveSessionHistory(restoredRuntime, generation);
402
+ if (restoredRuntime.session.messages.length === 0) {
403
+ await this.host.awaitCurrentSessionExtensions?.(restoredRuntime);
404
+ const text = this.host.contextInventoryText?.(restoredRuntime, `Opened an empty tab. cwd=${restoredRuntime.cwd}`);
405
+ if (text)
406
+ this.host.addEntry({ id: createId("system"), kind: "system", text });
407
+ }
353
408
  this.scheduleRestoredTabTitleRefresh(restoredSessionPaths, generation);
354
409
  }
355
410
  openNewTab() {
@@ -360,133 +415,181 @@ export class AppTabsController {
360
415
  this.host.showToast("Wait for the tab to finish loading", "info");
361
416
  return;
362
417
  }
363
- const runtime = this.runtimeForCommand("new_tab");
364
- if (!runtime)
418
+ if (this.host.options.noSession) {
419
+ this.host.showToast("/new_tab is unavailable with --no-session", "warning");
365
420
  return;
366
- if (!this.tabsAvailable(runtime))
421
+ }
422
+ const existingDraft = this.tabItems.find((tab) => tab.draft === true);
423
+ if (existingDraft) {
424
+ await this.switchToTabMutation(existingDraft.id, generation);
367
425
  return;
426
+ }
427
+ const runtime = this.host.runtime();
368
428
  this.cancelHistoryLoad();
369
- this.syncActiveTabFromRuntime();
370
- this.storeActiveSessionView();
429
+ if (runtime) {
430
+ this.syncActiveTabFromRuntime({ save: false });
431
+ this.storeActiveRuntime(runtime);
432
+ this.storeActiveSessionView();
433
+ }
371
434
  this.storeActiveInputState();
372
435
  this.storeActiveDeferredUserMessages();
373
- const previousTabId = this.activeTabId;
374
- const previousRuntime = runtime;
375
- const previousSession = runtime.session;
376
- const tab = {
377
- id: createId("tab"),
378
- title: "new",
379
- titlePlaceholder: "new",
380
- status: "active",
381
- activity: "thinking",
382
- };
436
+ const tab = this.createDraftTab();
383
437
  this.tabItems.push(tab);
384
- this.activeTabId = tab.id;
438
+ this.activateDraftTab(tab, { resetInput: true, openSelector: true });
439
+ await this.saveTabs();
440
+ }
441
+ materializeActiveDraftTab() {
442
+ return this.runLifecycleMutation((generation) => this.materializeActiveDraftTabMutation(generation));
443
+ }
444
+ async materializeActiveDraftTabMutation(generation) {
445
+ if (this.pendingActiveTabId)
446
+ return undefined;
447
+ const tab = this.activeTab();
448
+ if (!tab?.draft)
449
+ return this.host.runtime();
450
+ if (this.host.options.noSession) {
451
+ this.host.showToast("A persisted session is unavailable with --no-session", "warning");
452
+ return undefined;
453
+ }
454
+ this.storeActiveInputState();
455
+ this.storeActiveDeferredUserMessages();
385
456
  this.pendingActiveTabId = tab.id;
386
- this.clearTabAttention(tab);
387
- this.restoreInputState(tab.id);
388
- this.host.closeMenusForTabSwitch?.();
389
- this.host.resetSessionView();
390
- this.restoreDeferredUserMessages(tab.id);
457
+ this.host.closeDraftSessionSelector?.();
458
+ this.host.setStatus("starting new session");
391
459
  this.host.setSessionActivity("thinking");
392
- this.host.setStatus("starting new tab");
393
460
  this.host.render();
394
461
  let newRuntime;
395
462
  try {
396
- newRuntime = await this.host.createRuntimeForNewSession();
397
- }
398
- catch (error) {
399
- if (this.pendingActiveTabId === tab.id)
400
- this.pendingActiveTabId = undefined;
401
- this.removeTab(tab.id);
402
- this.activeTabId = previousTabId;
403
- if (previousTabId)
404
- this.restoreInputState(previousTabId);
405
- this.host.closeMenusForTabSwitch?.();
463
+ newRuntime = await this.host.createRuntimeForNewSession(this.host.draftModelOverrideRef?.());
464
+ if (!this.ownsDraftLifecycle(tab.id, generation)) {
465
+ await this.disposeRuntimeIfOrphan(newRuntime);
466
+ return undefined;
467
+ }
468
+ await this.host.activateRuntime(newRuntime, { awaitExtensions: false });
469
+ if (!this.ownsDraftLifecycle(tab.id, generation) || this.host.runtime() !== newRuntime) {
470
+ if (this.host.runtime() === newRuntime)
471
+ this.host.deactivateRuntimeForDraft?.();
472
+ await this.disposeRuntimeIfOrphan(newRuntime);
473
+ return undefined;
474
+ }
475
+ await this.host.awaitCurrentSessionExtensions?.(newRuntime);
476
+ if (!this.ownsDraftLifecycle(tab.id, generation) || this.host.runtime() !== newRuntime) {
477
+ if (this.host.runtime() === newRuntime)
478
+ this.host.deactivateRuntimeForDraft?.();
479
+ await this.disposeRuntimeIfOrphan(newRuntime);
480
+ return undefined;
481
+ }
482
+ this.pendingActiveTabId = undefined;
483
+ delete tab.draft;
484
+ delete tab.titlePlaceholder;
485
+ this.touchedDraftTabIds.delete(tab.id);
486
+ this.updateTabFromSession(tab, newRuntime.session);
487
+ this.setRuntimeForTab(tab.id, newRuntime);
488
+ this.host.closeDraftSessionSelector?.();
406
489
  this.host.resetSessionView();
407
- if (previousTabId)
408
- this.restoreDeferredUserMessages(previousTabId);
409
- this.host.loadSessionHistory();
410
- this.host.setSessionStatus(previousRuntime.session);
411
- this.host.setSessionActivity(this.sessionActivity(previousRuntime.session));
490
+ this.restoreDeferredUserMessages(tab.id);
491
+ this.host.setSessionStatus(newRuntime.session);
492
+ this.host.setSessionActivity(this.sessionActivity(newRuntime.session));
493
+ void this.saveTabs();
494
+ this.scheduleProjectSessionRetention();
412
495
  this.host.render();
413
- throw error;
496
+ return newRuntime;
414
497
  }
415
- if (!this.isLifecycleOwner(generation, previousRuntime, previousSession)) {
498
+ catch {
499
+ if (newRuntime && this.host.runtime() === newRuntime)
500
+ this.host.deactivateRuntimeForDraft?.();
501
+ if (newRuntime)
502
+ await this.disposeRuntimeIfOrphan(newRuntime);
416
503
  if (this.pendingActiveTabId === tab.id)
417
504
  this.pendingActiveTabId = undefined;
418
- this.removeTab(tab.id);
419
- this.activeTabId = previousTabId;
420
- await this.disposeRuntimeIfOrphan(newRuntime);
421
- this.syncActiveTabFromRuntime({ save: false, force: true });
422
- return;
505
+ if (this.tabItems.some((item) => item.id === tab.id) && this.activeTabId === tab.id) {
506
+ this.activateDraftTab(tab, { openSelector: !this.touchedDraftTabIds.has(tab.id) });
507
+ this.host.showToast("Could not start new conversation", "error");
508
+ }
509
+ return undefined;
423
510
  }
424
- const existingTab = this.findTabForSession(newRuntime.session);
425
- const targetTab = existingTab && existingTab.id !== tab.id ? existingTab : tab;
426
- if (targetTab !== tab)
427
- this.removeTab(tab.id);
428
- this.activeTabId = targetTab.id;
429
- this.pendingActiveTabId = targetTab.id;
430
- this.clearTabAttention(targetTab);
431
- this.updateTabFromSession(targetTab, newRuntime.session);
432
- this.restoreInputState(targetTab.id);
433
- this.host.resetSessionView();
434
- this.restoreDeferredUserMessages(targetTab.id);
511
+ }
512
+ openSessionInActiveDraftTab(sessionPath) {
513
+ return this.runLifecycleMutation((generation) => this.openSessionInActiveDraftTabMutation(sessionPath, generation));
514
+ }
515
+ async openSessionInActiveDraftTabMutation(sessionPath, generation) {
516
+ if (this.pendingActiveTabId)
517
+ return false;
518
+ const draft = this.activeTab();
519
+ if (!draft?.draft)
520
+ return false;
521
+ const resolvedSessionPath = resolve(this.host.options.cwd, sessionPath);
522
+ const existing = this.findTabBySessionPath(resolvedSessionPath);
523
+ if (existing) {
524
+ await this.switchDraftToPersistedTab(existing, generation);
525
+ if (this.activeTabId !== existing.id)
526
+ return false;
527
+ this.removeTab(draft.id);
528
+ this.inputStatesByTabId.delete(draft.id);
529
+ this.autoUserMessagesByTabId.delete(draft.id);
530
+ this.deferredUserMessagesByTabId.delete(draft.id);
531
+ this.touchedDraftTabIds.delete(draft.id);
532
+ void this.saveTabs();
533
+ return true;
534
+ }
535
+ this.cancelHistoryLoad();
536
+ this.storeActiveInputState();
537
+ this.pendingActiveTabId = draft.id;
538
+ this.host.closeDraftSessionSelector?.();
539
+ this.host.setStatus("opening session");
435
540
  this.host.setSessionActivity("thinking");
436
541
  this.host.render();
542
+ let targetRuntime;
437
543
  try {
438
- await this.host.activateRuntime(newRuntime, { awaitExtensions: false });
439
- }
440
- catch (error) {
441
- if (this.pendingActiveTabId === targetTab.id)
442
- this.pendingActiveTabId = undefined;
443
- if (targetTab === tab)
444
- this.removeTab(tab.id);
445
- this.activeTabId = previousTabId;
446
- if (this.host.runtime() !== previousRuntime) {
447
- try {
448
- await this.host.activateRuntime(previousRuntime, { awaitExtensions: false });
449
- }
450
- catch {
451
- // Keep the best available runtime and preserve the original activation error.
452
- }
544
+ targetRuntime = await this.host.createRuntimeForSession(resolvedSessionPath);
545
+ if (!this.ownsDraftLifecycle(draft.id, generation)) {
546
+ await this.disposeRuntimeIfOrphan(targetRuntime);
547
+ return false;
453
548
  }
454
- await this.disposeRuntimeIfOrphan(newRuntime);
455
- if (this.adoptCurrentRuntimeAfterFailedRollback(targetTab, newRuntime)) {
456
- this.host.setSessionStatus(newRuntime.session);
457
- this.host.setSessionActivity(this.sessionActivity(newRuntime.session));
458
- this.host.render();
459
- throw error;
549
+ await this.host.activateRuntime(targetRuntime, { awaitExtensions: false });
550
+ if (!this.ownsDraftLifecycle(draft.id, generation) || this.host.runtime() !== targetRuntime) {
551
+ if (this.host.runtime() === targetRuntime)
552
+ this.host.deactivateRuntimeForDraft?.();
553
+ await this.disposeRuntimeIfOrphan(targetRuntime);
554
+ return false;
460
555
  }
461
- if (previousTabId)
462
- this.restoreInputState(previousTabId);
556
+ this.pendingActiveTabId = undefined;
557
+ delete draft.draft;
558
+ delete draft.titlePlaceholder;
559
+ this.touchedDraftTabIds.delete(draft.id);
560
+ this.updateTabFromSession(draft, targetRuntime.session);
561
+ this.setRuntimeForTab(draft.id, targetRuntime);
562
+ this.inputStatesByTabId.delete(draft.id);
563
+ this.autoUserMessagesByTabId.delete(draft.id);
564
+ this.deferredUserMessagesByTabId.delete(draft.id);
565
+ this.restoreInputState(draft.id);
463
566
  this.host.closeMenusForTabSwitch?.();
464
- this.host.resetSessionView();
465
- if (previousTabId)
466
- this.restoreDeferredUserMessages(previousTabId);
467
- this.host.loadSessionHistory();
468
- this.host.setSessionStatus(this.host.runtime()?.session);
469
- this.host.setSessionActivity(this.sessionActivity(this.host.runtime()?.session));
470
- this.host.render();
471
- throw error;
567
+ void this.saveTabs();
568
+ this.scheduleProjectSessionRetention();
569
+ await this.loadActiveSessionHistory(targetRuntime, generation);
570
+ this.scheduleDelayedHistoryReload(draft.id, targetRuntime, generation);
571
+ return true;
572
+ }
573
+ catch {
574
+ if (targetRuntime && this.host.runtime() === targetRuntime)
575
+ this.host.deactivateRuntimeForDraft?.();
576
+ if (targetRuntime)
577
+ await this.disposeRuntimeIfOrphan(targetRuntime);
578
+ if (this.pendingActiveTabId === draft.id)
579
+ this.pendingActiveTabId = undefined;
580
+ if (this.tabItems.some((item) => item.id === draft.id) && this.activeTabId === draft.id) {
581
+ this.activateDraftTab(draft, { openSelector: true });
582
+ this.host.showToast("Could not open conversation", "warning");
583
+ }
584
+ return false;
472
585
  }
473
- if (this.pendingActiveTabId === targetTab.id)
474
- this.pendingActiveTabId = undefined;
475
- this.setRuntimeForTab(targetTab.id, newRuntime);
476
- void this.saveTabs();
477
- this.scheduleProjectSessionRetention();
478
- this.host.resetSessionView();
479
- this.restoreDeferredUserMessages(targetTab.id);
480
- this.host.addEntry({ id: createId("system"), kind: "system", text: `Opened a new tab. cwd=${newRuntime.cwd}` });
481
- if (newRuntime.modelFallbackMessage)
482
- this.host.addEntry({ id: createId("system"), kind: "system", text: newRuntime.modelFallbackMessage });
483
- for (const diag of newRuntime.diagnostics ?? []) {
484
- const kind = diag.type === "error" ? "error" : "system";
485
- this.host.addEntry({ id: createId("system"), kind, text: `[${diag.type}] ${diag.message}` });
486
- }
487
- this.host.setSessionStatus(newRuntime.session);
488
- this.host.setSessionActivity(this.sessionActivity(newRuntime.session));
489
- this.host.render();
586
+ }
587
+ ownsDraftLifecycle(tabId, generation) {
588
+ return this.host.isRunning()
589
+ && generation === this.lifecycleGeneration
590
+ && this.activeTabId === tabId
591
+ && this.pendingActiveTabId === tabId
592
+ && this.tabItems.some((tab) => tab.id === tabId && tab.draft === true);
490
593
  }
491
594
  openSessionInNewTab(sessionPath) {
492
595
  return this.runLifecycleMutation((generation) => this.openSessionInNewTabMutation(sessionPath, generation));
@@ -571,6 +674,7 @@ export class AppTabsController {
571
674
  this.host.render();
572
675
  try {
573
676
  await this.host.activateRuntime(newRuntime, { awaitExtensions: false });
677
+ await this.host.awaitCurrentSessionExtensions?.(newRuntime);
574
678
  }
575
679
  catch {
576
680
  this.pendingActiveTabId = undefined;
@@ -615,6 +719,11 @@ export class AppTabsController {
615
719
  void this.saveTabs();
616
720
  this.scheduleTabPrewarm();
617
721
  await this.loadActiveSessionHistory(newRuntime, generation);
722
+ if (newRuntime.session.messages.length === 0) {
723
+ const text = this.host.contextInventoryText?.(newRuntime, `Opened an empty tab. cwd=${newRuntime.cwd}`);
724
+ if (text)
725
+ this.host.addEntry({ id: createId("system"), kind: "system", text });
726
+ }
618
727
  return true;
619
728
  }
620
729
  forkSessionEntryInNewTab(entryId) {
@@ -763,17 +872,34 @@ export class AppTabsController {
763
872
  }
764
873
  if (tabId === this.activeTabId)
765
874
  return;
766
- const runtime = this.runtimeForCommand("tab switch");
767
- if (!runtime)
768
- return;
769
- this.syncActiveTabFromRuntime({ save: false });
770
875
  const target = this.tabItems.find((tab) => tab.id === tabId);
771
876
  if (!target)
772
877
  return;
878
+ if (target.draft) {
879
+ const runtime = this.host.runtime();
880
+ if (runtime) {
881
+ this.syncActiveTabFromRuntime({ save: false });
882
+ this.storeActiveRuntime(runtime);
883
+ this.storeActiveSessionView();
884
+ }
885
+ this.storeActiveInputState();
886
+ this.storeActiveDeferredUserMessages();
887
+ this.activateDraftTab(target, { openSelector: !this.touchedDraftTabIds.has(target.id) });
888
+ await this.saveTabs();
889
+ return;
890
+ }
773
891
  if (!target.sessionPath) {
774
892
  this.host.showToast("Tab has no persisted session path", "warning");
775
893
  return;
776
894
  }
895
+ if (this.activeTab()?.draft) {
896
+ await this.switchDraftToPersistedTab(target, generation);
897
+ return;
898
+ }
899
+ const runtime = this.runtimeForCommand("tab switch");
900
+ if (!runtime)
901
+ return;
902
+ this.syncActiveTabFromRuntime({ save: false });
777
903
  this.cancelHistoryLoad();
778
904
  const previousTabId = this.activeTabId;
779
905
  const previousRuntime = runtime;
@@ -858,6 +984,84 @@ export class AppTabsController {
858
984
  }
859
985
  this.scheduleDelayedHistoryReload(target.id, targetRuntime, generation);
860
986
  }
987
+ async switchDraftToPersistedTab(target, generation) {
988
+ const draft = this.activeTab();
989
+ if (!draft?.draft || !target.sessionPath)
990
+ return;
991
+ this.cancelHistoryLoad();
992
+ this.storeActiveInputState();
993
+ this.storeActiveDeferredUserMessages();
994
+ this.pendingActiveTabId = target.id;
995
+ const previousTargetActivity = target.activity;
996
+ target.activity = "thinking";
997
+ this.clearTabAttention(target);
998
+ this.host.closeDraftSessionSelector?.();
999
+ this.host.setStatus("switching tab");
1000
+ this.host.setSessionActivity("thinking");
1001
+ this.host.render();
1002
+ let targetRuntime;
1003
+ try {
1004
+ targetRuntime = await this.runtimeForTab(target);
1005
+ if (!targetRuntime)
1006
+ throw new Error("Could not load tab runtime");
1007
+ if (generation !== this.lifecycleGeneration
1008
+ || this.activeTabId !== draft.id
1009
+ || this.pendingActiveTabId !== target.id)
1010
+ throw new Error("Tab ownership changed while loading the runtime");
1011
+ await this.host.activateRuntime(targetRuntime, { awaitExtensions: false });
1012
+ }
1013
+ catch {
1014
+ if (this.pendingActiveTabId === target.id)
1015
+ this.pendingActiveTabId = undefined;
1016
+ if (previousTargetActivity === undefined)
1017
+ delete target.activity;
1018
+ else
1019
+ target.activity = previousTargetActivity;
1020
+ if (targetRuntime && this.host.runtime() === targetRuntime)
1021
+ this.host.deactivateRuntimeForDraft?.();
1022
+ if (targetRuntime)
1023
+ await this.disposeRuntimeIfOrphan(targetRuntime);
1024
+ this.activateDraftTab(draft, { openSelector: !this.touchedDraftTabIds.has(draft.id) });
1025
+ this.host.showToast("Could not switch tab", "warning");
1026
+ return;
1027
+ }
1028
+ if (this.pendingActiveTabId === target.id)
1029
+ this.pendingActiveTabId = undefined;
1030
+ this.activeTabId = target.id;
1031
+ this.clearTabAttention(target);
1032
+ this.updateTabFromSession(target, targetRuntime.session);
1033
+ this.setRuntimeForTab(target.id, targetRuntime);
1034
+ this.restoreInputState(target.id);
1035
+ this.host.closeMenusForTabSwitch?.();
1036
+ void this.saveTabs();
1037
+ this.scheduleTabPrewarm();
1038
+ const cachedView = this.sessionViewsByTabId.get(target.id);
1039
+ const cachedViewNeedsHistoryReload = this.tabIdsNeedingHistoryReload.has(target.id);
1040
+ const restoreCachedView = () => {
1041
+ if (!cachedView || !this.host.restoreSessionView)
1042
+ return false;
1043
+ if (this.activeTabId !== target.id || this.host.runtime() !== targetRuntime)
1044
+ return false;
1045
+ this.host.restoreSessionView(cachedView);
1046
+ this.restoreDeferredUserMessages(target.id);
1047
+ this.host.setSessionStatus(targetRuntime.session);
1048
+ this.host.setSessionActivity(this.sessionActivity(targetRuntime.session));
1049
+ this.host.render();
1050
+ return true;
1051
+ };
1052
+ if (cachedView && !cachedViewNeedsHistoryReload) {
1053
+ restoreCachedView();
1054
+ }
1055
+ else {
1056
+ const loaded = await this.loadActiveSessionHistory(targetRuntime, generation);
1057
+ // Returning from a transient draft must never strand a real tab on the
1058
+ // blank view used by the draft. If a stale-history refresh is cancelled
1059
+ // or fails, fall back to the snapshot captured when the draft opened.
1060
+ if (!loaded)
1061
+ restoreCachedView();
1062
+ }
1063
+ this.scheduleDelayedHistoryReload(target.id, targetRuntime, generation);
1064
+ }
861
1065
  closeTab(tabId) {
862
1066
  return this.runLifecycleMutation((generation) => this.closeTabMutation(tabId, generation));
863
1067
  }
@@ -869,6 +1073,13 @@ export class AppTabsController {
869
1073
  const index = this.tabItems.findIndex((tab) => tab.id === tabId);
870
1074
  if (index < 0)
871
1075
  return;
1076
+ const closingTab = this.tabItems[index];
1077
+ if (closingTab?.draft) {
1078
+ if (this.tabItems.length <= 1)
1079
+ return;
1080
+ await this.closeDraftTabMutation(closingTab, index, generation);
1081
+ return;
1082
+ }
872
1083
  this.cancelHistoryLoad();
873
1084
  if (this.tabItems.length <= 1) {
874
1085
  await this.replaceLastTabWithNewSession(tabId, generation);
@@ -956,6 +1167,48 @@ export class AppTabsController {
956
1167
  this.scheduleTabPrewarm();
957
1168
  await this.loadActiveSessionHistory(nextRuntime, generation);
958
1169
  }
1170
+ async closeDraftTabMutation(tab, index, generation) {
1171
+ this.cancelHistoryLoad();
1172
+ if (tab.id !== this.activeTabId) {
1173
+ this.removeTab(tab.id);
1174
+ this.inputStatesByTabId.delete(tab.id);
1175
+ this.autoUserMessagesByTabId.delete(tab.id);
1176
+ this.deferredUserMessagesByTabId.delete(tab.id);
1177
+ this.touchedDraftTabIds.delete(tab.id);
1178
+ await this.saveTabs();
1179
+ this.host.render();
1180
+ return;
1181
+ }
1182
+ const nextTab = this.tabItems[index + 1] ?? this.tabItems[index - 1];
1183
+ if (nextTab?.sessionPath) {
1184
+ await this.switchDraftToPersistedTab(nextTab, generation);
1185
+ if (this.activeTabId !== nextTab.id)
1186
+ return;
1187
+ this.removeTab(tab.id);
1188
+ this.inputStatesByTabId.delete(tab.id);
1189
+ this.autoUserMessagesByTabId.delete(tab.id);
1190
+ this.deferredUserMessagesByTabId.delete(tab.id);
1191
+ this.touchedDraftTabIds.delete(tab.id);
1192
+ await this.saveTabs();
1193
+ this.host.render();
1194
+ return;
1195
+ }
1196
+ this.removeTab(tab.id);
1197
+ this.inputStatesByTabId.delete(tab.id);
1198
+ this.autoUserMessagesByTabId.delete(tab.id);
1199
+ this.deferredUserMessagesByTabId.delete(tab.id);
1200
+ this.touchedDraftTabIds.delete(tab.id);
1201
+ this.activeTabId = undefined;
1202
+ this.host.closeDraftSessionSelector?.();
1203
+ this.host.deactivateRuntimeForDraft?.();
1204
+ this.host.restoreInputState({ text: "", cursor: 0 });
1205
+ this.host.resetSessionView();
1206
+ this.host.setSessionStatus(undefined);
1207
+ this.host.setSessionActivity("idle");
1208
+ this.host.setStatus("no conversation");
1209
+ await this.saveTabs();
1210
+ this.host.render();
1211
+ }
959
1212
  async replaceLastTabWithNewSession(tabId, generation) {
960
1213
  const tab = this.tabItems.find((item) => item.id === tabId);
961
1214
  if (!tab)
@@ -963,52 +1216,66 @@ export class AppTabsController {
963
1216
  const runtime = this.idleRuntime("new");
964
1217
  if (!runtime)
965
1218
  return;
966
- const session = runtime.session;
967
- this.activeTabId = tab.id;
968
- this.pendingActiveTabId = tab.id;
969
- this.host.setStatus("starting new session");
970
- this.host.render();
971
- let result;
972
- try {
973
- await this.host.awaitCurrentSessionExtensions?.(runtime);
974
- if (!this.isLifecycleOwner(generation, runtime, session))
1219
+ if (this.host.options.noSession) {
1220
+ const session = runtime.session;
1221
+ this.activeTabId = tab.id;
1222
+ this.pendingActiveTabId = tab.id;
1223
+ this.host.setStatus("starting new session");
1224
+ this.host.render();
1225
+ let result;
1226
+ try {
1227
+ await this.host.awaitCurrentSessionExtensions?.(runtime);
1228
+ if (!this.isLifecycleOwner(generation, runtime, session))
1229
+ return;
1230
+ result = await runtime.newSession();
1231
+ }
1232
+ finally {
1233
+ if (this.pendingActiveTabId === tab.id)
1234
+ this.pendingActiveTabId = undefined;
1235
+ }
1236
+ if (result.cancelled) {
1237
+ this.host.addEntry({ id: createId("system"), kind: "system", text: "New session cancelled." });
1238
+ this.host.setSessionStatus(runtime.session);
1239
+ this.host.setSessionActivity(this.sessionActivity(runtime.session));
1240
+ this.host.render();
975
1241
  return;
976
- result = await runtime.newSession();
977
- }
978
- finally {
979
- if (this.pendingActiveTabId === tab.id)
980
- this.pendingActiveTabId = undefined;
981
- }
982
- if (result.cancelled) {
983
- this.host.addEntry({ id: createId("system"), kind: "system", text: "New session cancelled." });
1242
+ }
1243
+ this.clearTabAttention(tab);
1244
+ this.updateTabFromSession(tab, runtime.session);
1245
+ this.setRuntimeForTab(tab.id, runtime);
1246
+ this.inputStatesByTabId.delete(tab.id);
1247
+ this.autoUserMessagesByTabId.delete(tab.id);
1248
+ this.deferredUserMessagesByTabId.delete(tab.id);
1249
+ this.restoreInputState(tab.id);
1250
+ this.host.closeMenusForTabSwitch?.();
1251
+ this.stopAttentionBlinkIfIdle();
1252
+ this.host.resetSessionView();
1253
+ this.restoreDeferredUserMessages(tab.id);
1254
+ this.host.addEntry({ id: createId("system"), kind: "system", text: `Started a new session. cwd=${runtime.cwd}` });
984
1255
  this.host.setSessionStatus(runtime.session);
985
1256
  this.host.setSessionActivity(this.sessionActivity(runtime.session));
1257
+ void this.saveTabs();
986
1258
  this.host.render();
987
1259
  return;
988
1260
  }
989
- this.clearTabAttention(tab);
990
- this.updateTabFromSession(tab, runtime.session);
991
- this.setRuntimeForTab(tab.id, runtime);
1261
+ if (!this.isLifecycleOwner(generation, runtime, runtime.session))
1262
+ return;
1263
+ this.host.deactivateRuntimeForDraft?.();
1264
+ this.deleteRuntimeForTab(tab.id);
1265
+ await this.host.disposeRuntime(runtime).catch(() => undefined);
1266
+ tab.title = "new";
1267
+ tab.titlePlaceholder = "new";
1268
+ tab.draft = true;
1269
+ delete tab.isFork;
1270
+ delete tab.sessionPath;
1271
+ tab.activity = "idle";
1272
+ this.touchedDraftTabIds.delete(tab.id);
992
1273
  this.inputStatesByTabId.delete(tab.id);
993
1274
  this.autoUserMessagesByTabId.delete(tab.id);
994
1275
  this.deferredUserMessagesByTabId.delete(tab.id);
995
- this.restoreInputState(tab.id);
996
- this.host.closeMenusForTabSwitch?.();
997
1276
  this.stopAttentionBlinkIfIdle();
998
- this.host.resetSessionView();
999
- this.restoreDeferredUserMessages(tab.id);
1000
- this.host.addEntry({ id: createId("system"), kind: "system", text: `Started a new session. cwd=${runtime.cwd}` });
1001
- if (runtime.modelFallbackMessage)
1002
- this.host.addEntry({ id: createId("system"), kind: "system", text: runtime.modelFallbackMessage });
1003
- for (const diag of runtime.diagnostics ?? []) {
1004
- const kind = diag.type === "error" ? "error" : "system";
1005
- this.host.addEntry({ id: createId("system"), kind, text: `[${diag.type}] ${diag.message}` });
1006
- }
1007
- this.host.setSessionStatus(runtime.session);
1008
- this.host.setSessionActivity(this.sessionActivity(runtime.session));
1009
- void this.saveTabs();
1010
- this.scheduleProjectSessionRetention();
1011
- this.host.render();
1277
+ this.activateDraftTab(tab, { resetInput: true, openSelector: true });
1278
+ await this.saveTabs();
1012
1279
  }
1013
1280
  async loadActiveSessionHistory(runtime, lifecycleGeneration) {
1014
1281
  const tabId = this.activeTabId;
@@ -1087,6 +1354,48 @@ export class AppTabsController {
1087
1354
  activeTab() {
1088
1355
  return this.activeTabId ? this.tabItems.find((tab) => tab.id === this.activeTabId) : undefined;
1089
1356
  }
1357
+ createDraftTab() {
1358
+ return {
1359
+ id: createId("tab"),
1360
+ title: "new",
1361
+ titlePlaceholder: "new",
1362
+ draft: true,
1363
+ status: "active",
1364
+ activity: "idle",
1365
+ };
1366
+ }
1367
+ activateDraftTab(tab, options = {}) {
1368
+ this.cancelHistoryLoad();
1369
+ // Draft activation must be atomic from the renderer's point of view.
1370
+ // Host cleanup (extension UI / voice / runtime teardown) may synchronously
1371
+ // render. While that is happening, keep the draft as the pending target so
1372
+ // tabs() cannot resync the old still-attached runtime back into activeTabId.
1373
+ this.pendingActiveTabId = tab.id;
1374
+ this.activeTabId = tab.id;
1375
+ this.clearTabAttention(tab);
1376
+ if (options.resetInput) {
1377
+ this.inputStatesByTabId.delete(tab.id);
1378
+ this.autoUserMessagesByTabId.delete(tab.id);
1379
+ this.deferredUserMessagesByTabId.delete(tab.id);
1380
+ this.touchedDraftTabIds.delete(tab.id);
1381
+ this.host.resetDraftModelSelection?.();
1382
+ }
1383
+ this.restoreInputState(tab.id);
1384
+ this.host.closeMenusForTabSwitch?.();
1385
+ this.host.resetSessionView();
1386
+ this.restoreDeferredUserMessages(tab.id);
1387
+ this.host.deactivateRuntimeForDraft?.();
1388
+ this.host.setSessionStatus(undefined);
1389
+ this.host.setSessionActivity("idle");
1390
+ this.host.setStatus("new conversation");
1391
+ if (options.openSelector !== false && !this.touchedDraftTabIds.has(tab.id))
1392
+ this.host.openDraftSessionSelector?.();
1393
+ else
1394
+ this.host.closeDraftSessionSelector?.();
1395
+ if (this.pendingActiveTabId === tab.id)
1396
+ this.pendingActiveTabId = undefined;
1397
+ this.host.render();
1398
+ }
1090
1399
  clearStartupTabPlaceholders() {
1091
1400
  for (const tab of this.tabItems) {
1092
1401
  delete tab.titlePlaceholder;
@@ -1832,8 +2141,6 @@ export class AppTabsController {
1832
2141
  }
1833
2142
  tabs.push(persistedTab);
1834
2143
  }
1835
- if (tabs.length === 0)
1836
- return Promise.resolve();
1837
2144
  const activePath = this.activeTab()?.sessionPath;
1838
2145
  const payload = JSON.stringify({
1839
2146
  version: TAB_STATE_VERSION,