Kaede AkinoandClaude Opus 5 cdea8d5448 Draw the blocks Markdown was already parsed into
Replies rendered their newlines away and their tables into mush. The
parser was never the problem. `AttributedString(markdown:)` is cmark-gfm
underneath and records everything — probed against a three-column table,
it returns the four characters `ab12` with every cell carrying
`[tableCell 0, tableHeaderRow, table [.left, .center, .right]]`. Column
count, alignments, header row and cell indices are all there. SwiftUI's
`Text` reads inline attributes only and ignores `presentationIntent`
entirely, so the structure was parsed and then discarded at draw time.
`a\n\nb` has the same shape: two paragraph intents, and a character
buffer that literally reads `ab`, because emitting a newline is the
renderer's job and there was no renderer.

So no new dependency. `apple/swift-markdown` would have bought a ready
tree and task-list checkboxes, at the price of a second Markdown parser
and a C target in a build that already carries three vendored ones — and
the actual labour, the SwiftUI block views, is identical either way.
What it costs instead is one pass that rebuilds nesting from the flat run
list: walk the runs, keep a stack of open containers, compare the
`identity` ints Foundation already hands out. `components` is ordered
innermost-first, which the grouper relies on and a test now pins, because
that is an observed fact about the framework rather than a contract.

Soft breaks are the one place we leave the spec. CommonMark renders a
lone newline as a space and Foundation obeys, emitting an untagged `" "`
run that is indistinguishable from a typed space — so the choice cannot
be made after parsing. It is made on the source, by appending a
backslash hard break the way GitHub's comment renderer does, because a
model that wrote a newline meant one. Every guard in `MarkdownSoftBreaks`
marks somewhere that edit would corrupt rather than break: fenced and
indented code, table rows, headings, thematic breaks, setext underlines,
list markers. One of them is not cosmetic. `composed(_:)` folds
attachments into the message as `<output label="…">…</output>` and
promises the bubble shows exactly what was uploaded; Foundation returns
that untouched as one HTML block, down to the `*` that emphasis would
otherwise eat, so editing a byte inside one would break that promise.

Two hazards found while wiring the shell fence card. `SSHSessionModel
.insert` writes raw bytes with no bracketed-paste wrapper, so an embedded
newline reaches the PTY as Return — "Insert" on a three-line script would
execute the first two while presenting itself as a preview. Multi-line
fences therefore get the risk badge and a copy button and nothing that
touches a session. And `selectedTab` is nil whenever the pinned Assistant
tab is showing, which is exactly where the full-width transcript lives:
`CommandCardView`'s Insert and Run have been dead there all along. The
new card disables them instead of looking live and doing nothing.

Rendering model output is a trust surface, and tool results are fed back
into it. Only `http`, `https` and `mailto` become clickable; a refused
scheme keeps its label as prose and names its destination on hover, which
is what stops `[click here](file:///…)` from reading as ordinary text.
Raw HTML is never interpreted. Images needed nothing — Foundation drops
the URL and keeps the alt text, closing the channel where a model-chosen
image address discloses the reader's IP and reading time the moment a
message renders.

The transcript is rebuilt around it. User turns are trailing bubbles that
size to their contents, assistant replies have no container at all, and
the rules between turns are gone because alignment and fill already say
who is speaking. Tool calls collapse to one line that opens itself while
the call is in flight or after it failed. Attachments fold to a labelled
row, unaltered and one click from open, rather than printing two hundred
lines of log into a right-aligned bubble. `ConversationView` and its
cards move out of `ContextPanelView`, where they had nothing to do with
the context panel.

Two fixes fall out of measuring the result. `TextEditor` has no
content-based ideal height — given `minHeight: 46, maxHeight: 120` it
takes 120, so an empty composer stood 185pt tall with the placeholder
marooned at the top; its height now comes from a hidden twin of the draft
and starts at 40. And the table rendered blank: a horizontal `ScrollView`
proposes unbounded width, the header `Divider` is greedy and took it, and
every centred or trailing column was aligned to an edge no one can reach.
Cells wrap instead, which also reads better at 260pt than a scrolling
region nested inside a scrolling one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 08:35:27 +08:00
2026-08-24 18:20:20 +08:00
2026-08-24 18:20:20 +08:00
2026-08-24 18:20:20 +08:00
S
Description
AI-driven remote workspace for Mac, iPhone, and iPad.
https://termind.akinokaede.com
GPL-3.0
23 MiB
0 Stars 1 Watchers 0 Forks
Languages
Swift 98.9%
Python 0.8%
Ruby 0.3%