Skip to content

API log: stop the hover flicker, make a selection stick, read prompts as markdown - #106

Merged
lvwerra merged 1 commit into
mainfrom
fix/api-log-pin-and-markdown
Aug 21, 2026
Merged

API log: stop the hover flicker, make a selection stick, read prompts as markdown#106
lvwerra merged 1 commit into
mainfrom
fix/api-log-pin-and-markdown

Conversation

@lvwerra

@lvwerra lvwerra commented Aug 21, 2026

Copy link
Copy Markdown
Member

Three items from the operator after using the API log on am-dev-2. #98 is merged; this is the follow-up.

Screens: https://lvwerra-agent-artifacts.static.hf.space/api-log-pinning.html

1. The flicker loop

The operator's diagnosis was complete and correct: card appears → page grows → scrollbar appears → layout narrows → plot shifts left → cursor is no longer over the mark → card disappears → scrollbar goes → shifts back.

Cut at both ends, deliberately:

  • scrollbar-gutter: stable on .settings-main, so a scrollbar arriving cannot change the layout width. Chosen over always-on overflow-y: scroll (a permanently visible track that is usually pointless) and over reserving the card's height (impossible in general — the card is as tall as the body it is showing). Where the property is unsupported, Safari before 18.2, that platform uses overlay scrollbars, which take no layout space and cannot start the loop.
  • The card no longer empties when the pointer leaves a mark. Even with no gutter the loop cannot close if the card never disappears — and it means reading an entry stops requiring that you keep the cursor pinned to a 1px arrow. This is also half of item 2.

The verification limit, stated plainly

This container's Chromium has no layout-affecting scrollbars at all. I measured before trusting anything: a plain overflow-y: scroll box reserves 0px, with scrollbar-width: thin, with ::-webkit-scrollbar { width: 8px }, and with --disable-features=OverlayScrollbars / OverlayScrollbar. So the operator's symptom cannot be reproduced in it, and a green here would have meant nothing — which is the trap you flagged.

What is verified:

the mechanism is present and honoured getComputedStyle('.settings-main').scrollbarGutter contains stable — asserted in the browser test
the consequence, under an emulated 15px classic scrollbar plot's left edge: 64.50px card-hidden → 64.50px card-shown → moved 0.00px
the loop is broken regardless of platform the card keeps its entry when the pointer leaves — asserted

The pixel claim still wants one look on a machine with classic scrollbars (Windows/Linux Chrome, or macOS with a mouse attached). Everything else stands on its own.

2. Selection was broken, not undiscoverable

It could not work, and the earlier test could not have caught it. Brushing calls setPointerCapture on the SVG; a captured pointer retargets the trailing click to the capture element, so a mark's own onClick never fired. The review round's test dispatched a synthetic click straight at the mark — the one path that did work.

  • Selection is now committed from the SVG's pointerup, reading what was pressed from the event's target (data-op) rather than from mouseenter bookkeeping that capture and re-renders both invalidate.
  • Capture is taken when the drag starts, not on press. Capturing on press sends the mark a mouseleave — so the press was throwing away the very thing it was selecting. This was the second half of the same bug.
  • Hover never overrides a pinned entry; the pinned mark wears a ring so the plot and the card agree on which entry you are reading; Escape and the let go. The old swallow-the-next-click flag is gone along with the click handler that needed it.

Every test here now drives a real mouse press, because the synthetic path is exactly what hid the bug.

3. Prompts as markdown, with a raw view

Rendered by default with Rendered / Source beside it — the file viewer's own control, wording and renderer (renderMarkdown, sanitized, same call FilesPane makes). No editing.

On #102 and zoom: no, and here is why. #102 gave the file pane's rendered markdown the pane's text-size control. Settings has no such control — there is nothing to follow. The card keeps the file viewer's 14px reading base so the two look the same at 100%, and if a zoom ever arrives in Settings this is the one line that would hook into it. Say the word if you would rather it borrowed the terminal zoom instead.

Verified

  • web npm test — 17 suites including 40 checks in apiLog.test.mjs; server npm test — 24 suites; typecheck and production build. Green on top of main (3ce91a9).
  • New coverage: the gutter, the plot not moving, the card surviving a pointer leave and a page scroll, hover not stealing a pinned entry, the ring, Escape, the ✕, a click still selecting after an unrelated drag, markdown rendering (headings/list/code/emphasis), the Rendered/Source wording, and Source showing the text byte-for-byte.

What could regress

  • The card never empties now. After hovering anything, the last entry stays on screen until you pick another or unpin. That is deliberate — it is what breaks the flicker loop — but it is a visible behaviour change from Settings → API log: the calls agents make, as a list and as lanes #98.
  • scrollbar-gutter reserves a strip on platforms with classic scrollbars even when the settings page fits. That is the trade for a layout that never jumps.
  • Selection now depends on data-op being on the mark groups. A future mark type without that attribute would be unselectable rather than throwing, so it would fail quietly — the pin tests would catch it for the existing three kinds.

