@namzu/cli 29.0.1 → 30.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 (413) hide show
  1. package/CHANGELOG.md +194 -0
  2. package/README.md +20 -0
  3. package/dist/browser/commands.d.ts +29 -0
  4. package/dist/browser/commands.d.ts.map +1 -0
  5. package/dist/browser/commands.js +361 -0
  6. package/dist/browser/commands.js.map +1 -0
  7. package/dist/browser/control.d.ts +79 -0
  8. package/dist/browser/control.d.ts.map +1 -0
  9. package/dist/browser/control.js +140 -0
  10. package/dist/browser/control.js.map +1 -0
  11. package/dist/browser/describe.d.ts +12 -0
  12. package/dist/browser/describe.d.ts.map +1 -0
  13. package/dist/browser/describe.js +27 -0
  14. package/dist/browser/describe.js.map +1 -0
  15. package/dist/cli.d.ts.map +1 -1
  16. package/dist/cli.js +29 -2
  17. package/dist/cli.js.map +1 -1
  18. package/dist/commands/acp.d.ts.map +1 -1
  19. package/dist/commands/acp.js +1 -0
  20. package/dist/commands/acp.js.map +1 -1
  21. package/dist/commands/browser.d.ts +7 -0
  22. package/dist/commands/browser.d.ts.map +1 -0
  23. package/dist/commands/browser.js +16 -0
  24. package/dist/commands/browser.js.map +1 -0
  25. package/dist/commands/drain.d.ts +2 -1
  26. package/dist/commands/drain.d.ts.map +1 -1
  27. package/dist/commands/drain.js +6 -3
  28. package/dist/commands/drain.js.map +1 -1
  29. package/dist/commands/exec-flags.d.ts +2 -1
  30. package/dist/commands/exec-flags.d.ts.map +1 -1
  31. package/dist/commands/exec-flags.js +6 -3
  32. package/dist/commands/exec-flags.js.map +1 -1
  33. package/dist/commands/exec-json.d.ts.map +1 -1
  34. package/dist/commands/exec-json.js +2 -1
  35. package/dist/commands/exec-json.js.map +1 -1
  36. package/dist/commands/exec.d.ts.map +1 -1
  37. package/dist/commands/exec.js +5 -1
  38. package/dist/commands/exec.js.map +1 -1
  39. package/dist/commands/host-queries.d.ts.map +1 -1
  40. package/dist/commands/host-queries.js +10 -3
  41. package/dist/commands/host-queries.js.map +1 -1
  42. package/dist/commands/schedule.d.ts +9 -0
  43. package/dist/commands/schedule.d.ts.map +1 -0
  44. package/dist/commands/schedule.js +126 -0
  45. package/dist/commands/schedule.js.map +1 -0
  46. package/dist/commands/serve.d.ts +3 -1
  47. package/dist/commands/serve.d.ts.map +1 -1
  48. package/dist/commands/serve.js +5 -3
  49. package/dist/commands/serve.js.map +1 -1
  50. package/dist/commands/skills.d.ts +10 -0
  51. package/dist/commands/skills.d.ts.map +1 -1
  52. package/dist/commands/skills.js +23 -7
  53. package/dist/commands/skills.js.map +1 -1
  54. package/dist/commands/upgrade.d.ts +5 -0
  55. package/dist/commands/upgrade.d.ts.map +1 -1
  56. package/dist/commands/upgrade.js +11 -0
  57. package/dist/commands/upgrade.js.map +1 -1
  58. package/dist/config/load.d.ts +39 -0
  59. package/dist/config/load.d.ts.map +1 -1
  60. package/dist/config/load.js +232 -1
  61. package/dist/config/load.js.map +1 -1
  62. package/dist/config/schema.d.ts +78 -0
  63. package/dist/config/schema.d.ts.map +1 -1
  64. package/dist/config/schema.js.map +1 -1
  65. package/dist/config/user-config.d.ts +23 -0
  66. package/dist/config/user-config.d.ts.map +1 -0
  67. package/dist/config/user-config.js +48 -0
  68. package/dist/config/user-config.js.map +1 -0
  69. package/dist/context/capabilities.d.ts +1 -1
  70. package/dist/context/capabilities.d.ts.map +1 -1
  71. package/dist/context/capabilities.js +1 -0
  72. package/dist/context/capabilities.js.map +1 -1
  73. package/dist/context/environment.d.ts +26 -3
  74. package/dist/context/environment.d.ts.map +1 -1
  75. package/dist/context/environment.js +68 -9
  76. package/dist/context/environment.js.map +1 -1
  77. package/dist/doctor/checks/browser.d.ts +15 -0
  78. package/dist/doctor/checks/browser.d.ts.map +1 -0
  79. package/dist/doctor/checks/browser.js +63 -0
  80. package/dist/doctor/checks/browser.js.map +1 -0
  81. package/dist/doctor/checks/index.d.ts +4 -1
  82. package/dist/doctor/checks/index.d.ts.map +1 -1
  83. package/dist/doctor/checks/index.js +8 -1
  84. package/dist/doctor/checks/index.js.map +1 -1
  85. package/dist/doctor/checks/scheduler.d.ts +10 -0
  86. package/dist/doctor/checks/scheduler.d.ts.map +1 -0
  87. package/dist/doctor/checks/scheduler.js +74 -0
  88. package/dist/doctor/checks/scheduler.js.map +1 -0
  89. package/dist/index.d.ts +1 -1
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +1 -1
  92. package/dist/index.js.map +1 -1
  93. package/dist/integrations/notifications/desktop/freedesktop.d.ts +10 -0
  94. package/dist/integrations/notifications/desktop/freedesktop.d.ts.map +1 -0
  95. package/dist/integrations/notifications/desktop/freedesktop.js +33 -0
  96. package/dist/integrations/notifications/desktop/freedesktop.js.map +1 -0
  97. package/dist/integrations/notifications/desktop/macos.d.ts +6 -0
  98. package/dist/integrations/notifications/desktop/macos.d.ts.map +1 -0
  99. package/dist/integrations/notifications/desktop/macos.js +17 -0
  100. package/dist/integrations/notifications/desktop/macos.js.map +1 -0
  101. package/dist/integrations/notifications/desktop/sanitize.d.ts +16 -0
  102. package/dist/integrations/notifications/desktop/sanitize.d.ts.map +1 -0
  103. package/dist/integrations/notifications/desktop/sanitize.js +49 -0
  104. package/dist/integrations/notifications/desktop/sanitize.js.map +1 -0
  105. package/dist/integrations/notifications/desktop/throttle.d.ts +10 -0
  106. package/dist/integrations/notifications/desktop/throttle.d.ts.map +1 -0
  107. package/dist/integrations/notifications/desktop/throttle.js +39 -0
  108. package/dist/integrations/notifications/desktop/throttle.js.map +1 -0
  109. package/dist/integrations/notifications/desktop/windows-toast.d.ts +23 -0
  110. package/dist/integrations/notifications/desktop/windows-toast.d.ts.map +1 -0
  111. package/dist/integrations/notifications/desktop/windows-toast.js +56 -0
  112. package/dist/integrations/notifications/desktop/windows-toast.js.map +1 -0
  113. package/dist/integrations/notifications/desktop.d.ts +84 -0
  114. package/dist/integrations/notifications/desktop.d.ts.map +1 -0
  115. package/dist/integrations/notifications/desktop.js +199 -0
  116. package/dist/integrations/notifications/desktop.js.map +1 -0
  117. package/dist/integrations/plugins/runtime.d.ts +8 -2
  118. package/dist/integrations/plugins/runtime.d.ts.map +1 -1
  119. package/dist/integrations/plugins/runtime.js +14 -8
  120. package/dist/integrations/plugins/runtime.js.map +1 -1
  121. package/dist/integrations/providers/harness-credentials.d.ts +9 -4
  122. package/dist/integrations/providers/harness-credentials.d.ts.map +1 -1
  123. package/dist/integrations/providers/harness-credentials.js +16 -8
  124. package/dist/integrations/providers/harness-credentials.js.map +1 -1
  125. package/dist/integrations/resident/session-step.d.ts.map +1 -1
  126. package/dist/integrations/resident/session-step.js +1 -0
  127. package/dist/integrations/resident/session-step.js.map +1 -1
  128. package/dist/integrations/subagents/runtime.d.ts +6 -0
  129. package/dist/integrations/subagents/runtime.d.ts.map +1 -1
  130. package/dist/integrations/subagents/runtime.js +14 -5
  131. package/dist/integrations/subagents/runtime.js.map +1 -1
  132. package/dist/integrations/web/open-url.d.ts +24 -0
  133. package/dist/integrations/web/open-url.d.ts.map +1 -0
  134. package/dist/integrations/web/open-url.js +73 -0
  135. package/dist/integrations/web/open-url.js.map +1 -0
  136. package/dist/permissions/browser-sites.d.ts +95 -0
  137. package/dist/permissions/browser-sites.d.ts.map +1 -0
  138. package/dist/permissions/browser-sites.js +270 -0
  139. package/dist/permissions/browser-sites.js.map +1 -0
  140. package/dist/schedule/build.d.ts +86 -0
  141. package/dist/schedule/build.d.ts.map +1 -0
  142. package/dist/schedule/build.js +267 -0
  143. package/dist/schedule/build.js.map +1 -0
  144. package/dist/schedule/changes.d.ts +28 -0
  145. package/dist/schedule/changes.d.ts.map +1 -0
  146. package/dist/schedule/changes.js +65 -0
  147. package/dist/schedule/changes.js.map +1 -0
  148. package/dist/schedule/commands/add.d.ts +24 -0
  149. package/dist/schedule/commands/add.d.ts.map +1 -0
  150. package/dist/schedule/commands/add.js +447 -0
  151. package/dist/schedule/commands/add.js.map +1 -0
  152. package/dist/schedule/commands/args.d.ts +22 -0
  153. package/dist/schedule/commands/args.d.ts.map +1 -0
  154. package/dist/schedule/commands/args.js +80 -0
  155. package/dist/schedule/commands/args.js.map +1 -0
  156. package/dist/schedule/commands/confirm-prompt.d.ts +6 -0
  157. package/dist/schedule/commands/confirm-prompt.d.ts.map +1 -0
  158. package/dist/schedule/commands/confirm-prompt.js +21 -0
  159. package/dist/schedule/commands/confirm-prompt.js.map +1 -0
  160. package/dist/schedule/commands/lifecycle.d.ts +39 -0
  161. package/dist/schedule/commands/lifecycle.d.ts.map +1 -0
  162. package/dist/schedule/commands/lifecycle.js +586 -0
  163. package/dist/schedule/commands/lifecycle.js.map +1 -0
  164. package/dist/schedule/commands/list.d.ts +33 -0
  165. package/dist/schedule/commands/list.d.ts.map +1 -0
  166. package/dist/schedule/commands/list.js +206 -0
  167. package/dist/schedule/commands/list.js.map +1 -0
  168. package/dist/schedule/commands/logs.d.ts +7 -0
  169. package/dist/schedule/commands/logs.d.ts.map +1 -0
  170. package/dist/schedule/commands/logs.js +106 -0
  171. package/dist/schedule/commands/logs.js.map +1 -0
  172. package/dist/schedule/commands/service.d.ts +16 -0
  173. package/dist/schedule/commands/service.d.ts.map +1 -0
  174. package/dist/schedule/commands/service.js +458 -0
  175. package/dist/schedule/commands/service.js.map +1 -0
  176. package/dist/schedule/daemon/daemon.d.ts +156 -0
  177. package/dist/schedule/daemon/daemon.d.ts.map +1 -0
  178. package/dist/schedule/daemon/daemon.js +1253 -0
  179. package/dist/schedule/daemon/daemon.js.map +1 -0
  180. package/dist/schedule/daemon/endpoint.d.ts +38 -0
  181. package/dist/schedule/daemon/endpoint.d.ts.map +1 -0
  182. package/dist/schedule/daemon/endpoint.js +158 -0
  183. package/dist/schedule/daemon/endpoint.js.map +1 -0
  184. package/dist/schedule/daemon/exit.d.ts +8 -0
  185. package/dist/schedule/daemon/exit.d.ts.map +1 -0
  186. package/dist/schedule/daemon/exit.js +8 -0
  187. package/dist/schedule/daemon/exit.js.map +1 -0
  188. package/dist/schedule/daemon/log.d.ts +12 -0
  189. package/dist/schedule/daemon/log.d.ts.map +1 -0
  190. package/dist/schedule/daemon/log.js +53 -0
  191. package/dist/schedule/daemon/log.js.map +1 -0
  192. package/dist/schedule/daemon/notify.d.ts +38 -0
  193. package/dist/schedule/daemon/notify.d.ts.map +1 -0
  194. package/dist/schedule/daemon/notify.js +115 -0
  195. package/dist/schedule/daemon/notify.js.map +1 -0
  196. package/dist/schedule/daemon/retention.d.ts +16 -0
  197. package/dist/schedule/daemon/retention.d.ts.map +1 -0
  198. package/dist/schedule/daemon/retention.js +46 -0
  199. package/dist/schedule/daemon/retention.js.map +1 -0
  200. package/dist/schedule/daemon/sessions.d.ts +23 -0
  201. package/dist/schedule/daemon/sessions.d.ts.map +1 -0
  202. package/dist/schedule/daemon/sessions.js +52 -0
  203. package/dist/schedule/daemon/sessions.js.map +1 -0
  204. package/dist/schedule/env.d.ts +32 -0
  205. package/dist/schedule/env.d.ts.map +1 -0
  206. package/dist/schedule/env.js +106 -0
  207. package/dist/schedule/env.js.map +1 -0
  208. package/dist/schedule/fire/browser-preflight.d.ts +41 -0
  209. package/dist/schedule/fire/browser-preflight.d.ts.map +1 -0
  210. package/dist/schedule/fire/browser-preflight.js +73 -0
  211. package/dist/schedule/fire/browser-preflight.js.map +1 -0
  212. package/dist/schedule/fire/calls.d.ts +49 -0
  213. package/dist/schedule/fire/calls.d.ts.map +1 -0
  214. package/dist/schedule/fire/calls.js +114 -0
  215. package/dist/schedule/fire/calls.js.map +1 -0
  216. package/dist/schedule/fire/fire.d.ts +78 -0
  217. package/dist/schedule/fire/fire.d.ts.map +1 -0
  218. package/dist/schedule/fire/fire.js +490 -0
  219. package/dist/schedule/fire/fire.js.map +1 -0
  220. package/dist/schedule/fire/result.d.ts +13 -0
  221. package/dist/schedule/fire/result.d.ts.map +1 -0
  222. package/dist/schedule/fire/result.js +18 -0
  223. package/dist/schedule/fire/result.js.map +1 -0
  224. package/dist/schedule/fire/unattended-note.d.ts +22 -0
  225. package/dist/schedule/fire/unattended-note.d.ts.map +1 -0
  226. package/dist/schedule/fire/unattended-note.js +46 -0
  227. package/dist/schedule/fire/unattended-note.js.map +1 -0
  228. package/dist/schedule/floor.d.ts +136 -0
  229. package/dist/schedule/floor.d.ts.map +1 -0
  230. package/dist/schedule/floor.js +1289 -0
  231. package/dist/schedule/floor.js.map +1 -0
  232. package/dist/schedule/folder.d.ts +34 -0
  233. package/dist/schedule/folder.d.ts.map +1 -0
  234. package/dist/schedule/folder.js +84 -0
  235. package/dist/schedule/folder.js.map +1 -0
  236. package/dist/schedule/paths.d.ts +49 -0
  237. package/dist/schedule/paths.d.ts.map +1 -0
  238. package/dist/schedule/paths.js +53 -0
  239. package/dist/schedule/paths.js.map +1 -0
  240. package/dist/schedule/policy.d.ts +141 -0
  241. package/dist/schedule/policy.d.ts.map +1 -0
  242. package/dist/schedule/policy.js +378 -0
  243. package/dist/schedule/policy.js.map +1 -0
  244. package/dist/schedule/resume-command.d.ts +36 -0
  245. package/dist/schedule/resume-command.d.ts.map +1 -0
  246. package/dist/schedule/resume-command.js +72 -0
  247. package/dist/schedule/resume-command.js.map +1 -0
  248. package/dist/schedule/service/detect.d.ts +22 -0
  249. package/dist/schedule/service/detect.d.ts.map +1 -0
  250. package/dist/schedule/service/detect.js +29 -0
  251. package/dist/schedule/service/detect.js.map +1 -0
  252. package/dist/schedule/service/index.d.ts +48 -0
  253. package/dist/schedule/service/index.d.ts.map +1 -0
  254. package/dist/schedule/service/index.js +361 -0
  255. package/dist/schedule/service/index.js.map +1 -0
  256. package/dist/schedule/service/launchd.d.ts +24 -0
  257. package/dist/schedule/service/launchd.d.ts.map +1 -0
  258. package/dist/schedule/service/launchd.js +84 -0
  259. package/dist/schedule/service/launchd.js.map +1 -0
  260. package/dist/schedule/service/manifest.d.ts +44 -0
  261. package/dist/schedule/service/manifest.d.ts.map +1 -0
  262. package/dist/schedule/service/manifest.js +16 -0
  263. package/dist/schedule/service/manifest.js.map +1 -0
  264. package/dist/schedule/service/names.d.ts +9 -0
  265. package/dist/schedule/service/names.d.ts.map +1 -0
  266. package/dist/schedule/service/names.js +22 -0
  267. package/dist/schedule/service/names.js.map +1 -0
  268. package/dist/schedule/service/quote.d.ts +24 -0
  269. package/dist/schedule/service/quote.d.ts.map +1 -0
  270. package/dist/schedule/service/quote.js +64 -0
  271. package/dist/schedule/service/quote.js.map +1 -0
  272. package/dist/schedule/service/runner.d.ts +17 -0
  273. package/dist/schedule/service/runner.d.ts.map +1 -0
  274. package/dist/schedule/service/runner.js +31 -0
  275. package/dist/schedule/service/runner.js.map +1 -0
  276. package/dist/schedule/service/systemd.d.ts +37 -0
  277. package/dist/schedule/service/systemd.d.ts.map +1 -0
  278. package/dist/schedule/service/systemd.js +96 -0
  279. package/dist/schedule/service/systemd.js.map +1 -0
  280. package/dist/schedule/service/windows-task.d.ts +58 -0
  281. package/dist/schedule/service/windows-task.d.ts.map +1 -0
  282. package/dist/schedule/service/windows-task.js +199 -0
  283. package/dist/schedule/service/windows-task.js.map +1 -0
  284. package/dist/schedule/service/wsl.d.ts +69 -0
  285. package/dist/schedule/service/wsl.d.ts.map +1 -0
  286. package/dist/schedule/service/wsl.js +121 -0
  287. package/dist/schedule/service/wsl.js.map +1 -0
  288. package/dist/schedule/store/atomic.d.ts +43 -0
  289. package/dist/schedule/store/atomic.d.ts.map +1 -0
  290. package/dist/schedule/store/atomic.js +135 -0
  291. package/dist/schedule/store/atomic.js.map +1 -0
  292. package/dist/schedule/store/claims.d.ts +26 -0
  293. package/dist/schedule/store/claims.d.ts.map +1 -0
  294. package/dist/schedule/store/claims.js +57 -0
  295. package/dist/schedule/store/claims.js.map +1 -0
  296. package/dist/schedule/store/digest.d.ts +18 -0
  297. package/dist/schedule/store/digest.d.ts.map +1 -0
  298. package/dist/schedule/store/digest.js +139 -0
  299. package/dist/schedule/store/digest.js.map +1 -0
  300. package/dist/schedule/store/history.d.ts +21 -0
  301. package/dist/schedule/store/history.d.ts.map +1 -0
  302. package/dist/schedule/store/history.js +114 -0
  303. package/dist/schedule/store/history.js.map +1 -0
  304. package/dist/schedule/store/jobs.d.ts +61 -0
  305. package/dist/schedule/store/jobs.d.ts.map +1 -0
  306. package/dist/schedule/store/jobs.js +210 -0
  307. package/dist/schedule/store/jobs.js.map +1 -0
  308. package/dist/schedule/store/state.d.ts +19 -0
  309. package/dist/schedule/store/state.d.ts.map +1 -0
  310. package/dist/schedule/store/state.js +38 -0
  311. package/dist/schedule/store/state.js.map +1 -0
  312. package/dist/schedule/types.d.ts +283 -0
  313. package/dist/schedule/types.d.ts.map +1 -0
  314. package/dist/schedule/types.js +9 -0
  315. package/dist/schedule/types.js.map +1 -0
  316. package/dist/skills/catalog.d.ts +96 -0
  317. package/dist/skills/catalog.d.ts.map +1 -0
  318. package/dist/skills/catalog.js +226 -0
  319. package/dist/skills/catalog.js.map +1 -0
  320. package/dist/skills/directory.d.ts +38 -0
  321. package/dist/skills/directory.d.ts.map +1 -0
  322. package/dist/skills/directory.js +51 -0
  323. package/dist/skills/directory.js.map +1 -0
  324. package/dist/skills/save.d.ts +148 -0
  325. package/dist/skills/save.d.ts.map +1 -0
  326. package/dist/skills/save.js +344 -0
  327. package/dist/skills/save.js.map +1 -0
  328. package/dist/skills/store.d.ts +103 -16
  329. package/dist/skills/store.d.ts.map +1 -1
  330. package/dist/skills/store.js +227 -30
  331. package/dist/skills/store.js.map +1 -1
  332. package/dist/tui/App.d.ts.map +1 -1
  333. package/dist/tui/App.js +609 -74
  334. package/dist/tui/App.js.map +1 -1
  335. package/dist/tui/LiveActivity.d.ts +8 -1
  336. package/dist/tui/LiveActivity.d.ts.map +1 -1
  337. package/dist/tui/LiveActivity.js +20 -4
  338. package/dist/tui/LiveActivity.js.map +1 -1
  339. package/dist/tui/PermissionOverlay.d.ts +36 -4
  340. package/dist/tui/PermissionOverlay.d.ts.map +1 -1
  341. package/dist/tui/PermissionOverlay.js +135 -15
  342. package/dist/tui/PermissionOverlay.js.map +1 -1
  343. package/dist/tui/SaveSkillOverlay.d.ts +33 -0
  344. package/dist/tui/SaveSkillOverlay.d.ts.map +1 -0
  345. package/dist/tui/SaveSkillOverlay.js +118 -0
  346. package/dist/tui/SaveSkillOverlay.js.map +1 -0
  347. package/dist/tui/agent.d.ts +208 -6
  348. package/dist/tui/agent.d.ts.map +1 -1
  349. package/dist/tui/agent.js +371 -58
  350. package/dist/tui/agent.js.map +1 -1
  351. package/dist/tui/browser-notices.d.ts +33 -0
  352. package/dist/tui/browser-notices.d.ts.map +1 -0
  353. package/dist/tui/browser-notices.js +102 -0
  354. package/dist/tui/browser-notices.js.map +1 -0
  355. package/dist/tui/open-browser.d.ts +29 -1
  356. package/dist/tui/open-browser.d.ts.map +1 -1
  357. package/dist/tui/open-browser.js +83 -11
  358. package/dist/tui/open-browser.js.map +1 -1
  359. package/dist/tui/permission-review.d.ts +10 -1
  360. package/dist/tui/permission-review.d.ts.map +1 -1
  361. package/dist/tui/permission-review.js +228 -22
  362. package/dist/tui/permission-review.js.map +1 -1
  363. package/dist/tui/schedule/host-commands.d.ts +34 -0
  364. package/dist/tui/schedule/host-commands.d.ts.map +1 -0
  365. package/dist/tui/schedule/host-commands.js +310 -0
  366. package/dist/tui/schedule/host-commands.js.map +1 -0
  367. package/dist/tui/schedule/integration.d.ts +62 -0
  368. package/dist/tui/schedule/integration.d.ts.map +1 -0
  369. package/dist/tui/schedule/integration.js +145 -0
  370. package/dist/tui/schedule/integration.js.map +1 -0
  371. package/dist/tui/schedule/loop-host.d.ts +42 -0
  372. package/dist/tui/schedule/loop-host.d.ts.map +1 -0
  373. package/dist/tui/schedule/loop-host.js +153 -0
  374. package/dist/tui/schedule/loop-host.js.map +1 -0
  375. package/dist/tui/schedule/resume.d.ts +122 -0
  376. package/dist/tui/schedule/resume.d.ts.map +1 -0
  377. package/dist/tui/schedule/resume.js +324 -0
  378. package/dist/tui/schedule/resume.js.map +1 -0
  379. package/dist/tui/schedule/startup.d.ts +11 -0
  380. package/dist/tui/schedule/startup.d.ts.map +1 -0
  381. package/dist/tui/schedule/startup.js +121 -0
  382. package/dist/tui/schedule/startup.js.map +1 -0
  383. package/dist/tui/schedule/tool-host.d.ts +58 -0
  384. package/dist/tui/schedule/tool-host.d.ts.map +1 -0
  385. package/dist/tui/schedule/tool-host.js +479 -0
  386. package/dist/tui/schedule/tool-host.js.map +1 -0
  387. package/dist/tui/skills/learning.d.ts +74 -0
  388. package/dist/tui/skills/learning.d.ts.map +1 -0
  389. package/dist/tui/skills/learning.js +131 -0
  390. package/dist/tui/skills/learning.js.map +1 -0
  391. package/dist/tui/skills/suggest-setting.d.ts +22 -0
  392. package/dist/tui/skills/suggest-setting.d.ts.map +1 -0
  393. package/dist/tui/skills/suggest-setting.js +55 -0
  394. package/dist/tui/skills/suggest-setting.js.map +1 -0
  395. package/dist/tui/skills/suggestion-ledger.d.ts +35 -0
  396. package/dist/tui/skills/suggestion-ledger.d.ts.map +1 -0
  397. package/dist/tui/skills/suggestion-ledger.js +52 -0
  398. package/dist/tui/skills/suggestion-ledger.js.map +1 -0
  399. package/dist/tui/slashCommands.d.ts +20 -0
  400. package/dist/tui/slashCommands.d.ts.map +1 -1
  401. package/dist/tui/slashCommands.js +76 -2
  402. package/dist/tui/slashCommands.js.map +1 -1
  403. package/dist/tui/turn-interruption.d.ts +20 -1
  404. package/dist/tui/turn-interruption.d.ts.map +1 -1
  405. package/dist/tui/turn-interruption.js +43 -0
  406. package/dist/tui/turn-interruption.js.map +1 -1
  407. package/dist/tui/types.d.ts +9 -1
  408. package/dist/tui/types.d.ts.map +1 -1
  409. package/package.json +7 -5
  410. package/skills/README.md +20 -0
  411. package/skills/browser-automation/SKILL.md +84 -0
  412. package/skills/schedule-task/SKILL.md +146 -0
  413. package/skills/skill-creator/SKILL.md +101 -0
