@namzu/cli 30.0.0 → 33.0.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 (347) hide show
  1. package/CHANGELOG.md +261 -1
  2. package/dist/browser/commands.js +11 -7
  3. package/dist/browser/commands.js.map +1 -1
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +26 -2
  6. package/dist/cli.js.map +1 -1
  7. package/dist/commands/acp.d.ts +20 -2
  8. package/dist/commands/acp.d.ts.map +1 -1
  9. package/dist/commands/acp.js +42 -5
  10. package/dist/commands/acp.js.map +1 -1
  11. package/dist/commands/archive.d.ts +4 -0
  12. package/dist/commands/archive.d.ts.map +1 -0
  13. package/dist/commands/archive.js +62 -0
  14. package/dist/commands/archive.js.map +1 -0
  15. package/dist/commands/drain.d.ts.map +1 -1
  16. package/dist/commands/drain.js +2 -1
  17. package/dist/commands/drain.js.map +1 -1
  18. package/dist/commands/exec-json.d.ts.map +1 -1
  19. package/dist/commands/exec-json.js +24 -1
  20. package/dist/commands/exec-json.js.map +1 -1
  21. package/dist/commands/exec.d.ts.map +1 -1
  22. package/dist/commands/exec.js +2 -1
  23. package/dist/commands/exec.js.map +1 -1
  24. package/dist/commands/login.d.ts +1 -1
  25. package/dist/commands/login.d.ts.map +1 -1
  26. package/dist/commands/login.js +33 -11
  27. package/dist/commands/login.js.map +1 -1
  28. package/dist/commands/mcp-oauth-login.d.ts +19 -0
  29. package/dist/commands/mcp-oauth-login.d.ts.map +1 -0
  30. package/dist/commands/mcp-oauth-login.js +213 -0
  31. package/dist/commands/mcp-oauth-login.js.map +1 -0
  32. package/dist/commands/mcp.d.ts +3 -0
  33. package/dist/commands/mcp.d.ts.map +1 -0
  34. package/dist/commands/mcp.js +288 -0
  35. package/dist/commands/mcp.js.map +1 -0
  36. package/dist/commands/prior-messages.d.ts.map +1 -1
  37. package/dist/commands/prior-messages.js +42 -2
  38. package/dist/commands/prior-messages.js.map +1 -1
  39. package/dist/commands/schedule.d.ts.map +1 -1
  40. package/dist/commands/schedule.js +7 -5
  41. package/dist/commands/schedule.js.map +1 -1
  42. package/dist/commands/skills.d.ts.map +1 -1
  43. package/dist/commands/skills.js +60 -5
  44. package/dist/commands/skills.js.map +1 -1
  45. package/dist/commands/types.d.ts +2 -0
  46. package/dist/commands/types.d.ts.map +1 -1
  47. package/dist/commands/worktree.d.ts +6 -0
  48. package/dist/commands/worktree.d.ts.map +1 -0
  49. package/dist/commands/worktree.js +85 -0
  50. package/dist/commands/worktree.js.map +1 -0
  51. package/dist/config/composer-triggers.d.ts +41 -0
  52. package/dist/config/composer-triggers.d.ts.map +1 -0
  53. package/dist/config/composer-triggers.js +80 -0
  54. package/dist/config/composer-triggers.js.map +1 -0
  55. package/dist/config/load.d.ts.map +1 -1
  56. package/dist/config/load.js +58 -0
  57. package/dist/config/load.js.map +1 -1
  58. package/dist/config/schema.d.ts +34 -0
  59. package/dist/config/schema.d.ts.map +1 -1
  60. package/dist/config/schema.js.map +1 -1
  61. package/dist/config/user-config.d.ts +2 -0
  62. package/dist/config/user-config.d.ts.map +1 -1
  63. package/dist/config/user-config.js +76 -6
  64. package/dist/config/user-config.js.map +1 -1
  65. package/dist/context/doctrine.d.ts +3 -3
  66. package/dist/context/doctrine.d.ts.map +1 -1
  67. package/dist/context/doctrine.js +3 -3
  68. package/dist/context/doctrine.js.map +1 -1
  69. package/dist/context/runtime-message.d.ts.map +1 -1
  70. package/dist/context/runtime-message.js +4 -0
  71. package/dist/context/runtime-message.js.map +1 -1
  72. package/dist/doctor/checks/credentials.d.ts.map +1 -1
  73. package/dist/doctor/checks/credentials.js +2 -0
  74. package/dist/doctor/checks/credentials.js.map +1 -1
  75. package/dist/integrations/mcp/oauth-store.d.ts +40 -0
  76. package/dist/integrations/mcp/oauth-store.d.ts.map +1 -0
  77. package/dist/integrations/mcp/oauth-store.js +549 -0
  78. package/dist/integrations/mcp/oauth-store.js.map +1 -0
  79. package/dist/integrations/mcp/servers.d.ts +72 -5
  80. package/dist/integrations/mcp/servers.d.ts.map +1 -1
  81. package/dist/integrations/mcp/servers.js +298 -24
  82. package/dist/integrations/mcp/servers.js.map +1 -1
  83. package/dist/integrations/mcp/user-config.d.ts +25 -0
  84. package/dist/integrations/mcp/user-config.d.ts.map +1 -0
  85. package/dist/integrations/mcp/user-config.js +92 -0
  86. package/dist/integrations/mcp/user-config.js.map +1 -0
  87. package/dist/integrations/plugins/runtime.d.ts +11 -9
  88. package/dist/integrations/plugins/runtime.d.ts.map +1 -1
  89. package/dist/integrations/plugins/runtime.js +17 -36
  90. package/dist/integrations/plugins/runtime.js.map +1 -1
  91. package/dist/integrations/providers/credential-store.d.ts +8 -0
  92. package/dist/integrations/providers/credential-store.d.ts.map +1 -1
  93. package/dist/integrations/providers/credential-store.js +111 -22
  94. package/dist/integrations/providers/credential-store.js.map +1 -1
  95. package/dist/integrations/providers/discover.d.ts +11 -5
  96. package/dist/integrations/providers/discover.d.ts.map +1 -1
  97. package/dist/integrations/providers/discover.js +26 -12
  98. package/dist/integrations/providers/discover.js.map +1 -1
  99. package/dist/integrations/providers/index.d.ts +1 -1
  100. package/dist/integrations/providers/index.d.ts.map +1 -1
  101. package/dist/integrations/providers/index.js +1 -1
  102. package/dist/integrations/providers/index.js.map +1 -1
  103. package/dist/integrations/resident/session-step.d.ts.map +1 -1
  104. package/dist/integrations/resident/session-step.js +2 -1
  105. package/dist/integrations/resident/session-step.js.map +1 -1
  106. package/dist/integrations/sessions/store.d.ts +23 -5
  107. package/dist/integrations/sessions/store.d.ts.map +1 -1
  108. package/dist/integrations/sessions/store.js +184 -26
  109. package/dist/integrations/sessions/store.js.map +1 -1
  110. package/dist/integrations/subagents/NamzuCliAgent.d.ts +8 -0
  111. package/dist/integrations/subagents/NamzuCliAgent.d.ts.map +1 -0
  112. package/dist/integrations/subagents/NamzuCliAgent.js +9 -0
  113. package/dist/integrations/subagents/NamzuCliAgent.js.map +1 -0
  114. package/dist/integrations/subagents/runtime.d.ts +9 -6
  115. package/dist/integrations/subagents/runtime.d.ts.map +1 -1
  116. package/dist/integrations/subagents/runtime.js +108 -19
  117. package/dist/integrations/subagents/runtime.js.map +1 -1
  118. package/dist/integrations/subagents/worktree-driver.d.ts +17 -0
  119. package/dist/integrations/subagents/worktree-driver.d.ts.map +1 -0
  120. package/dist/integrations/subagents/worktree-driver.js +72 -0
  121. package/dist/integrations/subagents/worktree-driver.js.map +1 -0
  122. package/dist/integrations/web/search.d.ts.map +1 -1
  123. package/dist/integrations/web/search.js +8 -6
  124. package/dist/integrations/web/search.js.map +1 -1
  125. package/dist/integrations/worktrees/managed.d.ts +49 -0
  126. package/dist/integrations/worktrees/managed.d.ts.map +1 -0
  127. package/dist/integrations/worktrees/managed.js +231 -0
  128. package/dist/integrations/worktrees/managed.js.map +1 -0
  129. package/dist/permissions/rules.d.ts +5 -1
  130. package/dist/permissions/rules.d.ts.map +1 -1
  131. package/dist/permissions/rules.js +85 -3
  132. package/dist/permissions/rules.js.map +1 -1
  133. package/dist/schedule/build.d.ts +19 -2
  134. package/dist/schedule/build.d.ts.map +1 -1
  135. package/dist/schedule/build.js +119 -21
  136. package/dist/schedule/build.js.map +1 -1
  137. package/dist/schedule/changes.d.ts.map +1 -1
  138. package/dist/schedule/changes.js +13 -0
  139. package/dist/schedule/changes.js.map +1 -1
  140. package/dist/schedule/commands/add.d.ts +1 -1
  141. package/dist/schedule/commands/add.d.ts.map +1 -1
  142. package/dist/schedule/commands/add.js +186 -40
  143. package/dist/schedule/commands/add.js.map +1 -1
  144. package/dist/schedule/commands/list.d.ts +2 -0
  145. package/dist/schedule/commands/list.d.ts.map +1 -1
  146. package/dist/schedule/commands/list.js +64 -19
  147. package/dist/schedule/commands/list.js.map +1 -1
  148. package/dist/schedule/daemon/daemon.d.ts +1 -1
  149. package/dist/schedule/daemon/daemon.d.ts.map +1 -1
  150. package/dist/schedule/daemon/daemon.js +17 -5
  151. package/dist/schedule/daemon/daemon.js.map +1 -1
  152. package/dist/schedule/daemon/notify.d.ts +21 -1
  153. package/dist/schedule/daemon/notify.d.ts.map +1 -1
  154. package/dist/schedule/daemon/notify.js +43 -0
  155. package/dist/schedule/daemon/notify.js.map +1 -1
  156. package/dist/schedule/fire/fire.d.ts +1 -1
  157. package/dist/schedule/fire/fire.d.ts.map +1 -1
  158. package/dist/schedule/fire/fire.js +205 -14
  159. package/dist/schedule/fire/fire.js.map +1 -1
  160. package/dist/schedule/fire/result.d.ts +1 -1
  161. package/dist/schedule/fire/result.d.ts.map +1 -1
  162. package/dist/schedule/fire/result.js +2 -1
  163. package/dist/schedule/fire/result.js.map +1 -1
  164. package/dist/schedule/fire/run-script.d.ts +46 -0
  165. package/dist/schedule/fire/run-script.d.ts.map +1 -0
  166. package/dist/schedule/fire/run-script.js +81 -0
  167. package/dist/schedule/fire/run-script.js.map +1 -0
  168. package/dist/schedule/fire/unattended-note.d.ts +8 -0
  169. package/dist/schedule/fire/unattended-note.d.ts.map +1 -1
  170. package/dist/schedule/fire/unattended-note.js +5 -0
  171. package/dist/schedule/fire/unattended-note.js.map +1 -1
  172. package/dist/schedule/fire/wake-gate.d.ts +39 -0
  173. package/dist/schedule/fire/wake-gate.d.ts.map +1 -0
  174. package/dist/schedule/fire/wake-gate.js +90 -0
  175. package/dist/schedule/fire/wake-gate.js.map +1 -0
  176. package/dist/schedule/floor.d.ts +1 -1
  177. package/dist/schedule/floor.d.ts.map +1 -1
  178. package/dist/schedule/floor.js +288 -16
  179. package/dist/schedule/floor.js.map +1 -1
  180. package/dist/schedule/policy.d.ts +40 -0
  181. package/dist/schedule/policy.d.ts.map +1 -1
  182. package/dist/schedule/policy.js +36 -0
  183. package/dist/schedule/policy.js.map +1 -1
  184. package/dist/schedule/script-check.d.ts +53 -0
  185. package/dist/schedule/script-check.d.ts.map +1 -0
  186. package/dist/schedule/script-check.js +145 -0
  187. package/dist/schedule/script-check.js.map +1 -0
  188. package/dist/schedule/script-shell.d.ts +3 -0
  189. package/dist/schedule/script-shell.d.ts.map +1 -0
  190. package/dist/schedule/script-shell.js +5 -0
  191. package/dist/schedule/script-shell.js.map +1 -0
  192. package/dist/schedule/service/manifest.js +1 -1
  193. package/dist/schedule/service/manifest.js.map +1 -1
  194. package/dist/schedule/store/atomic.d.ts +5 -2
  195. package/dist/schedule/store/atomic.d.ts.map +1 -1
  196. package/dist/schedule/store/atomic.js +7 -4
  197. package/dist/schedule/store/atomic.js.map +1 -1
  198. package/dist/schedule/store/claims.js +1 -1
  199. package/dist/schedule/store/claims.js.map +1 -1
  200. package/dist/schedule/store/history.d.ts +1 -1
  201. package/dist/schedule/store/history.d.ts.map +1 -1
  202. package/dist/schedule/store/history.js +4 -1
  203. package/dist/schedule/store/history.js.map +1 -1
  204. package/dist/schedule/store/jobs.d.ts +19 -3
  205. package/dist/schedule/store/jobs.d.ts.map +1 -1
  206. package/dist/schedule/store/jobs.js +83 -16
  207. package/dist/schedule/store/jobs.js.map +1 -1
  208. package/dist/schedule/store/state.js +1 -1
  209. package/dist/schedule/store/state.js.map +1 -1
  210. package/dist/schedule/types.d.ts +71 -6
  211. package/dist/schedule/types.d.ts.map +1 -1
  212. package/dist/schedule/types.js +25 -1
  213. package/dist/schedule/types.js.map +1 -1
  214. package/dist/schedule/visible-source.d.ts +18 -0
  215. package/dist/schedule/visible-source.d.ts.map +1 -0
  216. package/dist/schedule/visible-source.js +59 -0
  217. package/dist/schedule/visible-source.js.map +1 -0
  218. package/dist/skills/audit.d.ts +31 -0
  219. package/dist/skills/audit.d.ts.map +1 -0
  220. package/dist/skills/audit.js +75 -0
  221. package/dist/skills/audit.js.map +1 -0
  222. package/dist/test-support/toolset.d.ts +23 -0
  223. package/dist/test-support/toolset.d.ts.map +1 -0
  224. package/dist/test-support/toolset.js +27 -0
  225. package/dist/test-support/toolset.js.map +1 -0
  226. package/dist/tui/App.d.ts.map +1 -1
  227. package/dist/tui/App.js +568 -106
  228. package/dist/tui/App.js.map +1 -1
  229. package/dist/tui/ChoicePicker.d.ts +2 -0
  230. package/dist/tui/ChoicePicker.d.ts.map +1 -1
  231. package/dist/tui/ChoicePicker.js +10 -2
  232. package/dist/tui/ChoicePicker.js.map +1 -1
  233. package/dist/tui/Composer.d.ts +62 -3
  234. package/dist/tui/Composer.d.ts.map +1 -1
  235. package/dist/tui/Composer.js +162 -25
  236. package/dist/tui/Composer.js.map +1 -1
  237. package/dist/tui/ComposerFrame.d.ts +1 -1
  238. package/dist/tui/ComposerFrame.js +3 -3
  239. package/dist/tui/ComposerFrame.js.map +1 -1
  240. package/dist/tui/EffortSlider.d.ts +8 -9
  241. package/dist/tui/EffortSlider.d.ts.map +1 -1
  242. package/dist/tui/EffortSlider.js +15 -12
  243. package/dist/tui/EffortSlider.js.map +1 -1
  244. package/dist/tui/Picker.d.ts.map +1 -1
  245. package/dist/tui/Picker.js +18 -3
  246. package/dist/tui/Picker.js.map +1 -1
  247. package/dist/tui/ResumePicker.d.ts +2 -1
  248. package/dist/tui/ResumePicker.d.ts.map +1 -1
  249. package/dist/tui/ResumePicker.js +3 -3
  250. package/dist/tui/ResumePicker.js.map +1 -1
  251. package/dist/tui/StatusBar.d.ts +12 -12
  252. package/dist/tui/StatusBar.d.ts.map +1 -1
  253. package/dist/tui/StatusBar.js +15 -15
  254. package/dist/tui/StatusBar.js.map +1 -1
  255. package/dist/tui/agent.d.ts +46 -7
  256. package/dist/tui/agent.d.ts.map +1 -1
  257. package/dist/tui/agent.js +399 -182
  258. package/dist/tui/agent.js.map +1 -1
  259. package/dist/tui/browser-notices.d.ts.map +1 -1
  260. package/dist/tui/browser-notices.js +6 -1
  261. package/dist/tui/browser-notices.js.map +1 -1
  262. package/dist/tui/credential-entry.d.ts +4 -29
  263. package/dist/tui/credential-entry.d.ts.map +1 -1
  264. package/dist/tui/credential-entry.js +7 -29
  265. package/dist/tui/credential-entry.js.map +1 -1
  266. package/dist/tui/hypermode.d.ts +37 -0
  267. package/dist/tui/hypermode.d.ts.map +1 -0
  268. package/dist/tui/hypermode.js +58 -0
  269. package/dist/tui/hypermode.js.map +1 -0
  270. package/dist/tui/login-prompt.d.ts +3 -1
  271. package/dist/tui/login-prompt.d.ts.map +1 -1
  272. package/dist/tui/login-prompt.js +11 -0
  273. package/dist/tui/login-prompt.js.map +1 -1
  274. package/dist/tui/permission-review.d.ts +1 -1
  275. package/dist/tui/permission-review.js +1 -1
  276. package/dist/tui/provider-list.d.ts.map +1 -1
  277. package/dist/tui/provider-list.js +3 -1
  278. package/dist/tui/provider-list.js.map +1 -1
  279. package/dist/tui/schedule/host-commands.d.ts.map +1 -1
  280. package/dist/tui/schedule/host-commands.js +94 -33
  281. package/dist/tui/schedule/host-commands.js.map +1 -1
  282. package/dist/tui/schedule/integration.d.ts +6 -2
  283. package/dist/tui/schedule/integration.d.ts.map +1 -1
  284. package/dist/tui/schedule/integration.js +3 -1
  285. package/dist/tui/schedule/integration.js.map +1 -1
  286. package/dist/tui/schedule/resume.d.ts.map +1 -1
  287. package/dist/tui/schedule/resume.js +10 -1
  288. package/dist/tui/schedule/resume.js.map +1 -1
  289. package/dist/tui/schedule/tool-host.d.ts +4 -1
  290. package/dist/tui/schedule/tool-host.d.ts.map +1 -1
  291. package/dist/tui/schedule/tool-host.js +155 -40
  292. package/dist/tui/schedule/tool-host.js.map +1 -1
  293. package/dist/tui/slashCommands.d.ts +33 -6
  294. package/dist/tui/slashCommands.d.ts.map +1 -1
  295. package/dist/tui/slashCommands.js +96 -34
  296. package/dist/tui/slashCommands.js.map +1 -1
  297. package/dist/tui/theme.d.ts +10 -4
  298. package/dist/tui/theme.d.ts.map +1 -1
  299. package/dist/tui/theme.js +4 -3
  300. package/dist/tui/theme.js.map +1 -1
  301. package/dist/tui/triggers/analyze.d.ts +57 -0
  302. package/dist/tui/triggers/analyze.d.ts.map +1 -0
  303. package/dist/tui/triggers/analyze.js +221 -0
  304. package/dist/tui/triggers/analyze.js.map +1 -0
  305. package/dist/tui/triggers/context-text.d.ts +21 -0
  306. package/dist/tui/triggers/context-text.d.ts.map +1 -0
  307. package/dist/tui/triggers/context-text.js +44 -0
  308. package/dist/tui/triggers/context-text.js.map +1 -0
  309. package/dist/tui/triggers/copy.d.ts +38 -0
  310. package/dist/tui/triggers/copy.d.ts.map +1 -0
  311. package/dist/tui/triggers/copy.js +163 -0
  312. package/dist/tui/triggers/copy.js.map +1 -0
  313. package/dist/tui/triggers/detect.d.ts +83 -0
  314. package/dist/tui/triggers/detect.d.ts.map +1 -0
  315. package/dist/tui/triggers/detect.js +295 -0
  316. package/dist/tui/triggers/detect.js.map +1 -0
  317. package/dist/tui/triggers/fold.d.ts +34 -0
  318. package/dist/tui/triggers/fold.d.ts.map +1 -0
  319. package/dist/tui/triggers/fold.js +44 -0
  320. package/dist/tui/triggers/fold.js.map +1 -0
  321. package/dist/tui/triggers/pattern.d.ts +66 -0
  322. package/dist/tui/triggers/pattern.d.ts.map +1 -0
  323. package/dist/tui/triggers/pattern.js +243 -0
  324. package/dist/tui/triggers/pattern.js.map +1 -0
  325. package/dist/tui/triggers/provenance.d.ts +58 -0
  326. package/dist/tui/triggers/provenance.d.ts.map +1 -0
  327. package/dist/tui/triggers/provenance.js +98 -0
  328. package/dist/tui/triggers/provenance.js.map +1 -0
  329. package/dist/tui/triggers/registry.d.ts +113 -0
  330. package/dist/tui/triggers/registry.d.ts.map +1 -0
  331. package/dist/tui/triggers/registry.js +164 -0
  332. package/dist/tui/triggers/registry.js.map +1 -0
  333. package/dist/tui/triggers/setting.d.ts +18 -0
  334. package/dist/tui/triggers/setting.d.ts.map +1 -0
  335. package/dist/tui/triggers/setting.js +61 -0
  336. package/dist/tui/triggers/setting.js.map +1 -0
  337. package/dist/tui/triggers/verbs.d.ts +29 -0
  338. package/dist/tui/triggers/verbs.d.ts.map +1 -0
  339. package/dist/tui/triggers/verbs.js +140 -0
  340. package/dist/tui/triggers/verbs.js.map +1 -0
  341. package/dist/tui/turn-interruption.d.ts.map +1 -1
  342. package/dist/tui/turn-interruption.js +4 -0
  343. package/dist/tui/turn-interruption.js.map +1 -1
  344. package/dist/tui/types.d.ts +3 -1
  345. package/dist/tui/types.d.ts.map +1 -1
  346. package/package.json +13 -12
  347. package/skills/schedule-task/SKILL.md +115 -3
