Skip to content

fix(asks): render agent markdown in asks, and show only waiting asks in the chat strip (#3115) - #3122

Merged
vybe merged 2 commits into
devfrom
feature/3115-ask-markdown
Sep 30, 2026
Merged

vybe merged 2 commits into
devfrom
feature/3115-ask-markdown

Conversation

@dolho

@dolho dolho commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

An ask's agent-written markdown now renders on every surface where it's answered or reviewed. Before, the Workspace asks and /m showed the question as raw **bold** / - list / `code` text. Titles, option labels and the given answer were plain text everywhere.

  • One renderer, operator/AskMarkdown.vue, used by the operator queue card, resolved history, the Workspace asks (PortalAsks) and /m, so the surfaces can't drift again.
  • Answers are unchanged. An option's value, key and what gets sent stay the raw string; only the label renders.
  • /m gets the same elements without the prose classes. They're styled in currentColor, so its always-dark card needs no new colour literal.

Fixes #3115

Stopgap: the chat strip shows only waiting asks (ent#610 ruling)

Since #3023, the asks list also carries asks that ended in the last 7 days, and the strip above the composer rendered all of them, so answered approval tiles piled up in every chat with the agent. Per the ent#610 ruling, this PR makes the strip pending-only: an answered ask leaves the chat, and history lives in the Inbox. #3101 (ent#610 A2) rebases on top of this.

  • PortalConversation.vue: the strip hosts <PortalAsks pending-only>, as the Work tab already does. It no longer gates on an ask count in the parent, because PortalAsks decides its own visibility, including the ent#468 "Sent." confirmation, which must outlive the last pending ask; a parent v-if on the pending count would unmount it the moment it appears. The strip's top gap follows its content via :has().

  • tests/unit/portalAskStripPendingOnly.mount.spec.js (5 mount tests):

    • the strip passes pending-only;
    • a pending ask shows and an answered one never does;
    • when only ended asks exist, nothing renders;
    • answering the last ask keeps the confirmation;
    • without pending-only (history), ended asks are still listed.

    Mutation: dropping pending-only turns the wiring test red.

  • Three source-text pins that asserted the old gating shape (agentAsks.length, store.asksForAgent in the conversation) now assert the new shape, with the same intent.

Live check (the same Main chat that had three answered approvals stacked): only the one waiting ask shows. After Approve → Send, the tile goes, "Sent." shows briefly, the strip ends empty, and it stays empty after a reload.

One waiting ask; the answered ones are gone After Approve → Send
one pending after approve

Screenshots

One real approval ask, raised by an agent through ask_operator on a local stack. The stored text is title: "Approve the **Q4 launch** plan"; the question has bold, a list with inline code `3` and a link; the options are "**Approve**" and "Reject". The same ask is shown on each surface.

Operator queue card (it rendered the question before; now the title and options render too, and code is a pill):
Operator queue, light

Workspace asks: this showed the raw ** / - / backticks before the fix, and now matches the operator queue:
Workspace ask, light

/m at phone width (raw text before the fix):
/m ops card, phone width

Dark theme

Operator queue, dark
Workspace ask, dark

Changes

  • src/frontend/src/components/operator/AskMarkdown.vue: new; the one renderer (block and inline modes).
  • QueueCard.vue, ResolvedCard.vue, portal/PortalAsks.vue, views/MobileAdmin.vue: switched to it.
  • tests/unit/askMarkdown3115.mount.spec.js: new mount tests.
  • docs/memory/feature-flows/operating-room.md: component row.

Notes for review

Test Plan

  • Stopgap: npx vitest run tests/unit/portalAskStripPendingOnly.mount.spec.js — 5 mount tests; full suite 194 files / 3,882 tests green after the pins were moved.
  • npx vitest run tests/unit/askMarkdown3115.mount.spec.js: 11 mount tests covering:
    • the question renders <strong>, a <ul>, <code> and a hardened <a target=_blank rel=noopener>, with no raw **;
    • an inline field never keeps a block element or a class;
    • <script> / onerror are stripped (inline and block);
    • clicking a formatted option sends the raw string (queue card and Workspace);
    • a plain-text ask shows exactly its text;
    • /m renders the same elements.
  • Mutation: with AskMarkdown reverted to escaped plain text, 9 of the 11 go red. The 2 that stay green are the plain-text and sanitiser tests, which plain text passes by design.
  • Full frontend suite: 193 files, 3,877 tests, including the raw-colour, loading-gate and source-text ratchets. The design-token check and npm run build pass.
  • Manual: on a local stack, an agent raised a real approval ask (bold title, a list with inline code and a link, a **Approve** option). I checked it on the Workspace, the operator queue (light and dark) and /m at phone width, and it renders the same on each.

🤖 Generated with Claude Code

An ask's question rendered as markdown on the operator queue card but as raw
text in the Workspace asks and on /m, and titles, option labels and the given
answer were plain text everywhere. The same ask looked different depending on
where it was answered.

One component, operator/AskMarkdown.vue, now renders every ask field through
the app's one sanitiser:
- the question as block markdown (`renderMarkdown`, the chat/report policy),
  with the chat bubble's prose and code-pill classes, so it reads the same on
  every surface and in the agent's chat;
- the title, option labels and the given answer inline-only
  (`renderInlineMarkdown`, the #2771 cell policy: no block element, no class).

Wired into QueueCard, ResolvedCard, PortalAsks and /m. Option values and the
answer sent are unchanged raw strings. QueueItemDetail.vue is left alone:
nothing mounts it, and the store fields it reads no longer exist.

Fixes #3115

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vybe

vybe commented Sep 30, 2026

Copy link
Copy Markdown
Contributor

merge-train: this PR is on today's train with #3119, #3120 and #3085. It validated READY. Everything renders through AskMarkdown → utils/markdown.js, which is DOMPurify-sanitized, and the mounted spec fails on a revert: 9 of 11 tests go red.

Follow-ups. None of them block:

…ent#610 stopgap)

Since #3023 the Workspace asks list carries asks that ended in the last 7
days, and the strip above the composer rendered all of them. Answered
approval tiles piled up in every chat with the agent. Per the ent#610
ruling, an ended ask belongs to history, not to the strip.

The strip now hosts PortalAsks `pending-only`, like the Work tab. It is no
longer gated on a count in the conversation: PortalAsks decides its own
visibility, including the ent#468 "sent" confirmation that must outlive the
last pending ask. A parent v-if on the pending count would unmount it the
instant it appears. The top gap follows content via :has().

Mount tests cover:
- the strip passes pending-only;
- a pending ask shows and an answered one never does;
- only ended asks renders nothing;
- answering the last ask keeps the confirmation;
- without pending-only, history still lists ended asks.

Three source-text pins that asserted the old gating shape now assert the new
one, with the same intent. Mutation: dropping pending-only turns the wiring
test red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dolho dolho changed the title fix(asks): render agent markdown in asks on every surface (#3115) fix(asks): render agent markdown in asks, and show only waiting asks in the chat strip (#3115) Sep 30, 2026

@vybe vybe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

merge-train: batch validated on train/20260930-1429 (#3126, all gates green)

@vybe
vybe merged commit 8cae767 into dev Sep 30, 2026
25 of 26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ui PR touches the frontend UI — triggers Playwright e2e tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants