Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/lucky-panthers-brake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"hunkdiff": minor
---

Agent notes can now carry STML markup (**experimental**) — a small HTML-like markup rendered as real terminal UI inside the inline note card (bordered boxes, rows of shapes, gauges, lists, badges, code blocks, styled text). Provide it via the `markup` field on agent-context sidecar annotations, `hunk session comment add --markup`, or a `markup` field on `comment apply` batch items; the plain `summary` stays as the fallback and list view text. While the feature is experimental, the tag and color vocabulary may change between releases without a major bump.

Two new commands make markup easy to author well: `hunk markup guide` prints a pattern-driven authoring guide (gauges, pipelines, scorecards, checklists), and `hunk markup render (<file> | -)` previews markup as terminal text at any width without launching the TUI, with render notes on stderr or in `--json` output. Markup feedback follows the live session geometry: `hunk session context` reports `noteMarkupWidth` (the width notes render at in the current layout and terminal size), and `comment add`/`apply` responses echo the `markupWidth` they validated at plus `markupNotes` when the markup degraded — so agents design for the width the user is actually looking at, whether that is a narrow split dock or a full-width unified pane on a large screen.
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ CLI input
- Agent context belongs beside the code, not hidden in a separate mode or workflow.
- Agent notes are hunk-specific: show notes for the selected hunk, render them in the diff flow near the annotated row, and keep a clear spatial relationship to the code they explain.
- Keep note behavior explicit. If the UI intentionally prioritizes one note, one selection, or one active target, encode that as a named policy rather than scattering array-index assumptions through the codebase.
- STML markup notes (experimental) live in `src/ui/lib/stml/`. The layout engine is deliberately a deterministic line layout, not OpenTUI flexbox: the row-windowed review stream needs exact note heights before mount, so `(markup, width)` must always produce the same lines. Colors stay symbolic until render time so measurement never needs a theme. Do not "simplify" this into flexbox renderables, and keep note-card geometry in `agentNoteGeometry` as the single source for rendering, measurement, and agent-facing width reporting.
- If you choose to use a local sidecar for temporary review context, keep it concise and review-oriented: one changeset summary, file summaries in narrative order, and a few hunk-level annotations with real rationale.
- If a local sidecar is present, its file order is intentional, but the visible note UI should stay hunk-note driven rather than showing generic file or changeset explainer cards.
- `hunk diff` working-tree reviews include untracked files by default. Use `--exclude-untracked` if you explicitly want tracked changes only.
Expand Down
36 changes: 36 additions & 0 deletions examples/9-agent-markup-notes/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# 9 — agent markup notes (STML)

Shows agent notes that carry **STML markup** — a small HTML-like markup that
Hunk renders as real terminal UI inside the inline note card: bordered boxes,
rows of shapes, lists, badges, and code blocks instead of plain text.

Run from the repository root:

```sh
hunk patch examples/9-agent-markup-notes/change.patch \
--agent-context examples/9-agent-markup-notes/agent-context.json
```

Press `a` to reveal the agent notes for the selected hunk.

The same markup works for live comments from an agent driving a session:

```sh
hunk session comment add --repo . --file src/retry.ts --new-line 3 \
--summary "Retry flow" \
--markup '<box border border-color="accent">shapes in a note</box>' \
--focus
```

Learn and iterate from the CLI:

```sh
hunk markup guide # authoring guide with copy-paste patterns
echo '<badge color="success">OK</badge> ready' | \
hunk markup render - --width 56 # preview before publishing
```

