Links a transcript wrote open from the chat page, and read as links #87

Merged
mph merged 10 commits from cc-chat into main 2026-10-13 09:31:33 +00:00
Member

Goal (the use). I ask Gladys for a link to something she named and reach it in one click from
the chat page. No copying an id, no retyping a URL.

Fit test. A real conversation where every URL opens, a link reads as a link before it is
clicked, the conversation is still there when I come back, and nothing quoted from somewhere else
became a link to somewhere I did not expect.

Fit passed on this repository's half, on Michal's read of a real session: "Links work in both
cases correctly" — his own message and the agent's reply. The card settles both halves, and
Michal moved the other half — the instructions that make Gladys write a link instead of an id —
into its own session on 2026-10-13. The card is unchanged; that decision is recorded in the
tracker's fit log, and the work rides the effort map as gladys-writes-links.

What ships

  • A URL, a www. host and an email address typed out in prose become links. CommonMark core does
    not autolink, and only the tables extension was loaded; AutolinkExtension now runs beside it.
    No id pattern is recognised anywhere — CC-D4 stands.
  • A link to a page opens beside the conversation, with rel="noopener noreferrer", so the
    composer and the follow survive. A mailto: link opens the mail program and gets no tab.
  • A link reads as a link. cc-body a draws the underline and keeps the colour of the words around
    it, so no text changes contrast. A host reset takes the browser's underline away: measured on
    the procedures dashboard, where the same anchor computes underline with this stylesheet and
    none without it.
  • A destination the sanitizer refuses is shown as its words. It was drawn as <a href="">, which
    reads as a link and opens the page it sits on. This also covers a written
    [x](javascript:...), and typed ftp://, file:/// and javascript://.
  • An image described with a URL in it renders one reference. The URL in the description was linked
    where it stood, and the reference took that link as a child, so the page carried an anchor
    inside an anchor — found by the security review, reproduced by a red test, fixed by
    appendWithoutNesting.

ui.test: 303 tests, exit 0, no warnings. __.checkFormat clean.

Review

Five skills — style, testing, security, scala3, composition. No critical findings. Report:
project-management/efforts/cc-chat/slices/08-links-that-open/review-slice-2026-10-13.md.

