|
|
@@ -9,10 +9,141 @@
|
|
|
// 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;
|
|
|
@@ -52,6 +183,11 @@
|
|
|
if (window.initLongFormEditors) {
|
|
|
window.initLongFormEditors();
|
|
|
}
|
|
|
+
|
|
|
+ statum.afterEvaluate(function () {
|
|
|
+ bindSubnav();
|
|
|
+ });
|
|
|
+ bindSubnav();
|
|
|
})();
|
|
|
}
|
|
|
</script>
|
|
|
@@ -63,350 +199,423 @@
|
|
|
<p stm-if="task != null && !task.exists" class="text-muted">Task not found.</p>
|
|
|
|
|
|
<div stm-if="task != null && task.exists">
|
|
|
- <!-- Goal 8 task 4: a standalone task (no project) shows
|
|
|
- "(standalone)" instead of a project link — its
|
|
|
- workspace holds no clones. -->
|
|
|
- <p stm-if="task.project_id > 0"><a href="/tasks">All tasks</a> · project
|
|
|
- <a stm-attribute.href="'/projects/' + task.project_id" stm-text="task.project_name">project</a>
|
|
|
- <span 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">after</a></span></p>
|
|
|
- <p stm-if="task.project_id == 0"><a href="/tasks">All tasks</a> ·
|
|
|
- <strong>(standalone)</strong>
|
|
|
- <span class="text-muted">— no project: the agent works in its own ephemeral
|
|
|
- workspace and delivers its completion report</span></p>
|
|
|
-
|
|
|
- <!-- Progress feed ABOVE the status card (plan 11 task D):
|
|
|
- the agent's newest line leads the page, its history
|
|
|
- behind the "Show history" expander. -->
|
|
|
- <h3>Progress</h3>
|
|
|
- <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>
|
|
|
-
|
|
|
- <!-- Status + description side by side in a 50/50 grid (plan
|
|
|
- 11 task D), collapsing to stacked
|
|
|
- status-then-description on small screens. 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. -->
|
|
|
- <div class="grid-2">
|
|
|
- <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>
|
|
|
-
|
|
|
- <!-- 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>
|
|
|
-
|
|
|
- <!-- 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) — the
|
|
|
- expander no longer carries the long-form field,
|
|
|
- and non-draft/terminal tasks keep the plain read
|
|
|
- view. -->
|
|
|
- <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">
|
|
|
+ <!-- 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 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: the event log link
|
|
|
+ is per-task, not per-session - it rides
|
|
|
+ here at top level instead of a Sessions
|
|
|
+ table row. -->
|
|
|
+ <div class="cluster cluster-between">
|
|
|
+ <h3>Progress</h3>
|
|
|
+ <a stm-attribute.href="'/tasks/' + task.id + '/log'"
|
|
|
+ target="_blank" rel="noopener">Event log</a>
|
|
|
+ </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">
|
|
|
- <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>
|
|
|
+ <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>
|
|
|
- </div>
|
|
|
- </section>
|
|
|
- </div>
|
|
|
+ <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 heredoc completion report (`cat << 'EOF' | saddle
|
|
|
- done`): terminal tasks render tasks.summary as
|
|
|
- markdown (XSS-safe via stm-markdown) in this
|
|
|
- full-width card directly below the status/description
|
|
|
- grid — "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>
|
|
|
-
|
|
|
- <!-- The edit expander sits UNDER the "Show history" expander
|
|
|
- (plan 11 task D): draft tasks edit every field except
|
|
|
- status; non-draft non-terminal tasks edit priority
|
|
|
- only; terminal tasks edit nothing. Task 12: the
|
|
|
- long-form mandate moved to its own in-place editor on
|
|
|
- the mandate card — the expander keeps the short
|
|
|
- properties only. -->
|
|
|
- <details class="edit-expander" stm-if="task.editable_all || task.editable_priority">
|
|
|
- <summary><strong>Edit task</strong>
|
|
|
- <span class="text-muted" stm-if="task.editable_all">— every field except status while in draft</span>
|
|
|
- <span class="text-muted" stm-if="!task.editable_all && task.editable_priority">— priority only once work has begun</span></summary>
|
|
|
- <form stm-action="task.update" stm-if="task.editable_all">
|
|
|
- <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">Save</button>
|
|
|
- </form>
|
|
|
- <form stm-action="task.update" stm-if="!task.editable_all && task.editable_priority">
|
|
|
- <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">Save</button>
|
|
|
- </form>
|
|
|
- </details>
|
|
|
-
|
|
|
-<!-- Current blocks render in full (plan 11 task D);
|
|
|
- answered ones fold behind the history expander. -->
|
|
|
- <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>
|
|
|
+ <!-- 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>
|
|
|
- </div>
|
|
|
+ <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>
|
|
|
|
|
|
- <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>
|
|
|
+ <!-- ================ 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>
|
|
|
|
|
|
- <div stm-if="block.show_body && block.body.length > 0" class="md rfc-body"
|
|
|
- stm-markdown="block.body">body</div>
|
|
|
+ <!-- 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>
|
|
|
|
|
|
- <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>
|
|
|
+ <!-- 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>
|
|
|
|
|
|
- <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>
|
|
|
+ <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>
|
|
|
- </details>
|
|
|
-
|
|
|
- <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>
|
|
|
-
|
|
|
- <h3>Rules</h3>
|
|
|
- <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>
|
|
|
-
|
|
|
- <h3>Required capabilities</h3>
|
|
|
- <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>
|
|
|
-
|
|
|
- <h3>Sessions</h3>
|
|
|
- <p stm-if="task.saddle_health.length > 0" class="text-muted"
|
|
|
- stm-text="task.saddle_health">saddle health</p>
|
|
|
- <table>
|
|
|
- <thead><tr><th>#</th><th>Saddle</th><th>Started</th><th>Ended</th><th>Outcome</th><th></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>
|
|
|
- <td><a stm-attribute.href="'/tasks/' + task.id + '/log'"
|
|
|
- target="_blank" rel="noopener">Event log</a></td>
|
|
|
- </tr>
|
|
|
- <tr stm-if="task.sessions.length == 0"><td colspan="6" class="text-muted">No sessions yet.</td></tr>
|
|
|
- </tbody>
|
|
|
- </table>
|
|
|
</div>
|
|
|
</section>
|
|
|
</body>
|