package/dist/tui/App.js CHANGED
@@ -16,8 +16,8 @@ import { TURN_LIMIT_FIELDS, turnLimitCommands } from './turn-limits-settings.js'
16
16
  import { discoverProviders } from '../integrations/providers/discover.js';
17
17
  import { ProviderSetup } from './ProviderSetup.js';
18
18
  import { ToolOutputViewer } from './ToolOutputViewer.js';
19
- import { relative } from 'node:path';
20
- import { DEFAULT_MAX_GOAL_ROUNDS, DiskMessageFeedbackStore, GoalRoundLimitError, HostCommandRegistry, SessionGoalActivation, StaleGoalError, TurnCancelled, asSessionId, createAssistantMessage, createUserMessage, generateSessionId, generateTurnId, isCompactionMessage, kernelHostCommands, } from '@namzu/sdk';
19
+ import { join, relative } from 'node:path';
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
22
  import { useCallback, useEffect, useRef, useState } from 'react';
23
23
  import { pinTrustedProjectPath, resolveTrustedProjectContext, } from '../config/trusted-project-context.js';
@@ -35,7 +35,7 @@ import { checkUpdates } from '../integrations/updates.js';
35
35
  import { renderMemoryReport, renderMemorySaveResult, renderStoredMemorySection, renderTypedNoteResult, } from '../memory/presentation.js';