package/dist/tui/App.js CHANGED
@@ -19,15 +19,16 @@ import { ToolOutputViewer } from './ToolOutputViewer.js';
19
19
  import { join, relative } from 'node:path';
20
20
  import { DEFAULT_MAX_GOAL_ROUNDS, DiskMessageFeedbackStore, GoalRoundLimitError, HostCommandRegistry, SessionGoalActivation, StaleGoalError, TurnCancelled, asSessionId, createAssistantMessage, createUserMessage, generateSessionId, generateTurnId, isCompactionMessage, kernelHostCommands, SKILL_TOOL_NAME, } from '@namzu/sdk';
21
21
  import { Box, Text, useApp, useInput, useStdout, useWindowSize } from 'ink';
22
- import { useCallback, useEffect, useRef, useState } from 'react';
22
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
23
23
  import { pinTrustedProjectPath, resolveTrustedProjectContext, } from '../config/trusted-project-context.js';
24
24
  import { visibleProjectInstructionPath } from '../context/project-path.js';
25
25
  import { runtimeContextLabel } from '../context/runtime-message.js';
26
26
  import { writeClipboardText } from '../integrations/clipboard/text.js';
27
27
  import { terminalNotificationEnabled, writeTerminalNotification, } from '../integrations/notifications/terminal.js';
28
- import { beginCodexDeviceLogin, beginSubscriptionLogin, clearAllStoredCredentials, clearStoredCodexCredential, clearStoredSubscriptionCredential, credentialsPath, parsePastedInput, primaryProvider, readStoredCodexCredential, readStoredSubscriptionCredential, signedInSubscriptionProviders, writePreferences, } from '../integrations/providers/index.js';
29
- import { activeConversationTurn, archiveConversation, forkConversation, forkConversationBeforeUser, listRecent, loadConversation, loadResumableConversation, openSessions, requireWritableConversation, setTitle, startConversation, titleOf, } from '../integrations/sessions/store.js';
28
+ import { beginCodexDeviceLogin, beginSubscriptionLogin, clearAllStoredCredentials, clearStoredCodexCredential, clearStoredGeminiApiKey as clearStoredGoogleApiKey, clearStoredSubscriptionCredential, credentialsPath, googleApiKeyPath, parsePastedInput, primaryProvider, readStoredCodexCredential, readStoredGeminiApiKey as readStoredGoogleApiKey, readStoredSubscriptionCredential, signedInSubscriptionProviders, writePreferences, writeStoredGeminiApiKey as writeStoredGoogleApiKey, } from '../integrations/providers/index.js';
29
+ import { ConversationIndexRefreshError, activeConversationTurn, archiveConversation, forkConversation, forkConversationBeforeUser, listArchived, listRecent, loadConversation, loadResumableConversation, openSessions, requireWritableConversation, setTitle, startConversation, titleOf, unarchiveConversation, } from '../integrations/sessions/store.js';
30
30
  import { conversationMarkdown, writeConversationExport, } from '../integrations/sessions/transcript-export.js';
