Skip to content

The canvas column holds what a run is waiting on — its question and its result - #86

Open
pierreprudh wants to merge 13 commits into
mainfrom
feat/question-card-island
Open

The canvas column holds what a run is waiting on — its question and its result#86
pierreprudh wants to merge 13 commits into
mainfrom
feat/question-card-island

Conversation

@pierreprudh

@pierreprudh pierreprudh commented Sep 7, 2026

Copy link
Copy Markdown
Owner

Two things a run makes you deal with — a question it is parked on, and the result it finished with — lived at the bottom of the transcript, under whatever turn produced them. Both scroll away. On the Focus canvas there is already a column for things you watch rather than read: the plan island. They join it.

Focus canvas — plan, result, question, one column, sized off the window

The column at three canvas widths, light

The column at three canvas widths, dark

Three canvas widths, top to bottom: 1210px → a 320px column with the full Review result · 1 file ↗; 900px → 304px; 700px → 232px and the entry down to its mark and count. Same glass, same hairline, same radius all the way down; the question names itself the way the plan does; the review entry wears the folded plan mark's shape. The last frame is the workbench placement, where the transcript is the only surface there is and both stay inline.

The corner is measured, not fixed

320px of islands plus a 760px reading column needs 1116px of canvas, which a laptop window does not have — the column used to keep its width while the prose lost its. The islands now take whatever is left once the prose has a readable 560px, clamped to 232–320px, and the inset the conversation gives up follows that measurement. Under 260px the column is a corner rather than a panel, so the result entry drops its words for ReviewIcon + the file count (label moves to the tooltip and the accessible name). The drawer is a side sheet on a roomy window and full-bleed under 560px; under 620px of height its chrome gives padding back and one command's output can no longer own the whole sheet. The column also keeps a height budget and scrolls inside it, so a question stays reachable under a long plan on a short window.

The question card

The card the harness drew for userAnswerQuestion had a bordered answer field inside a bordered card and an accent-filled Submit ⌘↩ pill — two frames and a colour the design system spends nowhere else. It is a composer addressed to one question, so it is drawn as one: question on top, borderless answer box lined up under it, hairline foot carrying Skip and the composer's own round send (grey while empty, accent once there is text). In the island a hairline separates question from answer, because in 320px the placeholder read as one more line of the question.

An empty answer is no longer an answer — ⌘↩ and the send button stay inert until there is text; Skip is how you say nothing. Before, ⌘↩ on an empty box sent "" to the harness as the user's reply.

The result entry

The newest completion worth opening, once, instead of one row per finished turn down the transcript. CompletionCard takes the same inline | island split, and both placements now ask for changes through one handler rather than two copies of the same prompt.

The column itself

TodoStrip's island used to position itself at top:16 right:18; three self-positioning cards would have had to measure each other. AiPanel owns one absolutely-positioned column now — gap: 10, pointer-events: none so the conversation stays clickable around the cards, each card taking its own clicks back, a height budget it scrolls inside (a wheel over any card bubbles to it), and every card holding its own height so a long plan scrolls rather than squashing the question under it.

Open, it heads with one close button. Closed, the way back is the marks themselves, the way the plan has always folded: the plan's reopen pill with its count, and beside it the result's with its file count — icons in the corner of the main view, either of which opens the column. The conversation gives up the marks' 76px rather than the panel's width, because overlapping is the thing the column exists to prevent. A parked question overrides the close: that card holds the run, and a hidden question strands it.

Each level of "put away" has its own scope — the plan's ✕ folds the plan to its mark, the result's ✕ dismisses that result until the next run produces one (held by run id, not a flag), the column's ✕ clears the corner entirely.

Also

  • .ai-qa-card had no rule behind it — a class three review cards wore (question, diff, command) while every message around them rose. It now carries the settle-in, with .klide-ask-island beside it on the todo island's curve, both reduced-motion guarded.
  • AskIcon joins src/icons.tsx (Phosphor ChatTeardropDots, the AiIcon family) instead of the private ?-circle SVG the panel drew.

Checks

  • npx tsc --noEmit clean
  • 907 tests pass, including a new QuestionCard.test.tsx (renders in both placements, names itself only on the canvas, send inert on blank/whitespace)
  • Screenshots are the real components on the real tokens, rendered by the dev server

Not yet dogfooded against a live userAnswerQuestion pause. The result entry's placement is deliberately provisional — it is in the column "for now", and moving it costs one line.

🤖 Generated with Claude Code