36
36
  import { appendMemoryWithStatus, readMemory } from '../memory/store.js';
37
37
  import { effectivePermissionMode, permissionModeDescription, permissionModeLabel, } from '../permissions/mode.js';
38
- import { composeSkillsPrompt, discoverSkills, loadSkillBody } from '../skills/store.js';
38
+ import { NO_SKILLS_FOUND, composeSkillsPrompt, discoverSkills, loadSkillBody, renderSkillRoster, skillTierLabel, } from '../skills/store.js';
39
39
  import { discoverUserCommands } from '../user-commands/store.js';
40
40
  import { AgentCockpit, AgentNarrationBand, AgentTaskPanel, AgentTranscript, activeSubagentCohorts, agentCockpitIsWide, agentPhasePageSize, agentPhases, agentPickerPageSize, agentTranscriptPageSize, agentWorkflowPageSize, agentWorkflows, maxAgentTranscriptTailOffset, } from './AgentExplorer.js';
41
41
  import { completionRow, launchReceipt, settleLine } from './agent-transcript-rows.js';
@@ -49,9 +49,16 @@ import { CopyPicker } from './CopyPicker.js';
49
49
  import { EditPromptPicker } from './EditPromptPicker.js';
50
50
  import { LiveActivity, formatElapsed } from './LiveActivity.js';
51
51
  import { webCallTitle, webLiveStatus, webSettledLine } from './web-activity.js';
52
- import { PermissionOverlay } from './PermissionOverlay.js';
52
+ import { PermissionOverlay, permissionAnswers } from './PermissionOverlay.js';
53
53
  import { Picker } from './Picker.js';
54
54
  import { ResumePicker } from './ResumePicker.js';
55
+ import { resolveNamzuHome } from '../integrations/state/home.js';
56
+ import { createScheduleIntegration } from './schedule/integration.js';
57
+ import { SaveSkillOverlay } from './SaveSkillOverlay.js';
58
+ import { SKILL_CREATOR_SKILL, buildSaveSkillTool, learnSkillPrompt, newSkillPrompt, } from '../skills/save.js';
59
+ import { SUGGESTIONS_STOPPED_NOTICE, createTurnActivity, judgeSkillSuggestion, observeTurnEvent, skillSuggestionNotice, } from './skills/learning.js';
60
+ import { FRESH_LEDGER, admitSuggestion, readSuggestionLedger, writeSuggestionLedger, } from './skills/suggestion-ledger.js';
61
+ import { setSkillSuggestions } from './skills/suggest-setting.js';
55
62
  import { StatusBar } from './StatusBar.js';
56
63
  import { isRepeatedNotice } from './notices.js';
57
64
  import { checklistProgress } from './Checklist.js';
@@ -81,7 +88,8 @@ import { renderCheckpoints, renderRestore } from '../checkpoints/store.js';
81
88
  import { expandFileMentions, listMentionableFiles } from './mentions.js';
82
89
  import { openInBrowser } from './open-browser.js';
83
90
  import { buildPermissionReview, buildPermissionSummary, permissionReviewPageRows, permissionReviewRefusal, permissionReviewRows, releasedByApproveAll, } from './permission-review.js';
84
- import { describeTurnInterruption, describeTurnStop } from './turn-interruption.js';
91
+ import { PAUSED_TURN_LINES, describeTurnInterruption, describeTurnStop, handoffContinuationNote, } from './turn-interruption.js';
92
+ import { browserSiteNotes, describeBrowserHandoff, runBrowserSlash } from './browser-notices.js';
85
93
  import { moveSelection } from './selection-window.js';
86
94
  import { describeShellEscape, describeShellEscapeForModel, runShellEscape, shellEscapeCommand, } from './shell-escape.js';
87
95
  import { baseBranchReviewPrompt, commitReviewPrompt, hostCommandNames, kernelCommandDescriptors, mergeHostCommands, renderAgents, renderOutcome, reviewPrompt, runSlash, } from './slashCommands.js';
@@ -314,6 +322,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
314
322
  const initialConversationIdRef = useRef(initialCtx.initialConversationId);
315
323
  const [history, setHistory] = useState([]);
316
324
  const [state, setState] = useState('idle');
325
+ /** For the scheduler integration's loop timer, which reads it outside render. */
326
+ const stateRef = useRef(state);
327
+ stateRef.current = state;
317
328
  const [phase, setPhase] = useState('probing');
318
329
  const [session, setSession] = useState(null);
319
330
  // Once a usable session publishes the transcript, temporary lifecycle
@@ -617,6 +628,25 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
617
628
  }, []);
618
629
  const [composerDraft, setComposerDraft] = useState(null);
619
630
  const [composerHasDraft, setComposerHasDraft] = useState(false);
631
+ // A turn a tool paused for a person (`ToolResult.handoff`), while its
632
+ // notice is the newest thing on screen: Enter continues it, Esc stops it.
633
+ // The ref is what the key handler reads; the state is what the footer shows.
634
+ /**
635
+ * The stream of the turn running now, so a tool's own screen can close the
636
+ * reply it streamed before the tool ran. The kernel hands the tool's start
637
+ * to this loop only when its batch settles, so a reply streamed with a
638
+ * tool call stays pending while that tool asks the operator something; a
639
+ * row written then (the schedule tool's confirmation) cannot reach
640
+ * scrollback past the pending reply, and the top of a tall one was cut
641
+ * off while the operator was asked about it.
642
+ */
643
+ const liveStreamRef = useRef(null);
644
+ const [handoffPark, setHandoffParkState] = useState(null);
645
+ const handoffParkRef = useRef(null);
646
+ const setHandoffPark = useCallback((value) => {
647
+ handoffParkRef.current = value;
648
+ setHandoffParkState(value);
649
+ }, []);
620
650
  const composerDraftTokenRef = useRef(0);