31
+ import { openManagedWorktrees, worktreeOpenHint } from '../integrations/worktrees/managed.js';
31
32
  import { NARRATION_TOOL_NAME } from '../integrations/subagents/runtime.js';
32
33
  import { agentsSlashCommand } from '../integrations/subagents/slash.js';
33
34
  import { isTrusted, trustDir } from '../integrations/trust/store.js';
@@ -43,8 +44,15 @@ import { BrandHeader } from './BrandHeader.js';
43
44
  import { ChoicePicker } from './ChoicePicker.js';
44
45
  import { emptyPluginReport, pluginDetails, pluginOption, pluginStartupState } from './plugin-view.js';
45
46
  import { Composer, suggestionWindowSize, } from './Composer.js';
47
+ import { resolveComposerTriggers } from '../config/composer-triggers.js';
48
+ import { triggerContextTexts, triggerEffort } from './triggers/context-text.js';
49
+ import { transcriptTagLine } from './triggers/copy.js';
50
+ import { unavailable as triggerUnavailable } from './triggers/detect.js';
51
+ import { BUILTIN_TRIGGERS, compileRegistry, triggerDefinition, } from './triggers/registry.js';
52
+ import { describeComposerTriggers, setComposerTriggers } from './triggers/setting.js';
46
53
  import { ComposerFrame } from './ComposerFrame.js';
47
54
  import { EffortSlider, effortSliderLayout } from './EffortSlider.js';
55
+ import { HYPERMODE, HYPERMODE_SUMMARY, ORCHESTRATE_ALIAS_NOTICE, hypermodeEffort, hypermodeStopLabel, } from './hypermode.js';
48
56
  import { CopyPicker } from './CopyPicker.js';
49
57
  import { EditPromptPicker } from './EditPromptPicker.js';
50
58
  import { LiveActivity, formatElapsed } from './LiveActivity.js';
@@ -81,7 +89,7 @@ import { checklistInView, liveWindow, settledBeforeStreaming } from './live-wind
81
89
  import { ViewportBound } from './ViewportBound.js';
82
90
  import { resolveModelSwitch, } from './model-switch.js';
83
91
  import { parseModelSelectionIntent, resolveModelSelectionIntent } from './model-selection-intent.js';
84
- import { describeCodexDeviceLoginStart, describeLoginOutcome, describeLoginStart, describeLogout, describeProviderLogout, } from './login-prompt.js';
92
+ import { describeCodexDeviceLoginStart, describeLoginOutcome, describeLoginStart, describeAllCredentialsLogout, describeProviderLogout, } from './login-prompt.js';
85
93
  import { selectPrimaryProvider } from './provider-selection.js';
86
94
  import { eraseLastChoiceGrapheme } from './terminal-choice-text.js';
87
95
  import { renderCheckpoints, renderRestore } from '../checkpoints/store.js';
@@ -94,6 +102,7 @@ import { moveSelection } from './selection-window.js';
94
102
  import { describeShellEscape, describeShellEscapeForModel, runShellEscape, shellEscapeCommand, } from './shell-escape.js';
95
103
  import { baseBranchReviewPrompt, commitReviewPrompt, hostCommandNames, kernelCommandDescriptors, mergeHostCommands, renderAgents, renderOutcome, reviewPrompt, runSlash, } from './slashCommands.js';
96
104
  import { splitCompleteBlocks, splitSafeCut } from './stream-blocks.js';
105
+ import { terminalDisplayText } from './terminal-display.js';
97
106
  import { terminalSupportsHyperlinks } from './terminal-hyperlinks.js';
98
107
  import { theme } from './theme.js';
99
108
  import { useSelectionIndex } from './use-selection-index.js';
@@ -415,11 +424,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
415
424
  // A session setting layered above effort, not a level a provider publishes
416
425
  // — see docs/cli/slash-commands.md's /effort section. In-memory and
417
426
  // per-session like reasoningEffort above; no preferences file involved.
418
- const [orchestrateMode, setOrchestrateModeState] = useState(false);
419
- const orchestrateModeRef = useRef(false);
420
- const setOrchestrateMode = useCallback((next) => {
421
- orchestrateModeRef.current = next;
422
- setOrchestrateModeState(next);
427
+ const [hypermode, setHypermodeState] = useState(false);
428
+ const hypermodeRef = useRef(false);
429
+ const setHypermode = useCallback((next) => {
430
+ hypermodeRef.current = next;
431
+ setHypermodeState(next);
423
432
  }, []);
424
433
  const [activeSkills, setActiveSkills] = useState([]);
425
434
  // Read when the session comes up rather than on every keystroke: the
@@ -983,7 +992,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
983
992
  ask: (question, signal) => scheduleLiveRef.current?.ask(question, signal) ?? Promise.resolve({ kind: 'skip' }),
984
993
  askPermission: (request) => scheduleLiveRef.current?.askPermission(request) ??
985
994
  Promise.resolve({ kind: 'reject', feedback: 'Nobody can answer yet.' }),
986
- submit: (text) => scheduleLiveRef.current?.submit(text),
995
+ submit: (text, createdBy) => scheduleLiveRef.current?.submit(text, createdBy),
987
996
  });
988
997
  }
989
998
  /**
@@ -1158,9 +1167,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1158
1167
  // outlived the row it named would resolve `/expand 3` to output the operator
1159
1168
  // can no longer see anywhere, which is the failure this surface is being
1160
1169
  // cleaned of, one layer up.
1161
- const resetTranscript = useCallback(() => {
1170
+ const resetTranscript = useCallback((options = {}) => {
1162
1171
  if (stdout.isTTY)
1163
1172
  writeStdout('\x1b[2J\x1b[3J\x1b[H');
1173
+ if (!options.preserveOutputViewer)
1174
+ setOutputViewer(null);
1164
1175
  // The scrollback floor goes with the log it counted. <Static> is remounted
1165
1176
  // by the key below and has emitted nothing again; a floor that survived
1166
1177
  // would keep the next conversation's rows out of the live window.
@@ -1172,12 +1183,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1172
1183
  const previous = previousTerminalRef.current;
1173
1184
  previousTerminalRef.current = terminal;
1174
1185
  if (externalEditorRequestRef.current === null &&
1175
- (terminal.columns < previous.columns || terminal.rows < previous.rows)) {
1176
- // A contraction can reflow live rows into native scrollback before Ink
1177
- // receives resize. Its old row count cannot erase those rows. Rebuild
1178
- // Namzu's normal-buffer view at the committed size, retaining the same
1179
- // messages, draft and selected child. Regular frames remain incremental.
1180
- resetTranscript();
1186
+ (terminal.columns !== previous.columns || terminal.rows < previous.rows)) {
1187
+ // A contraction can move live rows into native scrollback before Ink
1188
+ // receives resize; expansion leaves already-printed Static rows wrapped
1189
+ // at their old width. Rebuild the normal-buffer transcript at the new
1190
+ // width, retaining messages, draft and selected child. Regular frames
1191
+ // remain incremental.
1192
+ resetTranscript({ preserveOutputViewer: true });
1181
1193
  }
1182
1194
  }, [terminal, resetTranscript]);
1183
1195
  const requestExternalEditor = useCallback((seed) => {
@@ -1297,6 +1309,46 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1297
1309
  const learningRef = useRef({ generation: -1, proposed: false, skillUsed: false });
1298
1310
  /** `/skills save off|on` in this session, over whatever the config files say. */
1299
1311
  const skillSuggestOverrideRef = useRef(undefined);
