Răsfoiți Sursa

ui: plan 11 task D — task page, queue and dashboard layout tranche

- task page: progress feed moved ABOVE the status card; status +
  description cards in a 50/50 CSS grid (.grid-2) collapsing to
  stacked status-then-description on small screens; the edit expander
  positioned under the progress "Show history" expander (description
  pre-filled when editable, per B+C's flags); plan detail behind an
  expander; historical (answered) blocks behind an expander with the
  current blocks in full above it; Enqueue/Cancel/Delete stay on the
  status card per the visibility rules
- queue screen: the "New task" expander moved ABOVE the table and the
  filter controls
- dashboard: the "waiting on subtasks to be enqueued" block renders
  from waiting_drafts — clear wording plus links to the parent and
  each draft subtask's page (where Enqueue lives)
- tests: pages-bundle layout assertions re-baselined on the new order
  (progress < grid < edit expander < plan expander, answered-behind-
  expander split, queue ordering, dashboard waiting render)
clanker 1 lună în urmă
părinte
comite
a661dcca94

+ 29 - 6
src/entrypoints/HomeEntrypoint.vala

@@ -70,9 +70,9 @@ namespace ClankerSaddle {
 
     /**
      * A suspended parent with subtasks still in draft (plan 11): the
-     * dashboard's "waiting on subtasks to be enqueued" block — task
-     * D+E renders it with links to the task page where the enqueue
-     * action lives.
+     * dashboard's "waiting on subtasks to be enqueued" block — task D+E
+     * renders it with links to the task pages where the enqueue action
+     * lives (the parent's page and each draft subtask's page).
      */
     public class DashboardWaitingDraftView : Object {
         public int64 task_id { get; set; }
@@ -80,6 +80,12 @@ namespace ClankerSaddle {
         public int64 draft_subtasks { get; set; }
     }
 
+    /** One draft subtask link inside a {@link DashboardWaitingDraftView}. */
+    public class DashboardDraftRef : Object {
+        public int64 id { get; set; }
+        public string title { get; set; default = ""; }
+    }
+
     /**
      * Builds the `dashboard` slot state shared by the home entrypoint and
      * the {@link RefreshDashboardAction} pulse refresh, so both hydrate
@@ -132,13 +138,24 @@ namespace ClankerSaddle {
             pub.ready_count = (yield tasks.get_ready_tasks()).length;
 
             // The plan-11 "waiting on subtasks to be enqueued" block:
-            // every suspended parent with subtasks still in draft.
+            // every suspended parent with subtasks still in draft, the
+            // drafts linked so the human reaches Enqueue in one click.
             var draft_children = new Dictionary<string, string>();
+            var drafts_by_parent = new Dictionary<string, Vector<DashboardDraftRef>>();
             foreach (var task in all_tasks) {
                 if (task.parent_task_id > 0 && task.status == STATUS_DRAFT) {
                     string parent = task.parent_task_id.to_string();
                     draft_children.set(parent,
                         (int64.parse(draft_children.get_or_default(parent) ?? "0") + 1).to_string());
+                    var rows = drafts_by_parent.get_or_default(parent);
+                    if (rows == null) {
+                        rows = new Vector<DashboardDraftRef>();
+                        drafts_by_parent.set(parent, rows);
+                    }
+                    ((!)rows).add(new DashboardDraftRef() {
+                        id = task.id,
+                        title = task.title
+                    });
                 }
             }
             var waiting_drafts = new Vector<DashboardWaitingDraftView>();
@@ -241,8 +258,14 @@ namespace ClankerSaddle {
             props["activity"] = StateJson.list<DashboardActivityView>(activity.to_immutable_buffer());
             props["active_tasks"] = StateJson.list<DashboardActiveView>(active.to_immutable_buffer());
             props["pinned"] = StateJson.list<DashboardBlockView>(pinned_sorted.to_immutable_buffer());
-            props["waiting_drafts"] = StateJson.list<DashboardWaitingDraftView>(
-                waiting_drafts.to_immutable_buffer());
+            props["waiting_drafts"] = StateJson.list_rich<DashboardWaitingDraftView>(
+                waiting_drafts.to_immutable_buffer(), (item, dict) => {
+                    var rows = drafts_by_parent.get_or_default(
+                        ((DashboardWaitingDraftView) item).task_id.to_string());
+                    dict["drafts"] = StateJson.list<DashboardDraftRef>(
+                        rows != null ? ((!)rows).to_immutable_buffer()
+                            : (new Vector<DashboardDraftRef>()).to_immutable_buffer());
+                });
 
             return state_service.new_slot(Scope.PAGE, new Statum.State() {
                 type_name = "dashboard",

+ 22 - 5
src/entrypoints/TaskDetailEntrypoint.vala

@@ -289,9 +289,19 @@ namespace ClankerSaddle {
                 blocks_ascending.add(view);
             }
             var blocks = new Vector<BlockView>();
+            var open_blocks = new Vector<BlockView>();
+            var answered_blocks = new Vector<BlockView>();
             var block_array = blocks_ascending.to_array();
             for (var index = block_array.length - 1; index >= 0; index--) {
-                blocks.add(block_array[index]);
+                var view = block_array[index];
+                blocks.add(view);
+                // Plan 11 task D: the current (unanswered) blocks render
+                // in full; the historical ones fold behind an expander.
+                if (view.answered) {
+                    answered_blocks.add(view);
+                } else {
+                    open_blocks.add(view);
+                }
             }
 
             var subtasks = new Vector<SubtaskView>();
@@ -409,10 +419,9 @@ namespace ClankerSaddle {
             var props = (PropertyDictionary) GObjectMapping.to_properties(pub);
             props["comments"] = StateJson.list<CommentView>(comments.to_immutable_buffer());
             props["progress"] = StateJson.list<ProgressView>(progress.to_immutable_buffer());
-            props["blocks"] = StateJson.list_rich<BlockView>(blocks.to_immutable_buffer(),
-                (item, dict) => {
-                    dict["options"] = StateJson.strings(((BlockView) item).options ?? new string[0]);
-                });
+            props["blocks"] = block_views(blocks);
+            props["open_blocks"] = block_views(open_blocks);
+            props["answered_blocks"] = block_views(answered_blocks);
             props["subtasks"] = StateJson.list<SubtaskView>(subtasks.to_immutable_buffer());
             props["dependencies"] = StateJson.list<DependencyView>(dependencies.to_immutable_buffer());
             props["candidates"] = StateJson.list<DependencyView>(candidates.to_immutable_buffer());
@@ -424,6 +433,14 @@ namespace ClankerSaddle {
             return slot_for(state_service, pub, props);
         }
 
+        private static InvercargillJson.JsonElement block_views(Vector<BlockView> views)
+                throws GLib.Error {
+            return StateJson.list_rich<BlockView>(views.to_immutable_buffer(),
+                (item, dict) => {
+                    dict["options"] = StateJson.strings(((BlockView) item).options ?? new string[0]);
+                });
+        }
+
         private static DependencyView task_brief(TaskDetail? detail, int64 id) {
             if (detail == null) {
                 return new DependencyView() { id = id, title = @"#$(id)", status = "missing" };

+ 18 - 2
src/pages/app.css

@@ -126,13 +126,28 @@ form.inline textarea { min-height: 3rem; }
 
 .card h3 { margin: 0 0 var(--grid-1); }
 
-/* The task page's status block — pinned to the very top of the page
-   (plan 10 phase 3), above the cards grid. */
+/* The task page's status block — the left cell of the 50/50
+   status/description grid (plan 11 task D), directly below the
+   progress feed. */
 .status-head {
     border-left: 4px solid #0969da;
+}
+
+/* Status + description side by side, 50/50 (plan 11 task D); stacks
+   status-then-description on small screens (source order carries the
+   collapse — no order flips needed). */
+.grid-2 {
+    display: grid;
+    grid-template-columns: 1fr 1fr;
+    gap: var(--grid-2);
+    align-items: start;
     margin: var(--grid-2) 0;
 }
 
+/* Dashboard: the draft subtasks of a parked parent (plan 11 task D). */
+.waiting-drafts { margin: var(--grid-1) 0; padding-left: var(--grid-2); }
+.waiting-drafts li { margin: var(--grid-1) 0; }
+
 table { border-collapse: collapse; width: 100%; background: var(--surface); }
 th, td { border: 1px solid var(--line); padding: 0.5rem 0.75rem; text-align: left; vertical-align: top; }
 th { background: var(--surface-sunken); }
@@ -641,5 +656,6 @@ dialog#quote-comment-dialog blockquote {
 @media (max-width: 40rem) {
     header { flex-direction: column; gap: var(--grid-1); }
     .cards { grid-template-columns: 1fr; }
+    .grid-2 { grid-template-columns: 1fr; }
     th, td { padding: 0.4rem 0.5rem; font-size: 0.9rem; }
 }

+ 41 - 16
src/pages/home.html

@@ -3,22 +3,23 @@
 <head>
     <pstm-uri>/</pstm-uri>
     <pstm-template>main</pstm-template>
-    <script>
-        // Live dashboard: the pulse subscription (see the entrypoint)
-        // re-runs the sealed refresh action whenever task or saddle state
-        // changes, so progress, blocks and review requests stream in.
-        if (window.statum) {
-            (function () {
-                'use strict';
-                statum.addStateListener('pulse', function () {
-                    var dashboard = statum.read('dashboard');
-                    if (dashboard && dashboard.refresh) {
-                        statum.act(dashboard.refresh).catch(function () { });
-                    }
-                });
-            })();
-        }
-    </script>
+<script>
+    // Live dashboard: the pulse subscription (see the entrypoint)
+    // re-runs the sealed refresh action whenever task or saddle state
+    // changes, so progress, blocks and review requests stream in.
+    if (window.statum) {
+        (function () {
+            'use strict';
+            statum.addStateListener('pulse', function () {
+                var dashboard = statum.read('dashboard');
+                if (dashboard && dashboard.refresh) {
+                    statum.act(dashboard.refresh).catch(function () { });
+                }
+            });
+
+        })();
+    }
+</script>
 </head>
 <body>
     <section>
@@ -59,6 +60,30 @@
             <p stm-if="dashboard.pinned.length == 0" class="muted">Nothing needs a human —
                 no open questions, RFCs or reviews.</p>
 
+            <!-- "Waiting on subtasks to be enqueued" (plan 11 task D):
+                 every suspended parent whose draft subtasks hold up the
+                 work, linked straight to where Enqueue lives. -->
+            <h3 stm-if="dashboard.waiting_drafts.length > 0">Waiting on subtasks to be enqueued
+                (<span stm-text="dashboard.waiting_drafts.length">0</span>)</h3>
+            <p class="muted" stm-if="dashboard.waiting_drafts.length > 0">These tasks are parked until their
+                draft subtasks are enqueued — follow a link and press "Enqueue task".</p>
+            <div class="cards" stm-if="dashboard.waiting_drafts.length > 0">
+                <section class="card" stm-for-waiting-in="dashboard.waiting_drafts" stm-key="waiting.task_id">
+                    <h3><a stm-attribute.href="'/tasks/' + waiting.task_id" stm-text="waiting.title">task</a></h3>
+                    <p><span class="badge badge-suspended">parked</span>
+                        <span stm-text="waiting.draft_subtasks">0</span>
+                        <span stm-text="waiting.draft_subtasks == 1 ? 'subtask' : 'subtasks'">subtasks</span>
+                        still in draft:</p>
+                    <ul class="waiting-drafts">
+                        <li stm-for-draft-in="waiting.drafts" stm-key="draft.id">
+                            <a stm-attribute.href="'/tasks/' + draft.id" stm-text="draft.title">draft</a>
+                            <span class="badge badge-draft">draft</span>
+                        </li>
+                    </ul>
+                    <p class="muted">Enqueue them from their pages so this task can finish.</p>
+                </section>
+            </div>
+
             <h3 stm-if="dashboard.active_tasks.length > 0">Active tasks</h3>
             <table stm-if="dashboard.active_tasks.length > 0">
                 <thead><tr><th>Task</th><th>Status</th><th>Branch</th><th>Latest progress</th></tr></thead>

+ 91 - 64
src/pages/task_detail.html

@@ -242,52 +242,14 @@
                 <span stm-if="task.parent_task_id > 0"> · subtask of
                     <a stm-attribute.href="'/tasks/' + task.parent_task_id" stm-text="'#' + task.parent_task_id">#parent</a></span></p>
 
-            <!-- Status block pinned to the very top (plan 10): the
-                 current state is the first thing on the page. The
-                 lifecycle buttons ride the card (plan 11): Enqueue and
-                 Delete in draft (delete guarded by dependents), Cancel
-                 in non-draft non-terminal; completion is bot-only. -->
-            <section class="card status-head">
-                <h3>Status</h3>
-                <p><span stm-attribute.class="'badge badge-' + task.status" stm-text="task.display">status</span>
-                    · priority <strong stm-text="task.priority">5</strong></p>
-                <p class="muted">Created <span stm-text="task.created_display">when</span></p>
-                <p class="muted" stm-if="task.started_display.length > 0">Started <span stm-text="task.started_display">when</span></p>
-                <p class="muted" stm-if="task.completed_display.length > 0">Completed <span stm-text="task.completed_display">when</span></p>
-                <p stm-if="task.task_branch.length > 0">Branch: <strong stm-text="task.task_branch">branch</strong>
-                    <span class="muted" stm-if="task.branch_from.length > 0"> from <span stm-text="task.branch_from">base</span></span></p>
-                <p stm-if="task.working_branch.length > 0" class="muted">Working branch: <span stm-text="task.working_branch">branch</span></p>
-                <p stm-if="task.status == 'suspended' && task.subtask_draft > 0" class="muted">
-                    Parked — <span stm-text="task.subtask_draft">0</span> subtask(s) still in draft:
-                    enqueue them so this task's work can finish.</p>
-                <p stm-if="task.status == 'suspended' && task.subtask_draft == 0" class="muted">Parked — waiting on
-                    subtasks (<span stm-text="task.subtask_open">0</span> open). The task
-                    resumes with a status report whenever one finishes.</p>
-                <form class="inline">
-                    <input type="hidden" name="task_id" stm-attribute.value="task.id">
-                    <button type="button" stm-if="task.draft" class="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="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>
-                    <button type="button" stm-if="!task.draft && !task.terminal"
-                            stm-action="task.cancel" stm-confirm="Cancel this task?">Cancel task</button>
-                </form>
-            </section>
-
-            <!-- The agent's progress feed sits directly below the status
-                 head: newest first (the latest line up top, the history
-                 behind the fold). -->
+            <!-- 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>
             <div stm-if="task.progress.length > 0">
                 <p class="progress-latest" stm-text="task.progress[0].summary">latest</p>
                 <p class="muted" stm-text="'reported ' + task.progress[0].when_display">when</p>
-                <details stm-if="task.progress.length > 1">
+                <details class="history-expander" stm-if="task.progress.length > 1">
                     <summary>Show history (<span stm-text="task.progress.length">0</span> updates)</summary>
                     <ul class="progress-history">
                         <li stm-for-item-in="task.progress">
@@ -300,7 +262,47 @@
             <p stm-if="task.progress.length == 0" class="muted">No progress reports yet — the agent reports via
                 <code>saddle progress-update</code>.</p>
 
-            <div class="cards">
+            <!-- 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 bot-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>
+                        · priority <strong stm-text="task.priority">5</strong></p>
+                    <p class="muted">Created <span stm-text="task.created_display">when</span></p>
+                    <p class="muted" stm-if="task.started_display.length > 0">Started <span stm-text="task.started_display">when</span></p>
+                    <p class="muted" stm-if="task.completed_display.length > 0">Completed <span stm-text="task.completed_display">when</span></p>
+                    <p stm-if="task.task_branch.length > 0">Branch: <strong stm-text="task.task_branch">branch</strong>
+                        <span class="muted" stm-if="task.branch_from.length > 0"> from <span stm-text="task.branch_from">base</span></span></p>
+                    <p stm-if="task.working_branch.length > 0" class="muted">Working branch: <span stm-text="task.working_branch">branch</span></p>
+                    <p stm-if="task.status == 'suspended' && task.subtask_draft > 0" class="muted">
+                        Parked — <span stm-text="task.subtask_draft">0</span> subtask(s) still in draft:
+                        enqueue them so this task's work can finish.</p>
+                    <p stm-if="task.status == 'suspended' && task.subtask_draft == 0" class="muted">Parked — waiting on
+                        subtasks (<span stm-text="task.subtask_open">0</span> open). The task
+                        resumes with a status report whenever one finishes.</p>
+                    <form class="inline">
+                        <input type="hidden" name="task_id" stm-attribute.value="task.id">
+                        <button type="button" stm-if="task.draft" class="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="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>
+                        <button type="button" stm-if="!task.draft && !task.terminal"
+                                stm-action="task.cancel" stm-confirm="Cancel this task?">Cancel task</button>
+                    </form>
+                </section>
+
                 <section class="card">
                     <h3>Description</h3>
                     <div stm-if="task.description.length > 0" class="md" stm-markdown="task.description">description</div>
@@ -309,12 +311,12 @@
                 </section>
             </div>
 
-            <!-- The edit expander (plan 11): draft tasks edit every
-                 field except status; non-draft non-terminal tasks edit
-                 priority only; terminal tasks edit nothing. The
-                 description arrives pre-filled whenever it is
-                 editable. -->
-            <details stm-if="task.editable_all || task.editable_priority">
+            <!-- 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. The description
+                 arrives pre-filled whenever it is editable. -->
+            <details class="edit-expander" stm-if="task.editable_all || task.editable_priority">
                 <summary><strong>Edit task</strong>
                     <span class="muted" stm-if="task.editable_all">— every field except status while in draft</span>
                     <span class="muted" stm-if="!task.editable_all && task.editable_priority">— priority only once work has begun</span></summary>
@@ -357,19 +359,25 @@
                 </form>
             </details>
 
-            <h3>Plan</h3>
-            <div stm-if="task.plan.length > 0" class="card md" stm-markdown="task.plan">plan</div>
-            <details stm-if="task.plan.length > 0">
-                <summary>Edit plan</summary>
-                <form stm-action="task.save_plan">
-                    <input type="hidden" name="task_id" stm-attribute.value="task.id">
-                    <div class="form-row"><label for="plan-editor">Plan (markdown)</label>
-                        <textarea id="plan-editor" name="plan" rows="12" stm-attribute.value="task.plan"></textarea></div>
-                    <button type="submit" class="primary">Save plan</button>
-                </form>
+            <!-- Plan detail behind an expander (plan 11 task D): the
+                 pending plan review still renders the markdown in its
+                 block section below — this expander is the reference
+                 copy plus the editor. -->
+            <details class="plan-expander" stm-if="task.plan.length > 0">
+                <summary><strong>Plan</strong></summary>
+                <div class="card md" stm-markdown="task.plan">plan</div>
+                <details>
+                    <summary>Edit plan</summary>
+                    <form stm-action="task.save_plan">
+                        <input type="hidden" name="task_id" stm-attribute.value="task.id">
+                        <div class="form-row"><label for="plan-editor">Plan (markdown)</label>
+                            <textarea id="plan-editor" name="plan" rows="12" stm-attribute.value="task.plan"></textarea></div>
+                        <button type="submit" class="primary">Save plan</button>
+                    </form>
+                </details>
             </details>
-            <details stm-if="task.plan.length == 0">
-                <summary>Write a plan</summary>
+            <details class="plan-expander" stm-if="task.plan.length == 0">
+                <summary><strong>Plan</strong> — none yet</summary>
                 <form stm-action="task.save_plan">
                     <input type="hidden" name="task_id" stm-attribute.value="task.id">
                     <div class="form-row"><label for="plan-editor">Plan (markdown)</label>
@@ -378,8 +386,10 @@
                 </form>
             </details>
 
-            <h3>Blocks <span class="muted" stm-if="task.blocks.length > 0">(<span stm-text="task.blocks.length">0</span>)</span></h3>
-            <div stm-for-block-in="task.blocks" stm-key="block.id"
+            <!-- Current blocks render in full (plan 11 task D);
+                 answered ones fold behind the history expander. -->
+            <h3>Blocks <span class="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>
@@ -451,7 +461,24 @@
                     <button type="submit" class="primary">Reply</button>
                 </form>
             </div>
-            <p stm-if="task.blocks.length == 0" class="muted">No blocks — the agent has no open questions.</p>
+            <p stm-if="task.open_blocks.length == 0 && task.answered_blocks.length == 0" class="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="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="muted" stm-text="block.answered_display">when</span>
+                        <span class="badge" stm-if="block.answered_by == 'auto'">auto</span></p>
+                </div>
+            </details>
 
             <h3>Comments</h3>
             <div stm-for-comment-in="task.comments" stm-attribute.class="'comment' + (comment.from_bot ? ' bot' : '')">

+ 55 - 51
src/pages/tasks.html

@@ -47,57 +47,10 @@
         <p stm-if="tasks == null">Loading…</p>
 
         <div stm-if="tasks != null">
-            <form method="get" action="/tasks" class="inline">
-                <label for="filter-status">Status</label>
-                <select id="filter-status" name="status">
-                    <option value="all" stm-attribute.selected="tasks.filter_status == ''">all</option>
-                    <option value="draft" stm-attribute.selected="tasks.filter_status == 'draft'">draft</option>
-                    <option value="queued" stm-attribute.selected="tasks.filter_status == 'queued'">queued</option>
-                    <option value="assigned" stm-attribute.selected="tasks.filter_status == 'assigned'">assigned</option>
-                    <option value="working" stm-attribute.selected="tasks.filter_status == 'working'">working</option>
-                    <option value="blocked" stm-attribute.selected="tasks.filter_status == 'blocked'">blocked</option>
-                    <option value="done" stm-attribute.selected="tasks.filter_status == 'done'">done</option>
-                    <option value="failed" stm-attribute.selected="tasks.filter_status == 'failed'">failed</option>
-                </select>
-                <label for="filter-project">Project</label>
-                <select id="filter-project" name="project_id">
-                    <option value="0" stm-attribute.selected="tasks.filter_project_id == 0">all</option>
-                    <option stm-for-project-in="tasks.projects"
-                             stm-attribute.value="project.id"
-                             stm-attribute.selected="tasks.filter_project_id == project.id"
-                             stm-text="project.name">project</option>
-                </select>
-                <button type="submit">Filter</button>
-            </form>
-
-            <table>
-                <thead><tr><th>#</th><th>Title</th><th>Project</th><th>Status</th><th>Prio</th><th>Depends on</th></tr></thead>
-                <tbody>
-                    <tr stm-for-task-in="tasks.tasks">
-                        <td stm-text="task.id">id</td>
-                        <td><a stm-attribute.href="'/tasks/' + task.id" stm-text="task.title">title</a>
-                            
-                            <span class="muted" stm-if="task.parent_task_id > 0"> · subtask of
-                                <a stm-attribute.href="'/tasks/' + task.parent_task_id" stm-text="'#' + task.parent_task_id">#parent</a></span>
-                            <span class="muted" stm-if="task.ready"> · ready</span>
-                            <span stm-if="task.capabilities.length > 0">
-                                <span stm-for-capability-in="task.capabilities" stm-key="capability">
-                                    <span class="badge" stm-text="capability">tag</span>
-                                </span>
-                                <span class="muted" stm-if="task.waiting_for_saddle"> · waiting for a capable saddle</span>
-                            <span class="muted" stm-if="task.unreachable"> · saddle unreachable — requeueing</span>
-                            </span></td>
-                        <td stm-text="task.project_name">project</td>
-                        <td><span stm-attribute.class="'badge badge-' + task.status" stm-text="task.display">status</span></td>
-                        <td stm-text="task.priority">priority</td>
-                        <td stm-if="task.has_dependencies" stm-text="task.dependency_summary">deps</td>
-                        <td stm-if="!task.has_dependencies" class="muted">—</td>
-                    </tr>
-                    <tr stm-if="tasks.tasks.length == 0"><td colspan="6" class="muted">No tasks match.</td></tr>
-                </tbody>
-            </table>
-
-            <details>
+            <!-- "New task" expander ABOVE the table and the filter
+                 controls (plan 11 task D): creation is the primary
+                 action, not an afterthought under the queue. -->
+            <details class="new-task-expander">
                 <summary><strong>New task</strong></summary>
                 <form stm-action="tasks.create">
                     <div class="form-row"><label>Project</label>
@@ -150,6 +103,57 @@
                     <button type="submit" class="primary">Create task</button>
                 </form>
             </details>
+
+            <form method="get" action="/tasks" class="inline">
+                <label for="filter-status">Status</label>
+                <select id="filter-status" name="status">
+                    <option value="all" stm-attribute.selected="tasks.filter_status == ''">all</option>
+                    <option value="draft" stm-attribute.selected="tasks.filter_status == 'draft'">draft</option>
+                    <option value="queued" stm-attribute.selected="tasks.filter_status == 'queued'">queued</option>
+                    <option value="assigned" stm-attribute.selected="tasks.filter_status == 'assigned'">assigned</option>
+                    <option value="working" stm-attribute.selected="tasks.filter_status == 'working'">working</option>
+                    <option value="blocked" stm-attribute.selected="tasks.filter_status == 'blocked'">blocked</option>
+                    <option value="done" stm-attribute.selected="tasks.filter_status == 'done'">done</option>
+                    <option value="failed" stm-attribute.selected="tasks.filter_status == 'failed'">failed</option>
+                </select>
+                <label for="filter-project">Project</label>
+                <select id="filter-project" name="project_id">
+                    <option value="0" stm-attribute.selected="tasks.filter_project_id == 0">all</option>
+                    <option stm-for-project-in="tasks.projects"
+                             stm-attribute.value="project.id"
+                             stm-attribute.selected="tasks.filter_project_id == project.id"
+                             stm-text="project.name">project</option>
+                </select>
+                <button type="submit">Filter</button>
+            </form>
+
+            <table>
+                <thead><tr><th>#</th><th>Title</th><th>Project</th><th>Status</th><th>Prio</th><th>Depends on</th></tr></thead>
+                <tbody>
+                    <tr stm-for-task-in="tasks.tasks">
+                        <td stm-text="task.id">id</td>
+                        <td><a stm-attribute.href="'/tasks/' + task.id" stm-text="task.title">title</a>
+                            
+                            <span class="muted" stm-if="task.parent_task_id > 0"> · subtask of
+                                <a stm-attribute.href="'/tasks/' + task.parent_task_id" stm-text="'#' + task.parent_task_id">#parent</a></span>
+                            <span class="muted" stm-if="task.ready"> · ready</span>
+                            <span stm-if="task.capabilities.length > 0">
+                                <span stm-for-capability-in="task.capabilities" stm-key="capability">
+                                    <span class="badge" stm-text="capability">tag</span>
+                                </span>
+                                <span class="muted" stm-if="task.waiting_for_saddle"> · waiting for a capable saddle</span>
+                            <span class="muted" stm-if="task.unreachable"> · saddle unreachable — requeueing</span>
+                            </span></td>
+                        <td stm-text="task.project_name">project</td>
+                        <td><span stm-attribute.class="'badge badge-' + task.status" stm-text="task.display">status</span></td>
+                        <td stm-text="task.priority">priority</td>
+                        <td stm-if="task.has_dependencies" stm-text="task.dependency_summary">deps</td>
+                        <td stm-if="!task.has_dependencies" class="muted">—</td>
+                    </tr>
+                    <tr stm-if="tasks.tasks.length == 0"><td colspan="6" class="muted">No tasks match.</td></tr>
+                </tbody>
+            </table>
+
         </div>
     </section>
 </body>

+ 66 - 10
test/web_stack_test.py

@@ -1214,6 +1214,15 @@ def bundle_pages(ctx):
     check(any(int(w["task_id"]) == waiting_parent and int(w["draft_subtasks"]) == 1
               for w in dash.get("waiting_drafts", [])),
           "dashboard data carries the waiting-on-drafts block")
+    waiting_view = [w for w in dash.get("waiting_drafts", [])
+                    if int(w["task_id"]) == waiting_parent][0]
+    check(len(waiting_view.get("drafts", [])) == 1,
+          "the waiting block links the draft subtask where Enqueue lives")
+    status, home_page = http("GET", "/", timeout=15)
+    check(status == 200 and "Waiting on subtasks to be enqueued" in home_page
+          and "stm-attribute.href=\"'/tasks/' + waiting.task_id\"" in home_page
+          and "stm-attribute.href=\"'/tasks/' + draft.id\"" in home_page,
+          "the dashboard renders the waiting-on-drafts block with task-page links")
 
     conn = db()
     conn.execute("insert into tasks (project_id, title, status, priority, created_at)"
@@ -1351,22 +1360,29 @@ def bundle_pages(ctx):
           and "fake daemon finished" in (task_state(auth, rejected_task).get("summary") or ""),
           "rejected entries never fail the task")
 
-    print("== phase 3: task page — status on top, blocks newest-first ==")
+    print("== plan 11 task D: task page — progress first, 50/50 grid, expanders ==")
     detail = task_state(auth, plan_task)
     block_ids = [int(b["id"]) for b in detail.get("blocks", [])]
     check(len(block_ids) >= 3 and block_ids == sorted(block_ids, reverse=True),
           "the blocks section orders newest-first via the sealed GET state (%s)"
           % block_ids)
     status, page = http("GET", "/tasks/%d" % plan_task, timeout=15)
-    check(status == 200 and "status-head" in page
-          and page.index("status-head") < page.index("<h3>Description</h3>")
-          and page.index("status-head") < page.index("<h3>Plan</h3>"),
-          "the status block sits at the very top of the task page")
-    # The agent's progress feed renders DIRECTLY below the status
-    # head (above the description cards), newest line first.
-    check(page.index("status-head") < page.index("<h3>Progress</h3>")
-          < page.index("<h3>Description</h3>"),
-          "the progress feed sits directly below the status head")
+    # Plan 11 task D: the progress feed sits ABOVE the status card;
+    # status + description share a 50/50 grid; the plan detail hides
+    # behind its expander below it.
+    check(status == 200 and "grid-2" in page
+          and page.index("<h3>Progress</h3>") < page.index("status-head"),
+          "the progress feed renders above the status card")
+    check(page.index("grid-2") < page.index("status-head")
+          and page.index("status-head") < page.index("<h3>Description</h3>"),
+          "status and description sit side by side inside the grid (status first)")
+    check("<h3>Description</h3>" in page
+          and page.index("<h3>Description</h3>") < page.index("plan-expander"),
+          "the plan detail hides behind its expander below the grid")
+    # Answered blocks fold behind the history expander (this fixture's
+    # three blocks are all answered by now).
+    check("blocks-history" in page and "Answered blocks" in page,
+          "answered blocks hide behind the blocks history expander")
     conn = db()
     conn.execute("insert into tasks (project_id, title, status, priority,"
                  " created_at) values (1, 'Progress layout task', 'working', 5,"
@@ -1386,6 +1402,46 @@ def bundle_pages(ctx):
     check(status == 200
           and history_page.index("progress-latest") < history_page.index("progress-history"),
           "the progress timeline renders newest-first (latest above the history)")
+    # The edit expander sits UNDER the "Show history" expander.
+    check(history_page.index("Show history") < history_page.index("edit-expander"),
+          "the edit expander sits under the Show history expander")
+
+    print("== plan 11 task D: block split + queue-screen layout ==")
+    # One open and one answered question: the open block renders in
+    # full, the answered one folds behind the history expander.
+    conn = db()
+    conn.execute("insert into sessions (task_id, saddle_id, started_at)"
+                 " values (?, ?, strftime('%s','now'))", (layout_task, fake_id))
+    conn.commit()
+    split_session = conn.execute("select max(id) from sessions").fetchone()[0]
+    conn.close()
+    saddle_api("POST", "/api/saddle/%d/update" % fake_id, fake_token,
+               {"type": "block", "session_id": split_session, "data": {
+                   "question": "Open question for layout?", "type": "question"}})
+    saddle_api("POST", "/api/saddle/%d/update" % fake_id, fake_token,
+               {"type": "block", "session_id": split_session, "data": {
+                   "question": "Answered question for layout?", "type": "question"}})
+    answered_block = [b for b in task_state(auth, layout_task)["blocks"]
+                      if b["question"] == "Answered question for layout?"][0]
+    act(auth, answered_block["answer_action"], {"answer": "Yes"})
+    detail = task_state(auth, layout_task)
+    check(len(detail.get("open_blocks", [])) == 1
+          and len(detail.get("answered_blocks", [])) == 1
+          and detail["open_blocks"][0]["question"] == "Open question for layout?"
+          and detail["answered_blocks"][0]["question"] == "Answered question for layout?",
+          "the task slot splits blocks into open (current) and answered lists")
+    status, page = http("GET", "/tasks/%d" % layout_task, timeout=15)
+    check(status == 200
+          and page.index('stm-for-block-in="task.open_blocks"') < page.index("blocks-history")
+          < page.index('stm-for-block-in="task.answered_blocks"'),
+          "current blocks render in full above the answered-behind-expander history")
+    check(page.index("status-head") < page.index("Cancel task"),
+          "the lifecycle buttons ride the status card inside the grid")
+    status, queue_page = http("GET", "/tasks", timeout=15)
+    check(status == 200
+          and queue_page.index("new-task-expander") < queue_page.index("filter-status")
+          < queue_page.index("<table"),
+          "the New Task expander sits above the filter controls and the table")
 
     print("== R10: branch_advanced on the /poll verdict channel ==")
     conn = db()