@mjasnikovs/pi-task 0.38.29 → 0.38.30

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 (370) hide show
  1. package/dist/config/config.d.ts +70 -70
  2. package/dist/config/config.js +26 -35
  3. package/dist/config/extension-list.d.ts +6 -5
  4. package/dist/config/extension-list.js +3 -2
  5. package/dist/config/reasoning-args.d.ts +9 -7
  6. package/dist/config/reasoning-args.js +12 -10
  7. package/dist/config/reasoning.d.ts +44 -105
  8. package/dist/config/reasoning.js +27 -704
  9. package/dist/config/register.d.ts +34 -48
  10. package/dist/config/register.js +41 -51
  11. package/dist/config/tool-list.d.ts +16 -16
  12. package/dist/config/tool-list.js +1 -1
  13. package/dist/remote/bridge.d.ts +19 -10
  14. package/dist/remote/bridge.js +3 -2
  15. package/dist/remote/broadcast.js +3 -1
  16. package/dist/remote/events.js +12 -11
  17. package/dist/remote/history.d.ts +1 -1
  18. package/dist/remote/protocol.d.ts +6 -3
  19. package/dist/remote/protocol.js +2 -1
  20. package/dist/remote/push.d.ts +16 -16
  21. package/dist/remote/push.js +27 -27
  22. package/dist/remote/register.d.ts +3 -3
  23. package/dist/remote/register.js +17 -19
  24. package/dist/remote/server.d.ts +9 -8
  25. package/dist/remote/server.js +15 -14
  26. package/dist/remote/session-state.d.ts +5 -4
  27. package/dist/remote/session-state.js +8 -5
  28. package/dist/remote/sw.d.ts +7 -6
  29. package/dist/remote/sw.js +7 -6
  30. package/dist/remote/tailscale.d.ts +4 -2
  31. package/dist/remote/tailscale.js +4 -2
  32. package/dist/remote/ui-highlight.js +6 -5
  33. package/dist/remote/ui-render.js +4 -4
  34. package/dist/remote/ui-script.js +24 -24
  35. package/dist/remote/ui-styles.d.ts +1 -1
  36. package/dist/remote/ui-styles.js +10 -13
  37. package/dist/remote/ui-tools.js +9 -6
  38. package/dist/shared/child-extensions.d.ts +29 -17
  39. package/dist/shared/child-extensions.js +29 -17
  40. package/dist/shared/child-output.d.ts +30 -24
  41. package/dist/shared/child-output.js +25 -17
  42. package/dist/shared/child-process.d.ts +47 -40
  43. package/dist/shared/child-process.js +50 -59
  44. package/dist/shared/command-watchdog.d.ts +22 -16
  45. package/dist/shared/command-watchdog.js +28 -21
  46. package/dist/shared/fs-text.d.ts +16 -10
  47. package/dist/shared/fs-text.js +16 -10
  48. package/dist/shared/git-runner.d.ts +25 -25
  49. package/dist/shared/git-runner.js +25 -25
  50. package/dist/shared/leaked-tool-call.d.ts +17 -11
  51. package/dist/shared/leaked-tool-call.js +23 -15
  52. package/dist/shared/model-endpoint.d.ts +29 -16
  53. package/dist/shared/model-endpoint.js +33 -21
  54. package/dist/shared/pi-invocation.d.ts +7 -4
  55. package/dist/shared/pi-invocation.js +12 -7
  56. package/dist/shared/pkg-version.d.ts +13 -5
  57. package/dist/shared/pkg-version.js +13 -5
  58. package/dist/shared/reasoning-capability.d.ts +35 -24
  59. package/dist/shared/reasoning-capability.js +35 -24
  60. package/dist/shared/stream-watchdog.d.ts +60 -44
  61. package/dist/shared/stream-watchdog.js +62 -45
  62. package/dist/task/accept-debt.d.ts +41 -43
  63. package/dist/task/accept-debt.js +73 -65
  64. package/dist/task/api-synthesis.d.ts +24 -21
  65. package/dist/task/api-synthesis.js +32 -26
  66. package/dist/task/apis-contract.d.ts +32 -64
  67. package/dist/task/apis-contract.js +32 -64
  68. package/dist/task/artifact-closure.d.ts +27 -13
  69. package/dist/task/artifact-closure.js +95 -67
  70. package/dist/task/auto-commit.d.ts +46 -35
  71. package/dist/task/auto-commit.js +51 -38
  72. package/dist/task/auto-io.d.ts +45 -25
  73. package/dist/task/auto-io.js +57 -29
  74. package/dist/task/auto-orchestrator.d.ts +26 -24
  75. package/dist/task/auto-orchestrator.js +178 -162
  76. package/dist/task/auto-prompts.d.ts +36 -24
  77. package/dist/task/auto-prompts.js +40 -26
  78. package/dist/task/autofix-ledger.d.ts +27 -25
  79. package/dist/task/autofix-ledger.js +29 -26
  80. package/dist/task/batch-test-task.d.ts +20 -12
  81. package/dist/task/batch-test-task.js +67 -60
  82. package/dist/task/boot-probe.d.ts +60 -44
  83. package/dist/task/boot-probe.js +91 -72
  84. package/dist/task/cancel-input.d.ts +30 -16
  85. package/dist/task/cancel-input.js +20 -11
  86. package/dist/task/cancel-points.d.ts +27 -20
  87. package/dist/task/cancel-points.js +30 -22
  88. package/dist/task/child-runner.d.ts +46 -51
  89. package/dist/task/child-runner.js +48 -49
  90. package/dist/task/child-status.d.ts +23 -16
  91. package/dist/task/child-status.js +23 -16
  92. package/dist/task/clamp-output.js +12 -5
  93. package/dist/task/command-run.d.ts +31 -28
  94. package/dist/task/command-run.js +44 -35
  95. package/dist/task/command-shrink.d.ts +25 -18
  96. package/dist/task/command-shrink.js +37 -31
  97. package/dist/task/command-watchdog.d.ts +9 -6
  98. package/dist/task/command-watchdog.js +21 -15
  99. package/dist/task/context-attribution.d.ts +34 -26
  100. package/dist/task/context-attribution.js +34 -26
  101. package/dist/task/context-silence.d.ts +39 -29
  102. package/dist/task/context-silence.js +35 -25
  103. package/dist/task/context-usage.d.ts +16 -9
  104. package/dist/task/context-usage.js +16 -9
  105. package/dist/task/contracts.d.ts +8 -4
  106. package/dist/task/contracts.js +25 -17
  107. package/dist/task/coverage-loop.d.ts +22 -18
  108. package/dist/task/coverage-loop.js +35 -30
  109. package/dist/task/critique-probes.d.ts +13 -14
  110. package/dist/task/critique-probes.js +50 -39
  111. package/dist/task/debug-log.d.ts +13 -5
  112. package/dist/task/debug-log.js +32 -20
  113. package/dist/task/decompose-fidelity.d.ts +11 -9
  114. package/dist/task/decompose-fidelity.js +38 -33
  115. package/dist/task/decompose-granularity.d.ts +41 -38
  116. package/dist/task/decompose-granularity.js +41 -38
  117. package/dist/task/deep-render-check.d.ts +22 -14
  118. package/dist/task/deep-render-check.js +40 -31
  119. package/dist/task/dropped-input.d.ts +12 -7
  120. package/dist/task/dropped-input.js +5 -2
  121. package/dist/task/enforce-attribution.d.ts +38 -47
  122. package/dist/task/enforce-attribution.js +46 -52
  123. package/dist/task/enforce-guidelines.d.ts +31 -20
  124. package/dist/task/enforce-guidelines.js +32 -21
  125. package/dist/task/enrichment.d.ts +7 -2
  126. package/dist/task/enrichment.js +26 -14
  127. package/dist/task/env-notes.d.ts +16 -7
  128. package/dist/task/env-notes.js +48 -31
  129. package/dist/task/env-template-closure.d.ts +4 -4
  130. package/dist/task/env-template-closure.js +42 -34
  131. package/dist/task/external-context.d.ts +28 -21
  132. package/dist/task/external-context.js +17 -12
  133. package/dist/task/failure-classifier.d.ts +4 -5
  134. package/dist/task/failure-classifier.js +6 -7
  135. package/dist/task/file-inventory.d.ts +15 -11
  136. package/dist/task/file-inventory.js +25 -22
  137. package/dist/task/final-gate-fix.d.ts +74 -86
  138. package/dist/task/final-gate-fix.js +97 -116
  139. package/dist/task/final-gate-progress.d.ts +29 -46
  140. package/dist/task/final-gate-progress.js +40 -51
  141. package/dist/task/final-gate.d.ts +64 -97
  142. package/dist/task/final-gate.js +192 -199
  143. package/dist/task/fix-child.d.ts +21 -27
  144. package/dist/task/fix-child.js +21 -27
  145. package/dist/task/foreign-path.d.ts +6 -5
  146. package/dist/task/foreign-path.js +0 -0
  147. package/dist/task/frozen-conflict.d.ts +9 -10
  148. package/dist/task/frozen-conflict.js +61 -64
  149. package/dist/task/frozen-path-guard.d.ts +35 -14
  150. package/dist/task/frozen-path-guard.js +56 -39
  151. package/dist/task/gate-child.d.ts +27 -28
  152. package/dist/task/gate-child.js +36 -35
  153. package/dist/task/gate-deps.d.ts +34 -27
  154. package/dist/task/gate-deps.js +169 -159
  155. package/dist/task/gate-tally.d.ts +77 -80
  156. package/dist/task/gate-tally.js +65 -68
  157. package/dist/task/git-state-guard.d.ts +15 -11
  158. package/dist/task/git-state-guard.js +76 -66
  159. package/dist/task/impl-widget.d.ts +25 -16
  160. package/dist/task/impl-widget.js +27 -17
  161. package/dist/task/implementation-thinking.d.ts +33 -31
  162. package/dist/task/implementation-thinking.js +5 -6
  163. package/dist/task/implementation-turn.d.ts +34 -31
  164. package/dist/task/implementation-turn.js +29 -27
  165. package/dist/task/inline-markdown.d.ts +20 -7
  166. package/dist/task/inline-markdown.js +15 -6
  167. package/dist/task/launch-config-gap.js +25 -39
  168. package/dist/task/launch-contract.d.ts +18 -21
  169. package/dist/task/launch-contract.js +28 -30
  170. package/dist/task/launch-manifest.d.ts +6 -2
  171. package/dist/task/launch-manifest.js +35 -34
  172. package/dist/task/ledger.js +16 -14
  173. package/dist/task/lint-fix.d.ts +6 -8
  174. package/dist/task/lint-fix.js +67 -69
  175. package/dist/task/loop-detector.d.ts +9 -8
  176. package/dist/task/loop-detector.js +16 -12
  177. package/dist/task/mid-run-input.d.ts +17 -15
  178. package/dist/task/mid-run-input.js +17 -15
  179. package/dist/task/orchestrator.d.ts +24 -28
  180. package/dist/task/orchestrator.js +62 -64
  181. package/dist/task/orientation.d.ts +18 -23
  182. package/dist/task/orientation.js +24 -31
  183. package/dist/task/owned-freeze-conflict.d.ts +21 -20
  184. package/dist/task/owned-freeze-conflict.js +52 -85
  185. package/dist/task/owned-freeze-reassign.d.ts +40 -60
  186. package/dist/task/owned-freeze-reassign.js +41 -61
  187. package/dist/task/parsers.d.ts +4 -2
  188. package/dist/task/parsers.js +4 -4
  189. package/dist/task/phases.d.ts +41 -48
  190. package/dist/task/phases.js +179 -248
  191. package/dist/task/plan-io.d.ts +6 -7
  192. package/dist/task/plan-io.js +6 -7
  193. package/dist/task/plan-orchestrator.d.ts +10 -8
  194. package/dist/task/plan-orchestrator.js +14 -10
  195. package/dist/task/plan-prompts.d.ts +6 -5
  196. package/dist/task/plan-prompts.js +6 -5
  197. package/dist/task/plan-readonly.d.ts +4 -5
  198. package/dist/task/plan-readonly.js +4 -5
  199. package/dist/task/plan-rounds.d.ts +17 -29
  200. package/dist/task/plan-rounds.js +21 -34
  201. package/dist/task/plan-session.d.ts +58 -72
  202. package/dist/task/plan-session.js +61 -83
  203. package/dist/task/probe-gaming.d.ts +28 -27
  204. package/dist/task/probe-gaming.js +0 -0
  205. package/dist/task/prohibition-probe.d.ts +14 -16
  206. package/dist/task/prompts.d.ts +3 -4
  207. package/dist/task/prompts.js +17 -26
  208. package/dist/task/qa-transcript.d.ts +15 -22
  209. package/dist/task/qa-transcript.js +15 -21
  210. package/dist/task/question-box.d.ts +17 -13
  211. package/dist/task/question-box.js +19 -15
  212. package/dist/task/question-dedup.d.ts +6 -7
  213. package/dist/task/question-dedup.js +13 -14
  214. package/dist/task/question-dialog.d.ts +22 -32
  215. package/dist/task/question-dialog.js +22 -32
  216. package/dist/task/question-source.d.ts +18 -44
  217. package/dist/task/question-source.js +22 -51
  218. package/dist/task/refuted-constraint.d.ts +11 -31
  219. package/dist/task/refuted-constraint.js +27 -51
  220. package/dist/task/regenerable-artifacts.d.ts +12 -31
  221. package/dist/task/regenerable-artifacts.js +12 -31
  222. package/dist/task/render-check.d.ts +11 -22
  223. package/dist/task/render-check.js +33 -46
  224. package/dist/task/repo-health-check.d.ts +10 -14
  225. package/dist/task/repo-health-check.js +17 -23
  226. package/dist/task/requirements.d.ts +38 -71
  227. package/dist/task/requirements.js +78 -126
  228. package/dist/task/research-fanout-budget.d.ts +51 -88
  229. package/dist/task/research-fanout-budget.js +51 -88
  230. package/dist/task/research-worker.d.ts +29 -39
  231. package/dist/task/research-worker.js +37 -61
  232. package/dist/task/resume-gap.d.ts +14 -15
  233. package/dist/task/root-cause-repair.d.ts +9 -9
  234. package/dist/task/root-cause-repair.js +28 -40
  235. package/dist/task/run-bracket.d.ts +10 -13
  236. package/dist/task/run-end.d.ts +12 -22
  237. package/dist/task/run-end.js +8 -16
  238. package/dist/task/run-final-gate.d.ts +19 -21
  239. package/dist/task/run-final-gate.js +62 -80
  240. package/dist/task/runner-globs.d.ts +12 -13
  241. package/dist/task/runner-globs.js +12 -13
  242. package/dist/task/runner-resolve.d.ts +9 -9
  243. package/dist/task/runner-resolve.js +22 -23
  244. package/dist/task/script-escape.d.ts +10 -12
  245. package/dist/task/script-escape.js +13 -14
  246. package/dist/task/serve-entry.d.ts +1 -1
  247. package/dist/task/serve-entry.js +22 -25
  248. package/dist/task/service-blocks.js +4 -2
  249. package/dist/task/shipped-source.d.ts +11 -29
  250. package/dist/task/shipped-source.js +11 -29
  251. package/dist/task/skip-escape.js +10 -14
  252. package/dist/task/spec-urls.d.ts +26 -65
  253. package/dist/task/spec-urls.js +26 -65
  254. package/dist/task/spec-validation.d.ts +17 -20
  255. package/dist/task/spec-validation.js +17 -20
  256. package/dist/task/stall-detector.d.ts +23 -30
  257. package/dist/task/stall-detector.js +23 -30
  258. package/dist/task/stream-watchdog.d.ts +14 -12
  259. package/dist/task/stream-watchdog.js +14 -12
  260. package/dist/task/substitution-probe.d.ts +17 -20
  261. package/dist/task/substitution-probe.js +17 -20
  262. package/dist/task/task-gates.d.ts +36 -41
  263. package/dist/task/task-gates.js +95 -106
  264. package/dist/task/task-io.d.ts +4 -4
  265. package/dist/task/task-io.js +4 -4
  266. package/dist/task/task-parsers.js +4 -3
  267. package/dist/task/task-provenance.d.ts +2 -2
  268. package/dist/task/task-provenance.js +11 -13
  269. package/dist/task/task-types.d.ts +4 -3
  270. package/dist/task/terminal-outcome.d.ts +14 -16
  271. package/dist/task/terminal-outcome.js +12 -14
  272. package/dist/task/test-assembly.d.ts +13 -20
  273. package/dist/task/test-assembly.js +13 -20
  274. package/dist/task/timings.d.ts +5 -3
  275. package/dist/task/timings.js +5 -3
  276. package/dist/task/title-label.d.ts +9 -4
  277. package/dist/task/title-label.js +9 -4
  278. package/dist/task/type-only-answer.d.ts +44 -52
  279. package/dist/task/type-only-answer.js +44 -52
  280. package/dist/task/unfailable-command.d.ts +18 -24
  281. package/dist/task/unfailable-command.js +21 -27
  282. package/dist/task/unknown-routing.d.ts +10 -4
  283. package/dist/task/unknown-routing.js +10 -4
  284. package/dist/task/user-directives.d.ts +5 -8
  285. package/dist/task/user-directives.js +5 -8
  286. package/dist/task/verify-quality.d.ts +18 -22
  287. package/dist/task/verify-quality.js +45 -46
  288. package/dist/task/verify-reconcile.d.ts +15 -10
  289. package/dist/task/verify-reconcile.js +45 -43
  290. package/dist/task/verify-resolution.d.ts +24 -20
  291. package/dist/task/verify-resolution.js +51 -50
  292. package/dist/task/verify-work.d.ts +59 -66
  293. package/dist/task/verify-work.js +101 -138
  294. package/dist/task/widget.d.ts +15 -14
  295. package/dist/task/widget.js +22 -17
  296. package/dist/task/wiring-claims.d.ts +25 -32
  297. package/dist/task/wiring-claims.js +30 -35
  298. package/dist/task/write-guard.d.ts +39 -39
  299. package/dist/task/write-guard.js +48 -51
  300. package/dist/task/yolo.d.ts +34 -30
  301. package/dist/task/yolo.js +42 -37
  302. package/dist/workers/abstention.d.ts +21 -41
  303. package/dist/workers/abstention.js +27 -48
  304. package/dist/workers/brave-search.d.ts +4 -3
  305. package/dist/workers/brave-search.js +5 -2
  306. package/dist/workers/brave-warning.d.ts +7 -4
  307. package/dist/workers/brave-warning.js +19 -7
  308. package/dist/workers/ddg-search.d.ts +6 -6
  309. package/dist/workers/ddg-search.js +18 -12
  310. package/dist/workers/docs-cache.js +5 -2
  311. package/dist/workers/docs-chunk.d.ts +30 -37
  312. package/dist/workers/docs-chunk.js +37 -41
  313. package/dist/workers/docs-core.d.ts +28 -44
  314. package/dist/workers/docs-core.js +25 -44
  315. package/dist/workers/docs-index.js +4 -3
  316. package/dist/workers/docs-lookup.d.ts +15 -22
  317. package/dist/workers/docs-lookup.js +12 -21
  318. package/dist/workers/docs-project.d.ts +15 -9
  319. package/dist/workers/docs-project.js +17 -10
  320. package/dist/workers/docs-resolve.d.ts +19 -20
  321. package/dist/workers/docs-resolve.js +35 -32
  322. package/dist/workers/docs-retrieve.d.ts +5 -6
  323. package/dist/workers/docs-retrieve.js +18 -15
  324. package/dist/workers/exa-search.d.ts +9 -6
  325. package/dist/workers/exa-search.js +23 -12
  326. package/dist/workers/fetch-core.d.ts +13 -16
  327. package/dist/workers/fetch-core.js +23 -23
  328. package/dist/workers/focused-extractor.d.ts +12 -12
  329. package/dist/workers/focused-extractor.js +16 -19
  330. package/dist/workers/html-clean.js +24 -14
  331. package/dist/workers/http-request.d.ts +28 -20
  332. package/dist/workers/http-request.js +22 -17
  333. package/dist/workers/npm-version.d.ts +28 -11
  334. package/dist/workers/npm-version.js +24 -15
  335. package/dist/workers/phantom-imports.d.ts +15 -12
  336. package/dist/workers/phantom-imports.js +30 -24
  337. package/dist/workers/pi-worker-core.d.ts +69 -71
  338. package/dist/workers/pi-worker-core.js +100 -109
  339. package/dist/workers/pi-worker-docs.d.ts +24 -19
  340. package/dist/workers/pi-worker-docs.js +67 -76
  341. package/dist/workers/pi-worker-fetch.d.ts +7 -3
  342. package/dist/workers/pi-worker-fetch.js +27 -19
  343. package/dist/workers/pi-worker-search.js +12 -8
  344. package/dist/workers/pi-worker.d.ts +9 -4
  345. package/dist/workers/pi-worker.js +21 -14
  346. package/dist/workers/reasoning-warning.d.ts +18 -17
  347. package/dist/workers/reasoning-warning.js +22 -20
  348. package/dist/workers/research-cache.js +50 -78
  349. package/dist/workers/search-core.js +7 -5
  350. package/dist/workers/search-types.d.ts +10 -9
  351. package/dist/workers/search-types.js +9 -8
  352. package/dist/workers/session-hint.d.ts +13 -14
  353. package/dist/workers/session-hint.js +8 -9
  354. package/dist/workers/shared.d.ts +21 -25
  355. package/dist/workers/shared.js +0 -0
  356. package/dist/workers/single-read-extension.d.ts +14 -7
  357. package/dist/workers/single-read-extension.js +14 -7
  358. package/dist/workers/single-read-guard.d.ts +25 -28
  359. package/dist/workers/single-read-guard.js +32 -32
  360. package/dist/workers/typeonly-log.d.ts +12 -9
  361. package/dist/workers/typeonly-log.js +29 -33
  362. package/dist/workers/worker-channels.d.ts +15 -23
  363. package/dist/workers/worker-channels.js +15 -23
  364. package/dist/workers/worker-failure.d.ts +38 -46
  365. package/dist/workers/worker-failure.js +31 -39
  366. package/dist/workers/worker-kill.d.ts +25 -26
  367. package/dist/workers/worker-kill.js +16 -19
  368. package/dist/workers/worker-profiles.d.ts +43 -53
  369. package/dist/workers/worker-profiles.js +30 -38
  370. package/package.json +10 -8