1312
+ /** `/config triggers on|off` in this session, over whatever the config files say. */
1313
+ const [composerTriggersOverride, setComposerTriggersOverride] = useState();
1314
+ const composerTriggerSettings = useMemo(() => resolveComposerTriggers(ctx.composerTriggers), [ctx.composerTriggers]);
1315
+ const composerTriggerRegistry = useMemo(() => compileRegistry(BUILTIN_TRIGGERS, {
1316
+ arming: composerTriggerSettings.arming,
1317
+ locales: composerTriggerSettings.languages,
1318
+ }), [composerTriggerSettings]);
1319
+ const composerTriggersOn = composerTriggersOverride ?? composerTriggerSettings.enabled;
1320
+ /**
1321
+ * Whether the model can run `skill-creator` here, which save-as-skill
1322
+ * needs. Read once for the tag row; read again when a turn starts and when
1323
+ * its follow-up would run, in case the skills changed meanwhile.
1324
+ */
1325
+ const readSkillCreator = useCallback(() => {
1326
+ try {
1327
+ const creator = discoverSkills({
1328
+ cwd: ctx.cwd,
1329
+ ...(ctx.skills ? { config: ctx.skills } : {}),
1330
+ }).find((skill) => skill.name === SKILL_CREATOR_SKILL);
1331
+ return Boolean(creator && !creator.problem && creator.invocation !== 'operator');
1332
+ }
1333
+ catch {
1334
+ return false;
1335
+ }
1336
+ }, [ctx.cwd, ctx.skills]);
1337
+ const skillCreatorAvailable = useMemo(() => readSkillCreator(), [readSkillCreator]);
1338
+ /** The session as the triggers see it right now, for a check at a turn's start or end. */
1339
+ const triggerContextNow = useCallback(() => ({
1340
+ permissionMode: permissionModeRef.current,
1341
+ sessionHypermode: hypermodeRef.current,
1342
+ effortMenu: (session?.reasoningEffortLevels?.length ?? 0) > 0,
1343
+ // The interactive session always mounts `Agent` and the `schedule`
1344
+ // tool; only scheduled runs withhold them, and those never read
1345
+ // composer triggers.
1346
+ agentTool: true,
1347
+ skillCreator: readSkillCreator(),
1348
+ scheduleTool: true,
1349
+ }), [readSkillCreator, session]);
1350
+ /** A turn's hand-off to its after-turn triggers; assigned below, where the queue lives. */
1351
+ const triggerFollowUpsRef = useRef(null);
1300
1352
  const conversationLearning = useCallback(() => {
1301
1353
  if (learningRef.current.generation !== conversationGenRef.current) {
1302
1354
  learningRef.current = {
@@ -1613,22 +1665,28 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1613
1665
  pushMessage('system', `Reasoning: ${effort} (this session).`);
1614
1666
  }, [hasUnsettledTurn, pushMessage, session, setReasoningEffort, state]);
1615
1667
  /**
1616
- * Turn the session's orchestrate mode on or off.
1668
+ * Turn the session's hypermode on or off.
1617
1669
  *
1618
1670
  * On: pins effort to the model's highest published level (last entry of
1619
1671
  * `reasoningEffortLevels`, which every provider publishes low-to-high) and
1620
- * strengthens delegation guidance for future turns via `SendOptions.orchestrate`
1672
+ * strengthens delegation guidance for future turns via `SendOptions.hypermode`
1621
1673
  * — see `applyReasoningEffort` above for the mirrored guard shape. When the
1622
1674
  * model publishes no exact menu, effort is left alone and the notice says
1623
1675
  * so explicitly rather than silently doing nothing.
1624
1676
  */
1625
- const applyOrchestrateMode = useCallback((enabled, selectedSession) => {
1677
+ /**
1678
+ * The effort in force before hypermode pinned the highest level, and the
1679
+ * level it pinned. Turning the mode off puts the effort back — unless the
1680
+ * operator chose another level meanwhile, which then stays.
1681
+ */
1682
+ const hypermodePinRef = useRef(null);
1683
+ const applyHypermode = useCallback((enabled, selectedSession) => {
1626
1684
  if (!session?.hasProvider) {
1627
- pushMessage('system', 'No active session — pick a provider before turning orchestrate mode on.');
1685
+ pushMessage('system', 'No active session — pick a provider before turning hypermode on.');
1628
1686
  return;
1629
1687
  }
1630
1688
  if (selectedSession !== undefined && selectedSession !== session) {
1631
- pushMessage('system', 'Orchestrate mode was not changed: the selected model is no longer active.');
1689
+ pushMessage('system', 'Hypermode was not changed: the selected model is no longer active.');
1632
1690
  return;
1633
1691
  }
1634
1692
  if (state !== 'idle' ||
@@ -1637,22 +1695,38 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1637
1695
  (queuedRef.current.length > 0 && selectedSession !== session) ||
1638
1696
  permissionResolveRef.current !== null ||
1639
1697
  compactingRef.current) {
1640
- pushMessage('system', 'Orchestrate mode was not changed: wait for the active turn, prompt, compaction, and queued work to settle.');
1698
+ pushMessage('system', 'Hypermode was not changed: wait for the active turn, prompt, compaction, and queued work to settle.');
1641
1699
  return;
1642
1700
  }
1643
- setOrchestrateMode(enabled);
1701
+ if (!enabled && !hypermodeRef.current) {
1702
+ pushMessage('system', 'Hypermode is off.');
1703
+ return;
1704
+ }
1705
+ setHypermode(enabled);
1644
1706
  if (!enabled) {
1645
- pushMessage('system', 'Orchestrate mode is off.');
1707
+ const pin = hypermodePinRef.current;
1708
+ hypermodePinRef.current = null;
1709
+ if (pin && reasoningEffortRef.current === pin.pinned) {
1710
+ setReasoningEffort(pin.before);
1711
+ pushMessage('system', `Hypermode is off — effort back to ${pin.before ?? 'the provider default'}.`);
1712
+ return;
1713
+ }
1714
+ pushMessage('system', 'Hypermode is off.');
1646
1715
  return;
1647
1716
  }
1648
- const highest = highestReasoningEffort(session.reasoningEffortLevels);
1649
- if (highest !== undefined) {
1650
- setReasoningEffort(highest);
1651
- pushMessage('system', `Orchestrate mode is on — effort pinned to ${highest} for ${session.modelSummary ?? 'this model'}, and delegation guidance is strengthened for this session.`);
1717
+ const pinned = hypermodeEffort(session.reasoningEffortLevels);
1718
+ if (pinned !== undefined) {
1719
+ // Turning it on twice keeps the effort from before the first time.
1720
+ if (!hypermodePinRef.current)
1721
+ hypermodePinRef.current = { before: reasoningEffortRef.current, pinned };
1722
+ else
1723
+ hypermodePinRef.current = { ...hypermodePinRef.current, pinned };
1724
+ setReasoningEffort(pinned);
1725
+ pushMessage('system', `Hypermode is on — effort pinned to ${pinned} for ${session.modelSummary ?? 'this model'}, and delegation guidance is strengthened for this session.`);
1652
1726
  return;
1653
1727
  }
1654
- pushMessage('system', `Orchestrate mode is on — ${session.modelSummary ?? 'this model'} does not publish an exact effort menu, so effort was left as is. Delegation guidance is still strengthened for this session.`);
1655
- }, [hasUnsettledTurn, pushMessage, session, setOrchestrateMode, setReasoningEffort, state]);
1728
+ pushMessage('system', `Hypermode is on — ${session.modelSummary ?? 'this model'} does not publish an exact effort menu, so effort was left as is. Delegation guidance is still strengthened for this session.`);
1729
+ }, [hasUnsettledTurn, pushMessage, session, setHypermode, setReasoningEffort, state]);
1656
1730
  const stepReasoningEffort = useCallback((direction) => {
1657
1731
  const levels = session?.reasoningEffortLevels;
1658
1732
  if (!session?.hasProvider || levels === undefined || levels.length === 0) {
@@ -1733,26 +1807,44 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1733
1807
  }
1734
1808
  }, [conversationLearning, ctx.cwd, ctx.skills, pushMessage]);
1735
1809
  const removeStoredCredential = useCallback((target) => {
1736
- const path = credentialsPath();
1810
+ const path = target === 'google' ? googleApiKeyPath() : credentialsPath();
1737
1811
  const hadClaude = readStoredSubscriptionCredential() !== null;
1738
1812
  const hadCodex = readStoredCodexCredential() !== null;
1813
+ const hadGoogleKey = readStoredGoogleApiKey() !== null;
1814
+ if (target === 'all') {
1815
+ const failures = [];
1816
+ try {
1817
+ clearAllStoredCredentials();
1818
+ }
1819
+ catch (error) {
1820
+ failures.push(`${credentialsPath()}: ${error instanceof Error ? error.message : String(error)}`);
1821
+ }
1822
+ try {
1823
+ clearStoredGoogleApiKey();
1824
+ }
1825
+ catch (error) {
1826
+ failures.push(`${googleApiKeyPath()}: ${error instanceof Error ? error.message : String(error)}`);
1827
+ }
1828
+ if (failures.length > 0) {
1829
+ pushMessage('system', `Some stored credentials could not be removed: ${failures.join('; ')}. Check both stores before retrying.`);
1830
+ return;
1831
+ }
1832
+ pushMessage('system', describeAllCredentialsLogout(path, googleApiKeyPath(), hadClaude || hadCodex || hadGoogleKey));
1833
+ return;
1834
+ }
1739
1835
  try {
1740
1836
  if (target === 'anthropic')
1741
1837
  clearStoredSubscriptionCredential();
1742
1838
  else if (target === 'codex')
1743
1839
  clearStoredCodexCredential();
1744
1840
  else
1745
- clearAllStoredCredentials();
1841
+ clearStoredGoogleApiKey();
1746
1842
  }
1747
1843
  catch (error) {
1748
1844
  pushMessage('system', `Could not remove ${path}: ${error instanceof Error ? error.message : String(error)}`);
1749
1845
  return;
1750
1846
  }
1751
- if (target === 'all') {
1752
- pushMessage('system', describeLogout(path, hadClaude || hadCodex));
1753
- return;
1754
- }
1755
- pushMessage('system', describeProviderLogout(path, target, target === 'anthropic' ? hadClaude : hadCodex));
1847
+ pushMessage('system', describeProviderLogout(path, target, target === 'anthropic' ? hadClaude : target === 'codex' ? hadCodex : hadGoogleKey));
1756
1848
  }, [pushMessage]);
1757
1849
  /** Publish one recoverable, read-only tombstone before leaving the TUI. */
1758
1850
  const archiveCurrentConversation = useCallback(() => {
@@ -1795,7 +1887,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1795
1887
  closeAndExit();
1796
1888
  }
1797
1889
  catch (error) {
1798
- pushMessage('system', `Could not archive this conversation: ${error instanceof Error ? error.message : String(error)}`);
1890
+ pushMessage('system', error instanceof ConversationIndexRefreshError
1891
+ ? error.message
1892
+ : `Could not archive this conversation: ${error instanceof Error ? error.message : String(error)}`);
1799
1893
  }
1800
1894
  finally {
1801
1895
  if (!archived && conversationMutationRef.current === 'archive') {
@@ -2214,8 +2308,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2214
2308
  removeStoredCredential(value);
2215
2309
  return;
2216
2310
  }
2217
- if (picker.kind === 'reasoning-effort' && value === ORCHESTRATE_MODE_VALUE) {
2218
- applyOrchestrateMode(!orchestrateModeRef.current, picker.selectedSession);
2311
+ if (picker.kind === 'reasoning-effort' && value === HYPERMODE_VALUE) {
2312
+ applyHypermode(!hypermodeRef.current, picker.selectedSession);
2219
2313
  return;
2220
2314
  }
2221
2315
  applyReasoningEffort(value, picker.selectedSession);
@@ -2223,7 +2317,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2223
2317
  activateSkill,
2224
2318
  advanceQueueContinuation,
2225
2319
  appLifetime.signal,
2226
- applyOrchestrateMode,
2320
+ applyHypermode,
2227
2321
  applyPermissionMode,
2228
2322
  applyReasoningEffort,
2229
2323
  archiveCurrentConversation,
@@ -2546,11 +2640,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2546
2640
  // A picker-owned provider/model change is one state transition. Clear the
2547
2641
  // old model's effort selection before publishing the replacement session
2548
2642
  // or releasing any paused queue. Failed and superseded candidates returned
2549
- // above, so they leave the current session selection untouched. Orchestrate
2643
+ // above, so they leave the current session selection untouched. Hypermode
2550
2644
  // mode survives the switch: re-pin to the new model's highest published
2551
2645
  // level instead of clearing, exactly as it pinned when first turned on.
2552
2646
  if (signal !== undefined) {
2553
- setReasoningEffort(orchestrateModeRef.current ? highestReasoningEffort(s.reasoningEffortLevels) : undefined);
2647
+ const repinned = hypermodeRef.current ? hypermodeEffort(s.reasoningEffortLevels) : undefined;
2648
+ // A new model starts at its own default; that is what turning
2649
+ // hypermode off returns to now.
2650
+ hypermodePinRef.current = repinned ? { before: undefined, pinned: repinned } : null;
2651
+ setReasoningEffort(repinned);
2554
2652
  }
2555
2653
  // Re-hydration (a provider switch via /model) builds a second session;
2556
2654
  // without this the first one's tool-server child processes stay alive
@@ -2559,9 +2657,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2559
2657
  void previousSessionRef.current?.close();
2560
2658
  previousSessionRef.current = s;
2561
2659
  setSession(s);
2562
- // Orchestrate mode already re-pinned effort above; reopening this picker
2660
+ // Hypermode already re-pinned effort above; reopening this picker
2563
2661
  // would ask the operator to redo a choice the mode just made for them.
2564
- if (!orchestrateModeRef.current && options.chooseReasoningEffort && s.reasoningEffortLevels?.length) {
2662
+ if (!hypermodeRef.current && options.chooseReasoningEffort && s.reasoningEffortLevels?.length) {
2565
2663
  // Own input before publishing ready or releasing a paused queue. The
2566
2664
  // menu closes only after choosing an effort or keeping the new default.
2567
2665
  setSelectedChoice(0);
@@ -2575,10 +2673,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2575
2673
  setMentionCandidates(files);
2576
2674
  });
2577
2675
  setCurrentProvider(primaryProvider(prefs).id);
2578
- // A picker-owned, usable provider switch is an explicit recovery from a
2579
- // paused failed turn. Failed and superseded candidates return above and
2580
- // therefore cannot release its dependent queue.
2581
- if (signal !== undefined)
2676
+ // Failed and superseded provider choices returned above. A usable
2677
+ // provider change can recover a failed turn's dependent queue, but a
2678
+ // paused turn still owns the conversation's durable active slot. Keep
2679
+ // that queue held until its checkpoint is resumed and settles.
2680
+ if (signal !== undefined && queuePauseRef.current?.outcome !== 'paused')
2582
2681
  advanceQueueContinuation();
2583
2682
  if (s.hasProvider) {
2584
2683
  setTranscriptOwned(true);
@@ -2971,9 +3070,6 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2971
3070
  // terminal scrollback; the live tail stays deliberately small so an activity
2972
3071
  // tick never repaints the whole conversation.
2973
3072
  //
2974
- useEffect(() => {
2975
- setOutputViewer(null);
2976
- }, [resetKey]);
2977
3073
  useEffect(() => {
2978
3074
  if (permission !== null)
2979
3075
  setOutputViewer(null);
@@ -3112,7 +3208,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3112
3208
  const scope = scopeRef.current;
3113
3209
  if (!sessions || !scope || !conversationMaterializedRef.current || !session?.hasProvider)
3114
3210
  return false;
3115
- const active = await activeConversationTurn(sessions, scope.sessionId).catch(() => undefined);
3211
+ const resumedSessionId = scope.sessionId;
3212
+ const active = await activeConversationTurn(sessions, resumedSessionId).catch(() => undefined);
3116
3213
  if (!active)
3117
3214
  return false;
3118
3215
  if (abortRef.current || state !== 'idle') {
@@ -3193,7 +3290,19 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3193
3290
  finalizeMessage(st.assistantId);
3194
3291
  // The resumed segment was appended to the log; the fold is the context
3195
3292
  // the next prompt continues from.
3196
- modelHistoryRef.current = await loadConversation(sessions, scope.sessionId);
3293
+ modelHistoryRef.current = await loadConversation(sessions, resumedSessionId);
3294
+ // A successfully completed checkpoint frees the conversation's active
3295
+ // slot. Only then may prompts queued behind the paused turn start. A
3296
+ // repeated pause, failed resume, unreadable log, or conversation switch
3297
+ // keeps dependent work held for an explicit operator decision.
3298
+ if (st.completed &&
3299
+ queuePauseRef.current?.outcome === 'paused' &&
3300
+ scopeRef.current?.sessionId === resumedSessionId &&
3301
+ (await activeConversationTurn(sessions, resumedSessionId)
3302
+ .then((next) => next === undefined)
3303
+ .catch(() => false))) {
3304
+ advanceQueueContinuation();
3305
+ }
3197
3306
  }
3198
3307
  catch (err) {
3199
3308
  pushMessage('system', `Could not resume the turn: ${err instanceof Error ? err.message : String(err)}`);
@@ -3210,7 +3319,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3210
3319
  await restoreBrowser?.().catch(() => undefined);
3211
3320
  }
3212
3321
  return true;
3213
- }, [detected, finalizeMessage, flushStream, pushMessage, session, setHandoffPark, state]);
3322
+ }, [advanceQueueContinuation, detected, finalizeMessage, flushStream, pushMessage, session, setHandoffPark, state]);
3214
3323
  // `namzu resume <id>` of a scheduled run parked on a decision: what the
3215
3324
  // notification and `/schedule` tell the operator to run. Continue it once
3216
3325
  // the screen is ready, exactly as `/resume` would, so the parked call is on
@@ -3258,6 +3367,35 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3258
3367
  pushMessage('system', `Could not list conversations: ${err instanceof Error ? err.message : String(err)}`);
3259
3368
  }
3260
3369
  }, [ensureSessions, pushMessage, resumeActiveTurn]);
3370
+ const doUnarchive = useCallback(async () => {
3371
+ if (state !== 'idle' ||
3372
+ abortRef.current !== null ||
3373
+ hasUnsettledTurn() ||
3374
+ queuedRef.current.length > 0 ||
3375
+ compactingRef.current ||
3376
+ exportingRef.current) {
3377
+ pushMessage('system', 'Finish the current turn and queued work before restoring a conversation.');
3378
+ return;
3379
+ }
3380
+ const sessions = sessionsRef.current ?? (await ensureSessions(), sessionsRef.current);
3381
+ if (!sessions) {
3382
+ pushMessage('system', 'Conversation history is unavailable in this folder.');
3383
+ return;
3384
+ }
3385
+ try {
3386
+ const archived = await listArchived(sessions, 100);
3387
+ if (archived.length === 0) {
3388
+ pushMessage('system', 'No archived conversations in this project.');
3389
+ return;
3390
+ }
3391
+ setResumeList(archived);
3392
+ setSelectedResume(0);
3393
+ setPhase('unarchive');
3394
+ }
3395
+ catch (err) {
3396
+ pushMessage('system', `Could not list archived conversations: ${err instanceof Error ? err.message : String(err)}`);
3397
+ }
3398
+ }, [ensureSessions, hasUnsettledTurn, pushMessage, state]);
3261
3399
  // Resolve a pending permission prompt with the user's decision and tear
3262
3400
  // down the overlay. No-op if nothing is pending.
3263
3401
  const resolvePermission = useCallback((decision) => {
@@ -3438,6 +3576,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3438
3576
  resumeCommittedRef.current = true;
3439
3577
  let msgs;
3440
3578
  try {
3579
+ await requireWritableConversation(sessions, conv.id, 'resume conversation');
3441
3580
  msgs = await loadConversation(sessions, conv.id);
3442
3581
  }
3443
3582
  catch (err) {
@@ -3507,6 +3646,42 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3507
3646
  resetTranscript,
3508
3647
  wakeGoalDriver,
3509
3648
  ]);
3649
+ const restoreArchivedConversation = useCallback(async (conv) => {
3650
+ const sessions = sessionsRef.current;
3651
+ if (!sessions) {
3652
+ setPhase('ready');
3653
+ return;
3654
+ }
3655
+ if (state !== 'idle' ||
3656
+ abortRef.current !== null ||
3657
+ hasUnsettledTurn() ||
3658
+ queuedRef.current.length > 0 ||
3659
+ compactingRef.current ||
3660
+ exportingRef.current) {
3661
+ setPhase('ready');
3662
+ pushMessage('system', 'Finish the current turn and queued work before restoring a conversation.');
3663
+ return;
3664
+ }
3665
+ resumeCommittedRef.current = true;
3666
+ try {
3667
+ await unarchiveConversation(sessions, conv.id);
3668
+ }
3669
+ catch (err) {
3670
+ resumeCommittedRef.current = false;
3671
+ setPhase('ready');
3672
+ pushMessage('system', err instanceof ConversationIndexRefreshError
3673
+ ? err.message
3674
+ : `Could not restore: ${err instanceof Error ? err.message : String(err)}`);
3675
+ return;
3676
+ }
3677
+ pushMessage('system', `Restored: ${conv.title}`);
3678
+ try {
3679
+ await resumeConversation(conv);
3680
+ }
3681
+ finally {
3682
+ resumeCommittedRef.current = false;
3683
+ }
3684
+ }, [hasUnsettledTurn, pushMessage, resumeConversation, state]);
3510
3685
  /**
3511
3686
  * Start a new conversation without deleting the one being left.
3512
3687
  *
@@ -3961,6 +4136,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3961
4136
  ...question.options.map((option) => ({
3962
4137
  label: option.label,
3963
4138
  description: option.description ?? '',
4139
+ // A badge, not label text: the SDK already took any
4140
+ // "(Recommended)" the model wrote out of the label.
4141
+ ...(option.recommended === true ? { recommended: true } : {}),
3964
4142
  })),
3965
4143
  ...(question.allowFreeText
3966
4144
  ? [{ label: 'Something else…', description: 'Answer in your own words' }]
@@ -4433,7 +4611,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4433
4611
  if (closing)
4434
4612
  pushMessage('system', closing, false, '✻', undefined, theme.text.muted);
4435
4613
  }
4436
- offerSkillSuggestion(st);
4614
+ // A save the operator asked for in words takes the place of
4615
+ // the proposal to save it.
4616
+ if (!triggerFollowUpsRef.current?.(st, st.completed ? 'completed' : 'stopped'))
4617
+ offerSkillSuggestion(st);
4437
4618
  break;
4438
4619
  }
4439
4620
  case 'paused':
@@ -4445,6 +4626,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4445
4626
  st.outcome = 'stopped';
4446
4627
  st.queuePauseOutcome = 'paused';
4447
4628
  st.notification = { kind: 'turn-settled', outcome: 'stopped' };
4629
+ triggerFollowUpsRef.current?.(st, 'paused');
4448
4630
  if (event.handoff) {
4449
4631
  // Nothing failed and nothing needs approving: a tool needs the
4450
4632
  // operator to do something first. Continuing is one key.
@@ -4459,12 +4641,14 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4459
4641
  `${describeTurnInterruption(event)}\nWhen that is done, press Enter to continue · Esc to stop.`, false, '‖');
4460
4642
  setHandoffPark({ turnId: event.turnId, reason: event.handoff.reason });
4461
4643
  }
4462
- else
4463
- pushMessage('system', describeTurnInterruption(event), false, '‖');
4644
+ else {
4645
+ pushMessage('system', `${describeTurnInterruption(event)}\nUse /resume to continue this checkpoint on the selected provider, or /abandon to start a new turn.`, false, '‖');
4646
+ }
4464
4647
  break;
4465
4648
  case 'error':
4466
4649
  closeAssistant();
4467
4650
  st.outcome = event.message === 'aborted' ? 'cancelled' : 'failed';
4651
+ triggerFollowUpsRef.current?.(st, st.outcome);
4468
4652
  if (event.message !== 'aborted') {
4469
4653
  st.notification = { kind: 'turn-settled', outcome: 'failed' };
4470
4654
  pushMessage('system', describeTurnInterruption(event), false, '!');
@@ -4581,11 +4765,29 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4581
4765
  // Reserved before the turn begins, so everything that refers to this
4582
4766
  // turn — its goal-round authority above all — names it from the start.
4583
4767
  const turnId = generateTurnId();
4768
+ // The composer triggers the operator armed for this message, checked
4769
+ // again now: the mode or the session may have changed while it waited
4770
+ // in the queue. None of them changes the operator's words.
4771
+ const requestedTriggers = prompt.kind === 'human' ? (prompt.triggers ?? []) : [];
4772
+ const triggerContext = triggerContextNow();
4773
+ const turnTriggers = requestedTriggers.filter((id) => triggerUnavailable(id, triggerContext) === undefined);
4774
+ const pinnedEffort = triggerEffort(turnTriggers, session.reasoningEffortLevels);
4584
4775
  if (goalRound) {
4585
4776
  pushMessage('system', `Goal round ${goalRound.round} / ${goalRound.maxGoalRounds}`, false, '◎', [`Objective: ${goalRound.objective}`]);
4586
4777
  }
4587
4778
  else {
4588
4779
  pushMessage('user', text, false, undefined, undefined, undefined, humanPromptMeta(attached.length, attachments));
4780
+ const tagLine = transcriptTagLine(turnTriggers, {
4781
+ effort: pinnedEffort,
4782
+ steered: false,
4783
+ });
4784
+ if (tagLine)
4785
+ pushMessage('system', tagLine, false, '✦', undefined, theme.accent.trigger);
4786
+ for (const id of requestedTriggers) {
4787
+ const reason = triggerUnavailable(id, triggerContext);
4788
+ if (reason)
4789
+ pushMessage('system', `${triggerDefinition(id).label} · not applied: ${reason}`, false, '✧', undefined, theme.text.muted);
4790
+ }
4589
4791
  }
4590
4792
  // A plan is scoped to the request it was made for. Cleared here, at
4591
4793
  // the start of the next one, rather than when the previous turn
@@ -4617,6 +4819,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4617
4819
  startedInPlanMode: permissionModeRef.current === 'plan',
4618
4820
  }
4619
4821
  : {}),
4822
+ followUps: turnTriggers.filter((id) => triggerDefinition(id).scope === 'after-turn'),
4823
+ contextTexts: triggerContextTexts(turnTriggers),
4620
4824
  };
4621
4825
  liveStreamRef.current = st;
4622
4826
  // A skill activated with /skills <name> stays in the prompt of every
@@ -4792,7 +4996,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4792
4996
  activeTurnInboxRef.current = inbox;
4793
4997
  const turnPermissionMode = permissionModeRef.current;
4794
4998
  const turnReasoningEffort = reasoningEffortRef.current;
4795
- const turnOrchestrateMode = orchestrateModeRef.current;
4999
+ const turnHypermode = hypermodeRef.current;
4796
5000
  const turnLimits = resolveTurnGuards(ctxRef.current.limits, turnLimitsOverrideRef.current);
4797
5001
  // Always carry the guarded callback. `auto` and `strict` decide before
4798
5002
  // calling it in makeResumeHandler; retaining it is what lets a session
@@ -4840,8 +5044,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4840
5044
  // while this turn runs, and the change governs what follows.
4841
5045
  currentPermissionMode: () => permissionModeRef.current,
4842
5046
  limits: turnLimits,
4843
- ...(turnReasoningEffort !== undefined ? { effort: turnReasoningEffort } : {}),
4844
- ...(turnOrchestrateMode ? { orchestrate: true } : {}),
5047
+ // A trigger that pins the highest effort does so for this
5048
+ // turn only; the session's own setting is untouched.
5049
+ ...((pinnedEffort ?? turnReasoningEffort) !== undefined
5050
+ ? { effort: pinnedEffort ?? turnReasoningEffort }
5051
+ : {}),
5052
+ ...(turnHypermode ? { hypermode: true } : {}),
5053
+ // Read at every iteration, so an after-turn trigger a steer
5054
+ // binds to this turn is said from the next one on.
5055
+ hostContext: () => st.contextTexts ?? [],
4845
5056
  ...(goalRound ? { goalRound } : {}),
4846
5057
  // The mode above decides whether this callback is consulted.
4847
5058
  onPermission: askPermission,
@@ -5055,6 +5266,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5055
5266
  sendTerminalNotification,
5056
5267
  session,
5057
5268
  setQueuePause,
5269
+ triggerContextNow,
5058
5270
  wakeGoalDriver,
5059
5271
  ]);
5060
5272
  const selectModelIntent = useCallback(async (query) => {
@@ -5176,7 +5388,31 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5176
5388
  pushMessage('system', `Could not save memory: ${err instanceof Error ? err.message : String(err)}`);
5177
5389
  }
5178
5390
  }, [ctx.cwd, pushMessage, session]);
5179
- const handleSubmit = useCallback((value, attachments, mode = 'submit') => {
5391
+ /**
5392
+ * Enter steered a message into the running turn. Its after-turn triggers
5393
+ * (save as skill) bind to that turn and are told to the model from its next
5394
+ * iteration; its turn triggers do not apply to a turn already running —
5395
+ * the tag row said so before Enter, and Tab would have queued it instead.
5396
+ */
5397
+ const bindSteeredTriggers = useCallback((ids) => {
5398
+ if (ids.length === 0)
5399
+ return;
5400
+ const live = liveStreamRef.current;
5401
+ const afterTurn = ids.filter((id) => triggerDefinition(id).scope === 'after-turn');
5402
+ const turnOnly = ids.filter((id) => triggerDefinition(id).scope === 'turn');
5403
+ if (live && afterTurn.length > 0) {
5404
+ live.followUps = [...new Set([...(live.followUps ?? []), ...afterTurn])];
5405
+ live.contextTexts = [
5406
+ ...new Set([...(live.contextTexts ?? []), ...triggerContextTexts(afterTurn)]),
5407
+ ];
5408
+ const line = transcriptTagLine(afterTurn, { effort: undefined, steered: true });
5409
+ if (line)
5410
+ pushMessage('system', line, false, '✦', undefined, theme.accent.trigger);
5411
+ }
5412
+ if (turnOnly.length > 0)
5413
+ pushMessage('system', `${turnOnly.map((id) => triggerDefinition(id).label).join(', ')} · not applied: Enter steered this message into the running turn. Tab queues a message as a new turn with it.`, false, '✧', undefined, theme.text.muted);
5414
+ }, [pushMessage]);
5415
+ const handleSubmit = useCallback((value, attachments, mode, meta) => {
5180
5416
  if (goalCommandInFlightRef.current) {
5181
5417
  pushMessage('system', 'A goal command is still reaching durable session state. Wait for its result before sending another command or prompt.');
5182
5418
  return;
@@ -5200,8 +5436,19 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5200
5436
  }
5201
5437
  // The operator moved on; a parked turn is still there for /resume.
5202
5438
  setHandoffPark(null);
5203
- setHistory((prev) => [...prev, value]);
5204
- const selectionIntent = !attachments?.length ? parseModelSelectionIntent(value) : undefined;
5439
+ // A loop is not something the operator typed here, so it is not a line
5440
+ // Up should bring back.
5441
+ const fromLoop = meta.source === 'operator-loop' || meta.source === 'model-loop';
5442
+ if (!fromLoop)
5443
+ setHistory((prev) => [...prev, value]);
5444
+ // What the model wrote is never the operator acting. A loop the model
5445
+ // made (`session_loop`) fires this path on a timer, for up to a week,
5446
+ // after one review of its creation — so its text must not reach the
5447
+ // host-side meanings of `/`, `!`, `#` or a model switch: `!` runs on
5448
+ // the host outside the sandbox with no review, and `#` writes memory
5449
+ // every later turn reads. It is sent as the prompt it looks like.
5450
+ const operatorText = meta.source !== 'model-loop';
5451
+ const selectionIntent = operatorText && !attachments?.length ? parseModelSelectionIntent(value) : undefined;
5205
5452
  if (selectionIntent) {
5206
5453
  void selectModelIntent(selectionIntent.query);
5207
5454
  return;
@@ -5209,12 +5456,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5209
5456
  // `#` remembers, `!` runs — neither is a prompt. Both are the
5210
5457
  // operator acting directly, the way other coding agents spell it,
5211
5458
  // and both leave a row the model reads on its next turn.
5212
- if (value.startsWith('#') && value.slice(1).trim().length > 0) {
5459
+ if (operatorText && value.startsWith('#') && value.slice(1).trim().length > 0) {
5213
5460
  const note = value.slice(1).trim();
5214
5461
  void rememberProjectNote(note);
5215
5462
  return;
5216
5463
  }
5217
- const escaped = shellEscapeCommand(value);
5464
+ const escaped = operatorText ? shellEscapeCommand(value) : null;
5218
5465
  if (escaped !== null) {
5219
5466
  pushMessage('user', value);
5220
5467
  const rowId = pushMessage('tool', `! ${escaped}`, true, '…');
@@ -5235,13 +5482,33 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5235
5482
  });
5236
5483
  return;
5237
5484
  }
5485
+ // Composer triggers (`./triggers/`). Only the composer's own submit
5486
+ // carries them, so nothing a loop, a picker or the model sends can
5487
+ // arm one. A message that is nothing but a trigger with a command of
5488
+ // its own (`bunu skill olarak kaydet`) runs that command here, in this
5489
+ // same call, so history holds the operator's words once; a trigger
5490
+ // that needs a task and got none is not applied, and says so.
5491
+ let armedTriggers = meta.source === 'composer' ? meta.triggers.map((trigger) => trigger.id) : [];
5492
+ let dispatch = value;
5493
+ if (meta.source === 'composer' && meta.onlyTriggers && armedTriggers.length > 0) {
5494
+ const command = armedTriggers
5495
+ .map((id) => triggerDefinition(id).standalone)
5496
+ .find((standalone) => standalone !== undefined);
5497
+ const needTask = armedTriggers.filter((id) => triggerDefinition(id).standalone === undefined);
5498
+ if (needTask.length > 0)
5499
+ pushMessage('system', `${needTask.map((id) => triggerDefinition(id).label).join(', ')} · not applied: the message has no task. Put the task in the same message${needTask.includes('hypermode') ? ', e.g. “hypermode fix the flaky test”' : ''}.`, false, '✧', undefined, theme.text.muted);
5500
+ armedTriggers = [];
5501
+ if (!command)
5502
+ return;
5503
+ dispatch = command;
5504
+ }
5238
5505
  // What actually gets sent. A `prompt` action replaces it with text the
5239
5506
  // command composed, and then takes the ordinary send path below —
5240
5507
  // including the queue — so a command-driven turn is not a second way
5241
5508
  // to run one.
5242
- let outgoing = value;
5509
+ let outgoing = dispatch;
5243
5510
  let skillFlow = false;
5244
- const slash = runSlash(value, slashCtx, hostCommands);
5511
+ const slash = operatorText ? runSlash(dispatch, slashCtx, hostCommands) : null;
5245
5512
  if (slash) {
5246
5513
  switch (slash.kind) {
5247
5514
  case 'message':
@@ -5380,6 +5647,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5380
5647
  description: `${session?.webSearchSummary ?? 'Not resolved'} · view tools, workspace and usage.`,
5381
5648
  },
5382
5649
  { name: 'setup', label: 'Provider setup', description: 'Check installed CLIs and access; connect or install without leaving Namzu.' },
5650
+ {
5651
+ name: `config triggers ${composerTriggersOn ? 'off' : 'on'}`,
5652
+ label: 'Composer triggers',
5653
+ description: composerTriggersOn
5654
+ ? 'On · hypermode and save-as-skill words act for their message. Select to turn off.'
5655
+ : 'Off · words in a message are only words. Select to turn on.',
5656
+ },
5383
5657
  {
5384
5658
  name: 'config sources',
5385
5659
  label: 'Setting sources',
@@ -5397,7 +5671,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5397
5671
  description: command.description,
5398
5672
  disabledReason: command.problem,
5399
5673
  })),
5400
- windowSize: 7,
5674
+ windowSize: 8,
5401
5675
  });
5402
5676
  return;
5403
5677
  }
@@ -5631,7 +5905,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5631
5905
  const values = [undefined, ...levels];
5632
5906
  const current = reasoningEffortRef.current;
5633
5907
  setSelectedChoice(Math.max(0, values.findIndex((value) => value === current)));
5634
- setChoicePicker(reasoningEffortPicker(session, current, false, orchestrateModeRef.current) ?? null);
5908
+ setChoicePicker(reasoningEffortPicker(session, current, false, hypermodeRef.current) ?? null);
5635
5909
  return;
5636
5910
  }
