| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673 |
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <pstm-uri>/tasks/{task_id}</pstm-uri>
- <pstm-template>main</pstm-template>
- <!-- Goal 41 T7: the page's Log tab mounts the SAME client engine
- the standalone log page runs (SSE feed, filters, search,
- paging) - it binds the [data-log-*] hooks the task-log-feed
- partial ships, so loading the resource here is all the
- wiring the embed needs. -->
- <script pstm-res-src="task_log.js"></script>
- <script>
- // Live updates: the page subscribes to the global pulse (see the
- // entrypoint); whenever task or saddle state changes anywhere in
- // the app, the sealed refresh action rehydrates the task slot -
- // no fixed-interval polling. RFC bodies get the shared
- // text-highlight -> comment popover (see main.html).
- //
- // Goal 41 T5: the anchor sub-nav. A small client-side idiom for
- // server-rendered statum pages: the strip's links are plain
- // anchors into the page's [data-subnav-section] regions, and this
- // script owns (a) smooth in-page jumps that clear the sticky
- // topbar's height, (b) an IntersectionObserver that highlights
- // whichever region the reader is actually in, and (c) the strip's
- // own stuck class once the page scrolls it under the topbar. The
- // statum render cycle never rebuilds static markup (only stm-for
- // clones), but the strip rebinds from statum.afterEvaluate anyway
- // - the idiom must survive a pulse refresh for free, so binding
- // is idempotent: a second bind finds the strip already flagged
- // and does nothing (no stacked listeners). T6 (goal detail) and
- // later detail pages copy this whole block.
- if (window.statum) {
- (function () {
- 'use strict';
- function subnavStrip() {
- return document.querySelector('[data-subnav]');
- }
- function subnavSections() {
- return Array.prototype.slice.call(
- document.querySelectorAll('[data-subnav-section]'));
- }
- // The strip parks under the sticky topbar; publish the
- // measured topbar height as a custom property so app.css's
- // `top:` tracks any wrap/viewport (the log page's sticky
- // toolbar consumes the same property from task_log.js).
- function syncTopbarOffset() {
- var bar = document.querySelector('.topbar');
- if (bar && bar.offsetHeight > 0) {
- document.documentElement.style.setProperty(
- '--topbar-bottom', bar.offsetHeight + 'px');
- }
- }
- // How much sticky chrome sits above a scrolled-to heading:
- // the topbar plus the strip itself (so the heading the
- // reader jumped to is not swallowed by either).
- function subnavOffset() {
- var height = 0;
- var bar = document.querySelector('.topbar');
- if (bar) height += bar.offsetHeight;
- var strip = subnavStrip();
- if (strip) height += strip.offsetHeight + 8;
- return height;
- }
- function highlightSection(id) {
- var strip = subnavStrip();
- if (!strip) return;
- var links = strip.querySelectorAll('a[href^="#"]');
- for (var i = 0; i < links.length; i++) {
- var active = links[i].getAttribute('href') === '#' + id;
- links[i].classList.toggle('active', active);
- if (active) {
- links[i].setAttribute('aria-current', 'true');
- } else {
- links[i].removeAttribute('aria-current');
- }
- }
- }
- function bindSubnav() {
- var strip = subnavStrip();
- if (!strip) return;
- if (strip.getAttribute('data-subnav') === 'bound') {
- return; // afterEvaluate rebind: already live
- }
- strip.setAttribute('data-subnav', 'bound');
- // Smooth jumps that stop below the sticky chrome.
- strip.addEventListener('click', function (event) {
- var anchor = event.target.closest
- ? event.target.closest('a[href^="#"]') : null;
- if (!anchor) return;
- var region = document.querySelector(
- anchor.getAttribute('href'));
- if (!region) return;
- event.preventDefault();
- window.scrollTo({
- top: region.getBoundingClientRect().top
- + window.pageYOffset - subnavOffset(),
- behavior: 'smooth'
- });
- if (history.replaceState) {
- history.replaceState(null, '',
- anchor.getAttribute('href'));
- }
- });
- // Highlight the region the reader is in. The observer
- // keeps every section under watch; a region wins the
- // highlight when it is intersecting AND its top sits
- // at or just below the sticky chrome - i.e. the LAST
- // heading that crossed the line, which a long region
- // keeps until the next heading approaches it.
- var observer = new IntersectionObserver(
- function (entries) {
- entries.forEach(function (entry) {
- var region = entry.target;
- if (entry.isIntersecting
- && region.getBoundingClientRect().top
- <= subnavOffset() + 32) {
- highlightSection(
- region.getAttribute('data-subnav-section'));
- }
- });
- },
- { rootMargin: '0px 0px -70% 0px', threshold: 0 });
- subnavSections().forEach(function (region) {
- observer.observe(region);
- });
- // The strip sticks once the page scrolls it under the
- // topbar: the class carries the opaque background and
- // edge the un-stuck strip must not have.
- var stuck = false;
- var onScroll = function () {
- var bar = document.querySelector('.topbar');
- syncTopbarOffset();
- var barBottom = bar ? bar.getBoundingClientRect().bottom : 0;
- var now = strip.getBoundingClientRect().top <= barBottom;
- if (now !== stuck) {
- stuck = now;
- strip.classList.toggle('is-stuck', stuck);
- }
- };
- window.addEventListener('scroll', onScroll, { passive: true });
- onScroll();
- }
- function refreshTask(source) {
- var task = statum.read('task');
- if (!task || !task.refresh) return;
- statum.act(task.refresh).catch(function (error) {
- // Never silently: a failed refresh means the page is
- // drifting from the server (e.g. the realtime channel
- // starved by the browser's per-origin SSE cap).
- console.warn('[task] live refresh (' + source + ') failed:', error);
- });
- }
- statum.addStateListener('pulse', function () {
- refreshTask('pulse');
- });
- // Belt and braces for SSE starvation: browsers cap ~6
- // connections per origin over HTTP/1.1, and every open
- // terminal tab holds a /terminal-stream EventSource - when
- // the budget is exhausted the pulse channel is silently
- // starved. Refreshing on focus recovers without a manual
- // reload.
- document.addEventListener('visibilitychange', function () {
- if (document.visibilityState === 'visible') refreshTask('focus');
- });
- if (window.initQuoteComments) {
- window.initQuoteComments({
- matches: function (node) {
- return node.closest && node.closest('.rfc-body') != null;
- },
- action: function () {
- var task = statum.read('task');
- return task && task.comment;
- }
- });
- }
- // The mandate card's in-place editor (task 12).
- if (window.initLongFormEditors) {
- window.initLongFormEditors();
- }
- statum.afterEvaluate(function () {
- bindSubnav();
- });
- bindSubnav();
- })();
- }
- </script>
- </head>
- <body>
- <section>
- <h2 stm-if="task != null && task.exists">#<span stm-text="task.id">id</span> —
- <span stm-text="task.title">title</span></h2>
- <p stm-if="task != null && !task.exists" class="text-muted">Task not found.</p>
- <div stm-if="task != null && task.exists">
- <!-- Goal 41 T5: breadcrumbs replace the hand-drawn "All
- tasks" context line (the partial is T1's; the crumbs
- vector is the entrypoint's). The project / enqueue
- context now rides the crumb chain itself. -->
- <pstm-fragment name="breadcrumb" crumbs="task.crumbs"/>
- <!-- Goal 41 T7 (ux-notes structural bet 3): the task page
- and its event log are ONE investigation flow, not two
- URLs and a new tab. Spry's CSS tabs (radios first,
- .tab-list labels, .tab-panels) mount the whole
- Overview surface as panel 1 and the event log's shell
- (the SAME task-log-feed partial the standalone
- /tasks/{id}/log page renders) as panel 2 — the page's
- entrypoint publishes both slots. The log link beside
- Progress toggles the tab in place (a <label> for the
- radio, not an anchor); the standalone route keeps its
- URL for deep links and the forensic full-window view. -->
- <div class="tabs task-tabs">
- <input type="radio" name="task-view" id="task-view-overview" checked>
- <input type="radio" name="task-view" id="task-view-log">
- <div class="tab-list">
- <label for="task-view-overview">Overview</label>
- <label for="task-view-log">Event log</label>
- </div>
- <div class="tab-panels">
- <section class="task-overview-panel" aria-label="Overview">
- <!-- Goal 41 T5: the anchor sub-nav - the page's map. Pure
- anchors into the [data-subnav-section] regions below;
- the page script owns jumps, in-view highlighting and
- the strip's stickiness. The open-block count rides
- the Blocks link so the page's verb announces itself
- in the map. -->
- <nav class="subnav" aria-label="Sections" data-subnav>
- <a href="#task-progress">Status & progress</a>
- <a href="#task-blocks">Blocks</a>
- <a href="#task-comments">Comments</a>
- <a href="#task-config">Configuration</a>
- </nav>
- <!-- Goal 41 T5: the Act|Consult split (ux-notes task
- detail A + structural bet 1). Left column: everything
- a human ACTS on - progress, blocks (the page's verb),
- the completion report, the status/lifecycle surface,
- the comment composer. Right column: everything a
- human CONSULTS - mandate, goal reference, the folded
- configuration + session history. Stacks
- Act-then-Consult under the framework breakpoint
- (app.css .task-columns mirrors the dashboard's
- .dash-board precedent). -->
- <div class="with-sidebar task-columns">
- <!-- ================= ACT ================= -->
- <div class="stack task-col-act">
- <section data-subnav-section="task-progress">
- <!-- Progress feed leads the page (plan 11 task
- D): the agent's newest line first, its
- history behind the "Show history"
- expander. ux-notes D + goal 41 T7: the
- event log is per-task, not per-session -
- it rides here at top level, and T7 makes
- it a same-tab toggle: the label checks the
- log radio, flipping the whole page into
- the log view without a navigation. -->
- <div class="cluster cluster-between">
- <h3>Progress</h3>
- <label class="tab-link" for="task-view-log">Event log</label>
- </div>
- <pstm-fragment name="progress-feed" entries="task.progress"/>
- <p stm-if="task.progress.length == 0" class="text-muted">No progress reports yet — the agent reports via
- <code>saddle progress-update</code>.</p>
- </section>
- <!-- Current blocks render in full (plan 11 task D);
- answered ones fold behind the history expander.
- This is the page's VERB: it leads the acting
- column, above every reference surface. -->
- <section data-subnav-section="task-blocks">
- <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>
- <div stm-for-block-in="task.open_blocks" stm-key="block.id"
- stm-attribute.class="'block-card ' + block.block_type + (block.answered ? ' answered' : '')">
- <div stm-if="!block.answered && block.block_type == 'review'" class="review-banner">
- <h3>Review requested</h3>
- <div class="md" stm-markdown="block.body.length > 0 ? block.body : block.question">summary</div>
- <p>Branch <strong stm-text="task.task_branch">branch</strong>
- <a stm-if="task.review_diff_url.length > 0" stm-attribute.href="task.review_diff_url"
- target="_blank" rel="noopener">· compare changes</a></p>
- <div class="actions">
- <form stm-action="block.answer_action" class="cluster">
- <input type="hidden" name="verdict" value="accept">
- <button type="submit" class="btn-primary">Approve</button>
- </form>
- <form stm-action="block.answer_action" class="cluster">
- <textarea name="feedback" rows="2" placeholder="What should change?"></textarea>
- <button type="submit" class="btn-danger">Request changes</button>
- </form>
- </div>
- </div>
- <p><span stm-attribute.class="'badge badge-' + block.block_type" stm-text="block.block_type">type</span>
- <span class="text-muted" stm-text="block.created_display">when</span></p>
- <div stm-if="block.show_body && block.body.length > 0" class="md rfc-body"
- stm-markdown="block.body">body</div>
- <div stm-if="block.answered">
- <p>Answer: <strong stm-text="block.answer">answer</strong>
- <span class="text-muted" stm-text="block.answered_display">when</span>
- <span class="badge" stm-if="block.answered_by == 'auto'">auto</span></p>
- </div>
- <pstm-fragment name="block-answer"/>
- <pstm-fragment name="block-reply"/>
- </div>
- <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>
- <!-- Historical (answered) blocks behind an expander, the
- current ones in full above (plan 11 task D). -->
- <details class="blocks-history" stm-if="task.answered_blocks.length > 0">
- <summary>Answered blocks (<span stm-text="task.answered_blocks.length">0</span>)</summary>
- <div stm-for-block-in="task.answered_blocks" stm-key="block.id"
- stm-attribute.class="'block-card answered ' + block.block_type">
- <p><span stm-attribute.class="'badge badge-' + block.block_type" stm-text="block.block_type">type</span>
- <span stm-text="block.question">question</span>
- <span class="text-muted" stm-text="block.created_display">when</span></p>
- <div stm-if="block.show_body && block.body.length > 0" class="md rfc-body"
- stm-markdown="block.body">body</div>
- <p>Answer: <strong stm-text="block.answer">answer</strong>
- <span class="text-muted" stm-text="block.answered_display">when</span>
- <span class="badge" stm-if="block.answered_by == 'auto'">auto</span></p>
- </div>
- </details>
- </section>
- <!-- The heredoc completion report (`cat << 'EOF' | saddle
- done`): terminal tasks render tasks.summary as
- markdown (XSS-safe via stm-markdown) in this
- card - "Completion report" when done, "Failure
- summary" when failed; an empty summary renders
- no card. -->
- <section class="card completion-report"
- stm-if="task.terminal && task.summary.length > 0">
- <h3 stm-text="task.report_title">Completion report</h3>
- <div class="md" stm-markdown="task.summary">summary</div>
- </section>
- <!-- Status + lifecycle: the acting column's anchor.
- The lifecycle buttons ride the status card (plan
- 11): new tasks are drafts - Enqueue and Delete in
- draft (delete guarded by dependents), Cancel in
- non-draft non-terminal; completion is agent-only.
- The old 50/50 grid-2 paired status with the
- mandate; the Act|Consult split replaces it - the
- mandate reads in the consult column. -->
- <section class="card status-head">
- <h3>Status</h3>
- <p><span stm-attribute.class="'badge badge-' + task.status" stm-text="task.display">status</span>
- <span stm-attribute.class="'badge badge-mode-' + task.mode" stm-text="task.mode">mode</span>
- · priority <strong stm-text="task.priority">5</strong></p>
- <p class="text-muted" stm-if="task.enqueue_after_task_id > 0">Auto-enqueues after
- <a stm-attribute.href="'/tasks/' + task.enqueue_after_task_id"
- stm-text="'#' + task.enqueue_after_task_id + ' ' + task.enqueue_after_title">after</a></p>
- <p class="text-muted">Created <span stm-text="task.created_display">when</span></p>
- <p class="text-muted" stm-if="task.started_display.length > 0">Started <span stm-text="task.started_display">when</span></p>
- <p class="text-muted" stm-if="task.completed_display.length > 0">Completed <span stm-text="task.completed_display">when</span></p>
- <!-- S5: the aggregate LLM usage stat line (empty for
- zero-usage tasks renders nothing). -->
- <p class="text-muted" stm-if="task.usage_line.length > 0"
- stm-text="task.usage_line">LLM: 1,234 calls · 45.2M in / 1.1M out tok · 3h 12m</p>
- <p stm-if="task.task_branch.length > 0">Branch: <strong stm-text="task.task_branch">branch</strong>
- <span class="text-muted" stm-if="task.branch_from.length > 0"> from <span stm-text="task.branch_from">base</span></span></p>
- <p stm-if="task.status == 'suspended'" class="text-muted">Parked (`saddle suspend`) —
- the goal agent resumes on its goal's task events, with the
- batched status report riding the resume.</p>
- <form class="cluster">
- <input type="hidden" name="task_id" stm-attribute.value="task.id">
- <button type="button" stm-if="task.draft" class="btn-primary"
- stm-action="task.enqueue">Enqueue task</button>
- <button type="button" stm-if="task.draft"
- stm-action="task.delete"
- stm-confirm="Delete this draft task? This cannot be undone.">Delete</button>
- <button type="button" stm-if="task.status == 'failed'" class="btn-primary"
- stm-action="task.retry">Retry task</button>
- <button type="button" stm-if="task.resume_available"
- stm-action="task.resume"
- stm-confirm="Requeue this task? The daemon restores its workspace context on the next dispatch.">Requeue to resume</button>
- <!-- Goal 4 task 1: Force wake — the parked-task
- recovery (a goal run routes to the goal's
- wake). -->
- <button type="button" stm-if="task.force_wake != null"
- stm-action="task.force_wake"
- stm-confirm="Force-woke this parked task? Its agent resumes immediately on its saddle - one wake per press.">Force wake</button>
- <button type="button" stm-if="!task.draft && !task.terminal"
- stm-action="task.cancel" stm-confirm="Cancel this task?">Cancel task</button>
- </form>
- <form class="cluster">
- <input type="hidden" name="task_id" stm-attribute.value="task.id">
- <button type="button" stm-action="task.toggle_notify"
- stm-text="task.notify_me ? 'Don't notify me' : 'Notify me'">Notify me</button>
- <span class="text-muted">— this task's first start, failure and completion
- (questions and reviews always notify subscribed users)</span>
- </form>
- </section>
- <!-- The short-property in-place editor (goal 41 T5):
- the edit surface adopts the data-long-form
- in-place pattern (ux-notes expander-fatigue
- rule 3: short forms prefer in-place - a read
- view + Edit toggle shows the current values
- instead of a cold form behind an expander).
- Drafts edit every field except status; active
- tasks edit priority only; terminal tasks edit
- nothing. The mandate keeps its own in-place
- editor on the mandate card (task 12). -->
- <section class="card" data-long-form data-field="task"
- data-lf-open="0"
- stm-if="task.editable_all || task.editable_priority">
- <h3>Edit task
- <button type="button" class="btn-ghost btn-sm" data-lf-toggle>Edit</button></h3>
- <div data-lf-view>
- <p class="text-muted" stm-if="task.editable_all">Draft — every field except status is editable.</p>
- <p class="text-muted" stm-if="!task.editable_all && task.editable_priority">Work has begun — priority only.</p>
- </div>
- <div stm-if="task.editable_all">
- <form data-lf-edit hidden stm-action="task.update">
- <input type="hidden" name="task_id" stm-attribute.value="task.id">
- <div class="field"><label>Title</label>
- <input type="text" name="title" stm-attribute.value="task.title"></div>
- <div class="field" stm-if="!task.is_goal_run"><label>Mode</label>
- <select name="mode" stm-attribute.value="task.mode">
- <option value="implement">implement — build the change on a task branch</option>
- <option value="investigate">investigate — explore and report, no code changes</option>
- <option value="review">review — read and critique existing work</option>
- <option value="test">test — write and run tests</option>
- </select></div>
- <div class="field" stm-if="task.creator == 'human' && !task.is_goal_run"><label>Auto-enqueue after
- <span class="text-muted">(enqueues itself when the predecessor is done; none clears it)</span></label>
- <select name="enqueue_after_task_id">
- <option value="">none</option>
- <option stm-for-option-in="task.enqueue_after_options" stm-attribute.value="option.id"
- stm-text="'#' + option.id + ' ' + option.title">task</option>
- </select></div>
- <div class="field"><label>Priority (1–9)</label>
- <input type="number" name="priority" min="1" max="9" stm-attribute.value="task.priority"></div>
- <div class="field"><label>Task branch</label>
- <input type="text" name="task_branch" stm-attribute.value="task.task_branch" placeholder="task/<id>-<slug>"></div>
- <div class="field"><label>Branch from</label>
- <input type="text" name="branch_from" stm-attribute.value="task.branch_from"></div>
- <button type="submit" class="btn-primary">Save</button>
- <button type="button" data-lf-toggle>Cancel</button>
- </form>
- </div>
- <div stm-if="!task.editable_all && task.editable_priority">
- <form data-lf-edit hidden stm-action="task.update">
- <input type="hidden" name="task_id" stm-attribute.value="task.id">
- <div class="field"><label>Priority (1–9)</label>
- <input type="number" name="priority" min="1" max="9" stm-attribute.value="task.priority"></div>
- <button type="submit" class="btn-primary">Save</button>
- <button type="button" data-lf-toggle>Cancel</button>
- </form>
- </div>
- </section>
- <section data-subnav-section="task-comments">
- <h3>Comments</h3>
- <!-- Plan 15 C2: harness-authored bookkeeping (aborts,
- requeues, retries, session restorations) renders as a
- distinct muted SYSTEM row — never an agent comment. -->
- <div stm-for-comment-in="task.comments" stm-attribute.class="'comment' + (comment.from_agent ? ' agent' : '') + (comment.is_system ? ' system' : '')">
- <strong stm-if="!comment.is_system" stm-text="comment.author">author</strong>
- <span stm-if="comment.is_system" class="system-label">system</span>
- <span class="text-muted" stm-text="comment.when_display">when</span>
- <div class="md" stm-markdown="comment.body">body</div>
- <blockquote stm-if="comment.quote.length > 0" stm-text="comment.quote">quote</blockquote>
- </div>
- <p stm-if="task.comments.length == 0" class="text-muted">No comments yet.</p>
- <!-- Goal 8 task 4: comments stay OPEN on terminal tasks.
- A terminal NON-GOAL task resurrects on comment (it
- re-queues with the comment injected); a terminal
- goal-owned worker keeps its goal agent as the only
- voice — the hint says so and the service refuses. -->
- <form stm-if="!(task.terminal && task.goal_id > 0)" stm-action="task.comment">
- <input type="hidden" name="task_id" stm-attribute.value="task.id">
- <div class="field"><label for="comment-body">Add comment</label>
- <textarea id="comment-body" name="body" rows="3" placeholder="A comment for the agent"></textarea></div>
- <p stm-if="task.terminal && task.goal_id == 0" class="text-muted">
- This task is terminal — commenting re-queues it for another round
- with your comment injected.</p>
- <button type="submit" class="btn-primary">Comment</button>
- </form>
- <p stm-if="task.terminal && task.goal_id > 0" class="text-muted">
- Comments are closed — this task belongs to its goal; the goal agent
- (its goal's page) owns the conversation.</p>
- <!-- The highlight-to-comment popover (main.html) fills and opens
- this dialog; it went missing in a template rework, which broke
- quoting on RFC bodies. -->
- <dialog id="quote-comment-dialog">
- <h3>Comment on selected text</h3>
- <blockquote id="quote-text" class="term-comment-quote"></blockquote>
- <form stm-action="task.comment">
- <input type="hidden" name="task_id" stm-attribute.value="task.id">
- <input type="hidden" name="quote">
- <input type="hidden" name="return_to">
- <textarea name="body" rows="4" placeholder="Your comment…" style="width:100%; margin:0.5em 0;"></textarea>
- <button type="submit" class="btn-primary">Send comment</button>
- <button type="button" onclick="document.getElementById('quote-comment-dialog').close()">Cancel</button>
- </form>
- </dialog>
- </section>
- </div>
- <!-- ================ CONSULT ================ -->
- <div class="stack task-col-consult">
- <!-- Plan 12: the mandate — the plan-11 "Description"
- card, framed as the binding work order. Task 12:
- drafts swap the rendered markdown for an in-place
- editor (initLongFormEditors in main.html).
- Consult column lead: the single most useful read. -->
- <section class="card" data-long-form data-field="task"
- data-lf-open="0">
- <h3>Mandate
- <button type="button" class="cluster" stm-if="task.editable_all"
- data-lf-toggle>Edit</button></h3>
- <div data-lf-view>
- <div stm-if="task.description.length > 0" class="md" stm-markdown="task.description">description</div>
- <p class="text-muted" stm-if="task.description.length == 0">No mandate.</p>
- </div>
- <!-- The stm-if wrapper (not the form itself) owns
- draft-gating: statum toggles `hidden` on
- stm-if branches, so the form inside keeps its
- visibility JS-owned (initLongFormEditors). -->
- <div stm-if="task.editable_all">
- <form data-lf-edit hidden stm-action="task.update">
- <input type="hidden" name="task_id" stm-attribute.value="task.id">
- <div class="field"><label>Mandate</label>
- <textarea name="description" rows="8"
- data-lf-src="task.description">description</textarea></div>
- <button type="submit">Save</button>
- <button type="button" data-lf-toggle>Cancel</button>
- </form>
- </div>
- </section>
- <!-- Plan 12: the goal reference — a goal task or the
- goal agent's own run links back to its goal. -->
- <section class="card" stm-if="task.goal_id > 0">
- <h3>Goal</h3>
- <p><span stm-attribute.class="'badge badge-' + task.goal_status"
- stm-text="task.goal_status.replace('_', ' ')">status</span>
- <a stm-attribute.href="'/goals/' + task.goal_id" stm-text="task.goal_title">goal</a></p>
- <p class="text-muted">Goal branch <code stm-text="task.goal_branch">branch</code>
- <span stm-if="task.is_goal_run">— this task IS the goal agent's run
- (orchestrate mode; the goal page owns its lifecycle)</span>
- <span stm-if="!task.is_goal_run">— this task runs under the goal agent's
- supervision on an ephemeral branch</span></p>
- </section>
- <!-- Goal 41 T5 (ux-notes task detail C + quick win 6):
- the low-traffic reference sections - Rules,
- Required capabilities, Sessions - cluster into
- ONE collapsed "Configuration & history" region
- with a summary count line, so reference
- material no longer competes with the action
- surfaces. Spry styles the bare details. -->
- <section data-subnav-section="task-config">
- <details class="config-history">
- <summary><strong>Configuration & history</strong>
- <span class="text-muted">— <span stm-text="task.rules.length">0</span> rules ·
- <span stm-text="task.capabilities.length">0</span> capabilities ·
- <span stm-text="task.sessions.length">0</span> sessions</span></summary>
- <div class="stack mt-2">
- <div>
- <h4>Rules</h4>
- <p class="text-muted">Task rules lead the effective set dispatched with this task.</p>
- <div stm-for-rule-in="task.rules" stm-key="rule.link_id" class="card">
- <h3 stm-text="rule.name">name</h3>
- <div class="md" stm-markdown="rule.body">body</div>
- <form class="cluster">
- <button type="button" stm-action="rule.detach" stm-confirm="Detach this rule?">Detach</button>
- </form>
- </div>
- <p stm-if="task.rules.length == 0" class="text-muted">No task-specific rules.</p>
- <form class="cluster" stm-action="task.attach_rule" stm-if="task.rule_options.length > 0">
- <label for="task-rule-target">Attach rule</label>
- <select id="task-rule-target" name="rule_id" required>
- <option stm-for-option-in="task.rule_options" stm-attribute.value="option.id"
- stm-text="option.name">rule</option>
- </select>
- <button type="submit">Attach</button>
- </form>
- </div>
- <div>
- <h4>Required capabilities</h4>
- <p class="text-muted">The dispatcher only assigns this task to a saddle carrying all of these tags.</p>
- <p stm-if="task.capabilities.length > 0">
- <span stm-for-capability-in="task.capabilities" stm-key="capability.link_id">
- <span class="badge"><span stm-text="capability.name">tag</span>
- <button type="button" class="btn-ghost chip-remove"
- stm-action="capability.detach" stm-confirm="Remove this requirement?">×</button></span>
- </span>
- </p>
- <p stm-if="task.capabilities.length == 0" class="text-muted">No requirements — any idle saddle may run it.</p>
- <form class="cluster" stm-action="task.attach_capability" stm-if="task.capability_options.length > 0">
- <label for="task-capability-target">Require capability</label>
- <select id="task-capability-target" name="capability_id" required>
- <option stm-for-option-in="task.capability_options" stm-attribute.value="option.id"
- stm-text="option.name">capability</option>
- </select>
- <button type="submit">Require</button>
- </form>
- </div>
- <div>
- <h4>Sessions</h4>
- <p stm-if="task.saddle_health.length > 0" class="text-muted"
- stm-text="task.saddle_health">saddle health</p>
- <div class="table-wrap">
- <table>
- <thead><tr><th>#</th><th>Saddle</th><th>Started</th><th>Ended</th><th>Outcome</th></tr></thead>
- <tbody>
- <tr stm-for-session-in="task.sessions">
- <td stm-text="session.id">id</td>
- <td stm-text="session.saddle_name">saddle</td>
- <td stm-text="session.started_display">started</td>
- <td stm-text="session.ended_display">ended</td>
- <td stm-if="session.open"><span class="badge badge-working">open</span></td>
- <td stm-if="!session.open" stm-text="session.outcome">outcome</td>
- </tr>
- <tr stm-if="task.sessions.length == 0"><td colspan="5" class="text-muted">No sessions yet.</td></tr>
- </tbody>
- </table>
- </div>
- </div>
- </div>
- </details>
- </section>
- </div>
- </div>
- </section>
- <!-- Goal 41 T7: the LOG panel - the shared shell.
- The partial binds the page's `task_log` slot
- (the entrypoint publishes it beside `task`);
- task_log.js mounts the SSE feed from the same
- [data-log-*] hooks it uses on the standalone
- page. The standalone route keeps its URL; the
- tab IS the same feed, so the natural loop
- (task -> log -> task) never leaves the page.
- The feed's internal 72vh scroller keeps deep
- history navigable inside the panel. -->
- <section class="task-log-panel" aria-label="Event log">
- <div class="cluster cluster-between">
- <p class="text-muted">the unified event ledger · newest at the bottom ·
- loads older history on demand</p>
- <span class="log-live" data-log-live><span class="live-dot"></span><span
- data-live-label>live</span></span>
- </div>
- <pstm-fragment name="task-log-feed"/>
- </section>
- </div>
- </div>
- </div>
- </section>
- </body>
- </html>
|