ptbk 0.119.0-16 → 0.119.0-21

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 (330) hide show
  1. package/README.md +27 -9
  2. package/dist/book/commitments.d.ts +38 -10
  3. package/dist/book/commitments.js +61 -17
  4. package/dist/book/commitments.js.map +1 -1
  5. package/dist/book/convert.d.ts +36 -0
  6. package/dist/book/convert.js +156 -0
  7. package/dist/book/convert.js.map +1 -0
  8. package/dist/book/parse.d.ts +50 -13
  9. package/dist/book/parse.js +132 -45
  10. package/dist/book/parse.js.map +1 -1
  11. package/dist/book/references.d.ts +12 -4
  12. package/dist/book/references.js +34 -9
  13. package/dist/book/references.js.map +1 -1
  14. package/dist/book/write.d.ts +42 -0
  15. package/dist/book/write.js +74 -0
  16. package/dist/book/write.js.map +1 -0
  17. package/dist/browser/browser.d.ts +122 -12
  18. package/dist/browser/browser.js +372 -21
  19. package/dist/browser/browser.js.map +1 -1
  20. package/dist/browser/recording.d.ts +3 -3
  21. package/dist/browser/recording.js +5 -5
  22. package/dist/browser/recording.js.map +1 -1
  23. package/dist/chats/chat-book.d.ts +105 -0
  24. package/dist/chats/chat-book.js +180 -0
  25. package/dist/chats/chat-book.js.map +1 -0
  26. package/dist/chats/conversation.d.ts +54 -0
  27. package/dist/chats/conversation.js +188 -0
  28. package/dist/chats/conversation.js.map +1 -0
  29. package/dist/chats/local.d.ts +46 -0
  30. package/dist/chats/local.js +184 -0
  31. package/dist/chats/local.js.map +1 -0
  32. package/dist/chats/migration.d.ts +12 -0
  33. package/dist/chats/migration.js +62 -0
  34. package/dist/chats/migration.js.map +1 -0
  35. package/dist/chats/service.d.ts +81 -0
  36. package/dist/chats/service.js +122 -0
  37. package/dist/chats/service.js.map +1 -0
  38. package/dist/chats/store.d.ts +132 -0
  39. package/dist/chats/store.js +181 -0
  40. package/dist/chats/store.js.map +1 -0
  41. package/dist/chats/task-chats.d.ts +52 -0
  42. package/dist/chats/task-chats.js +96 -0
  43. package/dist/chats/task-chats.js.map +1 -0
  44. package/dist/cli/chats.d.ts +45 -0
  45. package/dist/cli/chats.js +108 -0
  46. package/dist/cli/chats.js.map +1 -0
  47. package/dist/cli/commands/add.js +32 -22
  48. package/dist/cli/commands/add.js.map +1 -1
  49. package/dist/cli/commands/answer.d.ts +2 -1
  50. package/dist/cli/commands/answer.js +35 -41
  51. package/dist/cli/commands/answer.js.map +1 -1
  52. package/dist/cli/commands/ask.d.ts +7 -0
  53. package/dist/cli/commands/ask.js +37 -0
  54. package/dist/cli/commands/ask.js.map +1 -0
  55. package/dist/cli/commands/boilerplates.js +14 -3
  56. package/dist/cli/commands/boilerplates.js.map +1 -1
  57. package/dist/cli/commands/chat.d.ts +9 -0
  58. package/dist/cli/commands/chat.js +89 -0
  59. package/dist/cli/commands/chat.js.map +1 -0
  60. package/dist/cli/commands/dashboard.d.ts +3 -1
  61. package/dist/cli/commands/dashboard.js +41 -3
  62. package/dist/cli/commands/dashboard.js.map +1 -1
  63. package/dist/cli/commands/find.js +6 -8
  64. package/dist/cli/commands/find.js.map +1 -1
  65. package/dist/cli/commands/fix.js +4 -6
  66. package/dist/cli/commands/fix.js.map +1 -1
  67. package/dist/cli/commands/init.d.ts +0 -1
  68. package/dist/cli/commands/init.js +38 -38
  69. package/dist/cli/commands/init.js.map +1 -1
  70. package/dist/cli/commands/lint.js +16 -2
  71. package/dist/cli/commands/lint.js.map +1 -1
  72. package/dist/cli/commands/list.js +21 -13
  73. package/dist/cli/commands/list.js.map +1 -1
  74. package/dist/cli/commands/migrate.d.ts +2 -1
  75. package/dist/cli/commands/migrate.js +70 -24
  76. package/dist/cli/commands/migrate.js.map +1 -1
  77. package/dist/cli/commands/pause.d.ts +8 -0
  78. package/dist/cli/commands/pause.js +40 -0
  79. package/dist/cli/commands/pause.js.map +1 -0
  80. package/dist/cli/commands/plan.js +6 -9
  81. package/dist/cli/commands/plan.js.map +1 -1
  82. package/dist/cli/commands/recover.js +7 -3
  83. package/dist/cli/commands/recover.js.map +1 -1
  84. package/dist/cli/commands/restart.d.ts +3 -1
  85. package/dist/cli/commands/restart.js +74 -18
  86. package/dist/cli/commands/restart.js.map +1 -1
  87. package/dist/cli/commands/resume.d.ts +8 -0
  88. package/dist/cli/commands/resume.js +15 -0
  89. package/dist/cli/commands/resume.js.map +1 -0
  90. package/dist/cli/commands/run.js +6 -9
  91. package/dist/cli/commands/run.js.map +1 -1
  92. package/dist/cli/commands/start.js +40 -36
  93. package/dist/cli/commands/start.js.map +1 -1
  94. package/dist/cli/commands/status.d.ts +2 -1
  95. package/dist/cli/commands/status.js +5 -1
  96. package/dist/cli/commands/status.js.map +1 -1
  97. package/dist/cli/commands/stop.d.ts +3 -1
  98. package/dist/cli/commands/stop.js +18 -8
  99. package/dist/cli/commands/stop.js.map +1 -1
  100. package/dist/cli/commands/verify.js +7 -7
  101. package/dist/cli/commands/verify.js.map +1 -1
  102. package/dist/cli/common.d.ts +13 -0
  103. package/dist/cli/common.js +22 -0
  104. package/dist/cli/common.js.map +1 -1
  105. package/dist/cli/engine-delegation.d.ts +14 -0
  106. package/dist/cli/engine-delegation.js +46 -0
  107. package/dist/cli/engine-delegation.js.map +1 -0
  108. package/dist/cli/lifecycle.d.ts +40 -0
  109. package/dist/cli/lifecycle.js +71 -0
  110. package/dist/cli/lifecycle.js.map +1 -0
  111. package/dist/cli/registry.js +113 -31
  112. package/dist/cli/registry.js.map +1 -1
  113. package/dist/cli/service-view.d.ts +14 -2
  114. package/dist/cli/service-view.js +46 -5
  115. package/dist/cli/service-view.js.map +1 -1
  116. package/dist/cli/sources.js +5 -8
  117. package/dist/cli/sources.js.map +1 -1
  118. package/dist/cli/spec.js +1 -1
  119. package/dist/cli/spec.js.map +1 -1
  120. package/dist/common/app-data.d.ts +9 -0
  121. package/dist/common/app-data.js +20 -0
  122. package/dist/common/app-data.js.map +1 -0
  123. package/dist/common/errors.d.ts +2 -0
  124. package/dist/common/errors.js.map +1 -1
  125. package/dist/common/time.d.ts +6 -0
  126. package/dist/common/time.js +9 -0
  127. package/dist/common/time.js.map +1 -1
  128. package/dist/engine/agenda.d.ts +213 -43
  129. package/dist/engine/agenda.js +846 -260
  130. package/dist/engine/agenda.js.map +1 -1
  131. package/dist/engine/executor.d.ts +23 -7
  132. package/dist/engine/executor.js +131 -84
  133. package/dist/engine/executor.js.map +1 -1
  134. package/dist/engine/generated.d.ts +21 -1
  135. package/dist/engine/generated.js +36 -21
  136. package/dist/engine/generated.js.map +1 -1
  137. package/dist/engine/preview.js +10 -10
  138. package/dist/engine/preview.js.map +1 -1
  139. package/dist/engine/prompt.d.ts +16 -4
  140. package/dist/engine/prompt.js +22 -4
  141. package/dist/engine/prompt.js.map +1 -1
  142. package/dist/engine/routing.js +28 -4
  143. package/dist/engine/routing.js.map +1 -1
  144. package/dist/engine/snapshot.d.ts +178 -29
  145. package/dist/engine/state.d.ts +40 -13
  146. package/dist/engine/state.js +20 -5
  147. package/dist/engine/state.js.map +1 -1
  148. package/dist/engine/task-trace.d.ts +35 -7
  149. package/dist/engine/task-trace.js +74 -15
  150. package/dist/engine/task-trace.js.map +1 -1
  151. package/dist/engine/team.d.ts +20 -7
  152. package/dist/engine/team.js +30 -24
  153. package/dist/engine/team.js.map +1 -1
  154. package/dist/engine/traces.d.ts +1 -1
  155. package/dist/engine/traces.js +1 -1
  156. package/dist/expert/_main.md +67 -52
  157. package/dist/expert/agents/_index.md +11 -13
  158. package/dist/expert/agents/chatter.md +31 -0
  159. package/dist/expert/agents/consistency.md +6 -11
  160. package/dist/expert/agents/core-agents.md +9 -7
  161. package/dist/expert/agents/expert.md +2 -2
  162. package/dist/expert/agents/fixer.md +24 -0
  163. package/dist/expert/agents/learning.md +1 -1
  164. package/dist/expert/agents/non-agents.md +6 -4
  165. package/dist/expert/agents/teacher.md +1 -1
  166. package/dist/expert/agents/visual-identity.md +4 -4
  167. package/dist/expert/book-language/_index.md +9 -8
  168. package/dist/expert/book-language/commitments/_index.md +23 -3
  169. package/dist/expert/book-language/commitments/agent.md +11 -0
  170. package/dist/expert/book-language/commitments/chat-member.md +7 -0
  171. package/dist/expert/book-language/commitments/chat-message.md +11 -0
  172. package/dist/expert/book-language/commitments/chat-status.md +7 -0
  173. package/dist/expert/book-language/commitments/chat.md +11 -0
  174. package/dist/expert/book-language/commitments/entity.md +9 -0
  175. package/dist/expert/book-language/commitments/from.md +1 -1
  176. package/dist/expert/book-language/commitments/goal.md +6 -2
  177. package/dist/expert/book-language/commitments/link.md +9 -0
  178. package/dist/expert/book-language/commitments/task-depends-on.md +1 -1
  179. package/dist/expert/book-language/commitments/task-parent.md +1 -1
  180. package/dist/expert/book-language/commitments/task.md +7 -3
  181. package/dist/expert/book-language/commitments/team.md +1 -1
  182. package/dist/expert/book-language/inheritance.md +2 -2
  183. package/dist/expert/book-language/references.md +20 -24
  184. package/dist/expert/book-language/syntax.md +54 -11
  185. package/dist/expert/cli/acceptance.md +59 -24
  186. package/dist/expert/cli/api.md +23 -9
  187. package/dist/expert/cli/authoring.md +6 -4
  188. package/dist/expert/cli/autonomous-operation.md +3 -3
  189. package/dist/expert/cli/browser.md +29 -18
  190. package/dist/expert/cli/channels.md +5 -7
  191. package/dist/expert/cli/chat-ui.md +32 -0
  192. package/dist/expert/cli/chats.md +59 -0
  193. package/dist/expert/cli/cli.md +44 -26
  194. package/dist/expert/cli/configuration.md +3 -3
  195. package/dist/expert/cli/controls.md +13 -7
  196. package/dist/expert/cli/dashboard-details.md +23 -0
  197. package/dist/expert/cli/dashboard-layout.md +19 -12
  198. package/dist/expert/cli/dashboard.md +12 -8
  199. package/dist/expert/cli/execution.md +8 -6
  200. package/dist/expert/cli/first-task.md +4 -4
  201. package/dist/expert/cli/generated-names.md +44 -0
  202. package/dist/expert/cli/git-persistence.md +19 -17
  203. package/dist/expert/cli/global-dashboard.md +33 -0
  204. package/dist/expert/cli/goal-discovery.md +1 -1
  205. package/dist/expert/cli/help.md +1 -1
  206. package/dist/expert/cli/initialization-ui.md +2 -0
  207. package/dist/expert/cli/initialization.md +9 -7
  208. package/dist/expert/cli/interactive-ui.md +9 -5
  209. package/dist/expert/cli/locking.md +21 -0
  210. package/dist/expert/cli/migration.md +22 -6
  211. package/dist/expert/cli/process-modes.md +21 -13
  212. package/dist/expert/cli/recovery.md +8 -6
  213. package/dist/expert/cli/recurrence.md +1 -1
  214. package/dist/expert/cli/rich-input.md +2 -2
  215. package/dist/expert/cli/service-lifecycle.md +57 -0
  216. package/dist/expert/cli/start.md +2 -2
  217. package/dist/expert/cli/status-view.md +16 -12
  218. package/dist/expert/cli/task-books.md +11 -15
  219. package/dist/expert/cli/task-identity.md +8 -10
  220. package/dist/expert/cli/task-markdown.md +6 -10
  221. package/dist/expert/cli/task-origins.md +3 -1
  222. package/dist/expert/cli/task-relationships.md +5 -9
  223. package/dist/expert/cli/team.md +17 -7
  224. package/dist/expert/cli/terminal.md +20 -12
  225. package/dist/expert/cli/trace-configuration.md +1 -1
  226. package/dist/expert/cli/traces.md +19 -23
  227. package/dist/expert/cli/user-interactions.md +14 -26
  228. package/dist/expert/cli/workspace.md +15 -9
  229. package/dist/expert/concepts/engine-and-project.md +11 -0
  230. package/dist/expert/concepts/project-model.md +39 -0
  231. package/dist/expert/dictionary.md +21 -15
  232. package/dist/harness/plugins/claude-code/index.js +1 -0
  233. package/dist/harness/plugins/claude-code/index.js.map +1 -1
  234. package/dist/harness/plugins/cline/index.js +1 -0
  235. package/dist/harness/plugins/cline/index.js.map +1 -1
  236. package/dist/harness/plugins/openai-codex/index.js +1 -0
  237. package/dist/harness/plugins/openai-codex/index.js.map +1 -1
  238. package/dist/harness/plugins/opencode/index.js +1 -0
  239. package/dist/harness/plugins/opencode/index.js.map +1 -1
  240. package/dist/harness/types.d.ts +3 -1
  241. package/dist/project/agents.d.ts +29 -81
  242. package/dist/project/agents.js +48 -267
  243. package/dist/project/agents.js.map +1 -1
  244. package/dist/project/books.d.ts +150 -0
  245. package/dist/project/books.js +253 -0
  246. package/dist/project/books.js.map +1 -0
  247. package/dist/project/config.js +2 -2
  248. package/dist/project/config.js.map +1 -1
  249. package/dist/project/defaults.d.ts +35 -6
  250. package/dist/project/defaults.js +73 -40
  251. package/dist/project/defaults.js.map +1 -1
  252. package/dist/project/expert.js +18 -10
  253. package/dist/project/expert.js.map +1 -1
  254. package/dist/project/git.d.ts +4 -3
  255. package/dist/project/git.js +16 -5
  256. package/dist/project/git.js.map +1 -1
  257. package/dist/project/setup.d.ts +4 -1
  258. package/dist/project/setup.js +26 -6
  259. package/dist/project/setup.js.map +1 -1
  260. package/dist/service/actions.d.ts +34 -10
  261. package/dist/service/actions.js +51 -19
  262. package/dist/service/actions.js.map +1 -1
  263. package/dist/service/control.d.ts +53 -1
  264. package/dist/service/control.js +64 -4
  265. package/dist/service/control.js.map +1 -1
  266. package/dist/service/discovery.d.ts +133 -0
  267. package/dist/service/discovery.js +138 -0
  268. package/dist/service/discovery.js.map +1 -0
  269. package/dist/service/host.d.ts +8 -1
  270. package/dist/service/host.js +65 -18
  271. package/dist/service/host.js.map +1 -1
  272. package/dist/service/http.d.ts +3 -0
  273. package/dist/service/http.js +26 -6
  274. package/dist/service/http.js.map +1 -1
  275. package/dist/service/launcher.d.ts +66 -13
  276. package/dist/service/launcher.js +78 -18
  277. package/dist/service/launcher.js.map +1 -1
  278. package/dist/service/records.d.ts +12 -2
  279. package/dist/service/records.js +19 -5
  280. package/dist/service/records.js.map +1 -1
  281. package/dist/service/spa/app.css +83 -2
  282. package/dist/service/spa/app.js +393 -72
  283. package/dist/service/supervisor.js +7 -1
  284. package/dist/service/supervisor.js.map +1 -1
  285. package/dist/service/worker.js +14 -5
  286. package/dist/service/worker.js.map +1 -1
  287. package/dist/tasks/authoring.d.ts +3 -5
  288. package/dist/tasks/authoring.js +6 -7
  289. package/dist/tasks/authoring.js.map +1 -1
  290. package/dist/tasks/book-task.d.ts +18 -10
  291. package/dist/tasks/book-task.js +27 -23
  292. package/dist/tasks/book-task.js.map +1 -1
  293. package/dist/tasks/eligibility.d.ts +15 -7
  294. package/dist/tasks/eligibility.js +41 -14
  295. package/dist/tasks/eligibility.js.map +1 -1
  296. package/dist/tasks/identity.d.ts +41 -15
  297. package/dist/tasks/identity.js +100 -31
  298. package/dist/tasks/identity.js.map +1 -1
  299. package/dist/tasks/markdown.d.ts +7 -14
  300. package/dist/tasks/markdown.js +15 -29
  301. package/dist/tasks/markdown.js.map +1 -1
  302. package/dist/tasks/model.d.ts +20 -4
  303. package/dist/tasks/model.js +17 -0
  304. package/dist/tasks/model.js.map +1 -1
  305. package/dist/tasks/sources.d.ts +7 -2
  306. package/dist/tasks/sources.js +46 -35
  307. package/dist/tasks/sources.js.map +1 -1
  308. package/dist/ui/chat-client.d.ts +70 -0
  309. package/dist/ui/chat-client.js +209 -0
  310. package/dist/ui/chat-client.js.map +1 -0
  311. package/dist/ui/dashboard.d.ts +8 -5
  312. package/dist/ui/dashboard.js +227 -86
  313. package/dist/ui/dashboard.js.map +1 -1
  314. package/dist/ui/details.d.ts +64 -0
  315. package/dist/ui/details.js +175 -0
  316. package/dist/ui/details.js.map +1 -0
  317. package/dist/ui/global-dashboard.d.ts +59 -0
  318. package/dist/ui/global-dashboard.js +307 -0
  319. package/dist/ui/global-dashboard.js.map +1 -0
  320. package/dist/ui/status-view.d.ts +7 -0
  321. package/dist/ui/status-view.js +94 -36
  322. package/dist/ui/status-view.js.map +1 -1
  323. package/package.json +1 -1
  324. package/dist/engine/requests.d.ts +0 -111
  325. package/dist/engine/requests.js +0 -173
  326. package/dist/engine/requests.js.map +0 -1
  327. package/dist/expert/book-language/commitments/task-id.md +0 -9
  328. package/dist/tasks/prepare.d.ts +0 -33
  329. package/dist/tasks/prepare.js +0 -90
  330. package/dist/tasks/prepare.js.map +0 -1
@@ -19,6 +19,15 @@
19
19
  busy: new Set(),
20
20
  qrFor: null,
21
21
  frame: 0,
22
+ /** Open chat: its name, loaded messages and the message count they reflect. */
23
+ chat: null,
24
+ /** Agents offered for a new chat (loaded on demand). */
25
+ agents: null,
26
+ /** New-chat entry: 'entry' (Chat with Chatter / Choose another agent) or 'all' (every valid alternative). */
27
+ newChat: false,
28
+ showAllChats: false,
29
+ /** Viewer-local detail panels (initially collapsed; never engine state). */
30
+ details: { execution: false, browser: false },
22
31
  };
23
32
 
24
33
  /** Creates a DOM element; text children are always inserted as text (never parsed as HTML). */
@@ -95,80 +104,303 @@
95
104
  return h('span', { className: `pill ${tone || ''}` }, text);
96
105
  }
97
106
 
98
- /** Runs a control action through the API. */
99
- async function control(action, confirmText) {
107
+ /** Runs one of the engine's shared control operations through the API and reports its real outcome. */
108
+ async function control(action, confirmText, extra) {
100
109
  if (confirmText && !window.confirm(confirmText)) return;
110
+ const key = `control:${action}`;
111
+ if (state.busy.has(key)) return;
112
+ state.busy.add(key);
101
113
  try {
102
- const result = await api('/api/control', { method: 'POST', body: { action } });
114
+ const result = await api('/api/control', { method: 'POST', body: { action, ...(extra || {}) } });
103
115
  toast(result.outcome);
104
116
  } catch (error) {
105
- toast(`Failed: ${error.message}`);
117
+ toast(`Not applied: ${error.message}`);
118
+ } finally {
119
+ state.busy.delete(key);
120
+ }
121
+ }
122
+
123
+ /** Lifecycle actions applicable to the live service state (the same operations as ptbk pause/resume/stop/restart). */
124
+ function lifecycleActions(snapshot) {
125
+ const service = snapshot.service;
126
+ const lifecycle = service.lifecycle || {};
127
+ const stateName = service.state === 'paused' ? 'pause' : service.state;
128
+ if (stateName === 'stopped' || stateName === 'finished') return [];
129
+ const actions = [];
130
+ const restartable = lifecycle.restartable !== false && service.mode !== 'run';
131
+ const pending = lifecycle.intent === 'restart' ? 'restart' : 'stop';
132
+ if (stateName === 'running') actions.push({ action: 'pause', label: 'Pause' });
133
+ if (stateName === 'pause') actions.push({ action: 'resume', label: 'Resume', primary: true });
134
+ if (stateName === 'pausing') actions.push({ action: 'resume', label: 'Cancel pause' });
135
+ if (stateName === 'stopping' && lifecycle.cancellable) {
136
+ actions.push({ action: 'resume', label: `Cancel ${pending}`, primary: true });
137
+ actions.push({ action: 'pause', label: 'Pause instead', confirm: `Cancel the pending ${pending} and pause instead?`, extra: { replaceStop: true, revision: lifecycle.revision } });
138
+ }
139
+ if (stateName !== 'stopping' || (lifecycle.intent === 'restart' && lifecycle.cancellable)) {
140
+ actions.push({ action: 'stop', label: stateName === 'pausing' ? 'Stop instead' : 'Stop (graceful)', confirm: 'Stop the service after the current task settles? Every viewer shares this engine; closing this page does not stop it.' });
106
141
  }
142
+ if (restartable && stateName !== 'stopping') actions.push({ action: 'restart', label: 'Restart', confirm: 'Restart the service? The current task finishes first; then exactly one replacement starts with the same configuration.' });
143
+ if (lifecycle.intent !== 'force-stop') actions.push({ action: 'force-stop', label: 'Force stop', danger: true, confirm: 'Force stop now? Owned work is interrupted immediately (recoverable, never marked done).' });
144
+ return actions;
145
+ }
146
+
147
+ /** What the live service state means (never a fifth "stopped" live state). */
148
+ function lifecycleText(snapshot) {
149
+ const service = snapshot.service;
150
+ const lifecycle = service.lifecycle || {};
151
+ const task = snapshot.current ? `“${snapshot.current.taskTitle}”` : 'the current task';
152
+ switch (service.state === 'paused' ? 'pause' : service.state) {
153
+ case 'running':
154
+ return snapshot.current ? 'New tasks start when ready.' : 'Idle is normal; new tasks start when ready.';
155
+ case 'pausing':
156
+ return `${task} finishes first; then no task starts until you resume.`;
157
+ case 'pause':
158
+ return 'Alive and controllable; no task runs or starts.';
159
+ case 'stopping':
160
+ if (lifecycle.intent === 'force-stop') return 'Force stop: owned work is being interrupted (recoverable).';
161
+ if (lifecycle.shutdown) return lifecycle.intent === 'restart' ? 'Shutting down for a restart; one replacement starts next.' : 'Shutting down.';
162
+ return `${task} finishes first; then the service ${lifecycle.intent === 'restart' ? 'restarts (exactly one replacement)' : 'exits'}.`;
163
+ case 'stopped':
164
+ return 'No engine is running (an observation, not a live state). Start it with ptbk start.';
165
+ default:
166
+ return 'The finite run ended.';
167
+ }
168
+ }
169
+
170
+ /** Loads (or reloads) the open chat; opening marks it read but never answers anything. */
171
+ async function loadChat(name) {
172
+ try {
173
+ const detail = await api(`/api/chats/${encodeURIComponent(name)}`);
174
+ const changed = !state.chat || state.chat.name !== detail.summary.name || state.chat.detail?.messages.length !== detail.messages.length || state.chat.detail?.summary.status !== detail.summary.status;
175
+ state.chat = { name: detail.summary.name, detail };
176
+ if (changed) await api(`/api/chats/${encodeURIComponent(detail.summary.name)}/read`, { method: 'POST' }).catch(() => undefined);
177
+ } catch (error) {
178
+ toast(`Chat unavailable: ${error.message}`);
179
+ state.chat = null;
180
+ }
181
+ render(state.snapshot);
107
182
  }
108
183
 
109
- /** Submits an answer once. */
110
- async function answer(id, value, status) {
111
- if (state.busy.has(id)) return;
112
- state.busy.add(id);
184
+ function openChat(name) {
185
+ state.newChat = false;
186
+ state.chat = { name, detail: null };
187
+ render(state.snapshot);
188
+ loadChat(name);
189
+ }
190
+
191
+ /** Sends one message (an answer is an ordinary committed chat message); stale views are rejected by the service. */
192
+ async function sendMessage(name, text, expected) {
193
+ if (state.busy.has(name)) return;
194
+ state.busy.add(name);
113
195
  render(state.snapshot);
114
196
  try {
115
- await api(`/api/requests/${encodeURIComponent(id)}/answer`, { method: 'POST', body: { answer: value, status: status || 'answered' } });
116
- state.drafts.delete(id);
117
- toast('Answer applied.');
197
+ const result = await api(`/api/chats/${encodeURIComponent(name)}/messages`, { method: 'POST', body: { text, expected } });
198
+ state.drafts.delete(`chat:${name}`);
199
+ toast(result.commit ? `Saved (${result.commit.slice(0, 8)}).` : 'Saved.');
118
200
  } catch (error) {
119
- toast(`Not applied: ${error.message}`);
201
+ toast(`Not sent: ${error.message}`);
202
+ } finally {
203
+ state.busy.delete(name);
204
+ }
205
+ if (state.chat && state.chat.name === name) await loadChat(name);
206
+ else render(state.snapshot);
207
+ }
208
+
209
+ async function startChat(agent, text) {
210
+ if (state.busy.has('new-chat')) return;
211
+ state.busy.add('new-chat');
212
+ render(state.snapshot);
213
+ try {
214
+ // One request key per draft: a retried request returns the chat it already created.
215
+ state.newChatKey = state.newChatKey || `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
216
+ // Without an explicit agent the service uses Chatter (diagnosed, never silently replaced).
217
+ const result = await api('/api/chats', { method: 'POST', body: { ...(agent ? { agent } : {}), text, requestKey: state.newChatKey } });
218
+ state.newChatKey = null;
219
+ state.drafts.delete('chat:new');
220
+ toast(`Started “${result.name}”.`);
221
+ openChat(result.name);
222
+ } catch (error) {
223
+ toast(`Not started: ${error.message}`);
120
224
  } finally {
121
- state.busy.delete(id);
225
+ state.busy.delete('new-chat');
226
+ render(state.snapshot);
227
+ }
228
+ }
229
+
230
+ async function showNewChat() {
231
+ state.newChat = 'entry';
232
+ state.focusChatter = true;
233
+ state.chat = null;
234
+ render(state.snapshot);
235
+ if (!state.agents) {
236
+ try {
237
+ state.agents = (await api('/api/chat-agents')).agents;
238
+ } catch (error) {
239
+ toast(`Agents unavailable: ${error.message}`);
240
+ }
122
241
  render(state.snapshot);
123
242
  }
124
243
  }
125
244
 
245
+ /** Text area with a persistent draft (survives re-renders and reconnects). */
246
+ function composer(key, label, onSend, disabled) {
247
+ const field = h('textarea', { 'data-draft': key, 'aria-label': label, placeholder: 'Type a message…', disabled });
248
+ field.value = state.drafts.get(key) || '';
249
+ field.addEventListener('input', () => state.drafts.set(key, field.value));
250
+ field.addEventListener('keydown', (event) => {
251
+ if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
252
+ event.preventDefault();
253
+ if (field.value.trim()) onSend(field.value);
254
+ }
255
+ });
256
+ return h('div', {}, field, h('div', { className: 'row', style: 'margin-top:8px' }, h('button', { className: 'primary', disabled, onclick: () => (field.value.trim() ? onSend(field.value) : field.focus()) }, 'Send'), h('span', { className: 'muted' }, 'Ctrl+Enter sends')));
257
+ }
258
+
126
259
  function renderAttention(snapshot) {
127
- const requests = snapshot.requests;
128
- const card = h('section', { className: `card attention ${requests.length ? '' : 'empty'}`, 'aria-label': 'Needs your attention' });
129
- card.append(h('h2', {}, h('span', {}, requests.length ? `Needs your attention (${requests.length})` : 'Needs your attention'), requests.length ? pill('action required', 'warn') : null));
130
- if (!requests.length) {
131
- card.append(h('p', { className: 'muted' }, 'Nothing needs you right now.'));
260
+ const waiting = snapshot.chats.waiting;
261
+ const card = h('section', { className: `card attention ${waiting.length ? '' : 'empty'}`, 'aria-label': 'Needs your attention' });
262
+ card.append(h('h2', {}, h('span', {}, waiting.length ? `Needs your attention (${waiting.length})` : 'Needs your attention'), waiting.length ? pill('waiting for you', 'warn') : null));
263
+ if (!waiting.length) {
264
+ card.append(h('p', { className: 'muted' }, 'No agent is waiting for you.'));
132
265
  return card;
133
266
  }
134
- for (const request of requests) {
135
- const busy = state.busy.has(request.id);
136
- const item = h('article', { className: 'request', 'data-request': request.id });
137
- item.append(h('div', { className: 'reason' }, request.reason));
138
- if (request.attempted) item.append(h('div', { className: 'muted' }, `Already attempted: ${request.attempted}`));
139
- item.append(
267
+ for (const chat of waiting) {
268
+ const busy = state.busy.has(chat.name);
269
+ const item = h('article', { className: 'request', 'data-chat': chat.name });
270
+ item.append(h('div', { className: 'reason' }, `${chat.awaiting ? chat.awaiting.sender : chat.agent || 'An agent'} asks in “${chat.name}”`));
271
+ item.append(h('div', { className: 'meta' }, `${chat.members.join(', ')}${chat.links.length ? ` · ${chat.links.join(', ')}` : ''}${chat.last ? ` · ${time(chat.last.at)}` : ''}${chat.unread ? ` · ${chat.unread} unread` : ''}`));
272
+ if (chat.awaiting) item.append(h('div', { className: 'question' }, chat.awaiting.text));
273
+ const actions = h('div', { className: 'choices', role: 'group', 'aria-label': 'Answers' });
274
+ for (const choice of (chat.awaiting && chat.awaiting.choices) || []) actions.append(h('button', { className: 'primary', disabled: busy, onclick: () => sendMessage(chat.name, choice, chat.messages) }, choice));
275
+ actions.append(h('button', { onclick: () => openChat(chat.name) }, 'Open conversation'));
276
+ item.append(actions);
277
+ card.append(item);
278
+ }
279
+ return card;
280
+ }
281
+
282
+ function renderThread(snapshot) {
283
+ const chat = state.chat;
284
+ const card = h('section', { className: 'card chats thread', 'aria-label': 'Conversation' });
285
+ if (!chat.detail) {
286
+ card.append(h('h2', {}, h('span', {}, chat.name)), h('p', { className: 'muted' }, 'Loading…'));
287
+ return card;
288
+ }
289
+ const { summary, messages } = chat.detail;
290
+ card.append(
291
+ h(
292
+ 'h2',
293
+ {},
294
+ h('span', {}, summary.name),
295
+ h('span', { className: 'row' }, pill(summary.status === 'waiting-for-user' ? 'waiting for you' : summary.status, summary.status === 'waiting-for-user' ? 'warn' : summary.status === 'open' ? 'ok' : ''), h('button', { onclick: () => ((state.chat = null), render(state.snapshot)) }, 'Close')),
296
+ ),
297
+ );
298
+ card.append(h('div', { className: 'meta muted' }, `${summary.members.join(', ')} · ${summary.path}${summary.links.length ? ` · links ${summary.links.join(', ')}` : ''}`));
299
+ const thread = h('div', { className: 'messages', role: 'log', 'aria-live': 'polite' });
300
+ for (const message of messages) {
301
+ const user = message.sender.replace(/^@/, '').toLowerCase() === 'user';
302
+ thread.append(
140
303
  h(
141
- 'div',
142
- { className: 'meta' },
143
- `${request.id} · ${request.origin}${request.agent ? ` · asked by ${request.agent}` : ''}${request.taskTitle ? ` · affects “${request.taskTitle}”` : ''} · ${time(request.createdAt)}${request.blocking ? ' · blocking' : ''}`,
304
+ 'article',
305
+ { className: `message ${user ? 'user' : 'agent'}` },
306
+ h('div', { className: 'meta' }, h('strong', {}, user ? 'You' : message.sender), ` · ${time(message.at)}`),
307
+ h('div', { className: 'text' }, message.text),
308
+ message.choices && message.choices.length ? h('div', { className: 'muted' }, `Offered: ${message.choices.join(' | ')}`) : null,
144
309
  ),
145
310
  );
146
- item.append(h('div', { className: 'question' }, request.question));
147
- if (request.choices && request.choices.length) {
148
- item.append(h('div', { className: 'choices', role: 'group', 'aria-label': 'Answers' }, request.choices.map((choice) => h('button', { className: 'primary', disabled: busy, onclick: () => answer(request.id, choice) }, choice))));
149
- } else {
150
- const field = h('textarea', { 'data-draft': request.id, 'aria-label': `Answer to ${request.id}`, placeholder: 'Type your answer…', disabled: busy });
151
- field.value = state.drafts.get(request.id) || '';
152
- field.addEventListener('input', () => state.drafts.set(request.id, field.value));
153
- field.addEventListener('keydown', (event) => {
154
- if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
155
- event.preventDefault();
156
- if (field.value.trim()) answer(request.id, field.value);
157
- }
158
- });
159
- item.append(field);
160
- item.append(
161
- h(
162
- 'div',
163
- { className: 'row', style: 'margin-top:8px' },
164
- h('button', { className: 'primary', disabled: busy, onclick: () => (field.value.trim() ? answer(request.id, field.value) : field.focus()) }, 'Send answer'),
165
- h('span', { className: 'muted' }, 'Ctrl+Enter sends'),
166
- ),
167
- );
311
+ }
312
+ const last = messages[messages.length - 1];
313
+ if (last && last.sender.replace(/^@/, '').toLowerCase() === 'user' && summary.status === 'open') thread.append(h('p', { className: 'muted' }, `Waiting for ${summary.agent || 'the agent'}’s reply…`));
314
+ card.append(thread);
315
+ requestAnimationFrame(() => {
316
+ thread.scrollTop = thread.scrollHeight;
317
+ });
318
+ const busy = state.busy.has(summary.name);
319
+ if (summary.humanFacing) {
320
+ if (summary.awaiting && summary.awaiting.choices && summary.awaiting.choices.length && summary.status === 'waiting-for-user') {
321
+ card.append(h('div', { className: 'choices', role: 'group', 'aria-label': 'Offered answers' }, summary.awaiting.choices.map((choice) => h('button', { className: 'primary', disabled: busy, onclick: () => sendMessage(summary.name, choice, messages.length) }, choice))));
168
322
  }
169
- item.append(h('div', { className: 'row', style: 'margin-top:6px' }, h('button', { disabled: busy, onclick: () => window.confirm('Decline this request? The agent is told it was declined.') && answer(request.id, 'Declined by the user.', 'rejected') }, 'Decline')));
170
- card.append(item);
323
+ card.append(composer(`chat:${summary.name}`, `Message to ${summary.agent || 'the agent'}`, (text) => sendMessage(summary.name, text, messages.length), busy));
324
+ } else card.append(h('p', { className: 'muted' }, 'A task-internal consultation between agents (read-only).'));
325
+ return card;
326
+ }
327
+
328
+ /**
329
+ * New chat: first exactly two choices — Chat with Chatter (focused default) or Choose another agent — and only the
330
+ * second lists every valid alternative (project agents, hidden/core agents and Expert).
331
+ */
332
+ function renderNewChat() {
333
+ const card = h('section', { className: 'card chats', 'aria-label': 'New chat' });
334
+ const back = state.newChat === 'all' || state.newChat === 'chatter' ? h('button', { onclick: () => ((state.newChat = 'entry'), (state.focusChatter = true), render(state.snapshot)) }, 'Back') : null;
335
+ card.append(h('h2', {}, h('span', {}, 'New chat'), h('span', { className: 'row' }, back, h('button', { onclick: () => ((state.newChat = false), render(state.snapshot)) }, 'Cancel'))));
336
+ if (!state.agents) {
337
+ card.append(h('p', { className: 'muted' }, 'Loading agents…'));
338
+ return card;
339
+ }
340
+ const chatter = state.agents.find((agent) => agent.group === 'chatter');
341
+ const others = state.agents.filter((agent) => agent.group !== 'chatter');
342
+ if (!chatter) {
343
+ card.append(h('div', { className: 'notice error' }, 'Chatter (agents/.core/chatter.book), the default partner of new chats, is missing or invalid. Run `ptbk init` or `ptbk start` to restore it, or choose another agent explicitly below.'));
344
+ if (state.newChat === 'entry') state.newChat = 'all';
345
+ }
346
+ if (state.newChat === 'entry' && chatter) {
347
+ const primary = h('button', { className: 'primary', 'data-focus': 'chatter', onclick: () => ((state.newChat = 'chatter'), render(state.snapshot)) }, `Chat with ${chatter.name}`);
348
+ card.append(
349
+ h('div', { className: 'choices', role: 'group', 'aria-label': 'Who do you want to talk to?' }, primary, h('button', { onclick: () => ((state.newChat = 'all'), render(state.snapshot)) }, 'Choose another agent')),
350
+ h('p', { className: 'muted' }, `${chatter.name} answers project questions, consults the right specialists for you and creates tasks only when you ask for work.`),
351
+ );
352
+ if (state.focusChatter) {
353
+ state.focusChatter = false;
354
+ requestAnimationFrame(() => primary.focus());
355
+ }
356
+ return card;
171
357
  }
358
+ if (state.newChat === 'chatter' && chatter) {
359
+ card.append(h('p', {}, 'Talking to ', h('strong', {}, chatter.name)));
360
+ card.append(composer('chat:new', `First message to ${chatter.name}`, (text) => startChat(chatter.reference, text), state.busy.has('new-chat')));
361
+ } else {
362
+ const select = h('select', { 'aria-label': 'Agent', 'data-draft': 'chat:new-agent' });
363
+ const groups = [
364
+ ['Project agents', others.filter((agent) => agent.group === 'ordinary')],
365
+ ['Hidden/core agents', others.filter((agent) => agent.group === 'hidden')],
366
+ ['Engine-provided', others.filter((agent) => agent.group === 'expert')],
367
+ ];
368
+ for (const [label, members] of groups) if (members.length) select.append(h('optgroup', { label }, members.map((agent) => h('option', { value: agent.reference }, agent.name))));
369
+ if (state.drafts.get('chat:new-agent')) select.value = state.drafts.get('chat:new-agent');
370
+ select.addEventListener('change', () => state.drafts.set('chat:new-agent', select.value));
371
+ card.append(h('div', { className: 'row' }, h('label', {}, 'Talk to ', select)));
372
+ card.append(composer('chat:new', 'First message', (text) => startChat(select.value, text), state.busy.has('new-chat')));
373
+ }
374
+ card.append(h('p', { className: 'muted' }, 'Every message is saved in chats/ and committed. Agents reply read-only and create ordinary tasks only when you ask for work.'));
375
+ return card;
376
+ }
377
+
378
+ function renderChats(snapshot) {
379
+ if (state.newChat) return renderNewChat();
380
+ if (state.chat) return renderThread(snapshot);
381
+ const chats = snapshot.chats;
382
+ const card = h('section', { className: 'card chats', 'aria-label': 'Chats' });
383
+ card.append(h('h2', {}, h('span', {}, `Chats (${chats.total}${chats.unread ? `, ${chats.unread} unread` : ''})`), h('button', { className: 'primary', onclick: showNewChat }, 'New chat')));
384
+ if (!chats.recent.length) {
385
+ card.append(h('p', { className: 'muted' }, 'No chats yet. Start one to ask an agent about the project or to plan work.'));
386
+ return card;
387
+ }
388
+ const items = state.showAllChats ? chats.recent : chats.recent.slice(0, 6);
389
+ card.append(
390
+ h(
391
+ 'ul',
392
+ { className: 'list' },
393
+ items.map((chat) =>
394
+ h(
395
+ 'li',
396
+ {},
397
+ h('a', { href: '#', className: 'title', onclick: (event) => (event.preventDefault(), openChat(chat.name)) }, chat.name, h('span', { className: 'why' }, `${chat.members.join(', ')}${chat.last ? ` · ${chat.last.sender}: ${chat.last.preview}` : ''}`)),
398
+ chat.status === 'waiting-for-user' ? pill('waiting for you', 'warn') : chat.unread ? pill(`${chat.unread} unread`, 'ok') : pill(chat.status, ''),
399
+ ),
400
+ ),
401
+ ),
402
+ );
403
+ if (chats.recent.length > 6) card.append(h('button', { onclick: () => ((state.showAllChats = !state.showAllChats), render(state.snapshot)) }, state.showAllChats ? 'Show fewer' : `Show all ${chats.recent.length}`));
172
404
  return card;
173
405
  }
174
406
 
@@ -177,7 +409,6 @@
177
409
  card.append(h('h2', {}, h('span', {}, 'Current work'), pill(snapshot.phase, snapshot.phase === 'blocked' ? 'bad' : snapshot.current ? 'ok' : '')));
178
410
  const current = snapshot.current;
179
411
  if (current) {
180
- const model = current.model ? `${current.model}${current.modelSource === 'requested' ? ' (requested)' : ''}` : 'harness default (unknown model)';
181
412
  card.append(
182
413
  h(
183
414
  'div',
@@ -187,9 +418,8 @@
187
418
  ),
188
419
  );
189
420
  const rows = [
190
- ['Task', `${current.taskId || current.taskRef}${current.kind !== 'task' ? ` (${current.kind})` : ''}`],
421
+ ['Task', `${current.taskRef}${current.kind !== 'task' ? ` (${current.kind})` : ''}`],
191
422
  ...(current.trace ? [['Trace', current.trace]] : []),
192
- ['Harness', `${current.harnessName} · ${model}${current.thinkingLevel ? ` · ${current.thinkingLevel}` : ''}`],
193
423
  ['Phase', `${current.phase}${current.attempt ? ` · attempt ${current.attempt}/${current.attemptBudget}` : ''}${current.detail ? ` · ${current.detail}` : ''}`],
194
424
  ['Since', time(current.startedAt)],
195
425
  ];
@@ -216,27 +446,112 @@
216
446
  }
217
447
  const error = snapshot.errors[snapshot.errors.length - 1];
218
448
  if (error) card.append(h('details', {}, h('summary', {}, `Last error: ${error.summary}`), h('div', { className: 'muted' }, [error.phase && `Step: ${error.phase}`, error.next && `Next: ${error.next}`, error.trace && `Diagnostics: ${error.trace}`].filter(Boolean).join(' · '))));
449
+ // A significant browser failure stays visible while the browser details are collapsed.
450
+ const browser = snapshot.browser;
451
+ if (browser && (browser.state === 'failed' || (browser.recovery && browser.recovery.state === 'blocked')) && browser.lastFailure) {
452
+ card.append(h('div', { className: 'notice error' }, h('strong', {}, 'Project browser'), ` — ${browser.lastFailure.summary}`, browser.recovery && browser.recovery.state === 'retrying' ? h('div', { className: 'muted' }, 'Recovering automatically…') : null, h('div', { className: 'muted' }, 'Open “Browser details” to recheck, retry or restart it.')));
453
+ }
219
454
  return card;
220
455
  }
221
456
 
222
- function renderControls(snapshot) {
223
- const card = h('section', { className: 'card controls', 'aria-label': 'Controls' });
224
- card.append(h('h2', {}, 'Controls'));
225
- const running = !['stopped', 'finished'].includes(snapshot.service.state);
226
- const paused = snapshot.service.state === 'paused' || snapshot.service.state === 'pausing';
227
- card.append(
457
+ /** Labels of a model in one execution context (reported/requested/automatic/unknown stay distinct). */
458
+ function modelText(model) {
459
+ if (model.status === 'reported') return `${model.value} (reported by the harness)`;
460
+ if (model.status === 'requested') return `${model.value} (requested; not yet confirmed)`;
461
+ if (model.status === 'automatic') return 'automatic — harness default, concrete model not reported';
462
+ return 'unknown — not reported';
463
+ }
464
+
465
+ /** Labels of a thinking level, in the harness's own terminology. */
466
+ function thinkingText(thinking) {
467
+ const term = thinking.term ? ` — ${thinking.term}` : '';
468
+ if (thinking.status === 'requested') return `${thinking.value}${term}`;
469
+ if (thinking.status === 'automatic') return `automatic (harness default, level not reported)${term}`;
470
+ if (thinking.status === 'not-applicable') return 'not applicable — this harness has no thinking-level setting';
471
+ return thinking.value ? `${thinking.value} (unverified)` : 'unknown — not reported';
472
+ }
473
+
474
+ /** One collapsible detail panel whose open state is a viewer-local choice. */
475
+ function detailPanel(key, title, content) {
476
+ const element = h('details', { className: 'card', open: state.details[key] }, h('summary', {}, title), content);
477
+ element.addEventListener('toggle', () => {
478
+ state.details[key] = element.open;
479
+ });
480
+ return element;
481
+ }
482
+
483
+ function renderExecutionDetails(snapshot) {
484
+ const execution = snapshot.execution;
485
+ if (!execution) return h('p', { className: 'muted' }, 'No execution yet in this service.');
486
+ const rows = [
487
+ [execution.status === 'active' ? 'Current run' : 'Last run (historical)', execution.status === 'active' ? `${execution.task.kind} · phase ${execution.phase || 'unknown'}${execution.attempt ? ` · attempt ${execution.attempt}/${execution.attemptBudget}` : ''}` : `${execution.outcome || ''} ${execution.endedAt ? `at ${time(execution.endedAt)}` : ''} — not running now`],
488
+ ['Task', `${execution.task.title}${execution.task.source ? ` · ${execution.task.source}` : ''}`],
489
+ ['Agent', `${execution.agent.name}${execution.agent.source ? ` · ${execution.agent.source}` : ''}`],
490
+ ];
491
+ for (const context of execution.contexts) {
492
+ const prefix = context.role === 'consultation' ? `${context.by || '?'} → ${context.agent} (${context.state}): ` : '';
493
+ rows.push([context.role === 'consultation' ? 'Consultation' : 'Harness', `${prefix}${context.harness.name}${context.harness.version ? ` ${context.harness.version}` : ''}`]);
494
+ rows.push(['Model', `${prefix}${modelText(context.model)}${context.model.source ? ` · ${context.model.source}` : ''}`]);
495
+ rows.push(['Thinking', `${prefix}${thinkingText(context.thinkingLevel)}${context.thinkingLevel.source ? ` · ${context.thinkingLevel.source}` : ''}`]);
496
+ }
497
+ return h('dl', { className: 'kv' }, rows.map(([key, value]) => [h('dt', {}, key), h('dd', {}, value)]));
498
+ }
499
+
500
+ function renderBrowserDetails(snapshot) {
501
+ const browser = snapshot.browser;
502
+ if (!browser) return h('p', { className: 'muted' }, 'Browser information is not available from this service.');
503
+ const rows = [
504
+ ['Profile', `${browser.sourceLabel} (${browser.source})`],
505
+ ['Path', browser.profilePath],
506
+ ['Mode', `${browser.mode.requested}${browser.mode.actual && browser.mode.actual !== browser.mode.requested ? ` (actually ${browser.mode.actual})` : ''}${browser.mode.note ? ` · ${browser.mode.note}` : ''}`],
507
+ ['State', `${browser.state}${browser.pid ? ` · PID ${browser.pid}` : ''}`],
508
+ ['Ownership', `${browser.ownership.state}${browser.ownership.holder ? ` — ${browser.ownership.holder}` : ''}`],
509
+ ];
510
+ if (browser.executable) rows.push(['Browser', browser.executable]);
511
+ if (browser.savedProfile) rows.push(['Saved', browser.savedProfile]);
512
+ if (browser.pending) rows.push(['Pending', browser.pending]);
513
+ if (browser.lastFailure) rows.push(['Last failure', `${time(browser.lastFailure.at)} ${browser.lastFailure.summary}${browser.lastFailure.evidence ? ` — ${browser.lastFailure.evidence}` : ''}`]);
514
+ if (browser.recovery && (browser.recovery.state !== 'idle' || browser.recovery.attempts)) rows.push(['Recovery', `${browser.recovery.state}${browser.recovery.attempts ? ` · ${browser.recovery.attempts} attempt(s)` : ''}${browser.recovery.nextRetryAt ? ` · next ${time(browser.recovery.nextRetryAt)}` : ''}${browser.recovery.blocker ? ` · ${browser.recovery.blocker}` : ''}`]);
515
+ if (browser.lastAction) rows.push(['Last action', `${browser.lastAction.action}: ${browser.lastAction.state} — ${browser.lastAction.outcome}`]);
516
+ return h(
517
+ 'div',
518
+ {},
519
+ h('dl', { className: 'kv' }, rows.map(([key, value]) => [h('dt', {}, key), h('dd', {}, value)])),
228
520
  h(
229
521
  'div',
230
- { className: 'row' },
231
- h('button', { disabled: !running, onclick: () => control(paused ? 'resume' : 'pause') }, paused ? 'Resume' : 'Pause'),
232
- h('button', { disabled: !running || !(snapshot.wait && snapshot.wait.skippable), title: snapshot.wait && !snapshot.wait.skippable ? 'Only pacing/backoff waits can be skipped' : '', onclick: () => control('skip-wait') }, 'Skip wait'),
233
- snapshot.service.stopRequested
234
- ? h('button', { onclick: () => control('cancel-stop') }, 'Cancel stop')
235
- : h('button', { disabled: !running, onclick: () => control('stop', 'Stop the service after the current task? Closing this page does not stop it.') }, 'Stop after task'),
236
- h('button', { className: 'danger', disabled: !running, onclick: () => control('stop-now', 'Stop now? The current task is cancelled and resumes from its recovery record next time.') }, 'Stop now'),
522
+ { className: 'row', style: 'margin-top:8px', role: 'group', 'aria-label': 'Browser recovery' },
523
+ h('button', { onclick: () => control('browser-recheck') }, 'Recheck'),
524
+ h('button', { onclick: () => control('browser-retry') }, 'Retry startup'),
525
+ h('button', { onclick: () => control('browser-restart', 'Restart this project’s browser? Its profile and sessions are kept; the engine keeps running.') }, 'Restart browser'),
237
526
  ),
527
+ h('p', { className: 'muted' }, 'Recovery is automatic; these actions use the same engine-owned recovery and never restart the engine or other browsers.'),
528
+ );
529
+ }
530
+
531
+ function renderDetails(snapshot) {
532
+ return h('section', { className: 'details-panels', 'aria-label': 'Details' }, detailPanel('execution', 'Execution details — agent, harness, model, thinking level', renderExecutionDetails(snapshot)), detailPanel('browser', 'Browser details — profile, health, recovery', renderBrowserDetails(snapshot)));
533
+ }
534
+
535
+ /** The always-visible service-control panel: live state, pending intent and the shared lifecycle operations. */
536
+ function renderControls(snapshot) {
537
+ const stateName = snapshot.service.state === 'paused' ? 'pause' : snapshot.service.state;
538
+ const card = h('section', { className: `card controls ${stateName}`, 'aria-label': 'Service controls' });
539
+ const lifecycle = snapshot.service.lifecycle || {};
540
+ card.append(h('h2', {}, h('span', {}, 'Service controls'), h('span', { className: 'state' }, pill(stateName, stateName === 'running' ? 'ok' : stateName === 'stopping' ? 'bad' : 'warn'))));
541
+ card.append(h('p', {}, lifecycleText(snapshot), lifecycle.since ? h('span', { className: 'muted' }, ` Requested ${time(lifecycle.since)}.`) : null));
542
+ const buttons = lifecycleActions(snapshot).map((item) =>
543
+ h('button', { className: item.danger ? 'danger' : item.primary ? 'primary' : '', disabled: state.busy.has(`control:${item.action}`), onclick: () => control(item.action, item.confirm, item.extra) }, item.label),
238
544
  );
239
- card.append(h('p', { className: 'muted' }, 'Leaving this page never stops the service.'));
545
+ buttons.push(h('button', { disabled: !(snapshot.wait && snapshot.wait.skippable), title: snapshot.wait && !snapshot.wait.skippable ? 'Only pacing/backoff waits can be skipped' : '', onclick: () => control('skip-wait') }, 'Skip wait'));
546
+ card.append(h('div', { className: 'row', role: 'group', 'aria-label': 'Lifecycle actions' }, buttons));
547
+ card.append(h('p', { className: 'muted' }, 'These are the same operations as ptbk pause, resume, stop, stop --force and restart; every connected viewer shares this engine. Leaving this page never stops it.'));
548
+ return card;
549
+ }
550
+
551
+ /** Agents, harnesses and settings (secondary inventory). */
552
+ function renderInventory(snapshot) {
553
+ const card = h('section', { className: 'card inventory', 'aria-label': 'Agents and settings' });
554
+ card.append(h('h2', {}, 'Agents and settings'));
240
555
  const agents = h('details', {}, h('summary', {}, `Agents (${snapshot.agents.length})`), h('ul', { className: 'list' }, snapshot.agents.map((agent) => h('li', {}, h('span', { className: 'row' }, badge(agent, true), agent.name), h('span', { className: 'muted' }, ACTIVITY_LABEL[agent.activity])))));
241
556
  card.append(agents);
242
557
  const featured = snapshot.harnesses.filter((harness) => harness.featured || harness.enabled);
@@ -310,7 +625,7 @@
310
625
  'ul',
311
626
  { className: 'list' },
312
627
  snapshot.results.recent.slice(0, 6).map((result) =>
313
- h('li', {}, h('span', { className: 'title' }, result.title, h('span', { className: 'why' }, `${result.agent || ''} ${result.commit ? `· ${result.commit.slice(0, 8)}` : ''}${result.taskId ? ` · ${result.taskId}` : ''} · ${time(result.endedAt)}`)), pill(result.outcome, result.outcome.startsWith('completed') ? 'ok' : result.outcome === 'failed' ? 'bad' : 'warn')),
628
+ h('li', {}, h('span', { className: 'title' }, result.title, h('span', { className: 'why' }, `${result.agent || ''} ${result.commit ? `· ${result.commit.slice(0, 8)}` : ''} · ${time(result.endedAt)}`)), pill(result.outcome, result.outcome.startsWith('completed') ? 'ok' : result.outcome === 'failed' ? 'bad' : 'warn')),
314
629
  ),
315
630
  ),
316
631
  );
@@ -358,7 +673,7 @@
358
673
  const focusDraft = active && active.getAttribute && active.getAttribute('data-draft');
359
674
  const selection = focusDraft ? [active.selectionStart, active.selectionEnd] : null;
360
675
  const scroll = window.scrollY;
361
- app.replaceChildren(renderAttention(snapshot), renderWork(snapshot), renderControls(snapshot), renderQueue(snapshot), renderActivity(snapshot), renderConnection(snapshot));
676
+ app.replaceChildren(renderAttention(snapshot), renderControls(snapshot), renderChats(snapshot), renderWork(snapshot), renderDetails(snapshot), renderQueue(snapshot), renderActivity(snapshot), renderInventory(snapshot), renderConnection(snapshot));
362
677
  if (focusDraft) {
363
678
  const field = app.querySelector(`[data-draft="${CSS.escape(focusDraft)}"]`);
364
679
  if (field) {
@@ -370,9 +685,9 @@
370
685
  const live = state.connected ? 'live' : 'disconnected — showing last known state';
371
686
  headerStatus.replaceChildren(
372
687
  h('strong', {}, snapshot.project.name),
373
- ` · ptbk ${snapshot.engine.version} · ${snapshot.service.mode} · ${snapshot.service.state}${snapshot.service.commitMode === 'no-commit' ? ' · NO-COMMIT' : ''}${snapshot.requests.length ? ` · ${snapshot.requests.length} request(s)` : ''} · ${live} · observed ${time(snapshot.observedAt)}`,
688
+ ` · ptbk ${snapshot.engine.version} · ${snapshot.service.mode} · ${snapshot.service.state}${snapshot.service.commitMode === 'no-commit' ? ' · NO-COMMIT' : ''}${snapshot.chats.waiting.length ? ` · ${snapshot.chats.waiting.length} waiting for you` : ''} · ${live} · observed ${time(snapshot.observedAt)}`,
374
689
  );
375
- document.title = `${snapshot.requests.length ? `(${snapshot.requests.length}) ` : ''}${snapshot.project.name} · Promptbook`;
690
+ document.title = `${snapshot.chats.waiting.length ? `(${snapshot.chats.waiting.length}) ` : ''}${snapshot.project.name} · Promptbook`;
376
691
  }
377
692
 
378
693
  /** Pairing screen for unauthenticated visitors (no project data). */
@@ -401,7 +716,13 @@
401
716
  state.snapshot = snapshot;
402
717
  state.revision = snapshot.revision;
403
718
  render(snapshot);
719
+ // New messages in the open chat arrive without stealing focus or drafts.
720
+ if (state.chat && state.chat.detail) {
721
+ const summary = snapshot.chats.recent.find((chat) => chat.name === state.chat.name);
722
+ if (summary && (summary.messages !== state.chat.detail.messages.length || summary.status !== state.chat.detail.summary.status)) loadChat(state.chat.name);
723
+ }
404
724
  if (snapshot.service.state === 'stopped' || snapshot.service.state === 'finished') {
725
+ // An exited engine is an observation; the page never starts or recreates a service.
405
726
  await new Promise((resolve) => setTimeout(resolve, 5000));
406
727
  }
407
728
  } catch (error) {
@@ -2,7 +2,7 @@ import { spawn } from 'node:child_process';
2
2
  import { openSync } from 'node:fs';
3
3
  import { engineModule } from '../common/version.js';
4
4
  import { projectAt } from '../project/workspace.js';
5
- import { readLaunchMetadata, rotateServiceLog, servicePaths } from './records.js';
5
+ import { readLaunchMetadata, RESTART_EXIT_CODE, rotateServiceLog, servicePaths } from './records.js';
6
6
  /**
7
7
  * Daemon supervisor entry: `node dist/service/supervisor.js <projectRoot>`.
8
8
  *
@@ -49,6 +49,12 @@ async function supervise() {
49
49
  worker = undefined;
50
50
  if (terminating || code === 0)
51
51
  return;
52
+ if (code === RESTART_EXIT_CODE) {
53
+ // A requested restart: the old worker released ownership; start exactly one replacement now.
54
+ process.stdout.write(`${new Date().toISOString()} worker restarted on request\n`);
55
+ backoff = 1000;
56
+ continue;
57
+ }
52
58
  const launch = await readLaunchMetadata(project);
53
59
  if (!launch?.persistent || launch.intentionallyStopped)
54
60
  return;
@@ -1 +1 @@
1
- {"version":3,"file":"supervisor.js","sourceRoot":"","sources":["../../src/service/supervisor.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"supervisor.js","sourceRoot":"","sources":["../../src/service/supervisor.ts"],"names":[],"mappings":"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"}