5637
5911
  case 'login':
@@ -5645,6 +5919,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5645
5919
  const choices = [
5646
5920
  ...(readStoredSubscriptionCredential() ? ['anthropic'] : []),
5647
5921
  ...(readStoredCodexCredential() ? ['codex'] : []),
5922
+ ...(readStoredGoogleApiKey() ? ['google'] : []),
5648
5923
  ];
5649
5924
  if (choices.length === 0) {
5650
5925
  removeStoredCredential('all');
@@ -5657,14 +5932,16 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5657
5932
  setSelectedChoice(0);
5658
5933
  setChoicePicker({
5659
5934
  kind: 'credential-logout',
5660
- title: 'Choose a stored subscription to remove',
5935
+ title: 'Choose a stored credential to remove',
5661
5936
  notice: 'Only credentials created by Namzu are listed. Device sessions owned by other tools are left alone.',
5662
5937
  values: choices,
5663
5938
  options: choices.map((choice) => ({
5664
- label: choice === 'anthropic' ? 'Claude' : 'Codex',
5939
+ label: choice === 'anthropic' ? 'Claude' : choice === 'codex' ? 'Codex' : 'Gemini API key',
5665
5940
  description: choice === 'anthropic'
5666
5941
  ? 'Remove only Namzu’s Claude subscription.'
5667
- : 'Remove only Namzu’s Codex subscription.',
5942
+ : choice === 'codex'
5943
+ ? 'Remove only Namzu’s Codex subscription.'
5944
+ : 'Remove only Namzu’s saved Gemini API key.',
5668
5945
  })),
5669
5946
  });
5670
5947
  return;
@@ -5821,12 +6098,80 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5821
6098
  case 'resume':
5822
6099
  void doResume();
5823
6100
  return;
6101
+ case 'unarchive':
6102
+ void doUnarchive();
6103
+ return;
5824
6104
  case 'title':
5825
6105
  void doTitle(slash.title, slash.clear);
5826
6106
  return;
5827
6107
  case 'fork':
5828
6108
  void doFork();
5829
6109
  return;
6110
+ case 'worktree': {
6111
+ const [verb = 'list', label, extra] = slash.args;
6112
+ const notice = (content) => pushMessage('system', terminalDisplayText(content));
6113
+ if (!((verb === 'list' && label === undefined) ||
6114
+ (verb === 'create' && extra === undefined) ||
6115
+ (verb === 'fork' && extra === undefined) ||
6116
+ (verb === 'resume' &&
6117
+ label !== undefined &&
6118
+ extra === undefined))) {
6119
+ pushMessage('system', 'Usage: /worktree [list|create [name]|fork [name]|resume <name>]');
6120
+ return;
6121
+ }
6122
+ if (verb === 'fork' &&
6123
+ (abortRef.current ||
6124
+ hasUnsettledTurn() ||
6125
+ queuedRef.current.length > 0)) {
6126
+ pushMessage('system', 'Wait for the current turn and queued prompts to finish before forking to a worktree.');
6127
+ return;
6128
+ }
6129
+ if (conversationMutationRef.current)
6130
+ return;
6131
+ if (verb === 'fork') {
6132
+ conversationMutationRef.current = 'fork';
6133
+ setConversationMutation('fork');
6134
+ }
6135
+ void (async () => {
6136
+ try {
6137
+ const manager = await openManagedWorktrees(ctx.cwd);
6138
+ if (verb === 'list') {
6139
+ const items = await manager.list();
6140
+ notice(items.length === 0
6141
+ ? 'No managed worktrees in this repository.'
6142
+ : items
6143
+ .map((item) => `${item.label}: ${item.path}${item.dirty ? ' (uncommitted files)' : ''}`)
6144
+ .join('\n'));
6145
+ }
6146
+ else if (verb === 'create') {
6147
+ const created = await manager.create(label);
6148
+ notice(`Created ${created.branch}. Open it in a new terminal: ${worktreeOpenHint(created.path)}${created.sourceDirty ? '\nUncommitted files stayed in this checkout.' : ''}`);
6149
+ }
6150
+ else if (verb === 'fork') {
6151
+ const scope = await materializeConversation();
6152
+ if (!scope)
6153
+ throw new Error('Conversation history is unavailable in this folder.');
6154
+ await persistenceTailRef.current;
6155
+ const forked = await manager.fork(scope.sessionId, label);
6156
+ notice(`Forked ${forked.copied} messages into ${forked.title}. Open the new checkout in a new terminal: ${worktreeOpenHint(forked.path, forked.conversationId)}${forked.sourceDirty ? '\nUncommitted files stayed in this checkout.' : ''}`);
6157
+ }
6158
+ else if (label) {
6159
+ const target = await manager.resume(label);
6160
+ notice(`Open ${label} in a new terminal: ${worktreeOpenHint(target.worktree.path, target.conversationId)}`);
6161
+ }
6162
+ }
6163
+ catch (error) {
6164
+ notice(`Worktree: ${error instanceof Error ? error.message : String(error)}`);
6165
+ }
6166
+ finally {
6167
+ if (verb === 'fork') {
6168
+ conversationMutationRef.current = null;
6169
+ setConversationMutation(null);
6170
+ }
6171
+ }
6172
+ })();
6173
+ return;
6174
+ }
5830
6175
  case 'restore':
5831
6176
  void doRestore(slash.turn);
5832
6177
  return;
@@ -6153,9 +6498,25 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6153
6498
  : 'Raw output mode off — rich transcript rendering restored.');