@@ -2,23 +2,22 @@ import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import webpush from 'web-push';
5
- /** Resolve the XDG data-home base. Under data-home (not cache) so the files it
6
- * roots survive cache clears. Follows the repo's XDG convention; see
7
- * workers/docs-core.ts. */
5
+ /** Resolve the XDG data-home base. Same env-or-homedir shape the docs cache uses
6
+ * (workers/docs-core.ts), but rooted at data-home rather than cache-home: what
7
+ * lives under here is not reconstructible by re-fetching. */
8
8
  function dataHome() {
9
9
  return process.env.XDG_DATA_HOME?.trim() || path.join(os.homedir(), '.local', 'share');
10
10
  }
11
- /** Where the VAPID keypair is persisted losing these keys invalidates every
12
- * existing browser subscription. */
11
+ /** Where the VAPID keypair is persisted. The browser subscribes with this
12
+ * public key as its applicationServerKey (see ui-script.ts), so the pair has to
13
+ * outlive the process that created it. */
13
14
  export function vapidStorePath() {
14
15
  return path.join(dataHome(), 'pi-task', 'vapid.json');
15
16
  }
16
17
  /** Where browser push subscriptions are mirrored to disk (next to vapid.json).
17
- * Without this, a server restart e.g. after a rebuild silently drops every
18
- * device: the in-memory store is empty, and a backgrounded/suspended PWA won't
19
- * re-register until the user next foregrounds it, which is exactly when the push
20
- * is no longer useful. The in-memory store stays authoritative within a process;
21
- * this is its durable mirror. */
18
+ * Without this a server restart starts with an empty store and reaches nobody
19
+ * until every device happens to re-register. The in-memory store stays
20
+ * authoritative within a process; this is its durable mirror. */
22
21
  export function subscriptionsStorePath() {
23
22
  return path.join(dataHome(), 'pi-task', 'subscriptions.json');
24
23
  }