621
651
  /** Bounded child session projection published by the current AgentSession. */
622
652
  const [subagents, setSubagentsState] = useState([]);
@@ -837,6 +867,16 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
837
867
  * server's child process outlives the object that opened it.
838
868
  */
839
869
  const previousSessionRef = useRef(null);
870
+ /** The profile `/browser profile` chose, kept across session rebuilds. */
871
+ const browserProfileRef = useRef(undefined);
872
+ /** The current session's browser, for notices written from event handlers. */
873
+ const browserControlRef = useRef(undefined);
874
+ /** The current session, for the review screen to name a computer-use control by its ref. */
875
+ const reviewSessionRef = useRef(null);
876
+ useEffect(() => {
877
+ browserControlRef.current = session?.browser;
878
+ reviewSessionRef.current = session;
879
+ }, [session]);
840
880
  useEffect(() => {
841
881
  const source = session?.subagents;
842
882
  setAgentSurface(null);
@@ -910,6 +950,89 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
910
950
  // turns attribute to the resumed conversation.
911
951
  const sessionsRef = useRef(null);
912
952
  const scopeRef = useRef(null);
953
+ /**
954
+ * Scheduled jobs and session loops (`./schedule/`). Built once; what it
955
+ * reads later — the transcript writer, the question and permission
956
+ * screens, the submit path, the session's model — is filled in each
957
+ * render through `scheduleLiveRef`.
958
+ */
959
+ const scheduleLiveRef = useRef(null);
960
+ const scheduleRef = useRef(null);
961
+ if (scheduleRef.current === null) {
962
+ scheduleRef.current = createScheduleIntegration({
963
+ home: () => sessionsRef.current?.root ?? resolveNamzuHome(),
964
+ cwd: () => ctxRef.current.cwd,
965
+ extraRoots: () => ctxRef.current.additionalDirectories ?? [],
966
+ config: () => ctxRef.current,
967
+ model: () => scheduleLiveRef.current?.model,
968
+ sessionId: () => (conversationMaterializedRef.current ? scopeRef.current?.sessionId : undefined),
969
+ loopsFile: () => {
970
+ const sessions = sessionsRef.current;
971
+ const scope = scopeRef.current;
972
+ if (!sessions || !scope || !conversationMaterializedRef.current)
973
+ return undefined;
974
+ try {
975
+ return join(sessions.paths.sessionDir({ sessionId: scope.sessionId }), 'loops.json');
976
+ }
977
+ catch {
978
+ return undefined;
979
+ }
980
+ },
981
+ isIdle: () => stateRef.current === 'idle' && abortRef.current === null,
982
+ say: (text) => scheduleLiveRef.current?.say(text),
983
+ ask: (question, signal) => scheduleLiveRef.current?.ask(question, signal) ?? Promise.resolve({ kind: 'skip' }),
984
+ askPermission: (request) => scheduleLiveRef.current?.askPermission(request) ??
985
+ Promise.resolve({ kind: 'reject', feedback: 'Nobody can answer yet.' }),
986
+ submit: (text) => scheduleLiveRef.current?.submit(text),
987
+ });
988
+ }
989
+ /**
990
+ * `save_skill`'s confirmation. The tool is the TUI's alone (never exec, a
991
+ * scheduled run or a sub-agent), and its question is this screen, not the
992
+ * permission gate, so no permission mode answers it for the operator.
993
+ */
994
+ const [saveSkillPrompt, setSaveSkillPromptState] = useState(null);
995
+ const saveSkillPromptRef = useRef(null);
996
+ const setSaveSkillPrompt = useCallback((next) => {
997
+ saveSkillPromptRef.current = next;
998
+ setSaveSkillPromptState(next);
999
+ }, []);
1000
+ const saveSkillNotifyRef = useRef(null);
1001
+ const saveSkillToolRef = useRef(null);
1002
+ if (saveSkillToolRef.current === null) {
1003
+ saveSkillToolRef.current = buildSaveSkillTool({
1004
+ cwd: () => ctxRef.current.cwd,
1005
+ config: () => ctxRef.current.skills,
1006
+ sessionId: () => conversationMaterializedRef.current ? scopeRef.current?.sessionId : undefined,
1007
+ confirm: (request, signal) => new Promise((resolve) => {
1008
+ if (signal?.aborted) {
1009
+ resolve('cancel');
1010
+ return;
1011
+ }
1012
+ const entry = {
1013
+ request,
1014
+ resolve: (answer) => {
1015
+ signal?.removeEventListener('abort', onAbort);
1016
+ // The screen answers from its own key handler, which Ink calls
1017
+ // before App's (a child subscribes first). Clearing the ref now
1018
+ // would hand the same Esc or Ctrl+C to App, which reads it as
1019
+ // an interrupt of the turn this answer is for. Let the key's
1020
+ // dispatch finish first.
1021
+ queueMicrotask(() => {
1022
+ if (saveSkillPromptRef.current === entry)
1023
+ setSaveSkillPrompt(null);
1024
+ });
1025
+ resolve(answer);
1026
+ },
1027
+ };
1028
+ const onAbort = () => entry.resolve('cancel');
1029
+ signal?.addEventListener('abort', onAbort, { once: true });
1030
+ setSaveSkillPrompt(entry);
1031
+ saveSkillNotifyRef.current?.();
1032
+ }),
1033
+ saved: ({ name, path }) => scheduleLiveRef.current?.say(`✎ Saved skill ${name} to ${path}. The model is offered it from the next turn; /skills ${name} activates it now.`),
1034
+ });
1035
+ }
913
1036
  /**
914
1037
  * A provider session can be ready before a person has started a conversation.
915
1038
  * The scope still needs a mutable session cursor, but its generated id is
@@ -948,6 +1071,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
948
1071
  const [goalDriveVersion, setGoalDriveVersion] = useState(0);
949
1072
  const [goalStatus, setGoalStatus] = useState(null);
950
1073
  const pendingGoalResumeRef = useRef(null);
1074
+ /**
1075
+ * The conversation `namzu resume <id>` opened, until the screen is ready to
1076
+ * look at it once: a scheduled run parked on a decision there is continued
1077
+ * then, so its permission prompt is on screen without typing `/resume`.
1078
+ */
1079
+ const pendingScheduledResumeRef = useRef(null);
951
1080
  const wakeGoalDriver = useCallback(() => setGoalDriveVersion((version) => version + 1), []);
952
1081
  /**
953
1082
  * Conversation writes in the order the operator produced them.
@@ -1160,6 +1289,56 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1160
1289
  ]);
1161
1290
  return id;
1162
1291
  }, [nextId]);
1292
+ /**
1293
+ * The skill proposal's per-conversation facts: whether this conversation
1294
+ * has proposed one, and whether it has used a skill. Keyed by the
1295
+ * conversation generation, so `/clear`, `/resume` and a fork start over.
1296
+ */
1297
+ const learningRef = useRef({ generation: -1, proposed: false, skillUsed: false });
1298
+ /** `/skills save off|on` in this session, over whatever the config files say. */
1299
+ const skillSuggestOverrideRef = useRef(undefined);
1300
+ const conversationLearning = useCallback(() => {
1301
+ if (learningRef.current.generation !== conversationGenRef.current) {
1302
+ learningRef.current = {
1303
+ generation: conversationGenRef.current,
1304
+ proposed: false,
1305
+ skillUsed: false,
1306
+ };
1307
+ }
1308
+ return learningRef.current;
1309
+ }, []);
1310
+ /**
1311
+ * After a turn settles: one dim line proposing to save it as a skill, when
1312
+ * the turn earned it (`judgeSkillSuggestion`) and the anti-nag ledger lets
1313
+ * it through. Costs nothing and saves nothing; `/skills save` does that.
1314
+ */
1315
+ const offerSkillSuggestion = useCallback((st) => {
1316
+ if (!st.learning)
1317
+ return;
1318
+ const conversation = conversationLearning();
1319
+ const config = ctxRef.current.skills;
1320
+ const verdict = judgeSkillSuggestion(st.learning, {
1321
+ suggest: skillSuggestOverrideRef.current ?? config?.suggest,
1322
+ ...(config?.suggestMinToolCalls !== undefined
1323
+ ? { minToolCalls: config.suggestMinToolCalls }
1324
+ : {}),
1325
+ planMode: st.startedInPlanMode === true || permissionModeRef.current === 'plan',
1326
+ skillUsedInConversation: conversation.skillUsed,
1327
+ proposedInConversation: conversation.proposed,
1328
+ skillFlowTurn: st.skillFlow === true,
1329
+ });
1330
+ if (!verdict.suggest)
1331
+ return;
1332
+ const home = resolveNamzuHome();
1333
+ const admission = admitSuggestion(readSuggestionLedger(home));
1334
+ if (admission.show === 'nothing')
1335
+ return;
1336
+ conversation.proposed = true;
1337
+ writeSuggestionLedger(home, admission.next);
1338
+ pushMessage('system', admission.show === 'proposal'
1339
+ ? skillSuggestionNotice(verdict.steps, verdict.tools)
1340
+ : SUGGESTIONS_STOPPED_NOTICE, false, '✻', undefined, theme.text.muted);
1341
+ }, [conversationLearning, pushMessage]);
1163
1342
  /**
1164
1343
  * `/agents [running|available|batches]`, answered by the delegation UI's
1165
1344
  * `agentsSlashCommand` over this session's live monitor and the batches the
@@ -1532,7 +1711,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1532
1711
  })();
1533
1712
  }, [pushMessage]);
1534
1713
  const activateSkill = useCallback((name) => {
1535
- const info = discoverSkills({ cwd: ctx.cwd }).find((skill) => skill.name === name);
1714
+ const info = discoverSkills({
1715
+ cwd: ctx.cwd,
1716
+ ...(ctx.skills ? { config: ctx.skills } : {}),
1717
+ }).find((skill) => skill.name === name);
1536
1718
  if (!info) {
1537
1719
  pushMessage('system', `No skill named "${name}". See /skills.`);
1538
1720
  return;
@@ -1543,12 +1725,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1543
1725
  ...previous.filter((skill) => skill.name !== info.name),
1544
1726
  { name: info.name, body },
1545
1727
  ]);
1728
+ conversationLearning().skillUsed = true;
1546
1729
  pushMessage('system', `Activated skill: ${info.name}`);
1547
1730
  }
1548
1731
  catch (error) {
1549
1732
  pushMessage('system', `Could not load skill "${name}": ${error instanceof Error ? error.message : String(error)}`);
1550
1733
  }
1551
- }, [ctx.cwd, pushMessage]);
1734
+ }, [conversationLearning, ctx.cwd, ctx.skills, pushMessage]);
1552
1735
  const removeStoredCredential = useCallback((target) => {
1553
1736
  const path = credentialsPath();
1554
1737
  const hadClaude = readStoredSubscriptionCredential() !== null;
@@ -2151,6 +2334,23 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2151
2334
  const finalizeMessage = useCallback((id, finalContent) => {
2152
2335
  setMessages((prev) => prev.map((m) => m.id === id ? { ...m, content: finalContent ?? m.content, pending: false } : m));
2153
2336
  }, []);
2337
+ /**
2338
+ * Close the reply the running turn streamed before a tool took over, as a
2339
+ * tool's start does. For a tool that shows the operator something of its
2340
+ * own while it runs: the reply is complete by then (tools run after the
2341
+ * model's response ends), and left pending it holds every later row out
2342
+ * of scrollback. Text that arrives later starts a new reply.
2343
+ */
2344
+ const closeLiveReply = useCallback(() => {
2345
+ const st = liveStreamRef.current;
2346
+ if (!st)
2347
+ return;
2348
+ flushStream(st);
2349
+ if (st.assistantId) {
2350
+ finalizeMessage(st.assistantId);
2351
+ st.assistantId = null;
2352
+ }
2353
+ }, [finalizeMessage, flushStream]);
2154
2354
  // Open the SDK session store and select the durable conversation once.
2155
2355
  // This is an admission gate for every startup, not optional persistence: a
2156
2356
  // corrupt or split estate must not silently widen into cwd-local state and
@@ -2172,6 +2372,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2172
2372
  ? { text: persistedOutput, provenance: 'persisted' }
2173
2373
  : null;
2174
2374
  pendingGoalResumeRef.current = requestedConversationId;
2375
+ pendingScheduledResumeRef.current = requestedConversationId;
2175
2376
  initialConversationIdRef.current = undefined;
2176
2377
  conversationMaterializedRef.current = true;
2177
2378
  }
@@ -2243,21 +2444,50 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2243
2444
  if (signal?.aborted)
2244
2445
  return;
2245
2446
  const activeCtx = ctxRef.current;
2447
+ const primary = prefs.providers[0];
2448
+ if (primary && scheduleLiveRef.current) {
2449
+ scheduleLiveRef.current.model = {
2450
+ provider: primary.id,
2451
+ ...(primary.model ? { model: primary.model } : {}),
2452
+ };
2453
+ }
2454
+ const browserConfig = activeCtx.browser;
2455
+ const browserProfile = browserProfileRef.current ?? browserConfig?.defaultProfile;
2456
+ const browserOptions = browserConfig
2457
+ ? {
2458
+ ...(browserProfile ? { profile: browserProfile } : {}),
2459
+ ...(browserConfig.engine ? { engine: browserConfig.engine } : {}),
2460
+ ...(browserConfig.headless ? { headless: browserConfig.headless } : {}),
2461
+ ...(browserConfig.sites ? { sites: browserConfig.sites } : {}),
2462
+ ...(browserConfig.keepOpen ? { keepOpen: true } : {}),
2463
+ ...(sessionsRef.current ? { home: sessionsRef.current.root } : {}),
2464
+ }
2465
+ : undefined;
2246
2466
  const s = await createAgentSession(prefs, detectedNow, {
2247
2467
  scope,
2248
2468
  cwd: activeCtx.cwd,
2469
+ // A person is here to confirm: the model may propose scheduled jobs
2470
+ // and session loops (never in exec, a scheduled run or a sub-agent).
2471
+ extraTools: [
2472
+ ...(scheduleRef.current?.tools() ?? []),
2473
+ ...(saveSkillToolRef.current ? [saveSkillToolRef.current] : []),
2474
+ ],
2249
2475
  ...(activeCtx.structuredOutput ? { structuredOutput: activeCtx.structuredOutput } : {}),
2250
2476
  ...(activeCtx.additionalDirectories
2251
2477
  ? { additionalDirectories: activeCtx.additionalDirectories }
2252
2478
  : {}),
2253
2479
  ...(sessionsRef.current ? { stateRoot: sessionsRef.current.root } : {}),
2254
2480
  enableComputerUse: true,
2481
+ // The browser tools, when the config did not switch them off. The
2482
+ // profile is the one `/browser profile` chose, else the config's.
2483
+ ...(browserOptions ? { browser: browserOptions } : {}),
2255
2484
  rules: activeCtx.rules,
2256
2485
  ...(sessionsRef.current
2257
2486
  ? { sessionGoals: sessionsRef.current.goals, conversationSessions: sessionsRef.current }
2258
2487
  : {}),
2259
2488
  ...(activeCtx.mcpServers ? { mcpServers: activeCtx.mcpServers } : {}),
2260
2489
  ...(activeCtx.plugins ? { plugins: activeCtx.plugins } : {}),
2490
+ ...(activeCtx.skills ? { skills: activeCtx.skills } : {}),
2261
2491
  ...(activeCtx.web ? { web: activeCtx.web } : {}),
2262
2492
  ...(activeCtx.hooks ? { hooks: activeCtx.hooks } : {}),
2263
2493
  ...(activeCtx.compaction ? { compaction: activeCtx.compaction } : {}),
@@ -2276,6 +2506,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2276
2506
  // question when a decision is genuinely theirs.
2277
2507
  askUser: true,
2278
2508
  allowModelSwitch: true,
2509
+ // The user is at this desktop, so the model may open a page for them.
2510
+ openUrl: true,
2279
2511
  });
2280
2512
  if (signal?.aborted) {
2281
2513
  void s.close();
@@ -2862,7 +3094,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2862
3094
  return;
2863
3095
  }
2864
3096
  await session.abandonTurn(active.turnId, reason);
2865
- pushMessage('system', `Abandoned turn ${active.turnId}. The next prompt starts a new turn in this conversation.`);
3097
+ pushMessage('system', PAUSED_TURN_LINES.abandoned);
2866
3098
  }
2867
3099
  catch (err) {
2868
3100
  pushMessage('system', `Could not abandon the turn: ${err instanceof Error ? err.message : String(err)}`);
@@ -2875,7 +3107,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2875
3107
  */
2876
3108
  // `applyEvent` is declared further down; the resume path reads it at call time.
2877
3109
  const applyEventRef = useRef(null);
2878
- const resumeActiveTurn = useCallback(async () => {
3110
+ const resumeActiveTurn = useCallback(async (resume) => {
2879
3111
  const sessions = sessionsRef.current;
2880
3112
  const scope = scopeRef.current;
2881
3113
  if (!sessions || !scope || !conversationMaterializedRef.current || !session?.hasProvider)
@@ -2887,6 +3119,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2887
3119
  pushMessage('system', 'Wait for the running turn to finish before resuming the parked one.');
2888
3120
  return true;
2889
3121
  }
3122
+ setHandoffPark(null);
2890
3123
  const ac = new AbortController();
2891
3124
  abortRef.current = ac;
2892
3125
  setState('thinking');
@@ -2900,15 +3133,58 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2900
3133
  sessionId: scope.sessionId,
2901
3134
  notification: null,
2902
3135
  };
2903
- pushMessage('system', `Resuming turn ${active.turnId} from its checkpoint.`, false, '▶');
3136
+ liveStreamRef.current = st;
3137
+ let scheduledRun = false;
3138
+ let restoreBrowser;
2904
3139
  try {
3140
+ // A scheduled run parked on a decision: the operator answers the
3141
+ // parked batch here, and the turn continues under the job's rules.
3142
+ const scheduled = active.paused
3143
+ ? await scheduleRef.current?.prepareResume(permissionModeRef.current, {
3144
+ cwd: ctxRef.current.cwd,
3145
+ roots: session.directories?.list() ?? [],
3146
+ sandboxed: session.sandbox.workspace !== 'host',
3147
+ // The resumed turn runs on the job's provider: one this
3148
+ // session has no credential for is refused before asking.
3149
+ providers: detected.map((item) => item.entry.id),
3150
+ browser: session.browser !== undefined,
3151
+ })
3152
+ : undefined;
3153
+ if (scheduled && 'leave' in scheduled) {
3154
+ pushMessage('system', 'The scheduled run stays waiting. /resume asks again.');
3155
+ return true;
3156
+ }
3157
+ if (scheduled && 'abandon' in scheduled) {
3158
+ scheduledRun = true;
3159
+ if (!session.abandonTurn)
3160
+ throw new Error('this session cannot abandon turns');
3161
+ await session.abandonTurn(active.turnId, scheduled.abandon);
3162
+ pushMessage('system', PAUSED_TURN_LINES.abandonedScheduled);
3163
+ return true;
3164
+ }
3165
+ scheduledRun = scheduled !== undefined;
3166
+ const { browser: jobBrowser, ...scheduledParams } = scheduled ?? {};
3167
+ // A job's browser grant: this turn drives the job's profile, held
3168
+ // to its sites, and the session's own comes back when it ends.
3169
+ if (jobBrowser && session.browser) {
3170
+ restoreBrowser = await session.browser.runAs(jobBrowser);
3171
+ pushMessage('system', `The browser uses the job’s profile ${jobBrowser.profile} for this run, on its sites only.`);
3172
+ }
3173
+ // Said once the turn really continues: a scheduled park can still
3174
+ // be left waiting or abandoned above.
3175
+ pushMessage('system', PAUSED_TURN_LINES.resuming, false, '▶');
2905
3176
  for await (const event of session.resumePaused({
2906
3177
  turnId: active.turnId,
2907
3178
  signal: ac.signal,
2908
- // The operator's mode, read at every decision like a new turn's:
2909
- // `/resume` in plan mode stays read-only, and so do its children.
2910
- permissionMode: permissionModeRef.current,
2911
- currentPermissionMode: () => permissionModeRef.current,
3179
+ ...(scheduled ? scheduledParams : {
3180
+ // The operator's mode, read at every decision like a new turn's:
3181
+ // `/resume` in plan mode stays read-only, and so do its children.
3182
+ permissionMode: permissionModeRef.current,
3183
+ currentPermissionMode: () => permissionModeRef.current,
3184
+ ...(resume?.handoffReason
3185
+ ? { systemNote: handoffContinuationNote(resume.handoffReason) }
3186
+ : {}),
3187
+ }),
2912
3188
  })) {
2913
3189
  applyEventRef.current?.(event, st);
2914
3190
  }
@@ -2926,9 +3202,38 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2926
3202
  if (abortRef.current === ac)
2927
3203
  abortRef.current = null;
2928
3204
  setState('idle');
3205
+ // A scheduled run answered here records its end in its job now —
3206
+ // completed, failed or cancelled — not at a scheduler's next tick,
3207
+ // which may never come. One that parked again stays waiting.
3208
+ if (scheduledRun)
3209
+ await scheduleRef.current?.settleAnswered();
3210
+ await restoreBrowser?.().catch(() => undefined);
2929
3211
  }
2930
3212
  return true;
2931
- }, [finalizeMessage, flushStream, pushMessage, session, state]);
3213
+ }, [detected, finalizeMessage, flushStream, pushMessage, session, setHandoffPark, state]);
3214
+ // `namzu resume <id>` of a scheduled run parked on a decision: what the
3215
+ // notification and `/schedule` tell the operator to run. Continue it once
3216
+ // the screen is ready, exactly as `/resume` would, so the parked call is on
3217
+ // the permission screen; the job and session checks in `prepareResume`
3218
+ // still refuse a session that does not run as the job does. Any other
3219
+ // parked or interrupted turn is left for the operator's own `/resume`.
3220
+ useEffect(() => {
3221
+ const requested = pendingScheduledResumeRef.current;
3222
+ if (!requested || phase !== 'ready' || state !== 'idle' || !session?.hasProvider)
3223
+ return;
3224
+ const sessions = sessionsRef.current;
3225
+ if (!sessions || scopeRef.current?.sessionId !== requested || abortRef.current)
3226
+ return;
3227
+ pendingScheduledResumeRef.current = null;
3228
+ void (async () => {
3229
+ const active = await activeConversationTurn(sessions, asSessionId(requested)).catch(() => undefined);
3230
+ if (!active?.paused || !(await scheduleRef.current?.isParked()))
3231
+ return;
3232
+ if (scopeRef.current?.sessionId !== requested)
3233
+ return;
3234
+ await resumeActiveTurn();
3235
+ })();
3236
+ }, [phase, resumeActiveTurn, session, state]);
2932
3237
  const doResume = useCallback(async () => {
2933
3238
  const sessions = sessionsRef.current ?? (await ensureSessions(), sessionsRef.current);
2934
3239
  if (!sessions) {
@@ -2979,7 +3284,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2979
3284
  // only on its own.
2980
3285
  const kept = [];
2981
3286
  for (const pending of permissionQueueRef.current.splice(0)) {
2982
- if (releasedByApproveAll(pending.permission.toolCalls))
3287
+ // Nor a question about sharing the screen, which only its own
3288
+ // answer settles.
3289
+ if (pending.permission.screenConsent !== true &&
3290
+ releasedByApproveAll(pending.permission.toolCalls))
2983
3291
  pending.resolve(decision);
2984
3292
  else
2985
3293
  kept.push(pending);
@@ -3595,11 +3903,14 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3595
3903
  feedback: permissionReviewRefusal(review.reason),
3596
3904
  });
3597
3905
  }
3598
- const summary = buildPermissionSummary(review.text);
3906
+ const summary = buildPermissionSummary(review.text, {
3907
+ describeUiRef: (ref) => reviewSessionRef.current?.describeComputerUseRef?.(ref),
3908
+ });
3909
+ const siteNotes = browserSiteNotes(req.toolCalls, browserControlRef.current?.status());
3599
3910
  return new Promise((resolve) => {
3600
3911
  if (permissionResolveRef.current) {
3601
3912
  permissionQueueRef.current.push({
3602
- permission: { ...req, review: review.text, summary },
3913
+ permission: { ...req, review: review.text, summary, siteNotes },
3603
3914
  resolve,
3604
3915
  });
3605
3916
  setQueuedPermissionCount(permissionQueueRef.current.length);
@@ -3609,7 +3920,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3609
3920
  permissionOpenedAtRef.current = Date.now();
3610
3921
  setPermissionReviewOffset(0);
3611
3922
  setPermissionDetailsOpen(!summary.complete);
3612
- setPermission({ ...req, review: review.text, summary });
3923
+ setPermission({ ...req, review: review.text, summary, siteNotes });
3613
3924
  setState('awaiting-permission');
3614
3925
  sendTerminalNotification({ kind: 'approval-required' });
3615
3926
  });
@@ -3620,15 +3931,26 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3620
3931
  * and the tool's promise waits on the row the operator picks. One question
3621
3932
  * at a time: the tool is not concurrency-safe, so a second cannot arrive
3622
3933
  * while the first is up.
3934
+ *
3935
+ * `signal`, when the caller has one (the `schedule` tool's confirmation,
3936
+ * which waits on a person and so is given the tool's own abort signal),
3937
+ * is honoured the way `save_skill`'s screen honours the same signal: an
3938
+ * abort — the tool's own deadline elapsing, or the turn stopping —
3939
+ * answers as `abort` and takes the picker off screen, rather than
3940
+ * leaving it live after the tool has already given up on the answer.
3623
3941
  */
3624
- const askQuestion = useCallback((question) => new Promise((resolve) => {
3942
+ const askQuestion = useCallback((question, signal) => new Promise((resolve) => {
3943
+ if (signal?.aborted) {
3944
+ resolve({ kind: 'abort' });
3945
+ return;
3946
+ }
3947
+ closeLiveReply();
3625
3948
  questionRef.current = { question, resolve };
3626
3949
  const values = [
3627
3950
  ...question.options.map((option) => option.id),
3628
3951
  ...(question.allowFreeText ? [FREE_TEXT_ANSWER] : []),
3629
3952
  ];
3630
- setSelectedChoice(0);
3631
- setChoicePicker({
3953
+ const picker = {
3632
3954
  kind: 'user-question',
3633
3955
  title: question.question,
3634
3956
  notice: question.header
@@ -3644,9 +3966,17 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3644
3966
  ? [{ label: 'Something else…', description: 'Answer in your own words' }]
3645
3967
  : []),
3646
3968
  ],
3647
- });
3969
+ };
3970
+ const onAbort = () => {
3971
+ if (choicePickerRef.current === picker)
3972
+ setChoicePicker(null);
3973
+ resolveQuestion({ kind: 'abort' });
3974
+ };
3975
+ signal?.addEventListener('abort', onAbort, { once: true });
3976
+ setSelectedChoice(0);
3977
+ setChoicePicker(picker);
3648
3978
  sendTerminalNotification({ kind: 'approval-required' });
3649
- }), [sendTerminalNotification, setChoicePicker, setSelectedChoice]);
3979
+ }), [closeLiveReply, sendTerminalNotification, setChoicePicker, setSelectedChoice, resolveQuestion]);
3650
3980
  /**
3651
3981
  * Grow this turn's open task block, or open one. The whole plan rides along
3652
3982
  * so the block shows the checklist as it stood after the operation; a
@@ -3670,6 +4000,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3670
4000
  // `st` carries the streaming-assistant bubble id + accumulated text across
3671
4001
  // events within a turn/stream.
3672
4002
  const applyEvent = useCallback((event, st) => {
4003
+ if (st.learning)
4004
+ observeTurnEvent(st.learning, event);
4005
+ if (event.kind === 'tool-start' && event.toolName === SKILL_TOOL_NAME) {
4006
+ conversationLearning().skillUsed = true;
4007
+ }
3673
4008
  const ensureAssistant = () => {
3674
4009
  if (!st.assistantId)
3675
4010
  st.assistantId = pushMessage('assistant', '', true);
@@ -3935,6 +4270,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3935
4270
  setState(activeToolsRef.current.length > 0 ? 'tool' : 'thinking');
3936
4271
  break;
3937
4272
  }
4273
+ if (event.cancelled) {
4274
+ // A No on the tool's own screen: the person's answer, not a failure.
4275
+ pushMessage('tool', done?.label ?? formatToolCall(event.toolName, event.summary), false, '○', undefined, theme.text.muted, event.durationMs !== undefined ? formatElapsed(event.durationMs) : undefined);
4276
+ pushMessage('tool', event.resultLabel ?? 'Cancelled', false, '⎿');
4277
+ setState(activeToolsRef.current.length > 0 ? 'tool' : 'thinking');
4278
+ break;
4279
+ }
3938
4280
  const catalogue = !event.isError && event.toolName === 'agent_models' && event.output !== undefined
3939
4281
  ? modelCatalogueView(event.output) : undefined;
3940
4282
  if (catalogue !== undefined) {
@@ -4091,6 +4433,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4091
4433
  if (closing)
4092
4434
  pushMessage('system', closing, false, '✻', undefined, theme.text.muted);
4093
4435
  }
4436
+ offerSkillSuggestion(st);
4094
4437
  break;
4095
4438
  }
4096
4439
  case 'paused':
@@ -4102,7 +4445,22 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4102
4445
  st.outcome = 'stopped';
4103
4446
  st.queuePauseOutcome = 'paused';
4104
4447
  st.notification = { kind: 'turn-settled', outcome: 'stopped' };
4105
- pushMessage('system', describeTurnInterruption(event), false, '‖');
4448
+ if (event.handoff) {
4449
+ // Nothing failed and nothing needs approving: a tool needs the
4450
+ // operator to do something first. Continuing is one key.
4451
+ const browser = browserControlRef.current;
4452
+ const browserStatus = browser?.status();
4453
+ const browserNotice = describeBrowserHandoff(event.handoff, browserStatus);
4454
+ // Without a window the sign-in happens in `namzu browser
4455
+ // login`, which needs the profile this session holds.
4456
+ if (browserNotice && browser && browserStatus?.headless)
4457
+ void browser.release();
4458
+ pushMessage('system', browserNotice ??
4459
+ `${describeTurnInterruption(event)}\nWhen that is done, press Enter to continue · Esc to stop.`, false, '‖');
4460
+ setHandoffPark({ turnId: event.turnId, reason: event.handoff.reason });
4461
+ }
4462
+ else
4463
+ pushMessage('system', describeTurnInterruption(event), false, '‖');
4106
4464
  break;
4107
4465
  case 'error':
4108
4466
  closeAssistant();
@@ -4115,7 +4473,16 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4115
4473
  st.notification = null;
4116
4474
  break;
4117
4475
  }
4118
- }, [appendToMessage, finalizeMessage, flushStream, pushMessage, writeTaskBlock]);
4476
+ }, [
4477
+ appendToMessage,
4478
+ conversationLearning,
4479
+ finalizeMessage,
4480
+ flushStream,
4481
+ offerSkillSuggestion,
4482
+ pushMessage,
4483
+ setHandoffPark,
4484
+ writeTaskBlock,
4485
+ ]);
4119
4486
  applyEventRef.current = applyEvent;
4120
4487
  const runTurn = useCallback(async (prompt) => {
4121
4488
  // A passive queue effect can still hold the previous render's session
@@ -4243,7 +4610,19 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4243
4610
  outcome: null,
4244
4611
  sessionId: destination,
4245
4612
  notification: null,
4613
+ ...(prompt.kind === 'human'
4614
+ ? {
4615
+ learning: createTurnActivity(),
4616
+ ...(prompt.skillFlow ? { skillFlow: true } : {}),
4617
+ startedInPlanMode: permissionModeRef.current === 'plan',
4618
+ }
4619
+ : {}),
4246
4620
  };
4621
+ liveStreamRef.current = st;
4622
+ // A skill activated with /skills <name> stays in the prompt of every
4623
+ // later conversation too; a turn run under one already used a skill.
4624
+ if (st.learning && activeSkills.length > 0)
4625
+ st.learning.skillUsed = true;
4247
4626
  const ac = new AbortController();
4248
4627
  const turnToken = {};
4249
4628
  let abnormalTerminal;
@@ -4819,6 +5198,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4819
5198
  pushMessage('system', `Conversation history is being ${operation}. Wait for it to finish before sending another command or prompt.`);
4820
5199
  return;
4821
5200
  }
5201
+ // The operator moved on; a parked turn is still there for /resume.
5202
+ setHandoffPark(null);
4822
5203
  setHistory((prev) => [...prev, value]);
4823
5204
  const selectionIntent = !attachments?.length ? parseModelSelectionIntent(value) : undefined;
4824
5205
  if (selectionIntent) {
@@ -4859,6 +5240,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4859
5240
  // including the queue — so a command-driven turn is not a second way
4860
5241
  // to run one.
4861
5242
  let outgoing = value;
5243
+ let skillFlow = false;
4862
5244
  const slash = runSlash(value, slashCtx, hostCommands);
4863
5245
  if (slash) {
4864
5246
  switch (slash.kind) {
@@ -5330,20 +5712,16 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5330
5712
  // distinction the headless commands get from `--cwd`. They are
5331
5713
  // the same value today, and were the same value in `exec --json`
5332
5714
  // too until they were not.
5333
- const skills = discoverSkills({ cwd: ctx.cwd });
5715
+ const skills = discoverSkills({
5716
+ cwd: ctx.cwd,
5717
+ ...(ctx.skills ? { config: ctx.skills } : {}),
5718
+ });
5334
5719
  if (skills.length === 0) {
5335
- pushMessage('system', 'No skills found. Add one at ~/.namzu/skills/<name>/SKILL.md or ./skills/<name>/SKILL.md.');
5720
+ pushMessage('system', NO_SKILLS_FOUND);
5336
5721
  return;
5337
5722
  }
5338
5723
  const activeNames = new Set(activeSkills.map((s) => s.name));
5339
- const lines = skills.map((s) =>
5340
- // A refused skill is shown with its reason rather than hidden.
5341
- // Dropping it silently would leave someone wondering where a
5342
- // file they can see on disk went.
5343
- s.problem
5344
- ? `! ${s.name} — ${s.problem}`
5345
- : `${activeNames.has(s.name) ? '● ' : '○ '}${s.name} — ${s.description}`);
5346
- pushMessage('system', `Skills (● active):\n ${lines.join('\n ')}`);
5724
+ pushMessage('system', renderSkillRoster(skills, activeNames));
5347
5725
  return;
5348
5726
  }
5349
5727
  case 'plugins': {
@@ -5372,9 +5750,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5372
5750
  return;
5373
5751
  }
5374
5752
  case 'skill-picker': {
5375
- const skills = discoverSkills({ cwd: ctx.cwd });
5753
+ const skills = discoverSkills({
5754
+ cwd: ctx.cwd,
5755
+ ...(ctx.skills ? { config: ctx.skills } : {}),
5756
+ });
5376
5757
  if (skills.length === 0) {
5377
- pushMessage('system', 'No skills found. Add one at ~/.namzu/skills/<name>/SKILL.md or ./skills/<name>/SKILL.md.');
5758
+ pushMessage('system', NO_SKILLS_FOUND);
5378
5759
  return;
5379
5760
  }
5380
5761
  const activeNames = new Set(activeSkills.map((skill) => skill.name));
@@ -5387,7 +5768,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5387
5768
  label: skill.name,
5388
5769
  description: skill.problem
5389
5770
  ? `Unavailable: ${skill.problem}`
5390
- : `${skill.description} · ${skill.source}`,
5771
+ : `${skill.description} · ${skill.source} (${skillTierLabel(skill.tier)})`,
5391
5772
  current: activeNames.has(skill.name),
5392
5773
  })),
5393
5774
  });
@@ -5397,6 +5778,46 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5397
5778
  activateSkill(slash.name);
5398
5779
  return;
5399
5780
  }
5781
+ case 'new-skill':
5782
+ case 'save-skill': {
5783
+ const creator = discoverSkills({
5784
+ cwd: ctx.cwd,
5785
+ ...(ctx.skills ? { config: ctx.skills } : {}),
5786
+ }).find((skill) => skill.name === SKILL_CREATOR_SKILL);
5787
+ if (!creator || creator.problem || creator.invocation === 'operator') {
5788
+ pushMessage('system', `The ${SKILL_CREATOR_SKILL} skill is not available to the model here${creator?.problem ? ` (${creator.problem})` : ''}. Turn the built-in skills back on (skills.builtin) or write a SKILL.md by hand under ~/.namzu/skills/<name>/.`);
5789
+ return;
5790
+ }
5791
+ // Falls through to the queue-or-send a typed message takes, as
5792
+ // `prompt` does below.
5793
+ skillFlow = true;
5794
+ if (slash.kind === 'new-skill')
5795
+ outgoing = newSkillPrompt(slash.idea);
5796
+ else {
5797
+ outgoing = learnSkillPrompt(slash.name);
5798
+ // The proposal was taken up: this conversation proposes no
5799
+ // more, and the ignored-in-a-row count starts again.
5800
+ conversationLearning().proposed = true;
5801
+ const home = resolveNamzuHome();
5802
+ const ledger = readSuggestionLedger(home);
5803
+ if (ledger.unanswered > 0)
5804
+ writeSuggestionLedger(home, { ...ledger, unanswered: 0 });
5805
+ }
5806
+ break;
5807
+ }
5808
+ case 'skill-suggestions': {
5809
+ // The session follows the choice whatever the files say; the
5810
+ // message says whether a later start will too.
5811
+ skillSuggestOverrideRef.current = slash.on;
5812
+ if (slash.on)
5813
+ writeSuggestionLedger(resolveNamzuHome(), FRESH_LEDGER);
5814
+ const profile = ctx.configDebug?.selectedProfile?.name;
5815
+ pushMessage('system', setSkillSuggestions(slash.on, {
5816
+ cwd: ctx.cwd,
5817
+ ...(profile ? { profile } : {}),
5818
+ }).message);
5819
+ return;
5820
+ }
5400
5821
  case 'resume':
5401
5822
  void doResume();
5402
5823
  return;
@@ -5457,6 +5878,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5457
5878
  case 'none':
5458
5879
  return;
5459
5880
  case 'host-command': {
5881
+ if (scheduleRef.current?.handleSlash(slash.name, slash.args))
5882
+ return;
5460
5883
  // Dispatched through the kernel's registry, built with what
5461
5884
  // THIS session can answer from. The descriptors used for
5462
5885
  // the merge above carry no store — they are names — so the
@@ -5761,6 +6184,21 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5761
6184
  runConversationExport({ kind: 'file', path: slash.path });
5762
6185
  return;
5763
6186
  }
6187
+ case 'browser': {
6188
+ const generation = conversationGenRef.current;
6189
+ void runBrowserSlash(session?.browser, slash.args, {
6190
+ busy: state !== 'idle' || abortRef.current !== null,
6191
+ }).then((answer) => {
6192
+ // A later session keeps the operator's choice: `/model`
6193
+ // rebuilds the session, and the profile goes with it.
6194
+ if (answer.profile)
6195
+ browserProfileRef.current = answer.profile;
6196
+ if (conversationGenRef.current !== generation)
6197
+ return;
6198
+ pushMessage('system', answer.text);
6199
+ });
6200
+ return;
6201
+ }
5764
6202
  default: {
5765
6203
  // Exhaustive on purpose. Without it a `SlashAction` kind added
5766
6204
  // and not handled here falls out of the switch into the send
@@ -5786,8 +6224,16 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5786
6224
  kind: 'human',
5787
6225
  text: outgoing,
5788
6226
  ...(attachments && attachments.length > 0 ? { attachments: [...attachments] } : {}),
6227
+ ...(skillFlow ? { skillFlow: true } : {}),
5789
6228
  };
5790
- if (mode === 'submit') {
6229
+ // `/skills save` and `/skills new` are turns of their own: steered into
6230
+ // a running turn, they would lose the flag that keeps the skill-making
6231
+ // turn from being proposed as a skill, and the model would read them
6232
+ // mid-task. They wait in the queue for the turn to end.
6233
+ if (skillFlow && activeTurnInboxRef.current) {
6234
+ pushMessage('system', 'Queued: this runs when the current turn ends.');
6235
+ }
6236
+ if (mode === 'submit' && !skillFlow) {
5791
6237
  const inbox = activeTurnInboxRef.current;
5792
6238
  if (inbox) {
5793
6239
  const expanded = expandFileMentions(outgoing, ctx.cwd);
@@ -5805,6 +6251,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5805
6251
  activeSkills,
5806
6252
  advanceQueueContinuation,
5807
6253
  applyPermissionMode,
6254
+ conversationLearning,
5808
6255
  applyReasoningEffort,
5809
6256
  appLifetime,
5810
6257
  ctx.cwd,
@@ -5841,6 +6288,40 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5841
6288
  // a ref so selecting a help row re-enters the one ordinary slash-command
5842
6289
  // path instead of growing a second command executor.
5843
6290
  commandPickerSubmitRef.current = handleSubmit;
6291
+ saveSkillNotifyRef.current = () => sendTerminalNotification({ kind: 'approval-required' });
6292
+ scheduleLiveRef.current = {
6293
+ ...(scheduleLiveRef.current?.model ? { model: scheduleLiveRef.current.model } : {}),
6294
+ say: (text) => {
6295
+ closeLiveReply();
6296
+ pushMessage('system', text);
6297
+ },
6298
+ ask: askQuestion,
6299
+ askPermission: onPermission,
6300
+ submit: (text) => handleSubmit(text),
6301
+ };
6302
+ // Scheduled work since the TUI last looked: one line, after the first
6303
+ // paint, never blocking it. Loops fire when a turn ends, and on their timer.
6304
+ useEffect(() => {
6305
+ const timer = setTimeout(() => {
6306
+ const line = scheduleRef.current?.startupLine();
6307
+ if (line)
6308
+ scheduleLiveRef.current?.say(line);
6309
+ }, 250);
6310
+ return () => {
6311
+ clearTimeout(timer);
6312
+ scheduleRef.current?.dispose();
6313
+ };
6314
+ }, []);
6315
+ useEffect(() => {
6316
+ if (state !== 'idle')
6317
+ return;
6318
+ try {
6319
+ scheduleRef.current?.loops.tick();
6320
+ }
6321
+ catch {
6322
+ // A loop that cannot be read never takes the screen down with it.
6323
+ }
6324
+ }, [state]);
5844
6325
  // Keep the footer on the same durable goal record the driver mutates. A ref
5845
6326
  // alone cannot repaint React, so every goal mutation bumps goalDriveVersion;
5846
6327
  // generation/session checks stop a late disk read labelling a resumed chat.
@@ -6149,6 +6630,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6149
6630
  useInput((input, key) => {
6150
6631
  if (outputViewer !== null)
6151
6632
  return;
6633
+ // The save-skill screen owns its keys, Esc and Ctrl+C included: neither
6634
+ // may interrupt the turn that is waiting on its answer. A permission
6635
+ // request (an agent's, say) takes the screen over it and gets the keys.
6636
+ if (saveSkillPromptRef.current && permission === null)
6637
+ return;
6152
6638
  // Startup has refused admission, so there is no draft or active turn to
6153
6639
  // protect with the ready screen's two-press exit ladder.
6154
6640
  if (phase === 'unhealthy') {
@@ -6262,7 +6748,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6262
6748
  }
6263
6749
  if (permission && (key.upArrow || key.downArrow)) {
6264
6750
  const current = permissionChoiceRef.current;
6265
- const next = key.upArrow ? Math.max(0, current - 1) : Math.min(2, current + 1);
6751
+ const last = permissionAnswers(permission.toolCalls, {
6752
+ batchOnly: permission.batchOnly === true,
6753
+ screenConsent: permission.screenConsent === true,
6754
+ }).length - 1;
6755
+ const next = key.upArrow ? Math.max(0, current - 1) : Math.min(last, current + 1);
6266
6756
  setPermissionChoice(next);
6267
6757
  return;
6268
6758
  }
@@ -6301,14 +6791,26 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6301
6791
  // the operator never looked at. Refusals above never wait.
6302
6792
  if (!approvalIsDeliberate(permissionOpenedAtRef.current, Date.now()))
6303
6793
  return;
6304
- const numbered = ch === '1' ? 0 : ch === '2' ? 1 : ch === '3' ? 2 : null;
6794
+ // The answers on screen, so a digit or Enter means what it is next
6795
+ // to: a batch-only prompt (a scheduled run) has no "allow all", and
6796
+ // neither its `a` nor a `3` answers it.
6797
+ const answers = permissionAnswers(permission?.toolCalls ?? [], {
6798
+ batchOnly: permission?.batchOnly === true,
6799
+ screenConsent: permission?.screenConsent === true,
6800
+ });
6801
+ const numbered = /^[1-9]$/.test(ch) ? Number(ch) - 1 : null;
6305
6802
  const chosen = key.return ? permissionChoiceRef.current : numbered;
6306
- if (ch === 'y' || chosen === 0)
6307
- resolvePermission({ kind: 'approve' });
6308
- else if (ch === 'a' || chosen === 1)
6309
- resolvePermission({ kind: 'approve-all' });
6310
- else if (chosen === 2)
6311
- resolvePermission({ kind: 'reject' });
6803
+ const kind = ch === 'y'
6804
+ ? 'approve'
6805
+ : ch === 'a'
6806
+ ? answers.some((answer) => answer.kind === 'approve-all')
6807
+ ? 'approve-all'
6808
+ : undefined
6809
+ : chosen !== null
6810
+ ? answers[chosen]?.kind
6811
+ : undefined;
6812
+ if (kind)
6813
+ resolvePermission({ kind });
6312
6814
  return;
6313
6815
  }
6314
6816
  // The active child count already advertises this key beside Working.
@@ -6614,6 +7116,21 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6614
7116
  resetTranscript();
6615
7117
  return;
6616
7118
  }
7119
+ // A turn a tool paused for a person: Enter on an empty composer
7120
+ // continues it, Esc stops it (the turn is abandoned, like /abandon).
7121
+ if (handoffParkRef.current &&
7122
+ phase === 'ready' &&
7123
+ state === 'idle' &&
7124
+ !abortRef.current &&
7125
+ ((key.return && !composerHasDraft) || key.escape)) {
7126
+ const parked = handoffParkRef.current;
7127
+ setHandoffPark(null);
7128
+ if (key.return)
7129
+ void resumeActiveTurn({ handoffReason: parked.reason });
7130
+ else
7131
+ void abandonActiveTurn('The operator stopped the turn a tool paused for them.');
7132
+ return;
7133
+ }
6617
7134
  // Esc interrupts a running turn (Ctrl+C stays reserved for exit). Mirrors
6618
7135
  // the Ctrl+C interrupt path: abort, drop the queue, one "Interrupted." line.
6619
7136
  if (key.escape && (abortRef.current || pendingModelSwitchRef.current)) {
@@ -6738,29 +7255,33 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6738
7255
  ? 'starting a fresh conversation — input is paused'
6739
7256
  : compacting
6740
7257
  ? 'compacting conversation — input is paused'
6741
- : permission
6742
- ? hintForPhase(phase, state, session?.hasProvider === true)
6743
- : agentSurface?.kind === 'cockpit'
6744
- ? agentSurface.focus === 'workflows'
6745
- ? 'workflows — enter select · esc return'
6746
- : agentSurface.focus === 'phases'
6747
- ? 'agent phases — enter agents · esc return'
6748
- : 'agents — enter inspect · left phases · esc return'
6749
- : agentSurface?.kind === 'transcript'
6750
- ? 'observing agent — esc agents · q parent'
6751
- : textPrompt
6752
- ? 'editor open · enter apply · esc cancel'
6753
- : choicePicker
6754
- ? choicePickerSearchable(choicePicker)
6755
- ? 'type to filter · ↑↓ select · enter apply · esc back'
6756
- : effortSlider
6757
- ? '←/→ adjust · 1–9 select · enter apply · esc back'
6758
- : '↑↓ / 1–9 select · enter apply · esc back'
6759
- : copyPicker
6760
- ? 'copy target open — ↑↓ / 1–9 select · esc cancel'
6761
- : goalStatus && phase === 'ready' && state === 'idle'
6762
- ? undefined
6763
- : hintForPhase(phase, state, session?.hasProvider === true, composerHasDraft);
7258
+ : saveSkillPrompt
7259
+ ? 'save skill — ←/→ choose · enter apply · esc cancel'
7260
+ : permission
7261
+ ? hintForPhase(phase, state, session?.hasProvider === true)
7262
+ : agentSurface?.kind === 'cockpit'
7263
+ ? agentSurface.focus === 'workflows'
7264
+ ? 'workflows — enter select · esc return'
7265
+ : agentSurface.focus === 'phases'
7266
+ ? 'agent phases — enter agents · esc return'
7267
+ : 'agents — enter inspect · left phases · esc return'
7268
+ : agentSurface?.kind === 'transcript'
7269
+ ? 'observing agent — esc agents · q parent'
7270
+ : textPrompt
7271
+ ? 'editor open · enter apply · esc cancel'
7272
+ : choicePicker
7273
+ ? choicePickerSearchable(choicePicker)
7274
+ ? 'type to filter · ↑↓ select · enter apply · esc back'
7275
+ : effortSlider
7276
+ ? '←/→ adjust · 1–9 select · enter apply · esc back'
7277
+ : '↑↓ / 1–9 select · enter apply · esc back'
7278
+ : copyPicker
7279
+ ? 'copy target open — ↑↓ / 1–9 select · esc cancel'
7280
+ : handoffPark && phase === 'ready' && state === 'idle' && !composerHasDraft
7281
+ ? 'enter continue · esc stop'
7282
+ : goalStatus && phase === 'ready' && state === 'idle'
7283
+ ? undefined
7284
+ : hintForPhase(phase, state, session?.hasProvider === true, composerHasDraft);
6764
7285
  const permissionSource = reviewSource(permission, scopeRef.current?.sessionId);
6765
7286
  const permissionOwner = permissionSource
6766
7287
  ? subagents.find((agent) => agent.sessionId === permissionSource)
@@ -6799,7 +7320,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6799
7320
  // where it is decided rather than guessed on the screen: with no
6800
7321
  // session behind the picker, esc exits namzu and the footer says
6801
7322
  // so instead of offering a cancel nobody will see.
6802
- cancelExits: !session?.hasProvider })) : (_jsxs(_Fragment, { children: [agentSurface === null && outputViewer === null ? (_jsx(LiveActivity, { compact: compactWork, activeTools: visibleActiveTools, working: state === 'thinking' || state === 'tool', interruptible: abortRef.current !== null, animate: stdout.isTTY === true && permission === null && textPrompt === null, thinking: thinking, tokens: turnTokens })) : null, agentSurface === null && outputViewer === null && permission === null ? (_jsx(TaskList, { tasks: liveTasks })) : null, permission ? (_jsx(PermissionOverlay, { toolCalls: permission.toolCalls, review: permission.review, summary: permission.summary, detailsOpen: permissionDetailsOpen, reviewOffset: permissionReviewOffset, choice: permissionChoice, queuedCount: queuedPermissionCount, sourceLabel: permissionSourceLabel, columns: terminal.columns, rows: terminal.rows })) : null, textPrompt ? (_jsx(TextPrompt, { columns: Math.max(1, (terminal.columns ?? 80) - 2), title: textPrompt.title, placeholder: textPrompt.placeholder, initialValue: textPrompt.initialValue, emptyNotice: textPrompt.emptyNotice, hidden: permission !== null || (agentSurface !== null || outputViewer !== null), onSubmit: submitTextPrompt, onCancel: cancelTextPrompt }, textPrompt.token)) : permission === null &&
7323
+ cancelExits: !session?.hasProvider })) : (_jsxs(_Fragment, { children: [agentSurface === null && outputViewer === null ? (_jsx(LiveActivity, { compact: compactWork, activeTools: visibleActiveTools, working: state === 'thinking' || state === 'tool', interruptible: abortRef.current !== null, animate: stdout.isTTY === true &&
7324
+ permission === null &&
7325
+ textPrompt === null &&
7326
+ saveSkillPrompt === null, thinking: thinking, tokens: turnTokens, waitingForYou: choicePicker !== null ||
7327
+ permission !== null ||
7328
+ textPrompt !== null ||
7329
+ saveSkillPrompt !== null })) : null, agentSurface === null && outputViewer === null && permission === null ? (_jsx(TaskList, { tasks: liveTasks })) : null, permission ? (_jsx(PermissionOverlay, { toolCalls: permission.toolCalls, review: permission.review, summary: permission.summary, detailsOpen: permissionDetailsOpen, reviewOffset: permissionReviewOffset, choice: permissionChoice, queuedCount: queuedPermissionCount, sourceLabel: permissionSourceLabel, columns: terminal.columns, rows: terminal.rows, batchOnly: permission.batchOnly === true, siteNotes: permission.siteNotes, ...(permission.screenConsent === true
7330
+ ? { screenConsent: { provider: session?.providerSummary ?? null } }
7331
+ : {}) })) : null, saveSkillPrompt && permission === null ? (_jsx(SaveSkillOverlay, { request: saveSkillPrompt.request, cwd: ctx.cwd, columns: Math.max(1, (terminal.columns ?? 80) - 2), rows: terminal.rows, onAnswer: saveSkillPrompt.resolve }, saveSkillPrompt.request.markdown)) : null, textPrompt ? (_jsx(TextPrompt, { columns: Math.max(1, (terminal.columns ?? 80) - 2), title: textPrompt.title, placeholder: textPrompt.placeholder, initialValue: textPrompt.initialValue, emptyNotice: textPrompt.emptyNotice, hidden: permission !== null || (agentSurface !== null || outputViewer !== null), onSubmit: submitTextPrompt, onCancel: cancelTextPrompt }, textPrompt.token)) : permission === null &&
6803
7332
  agentSurface === null &&
6804
7333
  outputViewer === null &&
6805
7334
  choicePicker?.kind === 'reasoning-effort' &&
@@ -6811,7 +7340,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6811
7340
  textPrompt === null &&
6812
7341
  choicePicker === null &&
6813
7342
  copyPicker === null &&
7343
+ saveSkillPrompt === null &&
6814
7344
  agentSurface === null && outputViewer === null, hidden: permission !== null ||
7345
+ saveSkillPrompt !== null ||
6815
7346
  textPrompt !== null ||
6816
7347
  choicePicker !== null ||
6817
7348
  copyPicker !== null ||
@@ -6836,14 +7367,18 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6836
7367
  conversationMutation !== null ||
6837
7368
  externalEditorRequest !== null ||
6838
7369
  (agentSurface !== null || outputViewer !== null), hidden: permission !== null ||
7370
+ saveSkillPrompt !== null ||
6839
7371
  textPrompt !== null ||
6840
7372
  choicePicker !== null ||
6841
7373
  copyPicker !== null ||
6842
7374
  (agentSurface !== null || outputViewer !== null),
6843
7375
  // A turn is running, so Esc is the interrupt and not
6844
7376
  // the composer's clear.
6845
- escapeInterrupts: !compacting &&
6846
- (state === 'thinking' || state === 'tool' || pendingModelSwitchRef.current !== null), 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) => ({
7377
+ escapeInterrupts: (!compacting &&
7378
+ (state === 'thinking' ||
7379
+ state === 'tool' ||
7380
+ pendingModelSwitchRef.current !== null)) ||
7381
+ (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) => ({
6847
7382
  ...command,
6848
7383
  description: command.unavailable?.(slashCtx) ?? command.description,
6849
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 &&