6154
6499
  return;
6155
6500
  }
6156
- case 'orchestrate-mode': {
6157
- const enabled = slash.enabled === 'toggle' ? !orchestrateModeRef.current : slash.enabled;
6158
- applyOrchestrateMode(enabled);
6501
+ case 'composer-triggers': {
6502
+ const profile = ctx.configDebug?.selectedProfile?.name;
6503
+ const cascade = { cwd: ctx.cwd, ...(profile ? { profile } : {}) };
6504
+ if (slash.setting === 'list') {
6505
+ pushMessage('system', describeComposerTriggers(composerTriggersOn, cascade));
6506
+ return;
6507
+ }
6508
+ const on = slash.setting === 'on';
6509
+ // The session follows the choice whatever the files say; the
6510
+ // message says whether a later start will too.
6511
+ setComposerTriggersOverride(on);
6512
+ pushMessage('system', setComposerTriggers(on, cascade));
6513
+ return;
6514
+ }
6515
+ case 'hypermode': {
6516
+ if (slash.via === 'orchestrate')
6517
+ pushMessage('system', ORCHESTRATE_ALIAS_NOTICE);
6518
+ const enabled = slash.enabled === 'toggle' ? !hypermodeRef.current : slash.enabled;
6519
+ applyHypermode(enabled);
6159
6520
  return;
6160
6521
  }
6161
6522
  case 'export-picker': {
@@ -6225,6 +6586,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6225
6586
  text: outgoing,
6226
6587
  ...(attachments && attachments.length > 0 ? { attachments: [...attachments] } : {}),
6227
6588
  ...(skillFlow ? { skillFlow: true } : {}),
6589
+ ...(armedTriggers.length > 0 && !skillFlow ? { triggers: armedTriggers } : {}),
6228
6590
  };
6229
6591
  // `/skills save` and `/skills new` are turns of their own: steered into
6230
6592
  // a running turn, they would lose the flag that keeps the skill-making
@@ -6242,8 +6604,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6242
6604
  message: createUserMessage(expanded.sendText, prompt.attachments),
6243
6605
  attachedFiles: expanded.attached.length,
6244
6606
  queueBoundary: queuedRef.current.length,
6245
- }))
6607
+ })) {
6608
+ bindSteeredTriggers(armedTriggers);
6246
6609
  return;
6610
+ }
6247
6611
  }
