Skip to content

Breadcrumb Copy And Log Diagnostics - #14

Merged
tranphucbol merged 1 commit into
mainfrom
feature/log-diagnostics
Sep 12, 2026
Merged

tranphucbol merged 1 commit into
mainfrom
feature/log-diagnostics

Conversation

@tranphucbol

@tranphucbol tranphucbol commented Sep 12, 2026 •

Copy link
Copy Markdown
Owner

1. Purpose & Motivation

Description

Two things the desktop app could not do. A request's location was displayed above the editor but
could not be taken anywhere — quoting it in a ticket or a chat meant retyping three names. And the
log the app writes could only be read by leaving the app: open the folder, open a 2MB file in
whatever the OS thinks owns .log, scroll to the end, and hope the editor reloaded. That loop
answers "what happened before it broke" adequately and "what happens when I do this" not at all.

This PR makes the breadcrumb one click that copies Collection > Folder > Request, and puts the
log in the Diagnostics tab: a switch that shows the last 500 lines and then keeps up with the
writer, searchable, resizable, and selectable.

Context & Flow

A log line, before and after

note(level, line)  [main/main.ts — every log line in the app passes here]
      │
      ├── [OLD] diagnostics.write() → stderr + preman.log        ► the only readers are a file manager and an editor
      │
      └── [NEW] diagnostics.write() first, unchanged
                      │
                      └── logStream.push(level, line)
                                │
                                ├── not watching → dropped, no buffer held
                                └── watching → batched on a 100ms timer
                                                    │
                                                    └── send({ replace: false, lines }) ► renderer store ► appended

Switching the stream on

Stream pressed → watchLog(true) → logStream.watch(true)
                                        │
                                        ├── diagnostics.readTail(500)  [synchronous, same turn]
                                        │        └── parseLog() ► the stamp read back as { at, level, text }
                                        ├── watching = true
                                        └── send({ replace: true, lines }) ► store replaces ► box shows the tail

The tail read and the start of forwarding happen in one synchronous turn on main's single thread,
so no line can be written between them and be missed by both.

2. Key Changes

Description

The breadcrumb copies. The whole row is one button. breadcrumbPath() joins the ancestors and
the request name with >; a transient check and "Copied" confirm it. The crumbs still do not
navigate — one target, not one per crumb.

Main can tee its log to the window. main/logstream.ts is a new module that batches lines on a
100ms timer and forwards them over a new logLines channel, gated by watchLog. It holds nothing
while it is off. main/diagnostics.ts gained readTail(limit) and parseLog(), which reads back
the format it writes: a line with no timestamp stamp is treated as a continuation of the record
above it, so a stack trace survives the round trip as one record.

The Diagnostics tab renders it. A 500-line ring buffer in a renderer store, subscribed at the
top of App.tsx rather than in the pane, so the stream survives leaving the sub-tab and closing
Settings — which is the point, since everything worth watching a log for happens somewhere other
than the Settings pane. Search highlights in place with next/previous and a match counter rather
than filtering. The box is resizable by drag or arrow keys, its height remembered in the store, and
its text is selectable.

The section's controls became glyphs. Stream, Reveal, Clear, the search and Check now are
IconButtons. The consequential update actions — Skip, Download, Restart and install — keep their
words.

Technical Decisions

  • A tee, not a tail. The writer holds the level and the text as values. Tailing the file would
    mean watching it, reassembling a partial last line, and noticing the rename at LOG_MAX_BYTES —
    three ways to display something other than what was written, in exchange for nothing.
  • The tail replaces rather than appends, so switching on means one thing whether it is the first
    press or the fourth, with no re-read block in the middle of a list that reads as continuous.
  • Only the current preman.log. Padding a short tail from preman.log.1 would put a gap of
    unknown size in a list with no seam drawn in it.
  • Highlight, not filter. The question this pane gets is "what happened around X", and a filter
    deletes exactly the neighbouring lines that answer it.
  • This amends ADR 035, which said the pane "never renders a line from the file". The clause that
    made the amendment safe is 035's own: what may be written is still fixed and still excludes URLs,
    headers, bodies and variable values, so a window that draws the file decides nothing about
    redaction that the writer had not already decided. Recorded in docs/decisions/056.