… as markdown

Three items from the operator, on the view merged in #98.

**1. The flicker loop, diagnosed by the operator and fixed at both ends.** Card
appears → page grows → scrollbar appears → layout narrows → plot shifts left →
cursor is no longer on the mark → card goes → scrollbar goes → shifts back.

- `scrollbar-gutter: stable` on `.settings-main`, so a scrollbar arriving cannot
  change the layout width. Chosen over always-on `overflow-y: scroll` (which
  shows a track that is usually pointless) and over reserving the card's height
  (which cannot work: the card is as tall as the body it shows). Where the
  property is unsupported — Safari before 18.2 — that platform uses overlay
  scrollbars, which take no layout space and cannot start the loop.
- and the card no longer empties when the pointer leaves a mark. Even without a
  gutter the loop cannot close if the card never disappears, and it means reading
  an entry no longer requires keeping the cursor on a 1px arrow.

**2. Selection was broken, not undiscoverable.** Brushing calls
`setPointerCapture` on the SVG, and a captured pointer retargets the trailing
click to the capture element — so the mark's own `onClick` never fired and
clicking an entry did nothing. The review round's test dispatched a synthetic
click straight at the mark, which is exactly the one path that did work.

Selection is now committed from the SVG's `pointerup`, reading what was pressed
from the event's target (`data-op`) rather than from mouseenter bookkeeping that
capture and re-renders both invalidate. Around it: capture is taken when the drag
starts rather than on press (capturing on press sent the mark a `mouseleave`, so
the press threw away what it was selecting); hover never overrides a pinned
entry; the pinned mark wears a ring so the picture and the card agree; Escape and
the ✕ let go. The old swallow-the-next-click flag is gone with the click handler.

**3. Prompts render as markdown, with `Rendered`/`Source` beside them** — the
file viewer's own control, wording and renderer (`renderMarkdown`, sanitized),
and no editing. It keeps the file viewer's 14px reading base but does NOT follow
a zoom: #102 gave the file PANE's markdown the pane's text-size control, and
Settings has no such control to follow. If one ever arrives, this is where it
would hook in.

Tests: every one of the above, and they use a real mouse press rather than a
synthetic click, since the synthetic path is what hid the bug. The clip note and
the JSON block keep their coverage against a 400 KB body.

One honest limit on the verification, in the PR body: this container's Chromium
has no layout-affecting scrollbars at all (`overflow-y: scroll` reserves 0px in
every configuration tried), so the pixel-level flicker cannot be reproduced here.
The gutter is asserted through `getComputedStyle`, the loop is broken
independently by the card change, and the pixel claim wants one look on the
operator's own machine.

@lvwerra lvwerra left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: good to merge — no findings.

Correctness — no finding.

I found a way around the stated browser limitation and checked the layout mechanism with a real classic scrollbar under headed Chromium/Xvfb. The settings scroller reserved a 10 px gutter before overflow. Showing the 400 KB entry grew scrollHeight from 1400 to 4731 px, while the plot stayed at left 116 px / width 958 px. Removing only scrollbar-gutter: stable made clientWidth fall from 1200 to 1190 px when the card appeared and shifted the plot from left 121 to 116 px. Thus the gutter fix independently prevents the layout movement; the sticky card is not masking an unstable layout. The second cut also holds: leaving a mark no longer clears the card.

Selection is genuinely pinned. A real pointer press pins it, the card and mark both show the selection, pointer movement and scrolling do not clear it, and hovering another mark does not replace it. Both the visible close button and Escape release it.

Prompts initially use the same Rendered / Source vocabulary as the file viewer and are read-only. I added probe fixtures beyond the committed test: ordinary non-Markdown text rendered as a single paragraph, and a fenced JavaScript prompt produced one intact pre > code block; Source returned the exact original fence and text. Rendering goes through the existing sanitized Markdown helper. The #102 zoom decision is explicitly accounted for: this Settings view has no shared zoom control to follow, unlike the file pane.

I also mutation-checked what apiLog.test.mjs pins. Removing the stable gutter fails its computed-style assertion; restoring clear-on-leave fails two card-retention checks; replacing pin with preview fails three selection/ring checks; and defaulting to Source fails four Markdown/default-view checks. The committed gutter check pins the mechanism rather than measuring plot coordinates; the headed measurement above supplies the platform/layout proof that the normal container cannot.

Does it solve the operator's problems? — no finding.

The flicker-causing width change is eliminated, selection remains readable while moving and scrolling, and the prompt presentation works as Rendered-by-default with an exact Source escape hatch. At 390 px the card stayed inside the pane (14–366 px) and the Rendered / Source control remained fully visible. I also checked the supplied artifact and the full web typecheck/test suite; both are clean.

@lvwerra
lvwerra merged commit fa51f46 into main Aug 21, 2026
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