The security review verified the invariants against the real jars rather than assuming: no
destination can run code (explicit or autolinked), the allow-list holds, the autolink extension
emits only plain Link nodes so nothing evades the renderer, the image path cannot smuggle a
scheme past it, and the attribute provider only ever sees an already-sanitised href. It also
recorded two things deliberately: a destination that writes no scheme (/matters/x, //host/x)
is not scheme-checked and is left as written — now said plainly in the doc comment and pinned by
a test; and autolinking makes previously inert untrusted text one-click navigable, which is this
slice's deliberate trade, with tabnabbing and referrer leakage closed by rel.

Declined, with reasons

  • Collapse the two node-renderer factory objects to SAM lambdas (composition, scala3). It
    would also rewrite ImageAsReference, which this slice has no reason to touch, to save two
    lines.
  • Automated cover for the one stylesheet rule (testing). This module has no CSS or visual
    test, and building that for one rule is disproportionate. It was measured by hand in a browser
    instead, by reading the computed style with the stylesheet enabled and disabled.

Flushed to the effort map

  • gladys-writes-links as the next candidate slice, carrying what this slice measured: which
    dashboard addresses are real pages (/matters/<id> and /email-refs/<id> are; /inbox/<id> is
    not), that a root-relative link needs no origin, and that the dashboard chat and Telegram share
    one composed prompt.
  • Parked: Mattermost and other absent integrations; a link preview or fetched title; linkifying
    tool output; ContextItem.Viewing(url) reflow; a www. host autolinking to http://.

One thing to know before this helps Gladys

Live Gladys runs 0.6.1, which has none of this: her markdown links look like text and open in
place. A release carries it to her. 0.7.0-SNAPSHOT exists only in ~/.ivy2/local, and the
worktree /home/mph/ops/procedures-cc-chat-links (branch cc-chat-links) already takes it.

Lessons

One entry, triaged with Michal and promoted: a fit read on ui.demo proves markup and behaviour,
never appearance inside a consumer's page → CLAUDE.md.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XTrxcxBQiNjXh79dN8Sh5x

## Slice 08-links-that-open (effort cc-chat) **Goal (the use).** I ask Gladys for a link to something she named and reach it in one click from the chat page. No copying an id, no retyping a URL. **Fit test.** A real conversation where every URL opens, a link reads as a link before it is clicked, the conversation is still there when I come back, and nothing quoted from somewhere else became a link to somewhere I did not expect. **Fit passed on this repository's half**, on Michal's read of a real session: "Links work in both cases correctly" — his own message and the agent's reply. The card settles *both halves*, and Michal moved the other half — the instructions that make Gladys write a link instead of an id — into its own session on 2026-10-13. The card is unchanged; that decision is recorded in the tracker's fit log, and the work rides the effort map as `gladys-writes-links`. ## What ships - A URL, a `www.` host and an email address typed out in prose become links. CommonMark core does not autolink, and only the tables extension was loaded; `AutolinkExtension` now runs beside it. No id pattern is recognised anywhere — CC-D4 stands. - A link to a page opens beside the conversation, with `rel="noopener noreferrer"`, so the composer and the follow survive. A `mailto:` link opens the mail program and gets no tab. - A link reads as a link. `cc-body a` draws the underline and keeps the colour of the words around it, so no text changes contrast. A host reset takes the browser's underline away: measured on the `procedures` dashboard, where the same anchor computes `underline` with this stylesheet and `none` without it. - A destination the sanitizer refuses is shown as its words. It was drawn as `<a href="">`, which reads as a link and opens the page it sits on. This also covers a written `[x](javascript:...)`, and typed `ftp://`, `file:///` and `javascript://`. - An image described with a URL in it renders one reference. The URL in the description was linked where it stood, and the reference took that link as a child, so the page carried an anchor inside an anchor — found by the security review, reproduced by a red test, fixed by `appendWithoutNesting`. `ui.test`: 303 tests, exit 0, no warnings. `__.checkFormat` clean. ## Review Five skills — style, testing, security, scala3, composition. **No critical findings.** Report: `project-management/efforts/cc-chat/slices/08-links-that-open/review-slice-2026-10-13.md`. The security review verified the invariants against the real jars rather than assuming: no destination can run code (explicit or autolinked), the allow-list holds, the autolink extension emits only plain `Link` nodes so nothing evades the renderer, the image path cannot smuggle a scheme past it, and the attribute provider only ever sees an already-sanitised `href`. It also recorded two things deliberately: a destination that writes no scheme (`/matters/x`, `//host/x`) is not scheme-checked and is left as written — now said plainly in the doc comment and pinned by a test; and autolinking makes previously inert untrusted text one-click navigable, which is this slice's deliberate trade, with tabnabbing and referrer leakage closed by `rel`. ### Declined, with reasons - [ ] Collapse the two node-renderer factory objects to SAM lambdas (composition, scala3). It would also rewrite `ImageAsReference`, which this slice has no reason to touch, to save two lines. - [ ] Automated cover for the one stylesheet rule (testing). This module has no CSS or visual test, and building that for one rule is disproportionate. It was measured by hand in a browser instead, by reading the computed style with the stylesheet enabled and disabled. ## Flushed to the effort map - `gladys-writes-links` as the next candidate slice, carrying what this slice measured: which dashboard addresses are real pages (`/matters/<id>` and `/email-refs/<id>` are; `/inbox/<id>` is not), that a root-relative link needs no origin, and that the dashboard chat and Telegram share one composed prompt. - Parked: Mattermost and other absent integrations; a link preview or fetched title; linkifying tool output; `ContextItem.Viewing(url)` reflow; a `www.` host autolinking to `http://`. ## One thing to know before this helps Gladys Live Gladys runs **0.6.1**, which has none of this: her markdown links look like text and open in place. A release carries it to her. `0.7.0-SNAPSHOT` exists only in `~/.ivy2/local`, and the worktree `/home/mph/ops/procedures-cc-chat-links` (branch `cc-chat-links`) already takes it. ## Lessons One entry, triaged with Michal and promoted: a fit read on `ui.demo` proves markup and behaviour, never appearance inside a consumer's page → `CLAUDE.md`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01XTrxcxBQiNjXh79dN8Sh5x
Michal's own thread, not from the candidate pool: he asks Gladys for a link and
gets an id he must look up, or a URL that is not a link.

Measured at definition against commonmark 0.30.0 — a bare URL, a www. host and
an email address all stay plain text, because CommonMark core does not autolink
and only the tables extension is loaded; cc-chat.css:241 is the sole anchor rule
in 793 lines; and an id is not a URL. Both halves are in scope, and Mattermost
is out — it appears in no file of procedures.

Also corrects a false claim the close of 07 left in two places: the Stop fix
shipped as 0.6.1, not 0.7.0. The diff v0.6.0..main held that slice alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XTrxcxBQiNjXh79dN8Sh5x
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XTrxcxBQiNjXh79dN8Sh5x
A URL, a www. host or an email address typed out in prose was left as
text, because CommonMark has no autolinking and only the tables
extension was loaded. The autolink extension now runs beside it.

A destination the sanitizer refuses was still drawn as an anchor with
an empty href: it read as a link and opened the page it sat on. It is
now shown as its words. A link to a page opens in a new tab with
rel="noopener noreferrer"; a mailto link opens the mail program and
gets no tab.

A host reset such as Tailwind's preflight removes the browser's
underline, and our rule kept the colour of the text around it, so a
link had no visible sign. The rule now draws the underline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XTrxcxBQiNjXh79dN8Sh5x
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XTrxcxBQiNjXh79dN8Sh5x
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XTrxcxBQiNjXh79dN8Sh5x
A consumer writes its own pages as root-relative destinations, which is
how an agent names a page without being told the origin it is served
on. The behaviour already held; the test holds it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XTrxcxBQiNjXh79dN8Sh5x
A URL in what an image was called is linked where it stands, and the
reference took that link as a child, so the page carried an anchor
inside an anchor. A browser does not draw that as written: it closes
the outer one where the inner opens, and the words after it stop being
part of the reference at all.

A link among an image's children is now flattened onto the reference.
Its words are kept, and the destination the reference names is the
image's own.

Also from the review: a test that named a scheme the autolink scanner
never sees, a sanitizer comment that claimed more than the sanitizer
checks, and two discards of the same Java call written differently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XTrxcxBQiNjXh79dN8Sh5x
Promoted:
- the demo host cannot prove how a transcript looks inside a host page → CLAUDE.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XTrxcxBQiNjXh79dN8Sh5x
workflow(sl): close cc-chat/08-links-that-open (flush parked items)
All checks were successful
CI / Check Formatting (pull_request) Successful in 59s
CI / Compile (pull_request) Successful in 1m35s
CI / Check Linting (pull_request) Successful in 1m10s
CI / Run Tests (pull_request) Successful in 1m31s
CI / Run Integration Tests (pull_request) Successful in 1m35s
CI / Check Bundle Size (pull_request) Successful in 43s
408f1b5c31
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XTrxcxBQiNjXh79dN8Sh5x
chore(cc-chat): move review-state card out of the tree
All checks were successful
CI / Check Formatting (pull_request) Successful in 42s
CI / Compile (pull_request) Successful in 1m5s
CI / Check Linting (pull_request) Successful in 1m6s
CI / Run Tests (pull_request) Successful in 1m31s
CI / Check Bundle Size (pull_request) Successful in 43s
CI / Run Integration Tests (pull_request) Successful in 1m34s
d42f730ddb
mph merged commit 5672a1308e into main 2026-10-13 09:31:33 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
iterative-works/claude-code-query!87
No description provided.