task_detail.html 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <pstm-uri>/tasks/{task_id}</pstm-uri>
  5. <pstm-template>main</pstm-template>
  6. <!-- Goal 41 T7: the page's Log tab mounts the SAME client engine
  7. the standalone log page runs (SSE feed, filters, search,
  8. paging) - it binds the [data-log-*] hooks the task-log-feed
  9. partial ships, so loading the resource here is all the
  10. wiring the embed needs. -->
  11. <script pstm-res-src="task_log.js"></script>
  12. <script>
  13. // Live updates: the page subscribes to the global pulse (see the
  14. // entrypoint); whenever task or saddle state changes anywhere in
  15. // the app, the sealed refresh action rehydrates the task slot -
  16. // no fixed-interval polling. RFC bodies get the shared
  17. // text-highlight -> comment popover (see main.html).
  18. //
  19. // Goal 41 T5: the anchor sub-nav. A small client-side idiom for
  20. // server-rendered statum pages: the strip's links are plain
  21. // anchors into the page's [data-subnav-section] regions, and this
  22. // script owns (a) smooth in-page jumps that clear the sticky
  23. // topbar's height, (b) an IntersectionObserver that highlights
  24. // whichever region the reader is actually in, and (c) the strip's
  25. // own stuck class once the page scrolls it under the topbar. The
  26. // statum render cycle never rebuilds static markup (only stm-for
  27. // clones), but the strip rebinds from statum.afterEvaluate anyway
  28. // - the idiom must survive a pulse refresh for free, so binding
  29. // is idempotent: a second bind finds the strip already flagged
  30. // and does nothing (no stacked listeners). T6 (goal detail) and
  31. // later detail pages copy this whole block.
  32. if (window.statum) {
  33. (function () {
  34. 'use strict';
  35. function subnavStrip() {
  36. return document.querySelector('[data-subnav]');
  37. }
  38. function subnavSections() {
  39. return Array.prototype.slice.call(
  40. document.querySelectorAll('[data-subnav-section]'));
  41. }
  42. // The strip parks under the sticky topbar; publish the
  43. // measured topbar height as a custom property so app.css's
  44. // `top:` tracks any wrap/viewport (the log page's sticky
  45. // toolbar consumes the same property from task_log.js).
  46. function syncTopbarOffset() {
  47. var bar = document.querySelector('.topbar');
  48. if (bar && bar.offsetHeight > 0) {
  49. document.documentElement.style.setProperty(
  50. '--topbar-bottom', bar.offsetHeight + 'px');
  51. }
  52. }
  53. // How much sticky chrome sits above a scrolled-to heading:
  54. // the topbar plus the strip itself (so the heading the
  55. // reader jumped to is not swallowed by either).
  56. function subnavOffset() {
  57. var height = 0;
  58. var bar = document.querySelector('.topbar');
  59. if (bar) height += bar.offsetHeight;
  60. var strip = subnavStrip();
  61. if (strip) height += strip.offsetHeight + 8;
  62. return height;
  63. }
  64. function highlightSection(id) {
  65. var strip = subnavStrip();
  66. if (!strip) return;
  67. var links = strip.querySelectorAll('a[href^="#"]');
  68. for (var i = 0; i < links.length; i++) {
  69. var active = links[i].getAttribute('href') === '#' + id;
  70. links[i].classList.toggle('active', active);
  71. if (active) {
  72. links[i].setAttribute('aria-current', 'true');
  73. } else {
  74. links[i].removeAttribute('aria-current');
  75. }
  76. }
  77. }
  78. function bindSubnav() {
  79. var strip = subnavStrip();
  80. if (!strip) return;
  81. if (strip.getAttribute('data-subnav') === 'bound') {
  82. return; // afterEvaluate rebind: already live
  83. }
  84. strip.setAttribute('data-subnav', 'bound');
  85. // Smooth jumps that stop below the sticky chrome.
  86. strip.addEventListener('click', function (event) {
  87. var anchor = event.target.closest
  88. ? event.target.closest('a[href^="#"]') : null;
  89. if (!anchor) return;
  90. var region = document.querySelector(
  91. anchor.getAttribute('href'));
  92. if (!region) return;
  93. event.preventDefault();
  94. window.scrollTo({
  95. top: region.getBoundingClientRect().top
  96. + window.pageYOffset - subnavOffset(),
  97. behavior: 'smooth'
  98. });
  99. if (history.replaceState) {
  100. history.replaceState(null, '',
  101. anchor.getAttribute('href'));
  102. }
  103. });
  104. // Highlight the region the reader is in. The observer
  105. // keeps every section under watch; a region wins the
  106. // highlight when it is intersecting AND its top sits
  107. // at or just below the sticky chrome - i.e. the LAST
  108. // heading that crossed the line, which a long region
  109. // keeps until the next heading approaches it.
  110. var observer = new IntersectionObserver(
  111. function (entries) {
  112. entries.forEach(function (entry) {
  113. var region = entry.target;
  114. if (entry.isIntersecting
  115. && region.getBoundingClientRect().top
  116. <= subnavOffset() + 32) {
  117. highlightSection(
  118. region.getAttribute('data-subnav-section'));
  119. }
  120. });
  121. },
  122. { rootMargin: '0px 0px -70% 0px', threshold: 0 });
  123. subnavSections().forEach(function (region) {
  124. observer.observe(region);
  125. });
  126. // The strip sticks once the page scrolls it under the
  127. // topbar: the class carries the opaque background and
  128. // edge the un-stuck strip must not have.
  129. var stuck = false;
  130. var onScroll = function () {
  131. var bar = document.querySelector('.topbar');
  132. syncTopbarOffset();
  133. var barBottom = bar ? bar.getBoundingClientRect().bottom : 0;
  134. var now = strip.getBoundingClientRect().top <= barBottom;
  135. if (now !== stuck) {
  136. stuck = now;
  137. strip.classList.toggle('is-stuck', stuck);
  138. }
  139. };
  140. window.addEventListener('scroll', onScroll, { passive: true });
  141. onScroll();
  142. }
  143. function refreshTask(source) {
  144. var task = statum.read('task');
  145. if (!task || !task.refresh) return;
  146. statum.act(task.refresh).catch(function (error) {
  147. // Never silently: a failed refresh means the page is
  148. // drifting from the server (e.g. the realtime channel
  149. // starved by the browser's per-origin SSE cap).
  150. console.warn('[task] live refresh (' + source + ') failed:', error);
  151. });
  152. }
  153. statum.addStateListener('pulse', function () {
  154. refreshTask('pulse');
  155. });
  156. // Belt and braces for SSE starvation: browsers cap ~6
  157. // connections per origin over HTTP/1.1, and every open
  158. // terminal tab holds a /terminal-stream EventSource - when
  159. // the budget is exhausted the pulse channel is silently
  160. // starved. Refreshing on focus recovers without a manual
  161. // reload.
  162. document.addEventListener('visibilitychange', function () {
  163. if (document.visibilityState === 'visible') refreshTask('focus');
  164. });
  165. if (window.initQuoteComments) {
  166. window.initQuoteComments({
  167. matches: function (node) {
  168. return node.closest && node.closest('.rfc-body') != null;
  169. },
  170. action: function () {
  171. var task = statum.read('task');
  172. return task && task.comment;
  173. }
  174. });
  175. }
  176. // The mandate card's in-place editor (task 12).
  177. if (window.initLongFormEditors) {
  178. window.initLongFormEditors();
  179. }
  180. statum.afterEvaluate(function () {
  181. bindSubnav();
  182. });
  183. bindSubnav();
  184. })();
  185. }
  186. </script>
  187. </head>
  188. <body>
  189. <section>
  190. <h2 stm-if="task != null && task.exists">#<span stm-text="task.id">id</span> —
  191. <span stm-text="task.title">title</span></h2>
  192. <p stm-if="task != null && !task.exists" class="text-muted">Task not found.</p>
  193. <div stm-if="task != null && task.exists">
  194. <!-- Goal 41 T5: breadcrumbs replace the hand-drawn "All
  195. tasks" context line (the partial is T1's; the crumbs
  196. vector is the entrypoint's). The project / enqueue
  197. context now rides the crumb chain itself. -->
  198. <pstm-fragment name="breadcrumb" crumbs="task.crumbs"/>
  199. <!-- Goal 41 T7 (ux-notes structural bet 3): the task page
  200. and its event log are ONE investigation flow, not two
  201. URLs and a new tab. Spry's CSS tabs (radios first,
  202. .tab-list labels, .tab-panels) mount the whole
  203. Overview surface as panel 1 and the event log's shell
  204. (the SAME task-log-feed partial the standalone
  205. /tasks/{id}/log page renders) as panel 2 — the page's
  206. entrypoint publishes both slots. The log link beside
  207. Progress toggles the tab in place (a <label> for the
  208. radio, not an anchor); the standalone route keeps its
  209. URL for deep links and the forensic full-window view. -->
  210. <div class="tabs task-tabs">
  211. <input type="radio" name="task-view" id="task-view-overview" checked>
  212. <input type="radio" name="task-view" id="task-view-log">
  213. <div class="tab-list">
  214. <label for="task-view-overview">Overview</label>
  215. <label for="task-view-log">Event log</label>
  216. </div>
  217. <div class="tab-panels">
  218. <section class="task-overview-panel" aria-label="Overview">
  219. <!-- Goal 41 T5: the anchor sub-nav - the page's map. Pure
  220. anchors into the [data-subnav-section] regions below;
  221. the page script owns jumps, in-view highlighting and
  222. the strip's stickiness. The open-block count rides
  223. the Blocks link so the page's verb announces itself
  224. in the map. -->
  225. <nav class="subnav" aria-label="Sections" data-subnav>
  226. <a href="#task-progress">Status &amp; progress</a>
  227. <a href="#task-blocks">Blocks</a>
  228. <a href="#task-comments">Comments</a>
  229. <a href="#task-config">Configuration</a>
  230. </nav>
  231. <!-- Goal 41 T5: the Act|Consult split (ux-notes task
  232. detail A + structural bet 1). Left column: everything
  233. a human ACTS on - progress, blocks (the page's verb),
  234. the completion report, the status/lifecycle surface,
  235. the comment composer. Right column: everything a
  236. human CONSULTS - mandate, goal reference, the folded
  237. configuration + session history. Stacks
  238. Act-then-Consult under the framework breakpoint
  239. (app.css .task-columns mirrors the dashboard's
  240. .dash-board precedent). -->
  241. <div class="with-sidebar task-columns">
  242. <!-- ================= ACT ================= -->
  243. <div class="stack task-col-act">
  244. <section data-subnav-section="task-progress">
  245. <!-- Progress feed leads the page (plan 11 task
  246. D): the agent's newest line first, its
  247. history behind the "Show history"
  248. expander. ux-notes D + goal 41 T7: the
  249. event log is per-task, not per-session -
  250. it rides here at top level, and T7 makes
  251. it a same-tab toggle: the label checks the
  252. log radio, flipping the whole page into
  253. the log view without a navigation. -->
  254. <div class="cluster cluster-between">
  255. <h3>Progress</h3>
  256. <label class="tab-link" for="task-view-log">Event log</label>
  257. </div>
  258. <pstm-fragment name="progress-feed" entries="task.progress"/>
  259. <p stm-if="task.progress.length == 0" class="text-muted">No progress reports yet — the agent reports via
  260. <code>saddle progress-update</code>.</p>
  261. </section>
  262. <!-- Current blocks render in full (plan 11 task D);
  263. answered ones fold behind the history expander.
  264. This is the page's VERB: it leads the acting
  265. column, above every reference surface. -->
  266. <section data-subnav-section="task-blocks">
  267. <h3>Blocks <span class="text-muted" stm-if="task.open_blocks.length > 0">(<span stm-text="task.open_blocks.length">0</span> open)</span></h3>
  268. <div stm-for-block-in="task.open_blocks" stm-key="block.id"
  269. stm-attribute.class="'block-card ' + block.block_type + (block.answered ? ' answered' : '')">
  270. <div stm-if="!block.answered && block.block_type == 'review'" class="review-banner">
  271. <h3>Review requested</h3>
  272. <div class="md" stm-markdown="block.body.length > 0 ? block.body : block.question">summary</div>
  273. <p>Branch <strong stm-text="task.task_branch">branch</strong>
  274. <a stm-if="task.review_diff_url.length > 0" stm-attribute.href="task.review_diff_url"
  275. target="_blank" rel="noopener">· compare changes</a></p>
  276. <div class="actions">
  277. <form stm-action="block.answer_action" class="cluster">
  278. <input type="hidden" name="verdict" value="accept">
  279. <button type="submit" class="btn-primary">Approve</button>
  280. </form>
  281. <form stm-action="block.answer_action" class="cluster">
  282. <textarea name="feedback" rows="2" placeholder="What should change?"></textarea>
  283. <button type="submit" class="btn-danger">Request changes</button>
  284. </form>
  285. </div>
  286. </div>
  287. <p><span stm-attribute.class="'badge badge-' + block.block_type" stm-text="block.block_type">type</span>
  288. <span class="text-muted" stm-text="block.created_display">when</span></p>
  289. <div stm-if="block.show_body && block.body.length > 0" class="md rfc-body"
  290. stm-markdown="block.body">body</div>
  291. <div stm-if="block.answered">
  292. <p>Answer: <strong stm-text="block.answer">answer</strong>
  293. <span class="text-muted" stm-text="block.answered_display">when</span>
  294. <span class="badge" stm-if="block.answered_by == 'auto'">auto</span></p>
  295. </div>
  296. <pstm-fragment name="block-answer"/>
  297. <pstm-fragment name="block-reply"/>
  298. </div>
  299. <p stm-if="task.open_blocks.length == 0 && task.answered_blocks.length == 0" class="text-muted">No blocks — the agent has no open questions.</p>
  300. <!-- Historical (answered) blocks behind an expander, the
  301. current ones in full above (plan 11 task D). -->
  302. <details class="blocks-history" stm-if="task.answered_blocks.length > 0">
  303. <summary>Answered blocks (<span stm-text="task.answered_blocks.length">0</span>)</summary>
  304. <div stm-for-block-in="task.answered_blocks" stm-key="block.id"
  305. stm-attribute.class="'block-card answered ' + block.block_type">
  306. <p><span stm-attribute.class="'badge badge-' + block.block_type" stm-text="block.block_type">type</span>
  307. <span stm-text="block.question">question</span>
  308. <span class="text-muted" stm-text="block.created_display">when</span></p>
  309. <div stm-if="block.show_body && block.body.length > 0" class="md rfc-body"
  310. stm-markdown="block.body">body</div>
  311. <p>Answer: <strong stm-text="block.answer">answer</strong>
  312. <span class="text-muted" stm-text="block.answered_display">when</span>
  313. <span class="badge" stm-if="block.answered_by == 'auto'">auto</span></p>
  314. </div>
  315. </details>
  316. </section>
  317. <!-- The heredoc completion report (`cat << 'EOF' | saddle
  318. done`): terminal tasks render tasks.summary as
  319. markdown (XSS-safe via stm-markdown) in this
  320. card - "Completion report" when done, "Failure
  321. summary" when failed; an empty summary renders
  322. no card. -->
  323. <section class="card completion-report"
  324. stm-if="task.terminal && task.summary.length > 0">
  325. <h3 stm-text="task.report_title">Completion report</h3>
  326. <div class="md" stm-markdown="task.summary">summary</div>
  327. </section>
  328. <!-- Status + lifecycle: the acting column's anchor.
  329. The lifecycle buttons ride the status card (plan
  330. 11): new tasks are drafts - Enqueue and Delete in
  331. draft (delete guarded by dependents), Cancel in
  332. non-draft non-terminal; completion is agent-only.
  333. The old 50/50 grid-2 paired status with the
  334. mandate; the Act|Consult split replaces it - the
  335. mandate reads in the consult column. -->
  336. <section class="card status-head">
  337. <h3>Status</h3>
  338. <p><span stm-attribute.class="'badge badge-' + task.status" stm-text="task.display">status</span>
  339. <span stm-attribute.class="'badge badge-mode-' + task.mode" stm-text="task.mode">mode</span>
  340. · priority <strong stm-text="task.priority">5</strong></p>
  341. <p class="text-muted" stm-if="task.enqueue_after_task_id > 0">Auto-enqueues after
  342. <a stm-attribute.href="'/tasks/' + task.enqueue_after_task_id"
  343. stm-text="'#' + task.enqueue_after_task_id + ' ' + task.enqueue_after_title">after</a></p>
  344. <p class="text-muted">Created <span stm-text="task.created_display">when</span></p>
  345. <p class="text-muted" stm-if="task.started_display.length > 0">Started <span stm-text="task.started_display">when</span></p>
  346. <p class="text-muted" stm-if="task.completed_display.length > 0">Completed <span stm-text="task.completed_display">when</span></p>
  347. <!-- S5: the aggregate LLM usage stat line (empty for
  348. zero-usage tasks renders nothing). -->
  349. <p class="text-muted" stm-if="task.usage_line.length > 0"
  350. stm-text="task.usage_line">LLM: 1,234 calls · 45.2M in / 1.1M out tok · 3h 12m</p>
  351. <p stm-if="task.task_branch.length > 0">Branch: <strong stm-text="task.task_branch">branch</strong>
  352. <span class="text-muted" stm-if="task.branch_from.length > 0"> from <span stm-text="task.branch_from">base</span></span></p>
  353. <p stm-if="task.status == 'suspended'" class="text-muted">Parked (`saddle suspend`) —
  354. the goal agent resumes on its goal's task events, with the
  355. batched status report riding the resume.</p>
  356. <form class="cluster">
  357. <input type="hidden" name="task_id" stm-attribute.value="task.id">
  358. <button type="button" stm-if="task.draft" class="btn-primary"
  359. stm-action="task.enqueue">Enqueue task</button>
  360. <button type="button" stm-if="task.draft"
  361. stm-action="task.delete"
  362. stm-confirm="Delete this draft task? This cannot be undone.">Delete</button>
  363. <button type="button" stm-if="task.status == 'failed'" class="btn-primary"
  364. stm-action="task.retry">Retry task</button>
  365. <button type="button" stm-if="task.resume_available"
  366. stm-action="task.resume"
  367. stm-confirm="Requeue this task? The daemon restores its workspace context on the next dispatch.">Requeue to resume</button>
  368. <!-- Goal 4 task 1: Force wake — the parked-task
  369. recovery (a goal run routes to the goal's
  370. wake). -->
  371. <button type="button" stm-if="task.force_wake != null"
  372. stm-action="task.force_wake"
  373. stm-confirm="Force-woke this parked task? Its agent resumes immediately on its saddle - one wake per press.">Force wake</button>
  374. <button type="button" stm-if="!task.draft && !task.terminal"
  375. stm-action="task.cancel" stm-confirm="Cancel this task?">Cancel task</button>
  376. </form>
  377. <form class="cluster">
  378. <input type="hidden" name="task_id" stm-attribute.value="task.id">
  379. <button type="button" stm-action="task.toggle_notify"
  380. stm-text="task.notify_me ? 'Don&#39;t notify me' : 'Notify me'">Notify me</button>
  381. <span class="text-muted">— this task&#39;s first start, failure and completion
  382. (questions and reviews always notify subscribed users)</span>
  383. </form>
  384. </section>
  385. <!-- The short-property in-place editor (goal 41 T5):
  386. the edit surface adopts the data-long-form
  387. in-place pattern (ux-notes expander-fatigue
  388. rule 3: short forms prefer in-place - a read
  389. view + Edit toggle shows the current values
  390. instead of a cold form behind an expander).
  391. Drafts edit every field except status; active
  392. tasks edit priority only; terminal tasks edit
  393. nothing. The mandate keeps its own in-place
  394. editor on the mandate card (task 12). -->
  395. <section class="card" data-long-form data-field="task"
  396. data-lf-open="0"
  397. stm-if="task.editable_all || task.editable_priority">
  398. <h3>Edit task
  399. <button type="button" class="btn-ghost btn-sm" data-lf-toggle>Edit</button></h3>
  400. <div data-lf-view>
  401. <p class="text-muted" stm-if="task.editable_all">Draft — every field except status is editable.</p>
  402. <p class="text-muted" stm-if="!task.editable_all && task.editable_priority">Work has begun — priority only.</p>
  403. </div>
  404. <div stm-if="task.editable_all">
  405. <form data-lf-edit hidden stm-action="task.update">
  406. <input type="hidden" name="task_id" stm-attribute.value="task.id">
  407. <div class="field"><label>Title</label>
  408. <input type="text" name="title" stm-attribute.value="task.title"></div>
  409. <div class="field" stm-if="!task.is_goal_run"><label>Mode</label>
  410. <select name="mode" stm-attribute.value="task.mode">
  411. <option value="implement">implement — build the change on a task branch</option>
  412. <option value="investigate">investigate — explore and report, no code changes</option>
  413. <option value="review">review — read and critique existing work</option>
  414. <option value="test">test — write and run tests</option>
  415. </select></div>
  416. <div class="field" stm-if="task.creator == 'human' && !task.is_goal_run"><label>Auto-enqueue after
  417. <span class="text-muted">(enqueues itself when the predecessor is done; none clears it)</span></label>
  418. <select name="enqueue_after_task_id">
  419. <option value="">none</option>
  420. <option stm-for-option-in="task.enqueue_after_options" stm-attribute.value="option.id"
  421. stm-text="'#' + option.id + ' ' + option.title">task</option>
  422. </select></div>
  423. <div class="field"><label>Priority (1–9)</label>
  424. <input type="number" name="priority" min="1" max="9" stm-attribute.value="task.priority"></div>
  425. <div class="field"><label>Task branch</label>
  426. <input type="text" name="task_branch" stm-attribute.value="task.task_branch" placeholder="task/<id>-<slug>"></div>
  427. <div class="field"><label>Branch from</label>
  428. <input type="text" name="branch_from" stm-attribute.value="task.branch_from"></div>
  429. <button type="submit" class="btn-primary">Save</button>
  430. <button type="button" data-lf-toggle>Cancel</button>
  431. </form>
  432. </div>
  433. <div stm-if="!task.editable_all && task.editable_priority">
  434. <form data-lf-edit hidden stm-action="task.update">
  435. <input type="hidden" name="task_id" stm-attribute.value="task.id">
  436. <div class="field"><label>Priority (1–9)</label>
  437. <input type="number" name="priority" min="1" max="9" stm-attribute.value="task.priority"></div>
  438. <button type="submit" class="btn-primary">Save</button>
  439. <button type="button" data-lf-toggle>Cancel</button>
  440. </form>
  441. </div>
  442. </section>
  443. <section data-subnav-section="task-comments">
  444. <h3>Comments</h3>
  445. <!-- Plan 15 C2: harness-authored bookkeeping (aborts,
  446. requeues, retries, session restorations) renders as a
  447. distinct muted SYSTEM row — never an agent comment. -->
  448. <div stm-for-comment-in="task.comments" stm-attribute.class="'comment' + (comment.from_agent ? ' agent' : '') + (comment.is_system ? ' system' : '')">
  449. <strong stm-if="!comment.is_system" stm-text="comment.author">author</strong>
  450. <span stm-if="comment.is_system" class="system-label">system</span>
  451. <span class="text-muted" stm-text="comment.when_display">when</span>
  452. <div class="md" stm-markdown="comment.body">body</div>
  453. <blockquote stm-if="comment.quote.length > 0" stm-text="comment.quote">quote</blockquote>
  454. </div>
  455. <p stm-if="task.comments.length == 0" class="text-muted">No comments yet.</p>
  456. <!-- Goal 8 task 4: comments stay OPEN on terminal tasks.
  457. A terminal NON-GOAL task resurrects on comment (it
  458. re-queues with the comment injected); a terminal
  459. goal-owned worker keeps its goal agent as the only
  460. voice — the hint says so and the service refuses. -->
  461. <form stm-if="!(task.terminal && task.goal_id > 0)" stm-action="task.comment">
  462. <input type="hidden" name="task_id" stm-attribute.value="task.id">
  463. <div class="field"><label for="comment-body">Add comment</label>
  464. <textarea id="comment-body" name="body" rows="3" placeholder="A comment for the agent"></textarea></div>
  465. <p stm-if="task.terminal && task.goal_id == 0" class="text-muted">
  466. This task is terminal — commenting re-queues it for another round
  467. with your comment injected.</p>
  468. <button type="submit" class="btn-primary">Comment</button>
  469. </form>
  470. <p stm-if="task.terminal && task.goal_id > 0" class="text-muted">
  471. Comments are closed — this task belongs to its goal; the goal agent
  472. (its goal's page) owns the conversation.</p>
  473. <!-- The highlight-to-comment popover (main.html) fills and opens
  474. this dialog; it went missing in a template rework, which broke
  475. quoting on RFC bodies. -->
  476. <dialog id="quote-comment-dialog">
  477. <h3>Comment on selected text</h3>
  478. <blockquote id="quote-text" class="term-comment-quote"></blockquote>
  479. <form stm-action="task.comment">
  480. <input type="hidden" name="task_id" stm-attribute.value="task.id">
  481. <input type="hidden" name="quote">
  482. <input type="hidden" name="return_to">
  483. <textarea name="body" rows="4" placeholder="Your comment…" style="width:100%; margin:0.5em 0;"></textarea>
  484. <button type="submit" class="btn-primary">Send comment</button>
  485. <button type="button" onclick="document.getElementById('quote-comment-dialog').close()">Cancel</button>
  486. </form>
  487. </dialog>
  488. </section>
  489. </div>
  490. <!-- ================ CONSULT ================ -->
  491. <div class="stack task-col-consult">
  492. <!-- Plan 12: the mandate — the plan-11 "Description"
  493. card, framed as the binding work order. Task 12:
  494. drafts swap the rendered markdown for an in-place
  495. editor (initLongFormEditors in main.html).
  496. Consult column lead: the single most useful read. -->
  497. <section class="card" data-long-form data-field="task"
  498. data-lf-open="0">
  499. <h3>Mandate
  500. <button type="button" class="cluster" stm-if="task.editable_all"
  501. data-lf-toggle>Edit</button></h3>
  502. <div data-lf-view>
  503. <div stm-if="task.description.length > 0" class="md" stm-markdown="task.description">description</div>
  504. <p class="text-muted" stm-if="task.description.length == 0">No mandate.</p>
  505. </div>
  506. <!-- The stm-if wrapper (not the form itself) owns
  507. draft-gating: statum toggles `hidden` on
  508. stm-if branches, so the form inside keeps its
  509. visibility JS-owned (initLongFormEditors). -->
  510. <div stm-if="task.editable_all">
  511. <form data-lf-edit hidden stm-action="task.update">
  512. <input type="hidden" name="task_id" stm-attribute.value="task.id">
  513. <div class="field"><label>Mandate</label>
  514. <textarea name="description" rows="8"
  515. data-lf-src="task.description">description</textarea></div>
  516. <button type="submit">Save</button>
  517. <button type="button" data-lf-toggle>Cancel</button>
  518. </form>
  519. </div>
  520. </section>
  521. <!-- Plan 12: the goal reference — a goal task or the
  522. goal agent's own run links back to its goal. -->
  523. <section class="card" stm-if="task.goal_id > 0">
  524. <h3>Goal</h3>
  525. <p><span stm-attribute.class="'badge badge-' + task.goal_status"
  526. stm-text="task.goal_status.replace('_', ' ')">status</span>
  527. <a stm-attribute.href="'/goals/' + task.goal_id" stm-text="task.goal_title">goal</a></p>
  528. <p class="text-muted">Goal branch <code stm-text="task.goal_branch">branch</code>
  529. <span stm-if="task.is_goal_run">— this task IS the goal agent's run
  530. (orchestrate mode; the goal page owns its lifecycle)</span>
  531. <span stm-if="!task.is_goal_run">— this task runs under the goal agent's
  532. supervision on an ephemeral branch</span></p>
  533. </section>
  534. <!-- Goal 41 T5 (ux-notes task detail C + quick win 6):
  535. the low-traffic reference sections - Rules,
  536. Required capabilities, Sessions - cluster into
  537. ONE collapsed "Configuration & history" region
  538. with a summary count line, so reference
  539. material no longer competes with the action
  540. surfaces. Spry styles the bare details. -->
  541. <section data-subnav-section="task-config">
  542. <details class="config-history">
  543. <summary><strong>Configuration &amp; history</strong>
  544. <span class="text-muted">— <span stm-text="task.rules.length">0</span> rules ·
  545. <span stm-text="task.capabilities.length">0</span> capabilities ·
  546. <span stm-text="task.sessions.length">0</span> sessions</span></summary>
  547. <div class="stack mt-2">
  548. <div>
  549. <h4>Rules</h4>
  550. <p class="text-muted">Task rules lead the effective set dispatched with this task.</p>
  551. <div stm-for-rule-in="task.rules" stm-key="rule.link_id" class="card">
  552. <h3 stm-text="rule.name">name</h3>
  553. <div class="md" stm-markdown="rule.body">body</div>
  554. <form class="cluster">
  555. <button type="button" stm-action="rule.detach" stm-confirm="Detach this rule?">Detach</button>
  556. </form>
  557. </div>
  558. <p stm-if="task.rules.length == 0" class="text-muted">No task-specific rules.</p>
  559. <form class="cluster" stm-action="task.attach_rule" stm-if="task.rule_options.length > 0">
  560. <label for="task-rule-target">Attach rule</label>
  561. <select id="task-rule-target" name="rule_id" required>
  562. <option stm-for-option-in="task.rule_options" stm-attribute.value="option.id"
  563. stm-text="option.name">rule</option>
  564. </select>
  565. <button type="submit">Attach</button>
  566. </form>
  567. </div>
  568. <div>
  569. <h4>Required capabilities</h4>
  570. <p class="text-muted">The dispatcher only assigns this task to a saddle carrying all of these tags.</p>
  571. <p stm-if="task.capabilities.length > 0">
  572. <span stm-for-capability-in="task.capabilities" stm-key="capability.link_id">
  573. <span class="badge"><span stm-text="capability.name">tag</span>
  574. <button type="button" class="btn-ghost chip-remove"
  575. stm-action="capability.detach" stm-confirm="Remove this requirement?">×</button></span>
  576. </span>
  577. </p>
  578. <p stm-if="task.capabilities.length == 0" class="text-muted">No requirements — any idle saddle may run it.</p>
  579. <form class="cluster" stm-action="task.attach_capability" stm-if="task.capability_options.length > 0">
  580. <label for="task-capability-target">Require capability</label>
  581. <select id="task-capability-target" name="capability_id" required>
  582. <option stm-for-option-in="task.capability_options" stm-attribute.value="option.id"
  583. stm-text="option.name">capability</option>
  584. </select>
  585. <button type="submit">Require</button>
  586. </form>
  587. </div>
  588. <div>
  589. <h4>Sessions</h4>
  590. <p stm-if="task.saddle_health.length > 0" class="text-muted"
  591. stm-text="task.saddle_health">saddle health</p>
  592. <div class="table-wrap">
  593. <table>
  594. <thead><tr><th>#</th><th>Saddle</th><th>Started</th><th>Ended</th><th>Outcome</th></tr></thead>
  595. <tbody>
  596. <tr stm-for-session-in="task.sessions">
  597. <td stm-text="session.id">id</td>
  598. <td stm-text="session.saddle_name">saddle</td>
  599. <td stm-text="session.started_display">started</td>
  600. <td stm-text="session.ended_display">ended</td>
  601. <td stm-if="session.open"><span class="badge badge-working">open</span></td>
  602. <td stm-if="!session.open" stm-text="session.outcome">outcome</td>
  603. </tr>
  604. <tr stm-if="task.sessions.length == 0"><td colspan="5" class="text-muted">No sessions yet.</td></tr>
  605. </tbody>
  606. </table>
  607. </div>
  608. </div>
  609. </div>
  610. </details>
  611. </section>
  612. </div>
  613. </div>
  614. </section>
  615. <!-- Goal 41 T7: the LOG panel - the shared shell.
  616. The partial binds the page's `task_log` slot
  617. (the entrypoint publishes it beside `task`);
  618. task_log.js mounts the SSE feed from the same
  619. [data-log-*] hooks it uses on the standalone
  620. page. The standalone route keeps its URL; the
  621. tab IS the same feed, so the natural loop
  622. (task -> log -> task) never leaves the page.
  623. The feed's internal 72vh scroller keeps deep
  624. history navigable inside the panel. -->
  625. <section class="task-log-panel" aria-label="Event log">
  626. <div class="cluster cluster-between">
  627. <p class="text-muted">the unified event ledger · newest at the bottom ·
  628. loads older history on demand</p>
  629. <span class="log-live" data-log-live><span class="live-dot"></span><span
  630. data-live-label>live</span></span>
  631. </div>
  632. <pstm-fragment name="task-log-feed"/>
  633. </section>
  634. </div>
  635. </div>
  636. </div>
  637. </section>
  638. </body>
  639. </html>