Critical Modules/Files

  • packages/desktop/src/main/logstream.ts — the synchronous seam between the tail read and the
    start of live forwarding. An async read here would drop exactly one line under load.
  • packages/desktop/src/main/diagnostics.ts — parseLog() makes the log stamp a contract with
    itself. Continuation handling is what keeps stack traces intact.
  • packages/desktop/src/renderer/panes/SettingsPane.tsx — the largest diff. LogStream holds the
    search state, the pin-to-bottom ref and the resize handle. Its Escape handler must call
    preventDefault(), or the pane's own window-level Escape closes Settings behind it.
  • packages/desktop/src/preload/bridge.ts — the new LogBatch contract and two channels.

Recommended Review Order

  1. packages/desktop/src/preload/bridge.ts — the contract everything else is written against
  2. packages/desktop/src/main/diagnostics.ts and main/logstream.ts — the tail and the tee
  3. packages/desktop/src/main/main.ts — where the tee is installed in note()
  4. packages/desktop/src/renderer/model/log.ts and stores/log.ts — the pure logic and the store
  5. packages/desktop/src/renderer/panes/SettingsPane.tsx and panes/DocumentChrome.tsx — the views
  6. docs/decisions/056-the-log-can-be-watched-while-it-is-written.md — the argument, and what it
    amends
  7. The four test files

3. Verification & Testing

Provide reproducible inputs or steps for each test case, followed by the result and supporting evidence.

  • Clicking the breadcrumb row on a nested request copies Collection > Folder > Request and shows a transient "Copied"

    Opened payment / nested / Deep Echo in a built app and clicked the breadcrumb row.
    navigator.clipboard.readText() returned exactly payment > nested > Deep Echo, and the row
    showed a check with "Copied" at its end.

    01-breadcrumb-copy.png

  • Enabling the stream fills the box with the tail of preman.log, then appends new lines live

    Against a full log the switch produced exactly 500 rows, the cap. The screenshot below is against
    a deliberately short log — 29 rows, every line in the file — so the evidence carries no local
    paths beyond this repo's own fixtures. New lines appended live as workspaces were opened.

    02-log-stream-tail.png

  • The stream outlives the pane, and a second enable replaces the view rather than duplicating it

    Went Diagnostics → Protos → Diagnostics with the stream on: the button still read "Stop" and the
    lines were still arriving. Stopping and starting again re-read the tail and replaced the contents
    rather than appending a second copy.

  • Search highlights every match, reports n/total, steps and wraps; Escape closes only the search

    Typing engine host started banded every occurrence and reported 1/69 against the full log.
    Next moved to 2/69; two Previous presses wrapped to 68/69. Escape closed the search and left
    Settings open — a bug found by eye during this work, fixed by claiming the event, and now covered
    by a regression test.

    03-log-search.png

  • The log box resizes by drag and by arrow keys, clamps at 96px, and its text is selectable

    Dragging the handle took the box from 256px to 556px; dragging hard upward stopped at exactly
    96px, the floor. Eight ArrowDown presses on the handle moved it 96 → 288, exactly 8 × 24px. The
    box computes user-select: text and a double-click selects a word.

    04-log-resized-selectable.png

4. Impact and Scope

Desktop app only. @preman/core and the CLI are untouched; no engine, workspace-file or
request-execution behaviour changes.

IPC surface. Two new channels, logLines (main → renderer push) and watchLog (renderer →
main). Additive; no existing channel changes shape.

Performance. No new periodic work — the clock driving the stream is whatever the app was already
doing. While off, the cost is one branch per logged line and no retained buffer. While on, a
batching timer between a line and its delivery, plus one bounded 2MB file read per press. The idle
cost that ADR 040 protected is untouched.

Security and privacy. The log still contains no URLs, headers, bodies or variable values — ADR
035's invariant is unchanged and is the reason rendering it is safe. It does name workspace paths,
so anyone screenshotting this pane for a bug report is showing their home directory; that is stated
in ADR 056's consequences.

Accessibility. The controls became icon-only but keep accessible names through IconButton's
required label. The log box is a role="log" region and the resize handle a focusable
role="separator" with aria-valuenow, so the height is reachable without a pointer. Match
highlighting uses two new tokens derived from the accent at the same 14%/30% mixes the editor
already uses, chosen because --color-selected reads at about 1.2:1 and WCAG 1.4.11 asks 3:1.

Tests. 2062 passing. New suites for the tail parser, the stream, the breadcrumb path and the
search; typecheck, lint and format:check clean.

@tranphucbol
tranphucbol merged commit 3358853 into main Sep 12, 2026
3 checks passed
@tranphucbol
tranphucbol deleted the feature/log-diagnostics branch September 12, 2026 16:35
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