6248
6612
  }
6249
6613
  enqueueQueued(prompt);
@@ -6251,17 +6615,21 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6251
6615
  activeSkills,
6252
6616
  advanceQueueContinuation,
6253
6617
  applyPermissionMode,
6618
+ bindSteeredTriggers,
6619
+ composerTriggersOn,
6254
6620
  conversationLearning,
6255
6621
  applyReasoningEffort,
6256
6622
  appLifetime,
6257
6623
  ctx.cwd,
6258
6624
  doResume,
6625
+ doUnarchive,
6259
6626
  enqueueQueued,
6260
6627
  exitWithSummary,
6261
6628
  hasUnsettledTurn,
6262
6629
  goalActivation,
6263
6630
  goalStatus,
6264
6631
  hostCommands,
6632
+ materializeConversation,
6265
6633
  nextId,
6266
6634
  openAgentCockpit,
6267
6635
  runAgentsCommand,
@@ -6287,7 +6655,48 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6287
6655
  // picker is also used by review/export flows. Keep only this dispatch hop in
6288
6656
  // a ref so selecting a help row re-enters the one ordinary slash-command
6289
6657
  // path instead of growing a second command executor.
6290
- commandPickerSubmitRef.current = handleSubmit;
6658
+ commandPickerSubmitRef.current = (command) => handleSubmit(command, undefined, 'submit', { source: 'command-picker' });
6659
+ // A turn's after-turn triggers, when it ends. Save as skill runs exactly the
6660
+ // prompt `/skills save` sends, at the front of the queue, only when the
6661
+ // turn completed, did tool work, did not already save a skill, and saving
6662
+ // is still possible now. Otherwise one row says why, and `/skills save`
6663
+ // still works. Returns whether the turn had one, so the proposal to save
6664
+ // it is not also printed.
6665
+ triggerFollowUpsRef.current = (st, ending) => {
6666
+ const followUps = st.followUps ?? [];
6667
+ st.followUps = [];
6668
+ if (!followUps.includes('save-skill'))
6669
+ return false;
6670
+ const reason = ending === 'cancelled'
6671
+ ? 'the turn was cancelled'
6672
+ : ending === 'failed'
6673
+ ? 'the turn failed'
6674
+ : ending === 'paused'
6675
+ ? 'the turn paused'
6676
+ : ending === 'stopped'
6677
+ ? 'the turn stopped before it finished'
6678
+ : !st.learning || st.learning.steps < 1
6679
+ ? 'the turn did no tool work'
6680
+ : st.learning.savedSkill
6681
+ ? 'the turn already saved a skill'
6682
+ : triggerUnavailable('save-skill', triggerContextNow());
6683
+ if (reason) {
6684
+ pushMessage('system', `save as skill · not run: ${reason} · /skills save runs it anyway`, false, '✧', undefined, theme.text.muted);
6685
+ return true;
6686
+ }
6687
+ // The proposal was taken up, as `/skills save` records it.
6688
+ conversationLearning().proposed = true;
6689
+ const home = resolveNamzuHome();
6690
+ const ledger = readSuggestionLedger(home);
6691
+ if (ledger.unanswered > 0)
6692
+ writeSuggestionLedger(home, { ...ledger, unanswered: 0 });
6693
+ replaceQueued([
6694
+ { kind: 'human', text: learnSkillPrompt(), skillFlow: true },
6695
+ ...queuedRef.current,
6696
+ ]);
6697
+ pushMessage('system', 'save as skill · running /skills save for this turn', false, '✦', undefined, theme.accent.trigger);
6698
+ return true;
6699
+ };
6291
6700
  saveSkillNotifyRef.current = () => sendTerminalNotification({ kind: 'approval-required' });
6292
6701
  scheduleLiveRef.current = {
6293
6702
  ...(scheduleLiveRef.current?.model ? { model: scheduleLiveRef.current.model } : {}),
@@ -6297,7 +6706,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6297
6706
  },
6298
6707
  ask: askQuestion,
6299
6708
  askPermission: onPermission,
6300
- submit: (text) => handleSubmit(text),
6709
+ submit: (text, createdBy) => handleSubmit(text, undefined, 'submit', {
6710
+ source: createdBy === 'operator' ? 'operator-loop' : 'model-loop',
6711
+ }),
6301
6712
  };
6302
6713
  // Scheduled work since the TUI last looked: one line, after the first
6303
6714
  // paint, never blocking it. Loops fire when a turn ends, and on their timer.
@@ -6543,29 +6954,24 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6543
6954
  announce: true,
6544
6955
  chooseReasoningEffort: options.chooseReasoningEffort,
6545
6956
  });
6957
+ return !signal.aborted;
6546
6958
  }
6547
6959
  catch (err) {
6548
6960
  // A superseded choice no longer owns even its failure message. Its
6549
6961
  // eventual session object is disposed inside `hydrateSession`; a live
6550
6962
  // choice stays on the picker with the actionable construction error.
6551
6963
  if (signal.aborted)
6552
- return;
6964
+ return false;
6553
6965
  if (options.revealAllOnFailure) {
6554
6966
  setPickerDetected(null);
6555
6967
  setPickerSelectionKind('provider-and-model');
6556
6968
  setPickerInitialView('providers');
6557
6969
  }
6558
6970
  setPickerNotice(`Could not start the selected provider: ${err instanceof Error ? err.message : String(err)}`);
6971
+ return false;
6559
6972
  }
6560
6973
  }, [hydrateSession]);
6561
- /**
6562
- * A credential the operator typed. Held in memory for this process only.
6563
- *
6564
- * Deliberately does NOT call `writePreferences`: preferences are a file, and
6565
- * the whole contract of this entry point is that nothing lands on disk. The
6566
- * provider choice is not persisted either, because persisting it would leave
6567
- * a preference pointing at a credential that will not exist next launch.
6568
- */
6974
+ /** A typed Google API key is saved only after its provider session opens. */
6569
6975
  const handleTypedCredential = useCallback((credential, disposition, signal) => {
6570
6976
  const next = [credential, ...detected.filter((d) => d.entry.id !== credential.entry.id)];
6571
6977
  setDetected(next);
@@ -6581,7 +6987,32 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6581
6987
  const prefs = saved && primaryProvider(saved).id === credential.entry.id
6582
6988
  ? saved
6583
6989
  : selectPrimaryProvider(saved, { id: credential.entry.id });
6584
- void hydrateFromPicker(prefs, next, signal);
6990
+ void (async () => {
6991
+ const started = await hydrateFromPicker(prefs, next, signal);
6992
+ if (!started || credential.entry.id !== 'google')
6993
+ return;
6994
+ let path;
6995
+ try {
6996
+ path = writeStoredGoogleApiKey(credential.apiKey ?? '');
6997
+ }
6998
+ catch (error) {
6999
+ pushMessage('system', `Gemini is connected, but its API key could not be saved privately: ${error instanceof Error ? error.message : String(error)}. It is usable only in this session.`);
7000
+ return;
7001
+ }
7002
+ setDetected((current) => current.map((provider) => provider.entry.id === 'google' &&
7003
+ provider.source.kind === 'session' &&
7004
+ provider.apiKey === credential.apiKey
7005
+ ? { ...provider, source: { kind: 'stored-gemini-key', path } }
7006
+ : provider));
7007
+ try {
7008
+ writePreferences(prefs);
7009
+ }
7010
+ catch (error) {
7011
+ pushMessage('system', `Gemini API key was saved privately at ${path}, but the provider preference could not be saved: ${error instanceof Error ? error.message : String(error)}. Run /logout gemini to remove the key.`);
7012
+ return;
7013
+ }
7014
+ pushMessage('system', `Gemini API key saved privately at ${path} for future launches. Run /logout gemini to remove it.`);
7015
+ })();
6585
7016
  }, [detected, hydrateFromPicker, pushMessage]);