Tags: block (`box`, `card`, `row`, `text`, `h1`–`h3`, `list`/`item`, `hr`,
`spacer`, `code`) and inline (`b`, `i`, `u`, `s`, `dim`, `color`, `kbd`,
`badge`, `a`, `br`). Colors accept semantic tokens (`accent`, `success`,
`warning`, `danger`, `info`, `muted`), ANSI-style names, or hex.
15 changes: 15 additions & 0 deletions examples/9-agent-markup-notes/after/retry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export async function fetchWithRetry(url: string, attempts = 3): Promise<Response> {
let delayMs = 100;
for (let attempt = 1; attempt <= attempts; attempt += 1) {
try {
return await fetch(url);
} catch (error) {
if (attempt === attempts) {
throw error;
}
await new Promise((resolve) => setTimeout(resolve, delayMs));
delayMs *= 2;
}
}
throw new Error("unreachable");
}
24 changes: 24 additions & 0 deletions examples/9-agent-markup-notes/agent-context.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"version": 1,
"summary": "Replaces the single-shot fetch with bounded exponential-backoff retries.",
"files": [
{
"path": "src/retry.ts",
"summary": "fetchOnce becomes fetchWithRetry with doubling delays between attempts.",
"annotations": [
{
"newRange": [1, 13],
"summary": "Adds a bounded retry loop with exponential backoff.",
"author": "sonnet",
"markup": "<h2>Retry flow</h2><row gap=\"1\"><box border border-color=\"accent\" padding-x=\"1\">fetch</box><box border border-color=\"warning\" padding-x=\"1\">fail?</box><box border border-color=\"success\" padding-x=\"1\">backoff ×2</box></row><spacer/><list><item><badge color=\"success\">OK</badge> caps at <b>3 attempts</b> before rethrowing</item><item><badge color=\"warning\">TODO</badge> add <i>jitter</i> before shipping this</item></list>"
},
{
"newRange": [10, 11],
"summary": "Backoff policy could be extracted for reuse and testing.",
"author": "sonnet",
"markup": "<text>The doubling is inline; consider extracting the policy:</text><code>const backoff = (attempt: number) =>\n 100 * 2 ** (attempt - 1);</code><text><dim>Keeps the loop readable and lets tests pin delays.</dim></text>"
}
]
}
]
}
3 changes: 3 additions & 0 deletions examples/9-agent-markup-notes/before/retry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export async function fetchOnce(url: string): Promise<Response> {
return fetch(url);
}
22 changes: 22 additions & 0 deletions examples/9-agent-markup-notes/change.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
diff --git a/src/retry.ts b/src/retry.ts
index 609120b..8307a97 100644
--- a/src/retry.ts
+++ b/src/retry.ts
@@ -1,3 +1,15 @@
-export async function fetchOnce(url: string): Promise<Response> {
- return fetch(url);
+export async function fetchWithRetry(url: string, attempts = 3): Promise<Response> {
+ let delayMs = 100;
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
+ try {
+ return await fetch(url);
+ } catch (error) {
+ if (attempt === attempts) {
+ throw error;
+ }
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
+ delayMs *= 2;
+ }
+ }
+ throw new Error("unreachable");
}
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Each folder tells a small review story and includes the exact command to run fro
| `6-readme-screenshot` | README screenshot with agent notes | `hunk patch examples/6-readme-screenshot/change.patch --agent-context examples/6-readme-screenshot/agent-context.json --mode split --theme midnight` |
| `7-opentui-component` | embedding `HunkDiffView` in OpenTUI | `bun run examples/7-opentui-component/from-files.tsx` |
| `8-opentui-primitives` | composing Hunk's OpenTUI primitives | `bun run examples/8-opentui-primitives/primitives-demo.tsx` |
| `9-agent-markup-notes` | STML markup rendered inside notes | `hunk patch examples/9-agent-markup-notes/change.patch --agent-context examples/9-agent-markup-notes/agent-context.json` |

## Notes

Expand Down
8 changes: 7 additions & 1 deletion skills/hunk-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ hunk session reload --session-path /path/to/live-window --source /path/to/other-
### Comments

