feat: STML terminal markup for agent comments — guide, preview, live-width feedback#512
feat: STML terminal markup for agent comments — guide, preview, live-width feedback#512benvinegar wants to merge 6 commits into
Conversation
Agent comments can now carry a small HTML-like markup (STML, ported from the sideshow-term experiment) that renders as real terminal UI inside the inline note card: bordered boxes, rows of shapes, lists, badges, code blocks, and styled text. Plain summaries stay as the fallback so note lists and narrow layouts keep working. Because the review stream is row-windowed, the markup is laid out by a deterministic line-layout engine (src/ui/lib/stml) instead of flexbox, so planned note heights and mounted heights stay in exact lockstep. Colors stay symbolic until render time and resolve against the active theme. Markup arrives via the agent-context sidecar (annotation.markup), hunk session comment add --markup, or comment apply batch items. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UGrhiytMJoffcKg2GiaUve
…ite-path feedback Authoring good markup needs an iteration loop, not just a tag list, so give agents three: `hunk markup guide` prints a pattern-driven authoring guide (gauges, pipelines, scorecards, checklists, key-value blocks) whose snippets are test-validated against the real layout engine; `hunk markup render (<file> | -)` previews markup headlessly at any width with render notes on stderr or --json output; and comment add/apply responses now carry markupNotes whenever a comment's markup degraded, so agents get corrective feedback from the write itself. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UGrhiytMJoffcKg2GiaUve
A fixed reference width was blind to real sessions: a unified/stack view on a large terminal renders notes near full pane width, while a narrow split dock can be under 50 columns. Extract the note card's placement math into agentNoteGeometry (shared by rendering, measurement, and reporting so they cannot drift), publish the live layout and pane width to the review controller, and validate comment markup at the width the note actually renders at. Responses echo that markupWidth, `hunk session context` reports noteMarkupWidth so agents can preview at the real width before writing, and the guide teaches the width-discovery workflow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UGrhiytMJoffcKg2GiaUve
The guide is fetched on demand but agents pay its cost every read, so keep the copy-paste snippets (the part prose can't replace) and cut the narration: terser ground rules, one-line pattern headers, style advice softened to a single closing sentence. ~30% smaller. Also slim the always-loaded markup section in the review skill to a short paragraph. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UGrhiytMJoffcKg2GiaUve
Greptile SummaryThis PR introduces STML (a small, tolerant HTML-like markup language) for rendering structured terminal UI inside agent note cards — bordered boxes, gauges, badges, checklists — in place of plain summary text. It also ships
Confidence Score: 4/5Safe to merge — the new STML subsystem is well-isolated, never throws, sanitizes agent input through the existing terminal-text sanitizer, and height measurement is correctly locked to the same layout function used for rendering. The one layout correctness gap (over-wide The core invariant this feature depends on — planned row height matching mounted card height — is correctly maintained for all markup paths. The
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["Agent writes STML markup"] --> B["hunk session comment add --markup"]
A --> C["agent-context sidecar annotations[].markup"]
A --> D["hunk markup render (headless preview)"]
B --> E["brokerServer → useReviewController.addLiveComment"]
E --> F["markupFeedback(markup, anchorSide)"]
F --> G["agentNoteMarkupWidth\n(reads noteGeometryRef)"]
G --> H["validateStmlMarkup → layoutStmlCached"]
F --> I["Return markupWidth + markupNotes\nin AppliedCommentResult"]
C --> J["AgentAnnotation.markup stored\nin review state"]
J --> K["measureAgentInlineNoteHeight"]
K --> L["agentNoteBoxLayout → contentWidth"]
L --> M["agentInlineNoteMarkupLines\n(layoutStmlCached)"]
M -->|"lines.length"| N["Planned row height = 3 + N"]
J --> O["AgentInlineNote render"]
O --> L
M -->|"markupLines"| P["renderMarkupBodyRow\n(resolveStmlColor)"]
M -->|"null → fallback"| Q["renderSavedBodyRow\n(plain summary/rationale)"]
D --> R["parseStml → layoutStml → renderStmlToAnsi/Text"]
R --> S["stdout: ANSI or plain text\nstderr: render notes"]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A["Agent writes STML markup"] --> B["hunk session comment add --markup"]
A --> C["agent-context sidecar annotations[].markup"]
A --> D["hunk markup render (headless preview)"]
B --> E["brokerServer → useReviewController.addLiveComment"]
E --> F["markupFeedback(markup, anchorSide)"]
F --> G["agentNoteMarkupWidth\n(reads noteGeometryRef)"]
G --> H["validateStmlMarkup → layoutStmlCached"]
F --> I["Return markupWidth + markupNotes\nin AppliedCommentResult"]
C --> J["AgentAnnotation.markup stored\nin review state"]
J --> K["measureAgentInlineNoteHeight"]
K --> L["agentNoteBoxLayout → contentWidth"]
L --> M["agentInlineNoteMarkupLines\n(layoutStmlCached)"]
M -->|"lines.length"| N["Planned row height = 3 + N"]
J --> O["AgentInlineNote render"]
O --> L
M -->|"markupLines"| P["renderMarkupBodyRow\n(resolveStmlColor)"]
M -->|"null → fallback"| Q["renderSavedBodyRow\n(plain summary/rationale)"]
D --> R["parseStml → layoutStml → renderStmlToAnsi/Text"]
R --> S["stdout: ANSI or plain text\nstderr: render notes"]
|
| } | ||
|
|
||
| default: { | ||
| errors.add(`unknown tag <${tag}>`); | ||
| return layoutBlockNodes(el.children, width, style, errors); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** Walk a child list: group consecutive inline nodes, lay out blocks one by one. */ | ||
| function layoutBlockNodes( | ||
| nodes: StmlNode[], | ||
| width: number, | ||
| style: StmlStyle, | ||
| errors: LayoutErrors, | ||
| ): StmlLine[] { | ||
| const out: StmlLine[] = []; | ||
| let run: StmlNode[] = []; | ||
|
|
There was a problem hiding this comment.
<row> with over-specified column widths overflows note card silently
When multiple fixed or percent-based columns collectively exceed available, the individual Math.min(w, available) clamp on each column doesn't prevent the aggregate from overflowing. For example, <row><col width="60%">A</col><col width="60%">B</col></row> at width 50 produces columns of [29, 29] with gap=1, totaling 59 chars, and mergeColumns emits lines 59 chars wide against a 50-char content area. No layout error is recorded and no redistribution occurs. The hunk markup render preview shows the same overflow, giving agents a consistently wrong picture.
A guard after the widths are computed — checking that sum(widths) + totalGap <= available and scaling down or stacking if not — would fix this before mergeColumns is called.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/ui/lib/stml/layout.ts
Line: 761-779
Comment:
**`<row>` with over-specified column widths overflows note card silently**
When multiple fixed or percent-based columns collectively exceed `available`, the individual `Math.min(w, available)` clamp on each column doesn't prevent the aggregate from overflowing. For example, `<row><col width="60%">A</col><col width="60%">B</col></row>` at width 50 produces columns of [29, 29] with gap=1, totaling 59 chars, and `mergeColumns` emits lines 59 chars wide against a 50-char content area. No layout error is recorded and no redistribution occurs. The `hunk markup render` preview shows the same overflow, giving agents a consistently wrong picture.
A guard after the widths are computed — checking that `sum(widths) + totalGap <= available` and scaling down or stacking if not — would fix this before `mergeColumns` is called.
How can I resolve this? If you propose a fix, please make it concise.Stamp the guide and changeset so the tag/color vocabulary stays free to change while adoption is unproven, and add a CLAUDE.md bullet explaining why the layout engine is deterministic line layout rather than flexbox — so a fresh-context agent doesn't simplify it into broken scroll geometry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UGrhiytMJoffcKg2GiaUve
Four consolidations, net -60 lines with identical behavior: the note card reuses the diff view's resolveSplitPaneWidths instead of a private copy of the same split math; the summary/rationale body is built by one helper shared by measurement and rendering; the card body row frame is one function that markup and plain rows both fill; and the UTF-8 byte limit helpers in the STML parser use TextEncoder/TextDecoder instead of hand-rolled codepoint math. The plain/ANSI headless renderers now share one line walker parameterized by a span formatter. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UGrhiytMJoffcKg2GiaUve
What
Agent notes can now carry STML (experimental) — a small, tolerant HTML-like markup (ported from the sideshow-term experiment) rendered as real terminal UI inside the inline note card: bordered boxes, rows of shapes, gauges, badges, lists, and code blocks instead of plain text. The plain
summarystays as the fallback andcomment listtext.Markup arrives from all three note sources:
annotations[].markuphunk session comment add --markup '<stml>'comment applybatch items:"markup": "..."A live comment injected through the session daemon renders like this (real PTY capture):
Try it:
hunk patch examples/9-agent-markup-notes/change.patch --agent-context examples/9-agent-markup-notes/agent-context.jsonand pressa.Experimental status
STML is stamped experimental in the guide and changeset: the tag and color vocabulary may change between releases without a major bump while adoption is unproven. Markup degrades to plain text, so the worst case for stale markup is lost polish, not lost content. The feature is deliberately removable — one directory (
src/ui/lib/stml/), one optionalmarkupfield, one CLI subcommand — if dogfooding shows agents don't use it. A CLAUDE.md bullet records the architecture stance so future maintenance keeps the deterministic-layout invariant.How it works
src/ui/lib/stml/parse.ts): pure data-in/data-out, never throws — malformed markup degrades to a best-effort tree plus human-readable render notes. Control sequences are stripped via the existing terminal-text sanitizer before anything reaches the TUI. Input/node/depth limits bound hostile input.src/ui/lib/stml/layout.ts): the review stream is row-windowed, so every planned row must know its exact height before mount — flexbox can't promise that. Instead,(markup, width) → styled span lines, and a note's height is exactlylines.length. Wide-char safe via the existing width helpers, memoized for the measure/render hot path.src/ui/lib/stml/colors.ts): colors stay symbolic (accent,success,danger, hex) until render time, so measurement never needs a theme and markup notes re-theme instantly with everything else — verified against both dark and light themes.src/ui/lib/agentNoteGeometry.ts): the card's placement math is extracted so the renderer, the row-height measurement, and the width reporting below cannot drift.Making agents successful
hunk markup guide— pattern-driven authoring guide (gauges, pipelines, scorecards, checklists, key-value blocks). Every fenced snippet is laid out by a test at the reference width, so the guide can't drift from the renderer. Loaded on demand, ~700 tokens.hunk markup render (<file> | -) [--width N] [--json]— headless preview without launching the TUI; ANSI color on a TTY, render notes to stderr.hunk session context --jsonreportsnoteMarkupWidth;comment add/applyresponses echomarkupWidthand returnmarkupNoteswhen markup degraded, with the exact preview command in the message. Verified live: stack on a 200-col terminal → 188, split on 100 cols → 44.Testing
build:npm+check:pack.comment add --markuppaths), in dark and light themes.Notes / PoC boundaries
rowimplements simple column splitting (fixed/percent widths + equal shares), not full flexbox — enough for shapes and dashboards, easy to extend.warningmaps totheme.fileModified, which some themes color blue rather than amber; a dedicated warning slot onAppThemeis a possible follow-up.minor,hunkdiff, marked experimental).🤖 Generated with Claude Code
https://claude.ai/code/session_01UGrhiytMJoffcKg2GiaUve