6586
7017
  const handlePickerSubmit = useCallback((selection, signal) => {
6587
7018
  const prefs = selectPrimaryProvider(savedPrefsRef.current, {
@@ -6688,7 +7119,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6688
7119
  return;
6689
7120
  }
6690
7121
  // Resume picker owns the keyboard while open.
6691
- if (phase === 'resume') {
7122
+ if (phase === 'resume' || phase === 'unarchive') {
6692
7123
  // Once a conversation is being read, the keyboard does nothing here.
6693
7124
  // The choice is already being acted on; a second Enter would start a
6694
7125
  // second read and an Esc would hand back a screen that is about to be
@@ -6705,7 +7136,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6705
7136
  else if (key.return) {
6706
7137
  const conv = resumeList[selectedResumeRef.current];
6707
7138
  if (conv)
6708
- void resumeConversation(conv);
7139
+ void (phase === 'unarchive' ? restoreArchivedConversation(conv) : resumeConversation(conv));
6709
7140
  }
6710
7141
  else if (key.escape || (key.ctrl && input === 'c'))
6711
7142
  setPhase('ready');
@@ -7226,6 +7657,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
7226
7657
  const lifecycleOwnsViewport = outputViewer !== null ||
7227
7658
  phase === 'trust' ||
7228
7659
  phase === 'resume' ||
7660
+ phase === 'unarchive' ||
7229
7661
  phase === 'edit' ||
7230
7662
  phase === 'picker' ||
7231
7663
  agentSurface !== null;
@@ -7292,6 +7724,24 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
7292
7724
  : `Session: ${permissionSource}`
7293
7725
  : undefined;
7294
7726
  const displayedPermissionMode = effectivePermissionMode(permissionMode, session?.approvalLatched() ?? false);
7727
+ const highestEffort = highestReasoningEffort(session?.reasoningEffortLevels);
7728
+ const hypermodeLevel = hypermodeEffort(session?.reasoningEffortLevels);
7729
+ const composerTriggers = composerTriggersOn
7730
+ ? {
7731
+ registry: composerTriggerRegistry,
7732
+ suggest: composerTriggerSettings.suggest,
7733
+ context: {
7734
+ permissionMode: displayedPermissionMode,
7735
+ sessionHypermode: hypermode,
7736
+ effortMenu: highestEffort !== undefined,
7737
+ agentTool: true,
7738
+ skillCreator: skillCreatorAvailable,
7739
+ scheduleTool: true,
7740
+ },
7741
+ ...(highestEffort ? { highestEffort } : {}),
7742
+ ...(hypermodeLevel ? { hypermodeEffort: hypermodeLevel } : {}),
7743
+ }
7744
+ : undefined;
7295
7745
  // True exactly when the phase ternary below renders its final branch — the
7296
7746
  // normal composer view, as opposed to provider setup or a lifecycle
7297
7747
  // picker/prompt. The agent rail is a feature of that view: computing this
@@ -7315,7 +7765,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
7315
7765
  setPickerSelectionKind('provider-and-model');
7316
7766
  setPhase('picker');
7317
7767
  void discoverProviders({ skipProbes: true }).then(setPickerDetected).catch((error) => setPickerNotice(String(error)));
7318
- } })) : phase === 'trust' ? (_jsx(TrustPrompt, { cwd: ctx.cwd })) : phase === 'unhealthy' ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: theme.status.error, bold: true, children: "Startup stopped" }), _jsx(Text, { color: theme.text.secondary, children: "Resolve the error above, then restart Namzu." })] })) : phase === 'resume' ? (_jsx(ResumePicker, { conversations: resumeList, selected: selectedResume })) : phase === 'edit' ? (_jsx(EditPromptPicker, { prompts: editList, selected: selectedEdit })) : phase === 'picker' ? (_jsx(Picker, { detected: pickerDetected ?? detected, currentProvider: currentProvider, selectionKind: pickerSelectionKind, currentModel: session?.modelSummary ?? null, initialView: pickerInitialView, onSubmit: handlePickerSubmit, onCancel: handlePickerCancel, onSetup: () => setProviderSetup(true), onCredential: handleTypedCredential, onLogin: startLoginFromPicker, onLoginComplete: finishLoginFromPicker, keyEntryFor: keyEntryFor, notice: pickerNotice,
7768
+ } })) : phase === 'trust' ? (_jsx(TrustPrompt, { cwd: ctx.cwd })) : phase === 'unhealthy' ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: theme.status.error, bold: true, children: "Startup stopped" }), _jsx(Text, { color: theme.text.secondary, children: "Resolve the error above, then restart Namzu." })] })) : phase === 'resume' || phase === 'unarchive' ? (_jsx(ResumePicker, { conversations: resumeList, selected: selectedResume, mode: phase === 'unarchive' ? 'restore' : 'resume' })) : phase === 'edit' ? (_jsx(EditPromptPicker, { prompts: editList, selected: selectedEdit })) : phase === 'picker' ? (_jsx(Picker, { detected: pickerDetected ?? detected, currentProvider: currentProvider, selectionKind: pickerSelectionKind, currentModel: session?.modelSummary ?? null, initialView: pickerInitialView, onSubmit: handlePickerSubmit, onCancel: handlePickerCancel, onSetup: () => setProviderSetup(true), onCredential: handleTypedCredential, onLogin: startLoginFromPicker, onLoginComplete: finishLoginFromPicker, keyEntryFor: keyEntryFor, notice: pickerNotice,
7319
7769
  // The same condition `handlePickerCancel` branches on, read
7320
7770
  // where it is decided rather than guessed on the screen: with no
7321
7771
  // session behind the picker, esc exits namzu and the footer says
@@ -7332,7 +7782,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
7332
7782
  agentSurface === null &&
7333
7783
  outputViewer === null &&
7334
7784
  choicePicker?.kind === 'reasoning-effort' &&
7335
- effortSlider ? (_jsx(EffortSlider, { title: choicePicker.title, notice: choicePicker.notice, options: choicePicker.options, selected: selectedChoice, layout: effortSlider, columns: Math.max(1, (terminal.columns ?? 80) - 2), highest: choicePicker.options.at(-2)?.label === 'default' ? undefined : choicePicker.options.at(-2)?.label })) : permission === null && agentSurface === null && outputViewer === null && choicePicker ? (_jsx(ChoicePicker, { busy: 'busy' in choicePicker && choicePicker.busy === true, columns: Math.max(1, (terminal.columns ?? 80) - 2), title: choicePicker.title, notice: choicePicker.notice, options: filterChoiceOptions(choicePicker.options, choiceQuery), query: choicePickerSearchable(choicePicker) ? choiceQuery : undefined, selected: selectedChoice, windowSize: choicePicker.kind === 'command' ? choicePicker.windowSize : undefined })) : permission === null && agentSurface === null && outputViewer === null && copyPicker ? (_jsx(CopyPicker, { targets: copyPicker.targets, selected: selectedCopy })) : null, _jsxs(ComposerFrame, { working: state === 'thinking' || state === 'tool' || visibleActiveTools.length > 0, ...(orchestrateMode ? { mode: 'orchestrate' } : {}), focus: phase === 'ready' &&
7785
+ effortSlider ? (_jsx(EffortSlider, { title: choicePicker.title, notice: choicePicker.notice, options: choicePicker.options, selected: selectedChoice, layout: effortSlider, columns: Math.max(1, (terminal.columns ?? 80) - 2) })) : permission === null && agentSurface === null && outputViewer === null && choicePicker ? (_jsx(ChoicePicker, { busy: 'busy' in choicePicker && choicePicker.busy === true, columns: Math.max(1, (terminal.columns ?? 80) - 2), title: choicePicker.title, notice: choicePicker.notice, options: filterChoiceOptions(choicePicker.options, choiceQuery), query: choicePickerSearchable(choicePicker) ? choiceQuery : undefined, selected: selectedChoice, windowSize: choicePicker.kind === 'command' ? choicePicker.windowSize : undefined })) : permission === null && agentSurface === null && outputViewer === null && copyPicker ? (_jsx(CopyPicker, { targets: copyPicker.targets, selected: selectedCopy })) : null, _jsxs(ComposerFrame, { working: state === 'thinking' || state === 'tool' || visibleActiveTools.length > 0, ...(hypermode ? { mode: HYPERMODE } : {}), focus: phase === 'ready' &&
7336
7786
  state !== 'awaiting-permission' &&
7337
7787
  !compacting &&
7338
7788
  externalEditorRequest === null &&
@@ -7360,7 +7810,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
7360
7810
  ? queuePause.outcome === 'paused'
7361
7811
  ? 'held after a resumable turn paused; wait for recovery, change model, or send a message to release it'
7362
7812
  : `paused after a ${queuePause.outcome} turn; send a message or change model to continue`
7363
- : 'sending when ready'] }) })) : null, _jsx(Composer, { reasoningEffortLevels: session?.reasoningEffortLevels, onCycleMode: cyclePermissionMode, disabled: outputViewer !== null ||
7813
+ : 'sending when ready', queuedTriggerLabels(queued)] }) })) : null, _jsx(Composer, { reasoningEffortLevels: session?.reasoningEffortLevels, onCycleMode: cyclePermissionMode, disabled: outputViewer !== null ||
7364
7814
  phase !== 'ready' ||
7365
7815
  state === 'awaiting-permission' ||
7366
7816
  compacting ||
@@ -7381,7 +7831,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
7381
7831
  (handoffPark !== null && state === 'idle'), onSubmit: handleSubmit, onNotice: (text) => pushMessage('system', text), onStepReasoningEffort: stepReasoningEffort, onExternalEdit: requestExternalEditor, onEditPrevious: openPromptEditor, draftToRestore: composerDraft, onDraftRestored: (token) => setComposerDraft((draft) => (draft?.token === token ? null : draft)), onDraftPresenceChange: setComposerHasDraft, onOpenAgentPanel: openAgentCockpit, userCommands: userCommands, builtins: hostCommands.map((command) => ({
7382
7832
  ...command,
7383
7833
  description: command.unavailable?.(slashCtx) ?? command.description,
7384
- })), mentionCandidates: mentionCandidates, history: history })] })] })), _jsx(StatusBar, { cwd: ctx.cwd, provider: session?.providerSummary ?? null, model: session?.modelSummary ?? null, effort: reasoningEffort, orchestrate: orchestrateMode, goal: statusGoal, state: state, hint: statusHint, permissionMode: displayedPermissionMode, canCycleMode: true }), showComposerSurface &&
7834
+ })), mentionCandidates: mentionCandidates, history: history, ...(composerTriggers ? { triggers: composerTriggers } : {}), turnActive: !compacting && (state === 'thinking' || state === 'tool') })] })] })), _jsx(StatusBar, { cwd: ctx.cwd, provider: session?.providerSummary ?? null, model: session?.modelSummary ?? null, effort: reasoningEffort, hypermode: hypermode, goal: statusGoal, state: state, hint: statusHint, permissionMode: displayedPermissionMode, canCycleMode: true }), showComposerSurface &&
7385
7835
  permission === null &&
7386
7836
  agentSurface === null &&
7387
7837
  outputViewer === null ? (_jsx(AgentNarrationBand, { lines: narration })) : null, showComposerSurface &&
@@ -7434,6 +7884,8 @@ hasDraft = false) {
7434
7884
  return 'y trust this folder · n / esc exit';
7435
7885
  if (phase === 'resume')
7436
7886
  return '↑↓ navigate · enter resume · esc cancel';
7887
+ if (phase === 'unarchive')
7888
+ return '↑↓ navigate · enter restore · esc cancel';
7437
7889
  if (phase === 'edit')
7438
7890
  return '← older · → newer · enter fork and edit · esc cancel';
7439
7891
  if (phase === 'probing')
@@ -7596,25 +8048,36 @@ function permissionPicker(mode, approvedAll, more = false) {
7596
8048
  };
7597
8049
  }
7598
8050
  /**
7599
- * The picker's sentinel for the orchestrate-mode row, below the rule.
8051
+ * The picker's sentinel for the hypermode row, below the rule.
7600
8052
  *
7601
8053
  * A plain string outside the `ReasoningEffort` union by construction — never
7602
8054
  * added to any model's published menu — so it can share the same picker
7603
8055
  * without ever being mistaken for a level `/effort <token>` could select.
7604
8056
  */
7605
- const ORCHESTRATE_MODE_VALUE = 'orchestrate';
8057
+ const HYPERMODE_VALUE = 'hypermode';
8058
+ /** ` · ✦ save as skill` when queued messages carry composer triggers, so the queue line says so. */
8059
+ function queuedTriggerLabels(queued) {
8060
+ const labels = new Set();
8061
+ for (const prompt of queued) {
8062
+ if (prompt.kind !== 'human')
8063
+ continue;
8064
+ for (const id of prompt.triggers ?? [])
8065
+ labels.add(triggerDefinition(id).label);
8066
+ }
8067
+ return labels.size > 0 ? ` · ✦ ${[...labels].join(', ')}` : '';
8068
+ }
7606
8069
  /** Last entry of a low-to-high menu every provider publishes in that order; `undefined` when none is known. */
7607
8070
  function highestReasoningEffort(levels) {
7608
8071
  return levels && levels.length > 0 ? levels[levels.length - 1] : undefined;
7609
8072
  }
7610
- function reasoningEffortPicker(session, current, afterModelSelection = false, orchestrateOn = false) {
8073
+ function reasoningEffortPicker(session, current, afterModelSelection = false, hypermodeOn = false) {
7611
8074
  const levels = session.reasoningEffortLevels;
7612
8075
  if (!session.hasProvider || levels === undefined)
7613
8076
  return undefined;
7614
8077
  const effortValues = [undefined, ...levels];
7615
8078
  const values = [
7616
8079
  ...effortValues,
7617
- ORCHESTRATE_MODE_VALUE,
8080
+ HYPERMODE_VALUE,
7618
8081
  ];
7619
8082
  return {
7620
8083
  kind: 'reasoning-effort',
@@ -7635,12 +8098,11 @@ function reasoningEffortPicker(session, current, afterModelSelection = false, or
7635
8098
  })),
7636
8099
  {
7637
8100
  // Below a rule, visually apart from the levels above: a session
7638
- // setting, not a sixth level the provider published.
7639
- label: 'orchestrate',
7640
- description: orchestrateOn
7641
- ? 'On · highest level and delegate by default'
7642
- : 'Off · highest level and delegate by default',
7643
- current: orchestrateOn,
8101
+ // setting, not a sixth level the provider published. The label
8102
+ // names the level it pins, then the mode.
8103
+ label: hypermodeStopLabel(hypermodeEffort(levels)),
8104
+ description: `${hypermodeOn ? 'On' : 'Off'} · ${HYPERMODE_SUMMARY}`,
8105
+ current: hypermodeOn,
7644
8106
  ruleBefore: true,
7645
8107
  },
7646
8108
  ],