pierreprudh and others added 2 commits September 7, 2026 18:01
The card the harness drew for `userAnswerQuestion` boxed a bordered answer
field inside a bordered card and filled Submit with accent — two frames and a
colour the design system spends nowhere else. It is a composer addressed to
one question, so it is drawn as one now: the question on top, a borderless
answer box lined up under it, a hairline foot carrying Skip and the composer's
own round send. An empty answer is no longer sent: ⌘↩ and the button stay
inert until there is text, and Skip is how you say nothing.

In Focus it belongs beside the conversation rather than under it, so
QuestionCard takes the plan's dock/island split. The canvas corner is now a
column AiPanel owns: the plan island and the question stack in it with a gap,
instead of two cards positioning themselves against the canvas and having to
measure each other. TodoStrip's island wrapper became a flow item in that
column and its folded mark right-aligns with alignSelf. A question island
holds the column open on its own, so the reading column slides left for it the
same way it does for a plan.

`.ai-qa-card` was a class three review cards wore with no rule behind it —
every message around them rose while they appeared with no entrance. It now
carries the settle-in, with .klide-ask-island beside it on the todo island's
curve, both reduced-motion guarded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
In Focus the "Review result" entry sat at the far bottom of the transcript,
under the turn that produced it — so the moment anything else streamed in, the
way into the evidence scrolled away, and a long conversation carried one entry
per finished turn. It joins the canvas column instead, directly under the
plan: the newest reviewable completion, one entry, in the corner the reader is
already watching. Everywhere else it stays inline under its turn, where the
transcript is the only surface there is.

CompletionCard takes the same variant split as the plan and the question, and
the island entry wears the folded plan mark's shape — glass, hairline, 30px,
right-aligned — so the column reads as one family down its whole height. Both
placements now ask for changes through one handler rather than two copies of
the same prompt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@pierreprudh pierreprudh changed the title A question the run is parked on waits in the canvas island column The canvas column holds what a run is waiting on — its question and its result Sep 7, 2026
pierreprudh and others added 11 commits September 7, 2026 23:07
320px of islands plus a 760px reading column needs 1116px of canvas, and
Klide's own window on a laptop does not have it: the column kept its width
while the prose lost its. So the corner is measured now. The islands take
whatever is left once the prose has a readable 560px, clamped to 232–320px,
and the plan, the result and the question shrink together — the reading column
never drops below its share, and the inset the conversation gives up follows
the measurement instead of the constant.

Under 260px the column is a corner rather than a panel, so the result entry
drops its words for its mark: ReviewIcon plus the file count, with the label
it dropped moving to the tooltip and the accessible name. The inline entry
does the same in a panel too narrow to spell it out.

Two things that could not fit the window before:

- The drawer is a side sheet on a roomy window and a full-bleed one under
  560px — 360px of content plus two 22px gutters stopped fitting somewhere
  around a 520px viewport, and a sheet whose edges are off-screen reads as
  broken. Under 620px of height the chrome gives its padding back and one
  command's output can no longer own the whole sheet.
- The column keeps a height budget and scrolls inside it. A wheel over any
  card bubbles to that scroller even though the column takes no hits itself,
  so a question stays reachable under a long plan on a short window.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The review entry opened a native <dialog>: the only showModal() in Klide,
and the one overlay not built the way every other one is. On the canvas
that dialog was a child of the island entry, which passes its clicks
through to the column — so the sheet took no clicks at all while its
backdrop blocked everything else, and the app read as frozen.

So the result is a window in the column instead, built like the plan
above it: the same glass and corners, a header that is the whole hit
target, a hairline that wipes in under it, and the evidence opening
inside the card rather than over the app. Its chevron shows on hover or
once open, the way the plan's rows do.

Open, the card takes the rest of the side. It carries no height of its
own — the column is a definite height now and the card grows into what
the plan and a question are not using, with the evidence scrolling and
the two actions on the card's floor. A capped body ended the card
mid-word against a hard edge, which is the "black bar" it read as.

The column moved out of the canvas and onto the panel: the canvas stops
above the composer, so a column parented there cut an open card off at
that seam. `focusGutterRight` already insets the scroller *and* the
composer by the column's width, so the side below the canvas was
reserved for it and standing empty.

The transcript's entry keeps a sheet, but portalled to the body on the
app's Z scale — an AI panel sits inside transformed, clipped ancestors
that a sheet left in the tree inherits. `ResultEvidence` is shared by
both surfaces so they cannot drift, and is exported so its markup is
testable without a DOM to click in.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A right-aligned card in a column of full-width ones read as a different kind
of thing, and it is not: both are windows on the canvas. So the result card
fills the column at either size — compact keeps the width and centres its one
mark rather than shrinking to a button in the middle of the stack.

