Skip to content

Zen mode - #139

Closed
abosnjakovic wants to merge 1 commit into
AltanS:mainfrom
abosnjakovic:feat/zen-mode
Closed

Zen mode#139
abosnjakovic wants to merge 1 commit into
AltanS:mainfrom
abosnjakovic:feat/zen-mode

Conversation

@abosnjakovic

@abosnjakovic abosnjakovic commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Zen mode

collie-zen-mode.mp4

Adds an opt-in zen mode to the pane view: one tap hides every Collie surface and leaves the
terminal mirror alone on the screen, with a single floating button to come back.

On a phone the chrome is most of the viewport — header, tab strip, pane strip, statusline, the
controls row and the composer. When you're reading a long build log or a wide TUI, all of it is in
the way. Zen gives the mirror the whole screen without changing anything about how it renders.

Demo: settings toggle → one-tap entry → zen → scrollback → exit. Video attached below.

How it works

Off by default, behind a Settings toggle. Settings → Zen mode (per device, localStorage,
mirrors the Haptics pattern in lib/haptics.ts + haptics-control.tsx). While it's off nothing
changes anywhere — no new button, no new row. Zen removes every way back except one floating
control, so it seemed wrong to hand that to people who didn't ask for it.

One entry point, in the pane header. A Maximize2 button joins the header cluster next to Find,
gated on zenAvailable && display. It sits with Find and History because it's the same family —
"look at the output differently" — and because it must be one tap: this is the only control here
you toggle repeatedly in a session, so it can't live behind the ⚙. It's size-8 to match its
siblings rather than the 44px tap-target size, since a 44px button in a row of 32px ones reads as a
mistake. The status pill stays the rightmost item, per the existing note in agent-chat.tsx.

Exit is a floating pill in the same corner, size-11 (44px — it's a standalone control and the
only way out, so it gets the full tap target). Top-right so entering and leaving happen in one
place; opposite corners meant re-aiming on every toggle. It's absolute inside the safe-area-padded
wrapper, so top-3 clears the notch on its own and lands within a few px of the header button's
vertical centre.

Transient by design. The setting persists; the state doesn't. DetailRoute's
key={paneId} remount resets it on a pane switch, and a reload clears it, so a pane always opens
with its chrome. It's local useState, deliberately not a DisplayPrefs field — if stickiness is
ever wanted that's the place for it, but it isn't built here.

What stays mounted in zen, on purpose:

  • StatusArea — renders nothing when idle, and it's the only surface for prompt-tap failures
    ("menu changed", read-only errors). Hiding it would silently eat errors.
  • The whole mirror block, including interactive prompt blocks and the top-of-buffer
    "Load older" / "Show entire history" affordances. Those are content inside the scroller, not
    Collie chrome. Polling continues untouched.
  • The root UpdateAvailableBanner / ConnectionBanner (they live in RootLayout) — they only
    appear in exceptional states, and hiding a connection-lost bar would be dishonest.

Safe areas. The header carried env(safe-area-inset-top) and the composer the bottom inset;
with both unmounted the content wrapper takes them over, so the mirror doesn't run under the notch
or the home indicator.

Focus. enterZen() blurs the active element before the chrome unmounts. "Tap to type" is on by
default, so the composer's <textarea> is often focused at that moment, and on iOS the soft
keyboard belongs to the focused node — unmounting it focused can leave the keyboard up over a
screen that no longer has an input.

Tests

web/src/components/agent-chat.test.tsx, using the existing renderChat harness:

  • no entry offered while the setting is off (the default)
  • enters from the header in a single tap; chrome unmounts; mirror content stays; the pill restores it
  • the entry stays out of the Display dock — the dock keeps its prefs, and exactly one "Enter zen
    mode" button exists in the document (guards against a second entry point creeping back)
  • focus is dropped before the composer unmounts

agent-chat.test.tsx 37/37, composer.test.tsx 82/82, backend bun test 717/0, root bun run build (both typechecks) clean. Verified end-to-end on a real device against live Herdr panes:
gate off/on, one-tap entry, repeated enter/exit cycles, polling continuing in zen, scrollback and
"Load older" still working, reset on pane switch and on reload.

@abosnjakovic
abosnjakovic marked this pull request as ready for review August 29, 2026 09:33
@abosnjakovic abosnjakovic changed the title Zen mode — hide all chrome and view only the terminal mirror Zen mode Aug 29, 2026
…oggle

Hides every Collie surface (header, strips, statusline, composer and its
docks) and leaves only the terminal mirror plus one floating exit pill.
Entry is a row in the pane's View dock, present only while the new
Settings toggle is on — the optional callback's absence IS the gate.

Zen itself is transient: the persisted half is availability only, and
DetailRoute's key={paneId} remount resets the active state per pane.
@AltanS

AltanS commented Aug 29, 2026

Copy link
Copy Markdown
Owner

I like this! Didn't have time yet to check out the details of the implementation, but I'll get back to you soon.

@AltanS

AltanS commented Aug 31, 2026

Copy link
Copy Markdown
Owner

Merged onto v1 as 44f343d, shipping in 1.0.0-beta.48. Thank you, this is a great feature and the design is all yours. Two things moved in the port: entry now lives in the pane's actions menu (the v1 header has no free icon slot), and every surface leaves through our collapse primitive so the exit animates as one motion. Your commit and authorship are preserved. Closing since the work is on v1.

@AltanS AltanS closed this Aug 31, 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.

2 participants