```bash
hunk session comment add --repo . --file README.md --new-line 103 --summary "Tighten this wording" [--rationale "..."] [--author "agent"] [--focus]
hunk session comment add --repo . --file README.md --new-line 103 --summary "Tighten this wording" [--rationale "..."] [--markup "<stml>"] [--author "agent"] [--focus]
printf '%s\n' '{"comments":[{"filePath":"README.md","newLine":103,"summary":"Tighten this wording"}]}' | hunk session comment apply --repo . --stdin [--focus]
hunk session comment list --repo . [--file README.md] [--type live|all|ai|agent|user]
hunk session comment rm --repo . <comment-id>
Expand All @@ -112,6 +112,12 @@ hunk session comment clear --repo . --yes [--file README.md]
- `comment list` and `comment clear` accept optional `--file`
- Quote `--summary` and `--rationale` defensively in the shell

### Rich markup notes (STML)

`--markup` (or a `markup` field on apply items) renders the note body as STML — a small HTML-like markup for terminal UI (boxes, rows, gauges, badges, lists, code). Keep `--summary` a real sentence: it is the fallback and the `comment list` text.

Before writing markup, run `hunk markup guide` once — it has copy-paste patterns and the width rules. `hunk session context --json` reports `noteMarkupWidth` (the live render width); preview with `hunk markup render - --width <that>`. Comment responses echo `markupWidth` and return `markupNotes` when markup degraded — fix what they flag.

## New files in working-tree reviews

`hunk diff` includes untracked files by default. If the user wants tracked changes only, reload with `--exclude-untracked`:
Expand Down
1 change: 1 addition & 0 deletions src/core/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ function normalizeAnnotationFile(file: unknown): AgentFileContext {
newRange: normalizeRange(item.newRange),
summary: item.summary,
rationale: typeof item.rationale === "string" ? item.rationale : undefined,
markup: typeof item.markup === "string" && item.markup.length > 0 ? item.markup : undefined,
tags: Array.isArray(item.tags)
? item.tags.filter((tag): tag is string => typeof tag === "string")
: undefined,
Expand Down
78 changes: 78 additions & 0 deletions src/core/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,84 @@ describe("parseCli", () => {
});
});

test("parses markup render with defaults and options", async () => {
expect(await parseCli(["bun", "hunk", "markup", "render"])).toEqual({
kind: "markup-render",
file: "-",
width: 56,
color: "auto",
theme: undefined,
json: false,
});

expect(
await parseCli([
"bun",
"hunk",
"markup",
"render",
"note.stml",
"--width",
"72",
"--color",
"never",
"--theme",
"midnight",
"--json",
]),
).toEqual({
kind: "markup-render",
file: "note.stml",
width: 72,
color: "never",
theme: "midnight",
json: true,
});
});

test("rejects invalid markup render color modes and unknown markup subcommands", async () => {
await expect(
parseCli(["bun", "hunk", "markup", "render", "-", "--color", "sometimes"]),
).rejects.toThrow("--color must be auto, always, or never.");
await expect(parseCli(["bun", "hunk", "markup", "bogus"])).rejects.toThrow(
"Supported markup subcommands are render and guide.",
);
});

test("parses markup guide", async () => {
expect(await parseCli(["bun", "hunk", "markup", "guide"])).toEqual({ kind: "markup-guide" });
});

test("parses session comment add with --markup", async () => {
const parsed = await parseCli([
"bun",
"hunk",
"session",
"comment",
"add",
"session-1",
"--file",
"README.md",
"--new-line",
"7",
"--summary",
"Rendered note",
"--markup",
"<box border><b>hot path</b></box>",
]);

expect(parsed).toMatchObject({
kind: "session",
action: "comment-add",
selector: { sessionId: "session-1" },
filePath: "README.md",
side: "new",
line: 7,
summary: "Rendered note",
markup: "<box border><b>hot path</b></box>",
});
});

test("parses session comment add with --focus", async () => {
const parsed = await parseCli([
"bun",
Expand Down
Loading