It closes, too. The plan has always had a way out of the corner; the result
now carries the same mark at the same size, always visible at full width (a
window you can put away should say so before it is hovered) and revealed on
hover when the column is narrow, where the resting state is one mark and
nothing else. Dismissal is held by run id rather than a flag, so the next
run's result comes back on its own — the rule the plan follows when a new plan
replaces a hidden one — without persisting a preference that only matters
while the corner is in view.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closing a card put that card away; nothing put the column away. The corner
kept standing with a folded plan mark in it, and there was no way back to the
plain canvas — which is the view most of the reading happens in.

So the column takes a switch of its own, at its head: the app's pane control
from icons.tsx, mirrored, since this pane is on the right. Hidden, the switch
is the only thing left in the corner — a way back that disappears is a panel
lost for good — and the conversation gets its full width, because the inset
follows what is actually in the column.

One override: a parked question ignores the switch. That card holds the run,
and a hidden question is a run stranded with no way to answer it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The head carried a pane switch that toggled both ways, which meant the open
column wore a control that says "there is a panel here" to a reader already
looking at one. Open, it is one close button and nothing else.

Closed, the way back is the marks themselves, the way the plan has always
folded: the plan's reopen pill with its count, and beside it the result's with
its file count — icons in the corner of the main view, either of which opens
the column. TodoStrip grew a `folded` mode for that, reusing the mark it
already draws when a plan is dismissed, and keeping a dismissed plan dismissed
— closing the column is not a reason to bring one back.

The marks are still something prose can run under, so the conversation gives
up their width (76px) rather than the panel's. Overlapping was the thing the
column existed to prevent; folding it away should not reintroduce it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…the shots

Closing out the day's work on the canvas column. The code here is a peer
session's, committed rather than left in the tree overnight: every card in the
column now holds its own height (a card that clips its corners has an
automatic minimum size of zero, so the column was squashing the plan and the
question to fit instead of scrolling them), and the result card's header was
tidied around the one-mark rule.

The PR's screenshots move to the current state: the open column headed by one
close button, and the main view with the plan's and the result's marks in the
corner.

Typecheck clean, 913 tests green with all of it in the tree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The docked Artifact Inspector opened on every base surface, Focus included,
where its header ended up as a strip of file tabs squeezed between the
composer and the status bar — the same review work the canvas column already
does, in a worse place. Focus does not render it now: the result card in the
right-hand column lists a run's files and opens its evidence in place, and the
canvas keeps one review surface instead of two.

With no inspector to open, Focus passes no `onReviewChanges`, so the routes
into it (the result's per-file arrows, its "Review changes" footer, the
revert row's review link) hide themselves rather than pointing at a panel
that will not appear. The workbench is untouched: no column there, so the
docked inspector remains its review surface.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Staging only my own hunks out of a file another session was editing pulled one
of its lines in with the context: `onOpenArtifact`, whose handler lives in
that session's uncommitted work. The branch built here, where the tree has
both halves, and broke in CI, where only mine had landed — the tell that the
commit was not self-contained.

The line goes back to its owner's tree, untouched; the committed file
typechecks on its own again. Verified by checking the commit out into a
detached tree and running `tsc` against that alone, rather than against a
working copy that quietly supplies the missing half.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three things stood between the harness noticing a deliverable and the
reader seeing one, and the first two only showed up on the live path.

`git status --porcelain` collapses a wholly-untracked directory into one
`?? q3-demo/` entry, so the first real test recorded the folder — 96
bytes, no name of the deck inside it. `-uall` asks for the files, and a
path ending in `/` is dropped as the directory it is.

`turnDriver.handleEvent` is a switch naming the events that reach the
fold; everything else hits `default` and is dropped. `artifact_produced`
was missing from it, so a produced document persisted to disk, replayed
after a reload, and was invisible while the run was happening — which is
when it matters. The live-and-replay test now carries one through the
driver, so the next omission fails the build.

The card grows a Documents section, in the shared evidence so the island
and the sheet cannot drift. Each row names the file, its folder and its
size — the one fact that tells a real deck from an empty one a script
failed to fill — and opens it: text in the inspector, everything else in
the application the machine already owns it with, through a new
`open_entry`.

And a picture, because a list of filenames is not a preview. macOS
already draws decks, spreadsheets and PDFs for Quick Look, so
`preview_file` asks `qlmanage` for a thumbnail (~200ms, returned as a
data URI) rather than Klide growing a converter per format. Images skip
it and come straight off disk. No preview is a normal answer: the row
keeps its name and its size.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant