fix(asks): render agent markdown in asks, and show only waiting asks in the chat strip (#3115) - #3122
Merged
Merged
Conversation
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>
This was referenced Sep 30, 2026
Contributor
|
merge-train: this PR is on today's train with #3119, #3120 and #3085. It validated READY. Everything renders through 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
An ask's agent-written markdown now renders on every surface where it's answered or reviewed. Before, the Workspace asks and
/mshowed the question as raw**bold**/- list/`code`text. Titles, option labels and the given answer were plain text everywhere.operator/AskMarkdown.vue, used by the operator queue card, resolved history, the Workspace asks (PortalAsks) and/m, so the surfaces can't drift again.renderMarkdown, the same DOMPurify policy as chat and reports. It uses the chat bubble's prose classes and code-pill convention, so a list, inline code and a link look the same in the operator queue, the Workspace and the agent's chat.renderInlineMarkdown(the bug(canvas): markdown inside table cells is not rendered — table block cells show raw **bold**/code/[link]() text #2771 cell policy). Bold, italic, code and links are kept; block elements andclassare dropped, so a field can't break its layout./mgets the same elements without the prose classes. They're styled incurrentColor, 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 parentv-ifon 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):pending-only;pending-only(history), ended asks are still listed.Mutation: dropping
pending-onlyturns the wiring test red.Three source-text pins that asserted the old gating shape (
agentAsks.length,store.asksForAgentin 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.
Screenshots
One real approval ask, raised by an agent through
ask_operatoron a local stack. The stored text istitle: "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):

Workspace asks: this showed the raw

**/-/ backticks before the fix, and now matches the operator queue:/mat phone width (raw text before the fix):Dark theme
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
QueueItemDetail.vueis deliberately untouched. The issue lists it, but nothing mounts it, and it readsstore.selectedItem/store.selectedItemId, which no longer exist on the operator-queue store. It is dead code, worth a follow-up to remove._and*(bug(canvas): markdown inside table cells is not rendered — table block cells show raw **bold**/code/[link]() text #2771's reasoning for non-string cells), and the acceptance criteria don't cover them.renderMarkdown, they'll inherit that allowlist when it lands. Inline fields already dropclassentirely.Test Plan
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:<strong>, a<ul>,<code>and a hardened<a target=_blank rel=noopener>, with no raw**;class;<script>/onerrorare stripped (inline and block);/mrenders the same elements.AskMarkdownreverted 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.npm run buildpass.**Approve**option). I checked it on the Workspace, the operator queue (light and dark) and/mat phone width, and it renders the same on each.🤖 Generated with Claude Code