@@ -27,16 +26,17 @@ export function subscriptionsStorePath() {
27
26
  export function pushLogPath() {
28
27
  return process.env.PI_REMOTE_PUSH_LOG?.trim() || '/tmp/pi-task-push.log';
29
28
  }
30
- /** VAPID JWT `sub` claim. Apple (unlike Chrome/FCM) validates this and rejects
31
- * tokens with an unroutable subject like `mailto:...@localhost` as BadJwtToken,
32
- * so the default is a real https URL. Override with PI_REMOTE_PUSH_SUBJECT
33
- * (e.g. your own `mailto:you@domain.com`). */
29
+ /** VAPID JWT `sub` claim. web-push's validateSubject requires a parseable URL
30
+ * whose protocol is `https:` or `mailto:` and throws otherwise, so the default
31
+ * is a real https URL. Override with PI_REMOTE_PUSH_SUBJECT (e.g. your own
32
+ * `mailto:you@domain.com`). */
34
33
  export function pushSubject() {
35
34
  return process.env.PI_REMOTE_PUSH_SUBJECT?.trim() || 'https://github.com/mjasnikovs/pi-task';
36
35
  }
37
36
  /** Append a timestamped diagnostic line — but only when PI_REMOTE_PUSH_DEBUG is
38
- * set, so it stays silent (and test-safe) by default. Push failures are
39
- * otherwise swallowed, so this is the way to see Apple's HTTP status codes. */
37
+ * set, so it stays silent (and test-safe) by default. deliver() swallows every
38
+ * send failure, so this is the only way to see the push service's status
39
+ * codes. */
40
40
  export function logPush(line) {
41
41
  if (!process.env.PI_REMOTE_PUSH_DEBUG)
42
42
  return;
@@ -49,8 +49,8 @@ export function logPush(line) {
49
49
  // diagnostics are best-effort; never let logging break a push
50
50
  }
51
51
  }
52
- /** Short host label for an endpoint, for readable logs (full endpoints are long
53
- * and contain device tokens). */
52
+ /** Short host label for an endpoint, for readable logs a push endpoint's path
53
+ * carries the device's own subscription id and does not belong in a log. */
54
54
  function endpointHost(sub) {
55
55
  try {
56
56
  return new URL(sub.endpoint ?? '').host;
@@ -82,10 +82,9 @@ export function loadOrCreateVapidKeys(file = vapidStorePath()) {
82
82
  return keys;
83
83
  }
84
84
  // In-memory subscription store, keyed by endpoint. Persisted on globalThis so it
85
- // survives jiti re-evaluation on session switches (same pattern as broadcast.ts),
86
- // and mirrored to disk (subscriptionsStorePath) so it also survives a full
87
- // process restart. A re-subscribe on page load can't be relied on for that: the
88
- // devices that need server push are backgrounded/suspended and won't reload.
85
+ // survives the module being re-evaluated (same pattern as broadcast.ts), and
86
+ // mirrored to disk (subscriptionsStorePath) so it also survives a full process
87
+ // restart a re-subscribe on page load only helps devices whose page is open.
89
88
  const g = globalThis;
90
89
  const freshStore = !g.__piRemoteSubs;
91
90
  if (!g.__piRemoteSubs)
@@ -144,8 +143,9 @@ export function clearSubscriptions() {
144
143
  subs.clear();
145
144
  saveSubscriptions();
146
145
  }
147
- /** True when the push service says the subscription is permanently gone and
148
- * should be dropped (404 Not Found, 410 Gone). */
146
+ /** True when the push service's status says the subscription is permanently
147
+ * gone (404, 410) rather than transiently failing. web-push rejects with the
148
+ * `statusCode` on any non-2xx, which is what this reads. */
149
149
  function isGone(err) {
150
150
  const code = err?.statusCode;
151
151
  return code === 404 || code === 410;
@@ -182,9 +182,9 @@ function ensureConfigured() {
182
182
  export function publicKey() {
183
183
  return ensureConfigured().publicKey;
184
184
  }
185
- /** Send a notification to all subscribed devices. Best-effort: delivery is
186
- * server→push-service→device, so it reaches a suspended iOS PWA that the
187
- * in-page Notification API never could. */
185
+ /** Send a notification to all subscribed devices. Best-effort, and the route is
186
+ * server→push-service→device, so it does not need the page to be open the way
187
+ * the in-page Notification API does. */
188
188
  export async function pushNotify(title, body, tag) {
189
189
  if (process.env.NODE_ENV === 'test')
190
190
  return;
@@ -1,8 +1,8 @@
1
1
  import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
2
2
  /**
3
- * Where a plain (non-slash) browser line goes. Exported so the decision is
4
- * tested as SHIPPED rather than as a copy — the three branches are the whole of
5
- * issue #8, and the middle one is the regression.
3
+ * Where a plain (non-slash) browser line goes. Exported so
4
+ * test/remote/mid-run-routing.test.ts drives the shipped decision rather than a
5
+ * copy of it these three branches are the whole of issue #8.
6
6
  */
7
7
  export declare function routePlainLine(plain: string, send: (text: string, opts?: {
8
8
  deliverAs: 'steer' | 'followUp';
@@ -12,22 +12,17 @@ if (!_g.__piRemote)
12
12
  _g.__piRemote = { server: null, send: null, serveResult: null };
13
13
  const S = _g.__piRemote;
14
14
  /**
15
- * Where a plain (non-slash) browser line goes. Exported so the decision is
16
- * tested as SHIPPED rather than as a copy — the three branches are the whole of
17
- * issue #8, and the middle one is the regression.
15
+ * Where a plain (non-slash) browser line goes. Exported so
16
+ * test/remote/mid-run-routing.test.ts drives the shipped decision rather than a
17
+ * copy of it these three branches are the whole of issue #8.
18
18
  */
19
19
  export function routePlainLine(plain, send) {
20
20
  addUserTurn(plain);
21
21
  // Read the run flag from SessionState, which is the only thing that owns it.
22
- // There used to be a mirror of it here (remote/state.ts: a module-level `let`
23
- // plus a getter/setter, set alongside agentStart/agentEnd in events.ts). Two
24
- // sources of truth for one boolean, and they disagreed twice over: SessionState
25
- // also clears agentRunning in addError and reset, neither of which touched the
26
- // mirror — so an errored turn or a /new left this branch steering into a turn
27
- // SessionState already considered finished. And the mirror was a plain module
28
- // `let` while every other piece of remote state deliberately lives on
29
- // globalThis to survive jiti re-evaluation, so it silently reset on session
30
- // switch. Deleting it removed complexity rather than moving it.
22
+ // Three functions clear it agentEnd, addError and reset so anything
23
+ // mirroring the flag here would have to track an errored turn and a /new as
24
+ // well as a finished one, and would steer into a turn SessionState already
25
+ // considers over whenever it missed one.
31
26
  if (getState().agentRunning) {
32
27
  // A live turn: steer it (inject into the current generation) so the
33
28
  // nudge lands immediately.
@@ -35,9 +30,9 @@ export function routePlainLine(plain, send) {
35
30
  }
36
31
  else if (isRunActive()) {
37
32
  // Idle, but a task run owns the session — which is most of a run, since
38
- // the spec phases and every gate are child processes. Sending here opens
39
- // a SECOND turn beside the run; that is what killed a live run on pi
40
- // 0.82.1. Hold it for the next task turn instead.
33
+ // the spec phases and every gate are child processes. pi documents
34
+ // sendUserMessage as "Always triggers a turn", so sending here opens a
35
+ // SECOND turn beside the run. Hold it for the next task turn instead.
41
36
  holdInput(plain);
42
37
  }
43
38
  else {
@@ -65,7 +60,9 @@ export function registerRemote(pi) {
65
60
  }, wsUrl => html(wsUrl), interruptAgent, clearHeldInput);
66
61
  // Hands-off HTTPS: point Tailscale serve at our port so phones get a
67
62
  // secure context. Best-effort — any failure degrades to the http URL.
68
- S.serveResult = await ensureTailscaleServe(S.server.port).catch(() => ({ state: 'unavailable' }));
63
+ S.serveResult = await ensureTailscaleServe(S.server.port).catch(() => ({
64
+ state: 'unavailable'
65
+ }));
69
66
  return S.server;
70
67
  }
71
68
  pi.on('session_start', (_event, ctx) => {
@@ -119,9 +116,10 @@ export function registerRemote(pi) {
119
116
  });
120
117
  publishNotify('Compacting context…', 'info');
121
118
  });
122
- // Bridge-registered (not pi.registerCommand) so `/remote stop` also works when
123
- // typed in the browser the web UI advertises it, and while a /task-auto run
124
- // holds the host command loop the browser is the only live input surface.
119
+ // registerBridgeCommand, not registerRemoteOnlyCommand: this one needs to
120
+ // exist in the terminal AND on the bridge, so `/remote stop` also works typed
121
+ // in the browser the web UI advertises it, and while a /task-auto run holds
122
+ // the host command loop the browser is the only live input surface.
125
123
  registerBridgeCommand(pi, 'remote', {
126
124
  description: 'Show the remote QR code & URLs.',
127
125
  handler: async (args, ctx) => {
@@ -25,8 +25,9 @@ type MessageCallback = (text: string) => void;
25
25
  export declare function getLocalIPs(nets?: NodeJS.Dict<import("node:os").NetworkInterfaceInfo[]>): LocalIPs;
26
26
  /** Build the labeled URL lines shown under the QR code. Both Tailscale and LAN
27
27
  * when present; a single unlabeled primary URL when neither resolves. The
28
- * Tailscale line uses the MagicDNS host when known (resolves to the same node,
29
- * but is what SSH and webpush certs need), falling back to the raw IP. */
28
+ * Tailscale line uses the MagicDNS host when known — it resolves to the same
29
+ * node as the raw IP, but it is the name `tailscale cert` and the https URL in
30
+ * tailscale.ts are issued for — falling back to the raw IP. */
30
31
  export declare function formatAddresses(ips: LocalIPs, port: number, tsHost?: string): AddressLine[];
31
32
  /** Bind the REAL `server` to the first free port at or above `start`, trying up
32
33
  * to `max` consecutive ports. On EADDRINUSE we bump the port and re-listen; any
@@ -35,12 +36,12 @@ export declare function formatAddresses(ips: LocalIPs, port: number, tsHost?: st
35
36
  *
36
37
  * Binding the real server directly (rather than probing a throwaway socket with
37
38
  * createServer()/listen()/close() first, then binding the real one) removes a
38
- * TOCTOU race: between "probe says port free" and "real listen", the port can be
39
- * taken by someone else, and on Windows/Bun the PROBE socket's own port isn't
40
- * fully released before the real listen runs so the real bind hits EADDRINUSE
41
- * on the very port that just tested free, and (with no 'error' listener on the
42
- * real server) escapes as an uncaughtException that crashes pi (issue #7).
43
- * Retrying the real bind has no probe and no window. */
39
+ * TOCTOU race: between "probe says port free" and "real listen" the port can be
40
+ * taken — including by the probe's own socket, if the runtime has not released
41
+ * it yet so the real bind hits EADDRINUSE on the very port that just tested
42
+ * free. With no 'error' listener on the real server that escapes as an
43
+ * uncaughtException and takes pi down; that is issue #7. Retrying the real bind
44
+ * has no probe and no window. */
44
45
  export declare function listenWithRetry(server: import('node:http').Server, start: number, max: number): Promise<number>;
45
46
  export declare function startServer(onMessage: MessageCallback, getHtml: (wsUrl: string) => string, onInterrupt?: () => void, onClearHeld?: () => void): Promise<ServerHandle>;
46
47
  export {};
@@ -34,8 +34,9 @@ export function getLocalIPs(nets = networkInterfaces()) {
34
34
  }
35
35
  /** Build the labeled URL lines shown under the QR code. Both Tailscale and LAN
36
36
  * when present; a single unlabeled primary URL when neither resolves. The
37
- * Tailscale line uses the MagicDNS host when known (resolves to the same node,
38
- * but is what SSH and webpush certs need), falling back to the raw IP. */
37
+ * Tailscale line uses the MagicDNS host when known — it resolves to the same
38
+ * node as the raw IP, but it is the name `tailscale cert` and the https URL in
39
+ * tailscale.ts are issued for — falling back to the raw IP. */
39
40
  export function formatAddresses(ips, port, tsHost) {
40
41
  const out = [];
41
42
  if (tsHost)
@@ -55,12 +56,12 @@ export function formatAddresses(ips, port, tsHost) {
55
56
  *
56
57
  * Binding the real server directly (rather than probing a throwaway socket with
57
58
  * createServer()/listen()/close() first, then binding the real one) removes a
58
- * TOCTOU race: between "probe says port free" and "real listen", the port can be
59
- * taken by someone else, and on Windows/Bun the PROBE socket's own port isn't
60
- * fully released before the real listen runs so the real bind hits EADDRINUSE
61
- * on the very port that just tested free, and (with no 'error' listener on the
62
- * real server) escapes as an uncaughtException that crashes pi (issue #7).
63
- * Retrying the real bind has no probe and no window. */
59
+ * TOCTOU race: between "probe says port free" and "real listen" the port can be
60
+ * taken — including by the probe's own socket, if the runtime has not released
61
+ * it yet so the real bind hits EADDRINUSE on the very port that just tested
62
+ * free. With no 'error' listener on the real server that escapes as an
63
+ * uncaughtException and takes pi down; that is issue #7. Retrying the real bind
64
+ * has no probe and no window. */
64
65
  export function listenWithRetry(server, start, max) {
65
66
  return new Promise((resolve, reject) => {
66
67
  let port = start;
@@ -145,12 +146,12 @@ export async function startServer(onMessage, getHtml, onInterrupt, onClearHeld)
145
146
  // keep-alive / WebSocket connections. Without this, httpServer.close() only
146
147
  // stops accepting new connections and waits for existing ones to drain — an
147
148
  // open browser tab never drains, so the listening server and its sockets
148
- // stay as active event-loop handles forever. In headless print mode the host
149
- // exits by natural event-loop drain (it sets process.exitCode and returns,
150
- // with no process.exit()), so these lingering handles keep the pi process
151
- // alive indefinitely which blocks `docker stop` / OS shutdown until the
152
- // SIGKILL grace timeout fires. A/B-proven: terminating clients + destroying
153
- // sockets here makes the loop drain in ~2ms instead of never.
149
+ // stay as active event-loop handles forever. In print mode pi exits by
150
+ // natural event-loop drain: main.js sets `process.exitCode` and returns with
151
+ // no process.exit(), unlike the package-command path right above it which
152
+ // calls process.exit precisely so a bad extension cannot keep a one-shot
153
+ // command alive. Terminating clients and destroying sockets here is what
154
+ // lets the loop drain at all.
154
155
  const sockets = new Set();
155
156
  httpServer.on('connection', s => {
156
157
  sockets.add(s);
@@ -2,10 +2,11 @@ import { HistoryBuffer } from './history.js';
2
2
  import type { Turn, Part } from './history.js';
3
3
  import type { ContextUsage, PromptMessage, WidgetData } from './protocol.js';
4
4
  export interface LiveTurn {
5
- /** Ordered assistant content (text segments + tool calls) for this run. */
5
+ /** Ordered assistant content text, thinking and tool parts for this run. */
6
6
  parts: Part[];
7
- /** Is the trailing text part still accumulating deltas? Closed by text_end /
8
- * a tool start, so the next text begins a fresh segment (separate bubble). */
7
+ /** Is the trailing text part still accumulating deltas? Closed by text_end, a
8
+ * thinking delta and a tool start, so the next text begins a fresh segment
9
+ * (separate bubble). */
9
10
  textOpen: boolean;
10
11
  }
11
12
  export interface SnapshotMessage {
@@ -29,7 +30,7 @@ interface SessionState {
29
30
  taskWidgetData: WidgetData | null;
30
31
  prompt: PromptMessage | null;
31
32
  context: ContextUsage | null;
32
- /** Human-readable active model name (e.g. "Qwen3.6 27B"), for the header chip. */
33
+ /** Human-readable active model name, as pi reports it, for the header chip. */
33
34
  model: string | null;
34
35
  /** Lines typed while a task run owned the session, waiting for the next task
35
36
  * turn to steer. Mirrors src/task/mid-run-input.ts so the browser can show
@@ -3,12 +3,15 @@
3
3
  // Every change funnels through a mutator that (1) updates this object and (2)
4
4
  // broadcasts the matching live delta. On (re)connect the server serializes the
5
5
  // whole object with snapshot() and the client replaces its entire view. Because
6
- // the snapshot and the live deltas read/write the same object, they can never
7
- // disagree — which is what kills the duplicate-transcript / orphaned-widget /
8
- // two-task-widget drift the ad-hoc broadcasting used to cause.
6
+ // the snapshot and the live deltas read/write the same object, they cannot
7
+ // disagree.
9
8
  //
10
- // State lives on globalThis so it survives jiti module re-evaluation on session
11
- // switches, the same pattern broadcast.ts and bridge.ts use.
9
+ // updateTool is the one mutator that only broadcasts: a partial tool result is
10
+ // transient, endTool carries the real one, and no browser module listens for
11
+ // `tool_update` at all.
12
+ //
13
+ // State lives on globalThis so it survives the module being re-evaluated, the
14
+ // same pattern broadcast.ts and bridge.ts use.
12
15
  import { broadcast as wsBroadcast } from './broadcast.js';
13
16
  import { HistoryBuffer } from './history.js';
14
17
  const g = globalThis;
@@ -1,8 +1,9 @@
1
- /** Service worker source served at /sw.js. It must be a real same-origin URL
2
- * (a SW cannot be registered from a data: URL), and serving it at the root path
3
- * gives it root scope so it controls the whole app.
1
+ /** Service worker source served at /sw.js (see server.ts's request handler). It
2
+ * must be a real same-origin URL registering from a `data:` URL is refused
3
+ * with "The URL protocol of the script is not supported" — and serving it at
4
+ * the root path is what gives it root scope: a worker served at `/sw.js`
5
+ * registers with scope `/`, one at `/deep/sw.js` with scope `/deep/`.
4
6
  *
5
- * iOS home-screen PWAs deliver notifications ONLY through a service worker's
6
- * push event — the in-page `new Notification()` constructor is unavailable so
7
- * this is the only path that reaches a backgrounded iPhone. */
7
+ * Notifications go through this worker's push event rather than an in-page
8
+ * `new Notification()` so they reach a device whose page is not open. */
8
9
  export declare function swJs(): string;
package/dist/remote/sw.js CHANGED
@@ -1,10 +1,11 @@
1
- /** Service worker source served at /sw.js. It must be a real same-origin URL
2
- * (a SW cannot be registered from a data: URL), and serving it at the root path
3
- * gives it root scope so it controls the whole app.
1
+ /** Service worker source served at /sw.js (see server.ts's request handler). It
2
+ * must be a real same-origin URL registering from a `data:` URL is refused
3
+ * with "The URL protocol of the script is not supported" — and serving it at
4
+ * the root path is what gives it root scope: a worker served at `/sw.js`
5
+ * registers with scope `/`, one at `/deep/sw.js` with scope `/deep/`.
4
6
  *
5
- * iOS home-screen PWAs deliver notifications ONLY through a service worker's
6
- * push event — the in-page `new Notification()` constructor is unavailable so
7
- * this is the only path that reaches a backgrounded iPhone. */
7
+ * Notifications go through this worker's push event rather than an in-page
8
+ * `new Notification()` so they reach a device whose page is not open. */
8
9
  export function swJs() {
9
10
  return `self.addEventListener('install', function () { self.skipWaiting(); });
10
11
  self.addEventListener('activate', function (e) { e.waitUntil(self.clients.claim()); });
@@ -19,7 +19,8 @@ export type ServeResult = {
19
19
  } | {
20
20
  state: 'unavailable';
21
21
  };
22
- /** The MagicDNS host of a serve result, or undefined when the daemon had no name. */
22
+ /** The MagicDNS host of a serve result, or undefined for `unavailable` the one
23
+ * state that carries no host, because the daemon was unreachable or nameless. */
23
24
  export declare function hostFromResult(result: ServeResult): string | undefined;
24
25
  export interface RemoteUrlPlan {
25
26
  /** URL to encode in the QR and announce; the https one when serve is live. */
@@ -47,5 +48,6 @@ export declare function teardownTailscaleServe(port: number, run?: Run): Promise
47
48
  /** Pure: pick the primary URL (what the QR encodes) and any hint lines from a
48
49
  * serve result. Prefers the MagicDNS host over the raw IP whenever it's known —
49
50
  * the https URL when serve is live, else http://<host>:<port> — so the QR and
50
- * the announced URL carry the tailnet name (needed for SSH and webpush certs). */
51
+ * the announced URL carry the tailnet name, which is what `tailscale cert` and
52
+ * the `--https=443` handler above are issued for. */
51
53
  export declare function planRemoteUrls(httpPrimary: string, result: ServeResult, port: number): RemoteUrlPlan;
@@ -1,5 +1,6 @@
1
1
  import { execFile } from 'node:child_process';
2
- /** The MagicDNS host of a serve result, or undefined when the daemon had no name. */
2
+ /** The MagicDNS host of a serve result, or undefined for `unavailable` the one
3
+ * state that carries no host, because the daemon was unreachable or nameless. */
3
4
  export function hostFromResult(result) {
4
5
  return result.state === 'unavailable' ? undefined : result.host;
5
6
  }
@@ -83,7 +84,8 @@ export async function teardownTailscaleServe(port, run = defaultRun) {
83
84
  /** Pure: pick the primary URL (what the QR encodes) and any hint lines from a
84
85
  * serve result. Prefers the MagicDNS host over the raw IP whenever it's known —
85
86
  * the https URL when serve is live, else http://<host>:<port> — so the QR and
86
- * the announced URL carry the tailnet name (needed for SSH and webpush certs). */
87
+ * the announced URL carry the tailnet name, which is what `tailscale cert` and
88
+ * the `--https=443` handler above are issued for. */
87
89
  export function planRemoteUrls(httpPrimary, result, port) {
88
90
  switch (result.state) {
89
91
  case 'served':
@@ -1,10 +1,11 @@
1
1
  // Generalized single-pass syntax highlighter, shipped as a JS string and
2
- // concatenated into the page by ui.ts. It grew out of the client's original
3
- // JS-only tokenizer: the string/comment/number/identifier logic is now shared and
4
- // parameterized by a per-language config (keyword set + comment markers + string
5
- // flavors). Unknown languages fall back to escaped plain text (unchanged).
2
+ // concatenated into the page by ui.ts. One string/comment/number/identifier
3
+ // tokenizer, parameterized by a per-language config in HL_LANGS (keyword set +
4
+ // comment markers + string flavors). A language with no entry there falls back
5
+ // to escaped plain text with no spans at all.
6
6
  //
7
- // Depends on escHtml (ui-render.ts) — both land in the same <script>.
7
+ // Depends on escHtml (ui-render.ts) — ui.ts emits renderModule() immediately
8
+ // before highlightModule(), inside one <script>.
8
9
  /** Emitted client JS (top-level function declarations + language tables). */
9
10
  export function highlightModule() {
10
11
  return ` var HL_JS_KW = new Set(('break case catch class const continue debugger default delete do else export '
@@ -19,10 +19,10 @@ export function renderModule() {
19
19
  }
20
20
 
21
21
  // Split text into ordered {type:'text'|'code', ...} blocks by scanning lines
22
- // for a fenced code marker at line start. This replaces a RegExp that was built
23
- // from a string where '[\\\\s\\\\S]' collapsed to '[sS]', so it matched nothing
24
- // and every code block rendered as raw text. A line scanner also tolerates an
25
- // unclosed trailing fence, which the regex never could.
22
+ // for a fenced code marker at line START an indented fence is ordinary
23
+ // text. Scanning lines rather than matching one regex over the whole string
24
+ // is also what tolerates an unclosed trailing fence: everything after it
25
+ // becomes the code block.
26
26
  function parseBlocks(text) {
27
27
  var FENCE = String.fromCharCode(96, 96, 96);
28
28
  var lines = String(text == null ? '' : text).split('\\n');
@@ -21,7 +21,8 @@ export function clientScript(wsUrl) {
21
21
  if (usage && usage.percent != null) contextFill.style.width = usage.percent + '%';
22
22
  setStatusChip(usage);
23
23
  }
24
- // Compact token count for the header chip (mirrors formatContextTokens).
24
+ // Compact token count for the header chip. Matches the terminal's
25
+ // formatContextTokens up to 10M; above that this one keeps a decimal.
25
26
  function fmtTokens(n) {
26
27
  if (n == null) return '';
27
28
  if (n < 1000) return String(n);
@@ -58,11 +59,8 @@ export function clientScript(wsUrl) {
58
59
  // Last ~20 toasts, newest first, for the bell dropdown history.
59
60
  let notifHistory = [];
60
61
  let modelName = '';
61
- // Widgets are keyed (e.g. 'pi-tasks', 'pi-task-auto'); track them per key so a
62
- // clear for one key can't be masked by a stale message from another.
63
- // Single authoritative task-widget slot. The snapshot and the live 'widget'
64
- // delta both set this; null hides the panel. (No more per-key map that could
65
- // strand an orphaned widget on screen.)
62
+ // Single authoritative task-widget slot two variables, no keying. The
63
+ // snapshot and the live 'widget' delta both set them; null hides the panel.
66
64
  let taskWidgetLines = null;
67
65
  let taskWidgetData = null;
68
66
  function renderWidgets() {
@@ -156,10 +154,10 @@ export function clientScript(wsUrl) {
156
154
  { name: '/compact', desc: 'Compact context to save tokens' },
157
155
  { name: '/remote stop', desc: 'Stop the remote server' },
158
156
  ];
159
- // Every entry above must be dispatchable from the browser /clear, /help and
160
- // /fast used to sit here and were not (they are not pi commands at all), so
161
- // picking one only ever produced "Unknown command". register.test.ts asserts
162
- // this list stays a subset of what the bridge can actually run.
157
+ // Every entry above must be dispatchable from the browser, or picking it
158
+ // only ever toasts "Unknown command: /x". register.test.ts parses this array
159
+ // out of clientScript() and asserts each name is in the bridge's command map
160
+ // (/new excepted, which register.ts dispatches itself).
163
161
  let cmdActive = [];
164
162
  let cmdIndex = -1;
165
163
 
@@ -492,11 +490,13 @@ export function clientScript(wsUrl) {
492
490
  thinkingText = '';
493
491
  }
494
492
 
495
- // Pull the human-readable text out of a tool result. Many tools (Read, Bash,
496
- // MCP tools) return Anthropic content-block shapes { content: [{type:'text',
497
- // text:'...'}] } or a bare array of such blocks which JSON.stringify would
498
- // render as escaped, unreadable JSON. Extract the text blocks and join them;
499
- // return null when there's nothing text-shaped so the caller falls back to JSON.
493
+ // Pull the human-readable text out of a tool result. pi types AgentToolResult
494
+ // with content: (TextContent | ImageContent)[], and TextContent is
495
+ // {type:'text', text}, so a result usually arrives as { content: [...] } — or,
496
+ // since the event field is typed \`any\`, as a bare array of those blocks.
497
+ // JSON.stringify would render either as escaped, unreadable JSON. Extract the
498
+ // text blocks and join them; return null when there is nothing text-shaped so
499
+ // the caller falls back to JSON.
500
500
  function contentBlocksText(result) {
501
501
  const blocks = Array.isArray(result) ? result
502
502
  : (result && Array.isArray(result.content)) ? result.content
@@ -567,8 +567,8 @@ export function clientScript(wsUrl) {
567
567
  return el;
568
568
  }
569
569
 
570
- // Render one committed transcript turn. Assistant turns are an ordered list of
571
- // parts (text segments + tool calls), so the layout matches the terminal's
570
+ // Render one committed transcript turn. Assistant turns are an ordered list
571
+ // of parts text, thinking and tool so the layout matches the terminal's
572
572
  // interleaving instead of one merged blob with tools dumped at the end.
573
573
  function renderTurn(t) {
574
574
  if (t.error) { addBubble('error', t.text); addTurnTime(t.ts, 'assistant'); return; }
@@ -745,8 +745,8 @@ export function clientScript(wsUrl) {
745
745
  }
746
746
 
747
747
  // Register the service worker, subscribe via the Push API, and hand the
748
- // subscription to the server. The server (not the page) sends notifications,
749
- // so they arrive even when this PWA is backgrounded/suspended on iOS.
748
+ // subscription to the server. The server, not the page, sends the
749
+ // notification, so delivery does not need this page to be open.
750
750
  function subscribePush() {
751
751
  return navigator.serviceWorker.register('/sw.js')
752
752
  .then(function () { return navigator.serviceWorker.ready; })
@@ -1214,11 +1214,11 @@ export function clientScript(wsUrl) {
1214
1214
  window.addEventListener('online', connectNow);
1215
1215
  window.addEventListener('focus', connectNow);
1216
1216
 
1217
- // Pin the column to window.innerHeight (a stable px value) instead of letting
1218
- // it ride 100dvh, which iOS Safari interpolates DURING the rotation animation
1219
- // and makes the whole layout resize repeatedly ("spazzing out"). A rotation
1220
- // also fires several resize events and changes scrollHeight, so coalesce the
1221
- // updates in one rAF and re-pin to the bottom when the user was already there.
1217
+ // Pin the column to a stable pixel height instead of leaving it on the
1218
+ // 100dvh fallback in ui-styles.ts (height: var(--app-h, 100dvh)). A rotation
1219
+ // fires several resize events and changes scrollHeight, so coalesce the
1220
+ // updates in one rAF and re-pin to the bottom when the user was already
1221
+ // there.
1222
1222
  let appHeightRaf = null;
1223
1223
  let appHeightPin = false;
1224
1224
  function setAppHeight() {