tasty.gallery
Overlays · Windows
← Design
Specs
Theme

Windows

Browse the larger windows used for settings, launchers, and data tables. Compare their navigation and dimensions to choose a layout for your content.

This page uses a saved copy of the design system, last changed on . It may differ from the current design source.

Command palette

Command palette — ⌘K

Top-anchored launcher. A search Input header, a scrollable MenuItem list with the first row pre-highlighted, and a monospace hint footer. The fastest path to any command — every action should be reachable here.

Layout spec
width
480–540px
anchor
top, ~40–90px
header
Input + --tasty-separator
rows
28px MenuItem · first active
footer
mono hints ↑↓ ↵ esc
Tokens used
surface-raised— framesurface-active— highlightfont-mono— hints

Search field ground — normal · focused (2026-10-27)

The palette shell is --tasty-surface-raised, so its search field is an Input ground="raised" (the 10-14 rule, no exception). The box steps down to bg-panel with a border-strong edge, so it reads against the shell before it takes focus. The field opens focused (autoFocus); normal is the box after focus moves away. Width, icon, placeholder, list and footer are unchanged; no copy changes.

Layout spec
field ground
ground="raised" · no exception
normal
input-raised-bg → bg-panel · input-raised-border → border-strong
focused
bg unchanged · border-focus edge + 1px border-focus ring
Mocha
bg-panel = base · border-strong = surface1 · shell surface-raised = surface0
Latte
bg-panel = base · border-strong = surface1 · shell surface-raised = surface0
ink
text-primary · placeholder text-placeholder · icon text-muted (unchanged)
Tokens used
input-raised-bg— field fillinput-raised-border— field edgeborder-focus— focused edge + ringsurface-raised— shell

Listening ports

Listening ports — the Table popup

Tools › Listening ports. A 660×520 modal built directly on the Table component — the canonical example of a data-table surface. Header carries a live filter + refresh; a Show all checkbox toggles between Tasty-owned and system-wide ports; the scan state shows a Spinner. Rows are sortable; the State column renders a StatusDot.

Layout spec
frame
660 × 520 (canonical)
built on
Table
header
filter Input + refresh
scan
Spinner \u2192 \u201cCollecting\u2026\u201d
footer
count · Copy address · Close
Tokens used
bg-panel— framesurface-active— selected rowaccent-success— LISTEN dotfont-mono— port/addr/pid

The 660px width is design-canonical — the 7-column table (Port · Proto · Address · Process · Workspace · Tab · State) needs it, plus a tight 28px leading star column. Don't narrow the frame; let columns truncate instead.

Favorites — star toggle, pinned section, NONE badge

Port favorites are keyed by (addr, port) — never the PID, which changes on every restart. A tight 28px leading column in the table carries the star (outline = unregistered, gold starFill = registered, same pair the Explorer sidebar uses); clicking it registers or unregisters immediately, with no confirm step. This is the only writing control in an otherwise read-only popup. A Favorites section sits between the filter row and the table and lists every favorite regardless of the search box, the scope checkbox and the sort — and it always judges LISTEN/NONE against the full system scan, so a favorite that Tasty doesn't own still reads LISTEN. A favorite whose port is not in the scan gets the NONE badge: StatusDot idle, muted, no pulse — apart from LISTEN (green + pulse) and other states (yellow). Stars can be toggled from either region.

Layout spec
identity
(addr, port)
star
22×22 hit · tight 28px column
section
caption 22 + list ≤ 112 (5 rows)
row height
22 (tree density)
scope
always system-wide
frame
unchanged — 660 × 520
Tokens used
port-star-on— registered starport-star-off— outline starport-favorites-bg— section toneport-state-none-dot— NONE dotport-favorites-max-height— scroll cap

Do keep the favorites rows as summary rows (addr:port · process · state), not the 7-column grid — a stopped port has no process/workspace/tab data to show, and the summary row never needs the table's horizontal scroll. The star column width is shared, so stars still line up across both regions.

New strings: ports.favorites ("Favorites") · ports.favorites_empty ("No favorites yet") · ports.favorites_empty_hint · ports.state_none ("NONE") · ports.favorites_scope ("system-wide"). Leave 20–40% growth room for ko/ja/de — the caption row and the empty line are single-line by design.

Process column — a minimum, not a fixed width (on Table, 2026-10-08)

Drawn on the shared Table with the popup's own columns (star · Port · Proto · Address · Process · Workspace · State), so header, row height, fonts and padding are the popup's. The Process column has a floor, --tasty-port-process-col-min-width (200), passed as the Table column's minWidth: it takes spare width and never shrinks below the floor. A column's width is also its floor, so the fixed columns never shrink. Address is fixed at its floor --tasty-port-addr-col-min-width (140, fits 255.255.255.255 and :: forms; IPv6 ellipsises). Process takes all the spare width; Address never grows (2026-10-08). The sum is the popup's column budget. At the popup's own 660 the budget is a little wider, so the table body scrolls horizontally, the existing scroll policy of the popup. No column shrinks away or hides. Process text ellipsises inside its cell; the PID Tag stays visible.

Layout spec
component
Table — the popup's column defs, shared
Process
strong · minWidth = --tasty-port-process-col-min-width · ellipsis
value
200 — unchanged
Address
fixed · --tasty-port-addr-col-min-width 140 (floor = width)
spare width
100% to Process
stage gaps
gallery Column stage --tasty-space-lg between tables · cluster caption --tasty-space-sm
overflow
body scrolls horizontally below the column budget (also at 660); no column hides
fixed columns
width = floor (star · Port · Proto · Workspace · State)
metrics
Table's own — row/header table-cell-height 28 · cell pad 12 · caps header
zoom
scales with the UI scale, like every width token
Tokens used
port-process-col-min-width— Process floorport-addr-col-min-width— Address floor + widthport-star-col-width— leading star columntable-cell-height— header + rows

Replaces the hand-built schematic (header 24 · rows 26 · mono 11 · PID column). There are no specimen-only numbers left to name: every metric comes from Table or the popup's column widths. New Table column option minWidth (additive).

Remote connections

Remote connections — profiles + passkeys

Tools › Remote connections. A 520×460 modal with three top tabs: a type-agnostic remote-profile store (ssh / smb / http / anything), an Attach store (tasty-attach targets), and a separate Passkey credential store. Each tab routes list → form → confirm-delete off a shared header. SSH profiles get a dedicated form; everything else uses a generic key-value editor. The Profiles add-bar carries a protocol filter (right) — a dropdown of the protocols present, checkbox-toggled with Select all / Deselect all / Reset, committed on Apply. Secrets live only in passkeys, referenced by name.

Layout spec
frame
520 × 460
tabs
Remote profiles · Attach · Passkeys
routes
list → form → confirm
filter
protocol dropdown (Profiles only) · menu --tasty-remote-filter-menu-width = 240, measured as the border box
dismiss
×/Close/Esc only
Tokens used
bg-panel— framebg-sidebar— tab baraccent-primary— active tab / filter onaccent-warning— unknown-type / dangling-ref

Do keep secrets in the Passkey store and reference them by name. A profile never holds a secret inline.

The protocol filter is Profiles-only (Passkeys has no filter) and session-only — never persisted; Tasty restarts with every protocol selected. The button reads Filter when off and turns accent with a selected/total count when a filter is applied.

Passkeys tab — rows, reveal, unknown kind (2026-10-06)

The third tab, same 520-wide frame. Each row: name (600) + kind Tag — or the warning badge when the stored kind is unknown — then a mono line kind · value, masked until revealed. Actions on the right: reveal, edit, delete (trash, as on the other two tabs), IconButton sm. Revealed = IconButton active and the glyph swaps eye → eyeOff. The value line ellipsizes, revealed or not; the action cluster is flex: none and is never pushed out of the row.

Layout spec
row
name 600 + Tag / warn badge · mono caption kind · value
mask
fixed 8-dot mask while hidden
revealed
IconButton active + eyeOff
long value
ellipsis at the end · actions flex none, never pushed out
actions
reveal · edit · trash · IconButton sm, gap 1
add-bar
Add passkey (Button secondary sm) · no filter
Tokens used
accent-warning— unknown kindtext-muted— value line

Attach tab — tasty-attach targets

The middle tab. An Attach holds everything needed to attach to a remote tasty instance — it either references an ssh profile (→ prod-web) or carries inline ssh info, plus a remote tasty executable path and a port discovery mode. Splitting this off the ssh profile keeps ssh profiles pure connection info. Rows show name + (label), the reference/host summary, remote-tasty + port-mode captions, and an inactive badge when the referenced profile or inline shell isn't reachable.

Layout spec
rows
name+label · target · tasty/port
mode
profile (→ ref) · inline
target
→ profile or user@host
inactive
peach badge (unreachable)
add-bar
Add attach (no protocol filter)
Tokens used
accent-warning— inactive / profile-missingtext-disabled— inactive nameseparator— row dividers

Do keep remote_tasty and port discovery on the Attach, not the ssh profile — an ssh profile is reusable connection info; how to find the remote tasty binary is attach-specific.

Attach form — reference vs. inline

The add/edit route on the Attach tab, on the shared [--tasty-remote-label-col · 1fr] grid. A Connection segmented toggle switches between SSH profile (an ssh_ref dropdown of existing ssh profiles) and Direct (inline) (the ssh profile fieldset — host / user / port / shell / passkey). Both share a Remote tasty group: Executable (default tasty), Port mode (auto / subcommand / file-unix / file-windows), and an optional Port file that overrides the mode.

Layout spec
toggle
SSH profile ↔ Direct (inline)
ref
ssh_ref dropdown of ssh profiles
inline
host · user · port · shell · passkey
remote tasty
Executable (def. tasty)
port
auto / subcommand / file-unix / file-windows
port file
optional — overrides port mode
Tokens used
remote-label-col— shared 112 label columnaccent-primary— active tab / segmenttext-muted— labels / hints

Profile form — SSH (the [112px · 1fr] row grid)

The add/edit route inside the popup. Every row is the same grid: a fixed 112px right-aligned label + a 1fr control (columnGap 12, rowGap 8) so labels never collapse/truncate and all controls share one left edge. SSH gets the dedicated fields (Type · Name · Host · User · Port · Label · Shell + hint · Passkey) — remote_tasty moved to the Attach tab. Body scrolls; the footer is pinned with a full-width separator and right-aligned Cancel (ghost) / Save (primary). Right: the same form with a validation error.

Layout spec
frame
520 × 460 · resizable · headless
row
[--tasty-remote-label-col 112 · 1fr] · gap 12/8
label
right-aligned · text-muted · 13
body/footer
flex:1 scroll · pinned footer
footer sep
full-width 1px · buttons inset 16/12
error
accent-danger caption (11)
Tokens used
remote-label-col— 112px label columntext-muted— labels / hintsseparator— footer + tab divideraccent-primary— active tab · Saveaccent-danger— validation error

Do keep the label column fixed at 112px (not 1fr-negotiated) — egui's auto grid collapsed it and truncated labels, so the form uses a manual two-column row.

Generic & Passkey forms · badges

A non-ssh Type swaps the SSH grid for a generic key-value editor (Type · Name · FIELDS rows `[112 · 1fr · 28]` + Add field · Passkey), with a peach Unknown type badge when the type isn't registered (still saves). The Passkey tab's form has Name · Kind (path / inline segmented) · Value — singleline path vs. 3-row multiline secret — plus the local-only note.

Layout spec
generic
Type · Name · FIELDS · Passkey
field row
[112 key · 1fr value · 28 ✕]
unknown type
peach badge (saves anyway)
passkey kind
path = singleline · inline = 3-row
secret
name-reference only · note always shown
footer
ghost Cancel / primary Save (both forms)
Tokens used
accent-warning— unknown-type / dangling badgeaccent-primary— selected kind segment (accent fill)input-bg— inline secret fieldremote-label-col— shared 112 column

Other states (brief §5/§ST): dangling passkey reuses the same peach badge ("passkey missing"); detecting shows "detecting…" / "detection failed (disabled)" + Re-detect after a shell=auto save. Secrets live only in passkeys — a profile holds a name reference, never an inline secret.

Segmented active is an accent fill — tab strips keep the underline

Two components were drifting into one another. A tab strip (the window's Profiles / Attach / Passkeys bar) marks the current tab with a 2px accent underline and a weight change, no fill. A segmented control (the attach form's profile / inline switch, the clipboard type segment, the apply-preset scope) marks the active segment with an accent-primary fill and --tasty-text-on-accent ink. --tasty-surface-active is the row-selection fill and is not used by either — that was the gallery's bug, now fixed.

Layout spec
tab strip
2px --tasty-accent-primary underline · weight 600
segmented
fill --tasty-accent-primary · ink --tasty-text-on-accent
inactive segment
--tasty-surface-raised
scope
every remote segment + clipboard type + preset scope
not used
--tasty-surface-active (row selection)
Tokens used
accent-primary— active segment / underlinetext-on-accent— active segment inksurface-raised— inactive segment

Rule of thumb: an underline switches a view, a fill switches a value. The remote tab bar navigates; the attach switch sets a field.

From ssh config — a second, subordinate list

Hosts parsed out of ~/.ssh/config sit below the profiles, in the same scroll — it is the same question (“which machine?”), and a separate tab would hide the answer. Everything about the section says one tier down: a section header instead of a card, the source path in mono so the origin is unambiguous, two lines per row instead of the profile row's three, secondary ink on the alias, and no per-row icon buttons — one ghost Add profile, which is the import action that already exists. An already-imported host shows a muted in profiles Tag and no action, so importing twice is not offered.

Layout spec
place
below the profile list, same scroll
header
11px uppercase label · --tasty-font-mono source path · count
separator
1px --tasty-border-frame above the section
row
alias (13, secondary) + user@host:port (mono 11, muted)
action
ghost Add profile — the existing import, no new behaviour
already imported
Tag in profiles, no action
empty / failure
one muted line each, no error tone
long alias
ellipsises; the target line never wraps
Tokens used
border-frame— section ruletext-secondary— alias + section labeltext-muted— target · source path · states

Don't give these rows the profile row's edit / delete / re-detect buttons. The file is the user's own; tasty reads it and never writes it.

A missing or unreadable ~/.ssh/config is not an error — one muted line, no warning tone, section header stays so the origin is still explained. States (2026-09-29): empty = read, 0 hosts → “No hosts in ~/.ssh/config.” · missing = no file → “No ~/.ssh/config found.” · unreadable = the file exists but can't be read — permission denied, any other open error, a directory at that path, or invalid UTF-8 — one cause-neutral line “Can't read ~/.ssh/config. Check the file and its permissions.” The UI does not split causes; an undecodable file must not fall through to the empty line.

Add remote workspace

Two-pane remote-workspace picker — 4 states

Sidebar workspaces-header / new-workspace(+) right click → Add remote workspace. A 680×460 modal in the remote_tool language (Scrim · content-drawn header · --tasty-bg-panel frame). It consumes the tasty-attach profiles registered on remote_tool's Attach tab — it never edits them. Left (240px): the attach-profile list, single select, with an inactive badge on profiles whose last detection failed. Right: the selected profile's remote workspaces. Footer: Connect (primary, enabled only when a selectable remote workspace is chosen) · Cancel (ghost, always enabled — closes, aborting any in-flight connect).

Layout spec
frame
680 × 460
left
240px attach-profile list (single select)
right
remote workspaces — 4 states
ws row
dot · name · panes · busy / in-use
connect
enabled only with a selectable ws
cancel
always enabled · aborts connect
dismiss
×/Cancel/Esc
Tokens used
bg-panel— framebg-sidebar— left panesurface-active— selected rowaccent-primary— selected bar · Connectaccent-attached— in-use (claimed elsewhere)accent-success— busy dot

Do block selection of a remote workspace already attached on another client (lavender in use badge, dot dimmed) — mirror it there, not twice. This is the same --tasty-accent-attached axis as the sidebar attached ring.

The right pane runs four states off the left selection: initial (nothing picked — placeholder), connecting (Spinner — SSH tunnel + list can take seconds), error (danger glyph + reason + Retry), and loaded (the workspace list, or a muted “no workspaces” empty when the remote is reachable but idle).

Right-pane states — initial · connecting · error · empty

The three non-list states, plus the empty remote (plan B: the list path with the pre-selected “+ New workspace” row), driven by the left selection. Each is a centered column (glyph → title → one muted line), so the pane never looks broken while a connect is pending or failed.

Layout spec
initial
remote glyph + prompt · Connect disabled
connecting
Spinner + “Connecting…”
error
danger glyph + reason + Retry
empty
caps header + pre-selected “+ New workspace” row + one muted line · Create & connect enabled
Tokens used
text-placeholder— initial glyphaccent-danger— error glyphspinner-track— connecting spinner

“+ New workspace” row — the escape from a dead-end remote

The first row of the loaded list opens the other path: instead of mirroring a workspace the remote already has, create one there and mirror that. It is deliberately a row in the list, not a button or a second tab — so it inherits the list's select-then-confirm contract: clicking selects, the footer confirms (§6-5). No name or cwd is ever asked for; the remote's defaults are used, which the row's tooltip states outright.

Layout spec
position
first row of the scroll area, under the caps header
height
34 — identical to a ws row (8/12 padding, 13px/500)
glyph
plus 14px inside the status-dot slot → same name-column left edge
right slot
muted “on remote” instead of panes/busy
separator
1px --tasty-separator, 4 above / 4 below
sticky
no — scrolls with the list
footer
label becomes Create & connect while selected
Tokens used
accent-primary— glyph + label + select baroverlay-hover— hoversurface-active— selectedseparator— group divideraccent-danger— create failed

Do separate it from the real workspaces on three channels at once — glyph, accent label, and the divider. Colour alone would fail both colour-blind users and the 4.5:1 rule; the plus glyph and the divider carry the meaning without it. When the row is selected the label drops the accent for --tasty-text-primary (accent over --tasty-surface-active measures 3.17:1) — the glyph and the 2px bar keep the accent, so nothing is lost.

Don't give it a status dot, pane count, or a busy/in-use badge — none of them exist for a workspace that hasn't been created. The dot slot is reused (not padded out) so the name column still lines up.

New-workspace row — rest · hover · selected · creating · failed

All five row states at pane width (440px), each with the first real ws row beneath so the shared alignment line is visible. Creating swaps the glyph for a 14px Spinner and the label for “Creating workspace…” (§6-3: inline, not a full-pane takeover — the user keeps the list they were reading, and the roundtrip is seconds, not minutes). Failed keeps the row and hangs the remote's own message under it with a Try again button (§6-4), clamped to 3 lines with the full text in title.

Layout spec
rest
transparent · accent glyph + label
hover
--tasty-overlay-hover
selected
+ 2px inset accent bar · label switches to --tasty-text-primary (accent on --tasty-surface-active is only 3.17:1) — identical to a ws row
creating
Spinner 14 in the glyph slot · label muted · list dimmed, not replaced
failed
danger glyph · message clamp 3 lines · Try again (secondary sm)
motion
state swaps are instant (0ms) — the Spinner is the one allowed exception
Tokens used
accent-primary— rest / selected labeltext-muted— creating labelaccent-danger— failed glyph + message

i18n. “New workspace” grows to “Neuer Arbeitsbereich” / “새 워킬스페이스”. The label is flex: 0 1 auto; min-width: 0 with single-line ellipsis and the trailing “on remote” caption is the first thing to be pushed out — the row never wraps and never grows past 34px.

Empty remote — plan A (center-state + CTA) vs plan B (list path)

§6-1, the decision that sets the implementation's render branch. Before this change a reachable-but-empty remote ended at a center-state with nothing but Cancel. Plan A keeps that center-state and adds a CTA button. Plan B takes the list path even when empty: caps header + the one “+ New workspace” row, with a muted line explaining why nothing follows it.

Layout spec
recommendation
plan B
why
one render path, one affordance, one confirm route
plan B copy
“‹host› is reachable but has no workspaces yet.”
footer
Create & connect, enabled (the row is pre-selected)
plan A cost
a second way to trigger the same action, confirmed differently
Tokens used
text-muted— explanatory lineaccent-primary— row label

Recommended: plan B. Plan A's CTA is a button, so it fires immediately — the same action would then be confirmed two different ways depending on whether the remote happened to be empty, and the implementation would carry two render branches plus two handlers. Plan B has one list, one row, one confirm route, and the empty case degrades to “the list has exactly one row.” In plan B the row is pre-selected on an empty remote, so Create & connect is live the moment the pane loads — the dead end is gone without adding a control.

Decisions — the eight open questions, resolved

What was chosen and why. This is the implementation spec.

1 · empty state — plan B (list path, one pre-selected row). One render branch, one confirm route.
2 · distinction — plus glyph in the dot slot + accent label + 1px separator below (4/4 margins). Weight stays 500, size stays 13 — the row must read as a peer of the rows below it, not as a header.
3 · creating — inline in the row (glyph → Spinner, label → “Creating workspace…”), list below dimmed to 50% and inert. A full-pane connecting takeover would throw away the list for a 1–3s roundtrip.
4 · failure — inline under the row, message clamped to 3 lines (full string in title) + Try again. The connect-error center-state is right for “we never got a list”; here we have the list and the user's next move is usually to pick an existing workspace instead — don't hide it.
5 · confirm (core) — select, then footer. The user chose a placement inside a list; a single row that fires on click while its neighbours only select is the inconsistency, and it also loses the reversible “I clicked it, now what?” moment before a remote-mutating action. Cost accepted: the row carries a selected state.
6 · height & sticky — 34px, not sticky. Same box as a ws row; sticky would stack a second frozen band under the caps header for a list that rarely exceeds ~8 rows.
7 · footer label — “Create & connect” while the new row is selected, “Connect” otherwise. Because §6-5 chose footer confirmation, the button must say which of the two things it will do.
8 · tooltip — yes, on the row: “Creates a workspace on the remote with its default name and cwd — you won't be asked for a name — then mirrors it here.” Not asking for a name is the surprising part, so it gets said where the click happens.

Interaction contract. Arrow keys traverse the new row as row 1; Enter confirms the selection (same as the footer). Changing the left-hand profile resets the selection and the row's phase to rest. During creation the left profile list and the right list are inert; Cancel stays live — it closes the popup and abandons the in-flight request, and a workspace already created on the remote is not rolled back (a flash message on close says so; no extra confirm). On success the popup closes straight into attach — no interstitial “created” step. The caps header keeps its wording: REMOTE WORKSPACES · ‹profile› still describes the group, and the new row's own label says it is a creation.

New icons: none. plus, alertTriangle and refresh already exist in icons/. New tokens: none — the row is built from --tasty-accent-primary, --tasty-overlay-hover, --tasty-surface-active, --tasty-separator, --tasty-accent-danger and the existing spacing steps.

File picker

Native file picker — local & remote, one component

Tasty's own Open file dialog, replacing the OS-native picker so it can browse a remote attach host over the same SSH mechanism as attach — the OS picker only ever sees the local disk. It's a select-and-confirm dialog (pick a path, hand it back, close), not the Explorer surface (that's a free-roam tab). A 640×480 modal in the remote_tool popup language: Scrim · content-drawn header · --tasty-bg-panel frame. Local and remote are the same component — they differ only in the header host indicator and the breadcrumb root. Rows reuse the folder/file list language; folder double-click descends, file click fills the name field, file double-click = Open.

Layout spec
frame
640 × 480 · PopupDef
header
glyph · title · host indicator · ✕
path bar
breadcrumb + refresh · bg-sidebar
row
checkbox? · icon · name · size · modified
footer
name field · type filter · Cancel / Open
open
enabled only with a selection
dismiss
×/Cancel/Esc · Open = Enter
Tokens used
bg-panel— framebg-sidebar— path / list-header barsurface-active— selected rowaccent-primary— selected bar · folder glyph · Open · crumb linkaccent-info— remote host indicatoraccent-danger— error glyph

Do keep local and remote a single component with a single set of states — the target only changes the host indicator and the breadcrumb root (--tasty-font-mono user@host vs. /), never the layout.

Out of scope this pass (deferred, not designed): a favorites/recent-locations rail and per-host history. Multi-select is spec'd below but ships behind single-select by default.

Remote indicator — three candidates

§6.1 open decision: how to signal "this dialog is looking at a remote host, not your local disk," clearly but not loudly. Three candidates on the same --tasty-accent-info axis. Badge (recommended): a mono user@host chip beside the title — explicit about which host, reuses the attach/remote visual language. Glyph + host: swap the header icon to remote and print the host inline — lighter, no chip. Border: tint the whole frame edge + a 2px top strip — unmissable but the heaviest; use only if remote/local confusion is a real hazard.

Layout spec
A badge
mono host chip · info-tinted · explicit
B glyph
remote icon + inline host · lightest
C border
info edge + 2px top strip · loudest
shared axis
--tasty-accent-info (never danger)
local
no indicator · file glyph · / root
Tokens used
accent-info— all three indicatorsfont-mono— host string

Don't use a danger/warning tone for the remote indicator — remote is a normal, expected mode, not an error. Reserve peach/red for the connection-lost state below.

States — loading · empty · permission · connection lost · multi-select

The body region swaps between list and status states without changing the frame. Loading: Spinner while the directory reads (remote adds "over SSH"). Empty: muted folderOpen + "This folder is empty." Permission denied (local) and Remote connection lost share the layout — danger glyph · title · one reason line · action — but differ in copy and action (Retry vs. Reconnect, which resumes from the last folder). Multi-select (§6.2): a checkbox column, a comma-joined name field, and an N selected count — spec'd for the future, single-select is the default.

Layout spec
loading
Spinner · reads dir (remote: over SSH)
empty
folderOpen glyph + muted line
error
danger glyph · title · reason · action
perm vs. conn
Retry vs. Reconnect (resumes)
multi
checkbox col · joined names · N selected
Open
disabled while loading / error / empty
Tokens used
accent-danger— error glyphtext-placeholder— empty glyphspinner-track— loading spinner

Row focus ring (keyboard nav) is shown on pipeline.yaml in the loaded frames — 1px --tasty-accent-primary outline, distinct from the filled selection background so focus and selection never merge visually.

Save mode — one confirm, in the footer

The same picker, asked to produce a path instead of read one (Settings → Export, and every future save target — rfd blocks the calling thread, so no OS dialog). There is exactly one confirm control, and it is the footer's primary button — the File name row the picker already owns becomes editable and the button reads Save. No second row is appended below the view, because a second button would point at a second target and the screen could hold two answers at once.

One target. Selecting a list row does not confirm anything — it writes that row's name into the input, and the input is the only thing the button reads. Typing after a pick keeps the row selected only while the name still matches; edit a character and the selection clears. So "the picked file" and "the typed name" are never two different paths.

Overwrite is inline, not a second dialog. When the typed name already exists in this folder, a warning line appears above the buttons and the primary relabels to Overwrite — the decision stays on the button the user is already reaching for. A modal confirm on top of a modal picker would be a second scrim for a reversible write.

Layout spec
title
Save file (open mode: Open file)
confirm
footer primary only — one control
labels
Save · Overwrite when the name exists
input
editable · placeholder “Type a file name”
list pick
writes the name into the input → Overwrite
selection
clears as soon as the name diverges → Save
overwrite
11px warning line above the buttons
disabled
Save disabled while the name is empty
breadcrumb
folds only when the measured row doesn't fit — see Path bar
footer
never shrinks — the input absorbs it
Tokens used
accent-warning— overwrite lineinput-bg— name fieldtext-placeholder— empty nameseparator— footer rule

Long-path shrink rule (fixes a defect in open mode too). Overflow is absorbed in the path bar, never by the footer: the breadcrumb is the only flexible child (flex:1; min-width:0) and elides in the middle — root + … + the last two segments, since the current folder and its parent are what orient you; the … lists the hidden ancestors on click. Footer label, filter chip and both buttons are flex:none; only the name input shrinks. No horizontal scroll, and Cancel / Open / Save can never be clipped.

Don't append a save row under the list. Two confirm controls in one dialog means two targets — pick package.json, then type a different name, and the screen holds two answers with no rule for which wins.

Gestures & folder targets — the six open branches

The save-mode decision left six branches undecided, and because save and open are the same view, each answer had to hold in both modes. One gesture table now covers them: single click selects, double click descends — in both modes, for both kinds. A folder is never a save target: the footer button still reads the input only, so a selected folder can't change what gets written; a muted line says so where the reading happens. In save mode a file's double-click stops at select — it writes the name, which is the overwrite state, and the warning line has to be readable before the write. Only open mode confirms on a file's double-click.

Layout spec
single click
selects the row — file or folder, both modes
double click folder
descends — both modes
double click file
open: confirms · save: selects only
folder + Save
never a target — muted footer line, button unchanged
folder + Open
enters it (Open is the keyboard route to descend)
elision
overflow-driven — one ancestor at a time, no depth threshold
… tooltip
Show 3 hidden folders (singular: 1 hidden folder)
… menu
content-measured, 180–320 band = BORDER-BOX outer width, path order
… menu rows
shared MenuItem — 28 · pad-x 12 · folder 16 · text-primary
specimen
320 (--tasty-fp-popup-min-width) — the width where it folds
Tokens used
fp-crumb-max-width— 180 — one crumb's cap (NEW)fp-crumb-menu-min-width— 180 — … menu floor (NEW)fp-crumb-menu-max-width— 320 — … menu ceiling (NEW)text-muted— folder-target linemenu-bg— … menu fillmenu-border— … menu edgemenu-item-padding-x— row pad-x 12

Elision is measured, not counted. The path bar elides only when the crumb row doesn't fit, and it drops one ancestor at a time from the middle — a six-segment path that fits stays whole. The floor is root + … + the current folder; the parent is the first tail segment to go. Separately, a single crumb longer than --tasty-fp-crumb-max-width ellipsises inside itself — a different axis from the path's middle elision, which is why it has its own token.

Open mode changes too (same view, same table): folder single-click now selects instead of doing nothing, and Open with a folder selected descends — which is also what Enter does, so the keyboard route to "go into this folder" exists without a second control. File behaviour in open mode is unchanged.

Path bar — what gives way when the folded path still doesn't fit

Folding the middle (root › … › parent › current) is not always enough: at 400px with 53-character folder names the current folder used to be clipped without an ellipsis. The bar now allocates the width it actually has — the path bar minus the Up and Refresh buttons and their gaps, never the popup width — in a fixed priority order: current folder → parent → root → the … menu. Five steps, each taken only when the one before it has hit its floor.

Layout spec
measure
path-bar width − Up − Refresh − gaps (not the popup width)
specimens
cards marked “stage diagram” force the fold to show the step; unmarked cards are measured
product gallery
measured only — a width ladder: longtwo at 640 · 520 · 440 · 360 · 320, longroot at 320, each labelled with the step crumb_alloc::plan returns
crumb type
11 · --tasty-font-size-caption · current = text-primary, no extra weight
1 fold
ancestors → … menu, one per step (approved 09-14)
2 parent
180 → 64 (--tasty-fp-crumb-min-width)
3 current
180 → 96 (--tasty-fp-crumb-current-min-width)
4 parent folds
root › … › current
5 root folds
… › current — the design floor
grow back
floor + 8 (--tasty-fp-bar-hysteresis)
current ellipsis
at the FRONT (…-bbbb) — the tail names the folder
ancestor ellipsis
at the tail
picker floor
--tasty-fp-popup-min-width 320 — the owning surface always wins
Tokens used
fp-crumb-max-width— cap (approved, unchanged)fp-crumb-min-width— ancestor floorfp-crumb-current-min-width— current-folder floorfp-bar-hysteresis— grow-back marginfp-popup-min-width— popup floor

Do keep the … menu in place through every step: hidden ancestors enter it in path order, so step 4 and 5 just prepend the parent and the root. Its hit area never exceeds its painted box.

Don't buy width from the footer, the file-name input, the Refresh button, or the font size. The path bar takes what is left over after those, and folds.

Same table at ui_scale 0.85 / 1 / 1.2: floors and caps are width tokens, so they scale with everything else and the order is unchanged. Windows drives, UNC roots and remote user@host roots are ordinary root crumbs — long ones fold at step 5, they get no exemption.

File-type filter chip — a read-only readout (2026-10-06)

The chip shows the filter the caller passed and nothing else: there is no menu and no way to change it, so it is drawn as a readout, not a control — no chevron, no fill, no hover, not focusable. Text is the extension list in mono (*.toml, *.json), lowercase, in the caller's order. No filter → no chip; the name field takes the width. Width is the content, capped at --tasty-fp-filter-max-width; past that the list ends in an ellipsis and the tooltip ("Showing …") carries all of it. Both consumers (Tools menu picker, the picker inside Settings) and both modes show the same chip. In save mode the filter does not touch the name field — no extension is appended.

Layout spec
role
read-only readout — not a button, no menu, not in tab order
label
*.ext list, mono caption, caller order, joined by ", "
no filter
chip absent
width
content, ≤ fp-filter-max-width (160) · then end ellipsis
tooltip
Showing *.png, *.jpg, … (full list)
height
fp-filter-height = the Input beside it
box
1px separator · radius · no fill · pad-x space-sm
save mode
no extension auto-append
i18n
only the tooltip prefix is translated; the list is literal
Tokens used
fp-filter-height— 28 — matches the Inputfp-filter-max-width— 160 cap (new)separator— chip edgetext-muted— list

Don't draw it with a chevron or a hover fill while nothing opens — the earlier static "All files ▾" chip promised a menu that does not exist.

Folder mode — mode="folder" returns a folder (2026-10-21)

The third mode of the same frame, for the three callers that used the OS folder dialog (Settings › Remote transfer › Save folder, Plugins › Add › Find folder…, Preset editor dir). Local only. The target is the selected folder row, or the current folder when nothing is selected, so Select folder is enabled whenever the folder reads (also when it is empty) and disabled while loading or on an error. The footer swaps the File name input for a read-only Folder readout of the full target path (front ellipsis, tooltip = full path) and one muted hint line. Files stay listed so the folder is recognisable, but they are inert: disabled ink, no hover, skipped by ↑↓, tooltip "Only folders can be selected." The filter chip is hidden: filters don't apply to folders. Not the same thing as folderSel above, which is a folder row in open / save mode.

Layout spec
target
selected folder row, else current folder — one path returned, picker closes
header
folder glyph + “Select folder” (14 semibold) — same header box
footer row 1
label fp-footer-label-width 64 (caption, ellipsis) · gap fp-section-gap 8 · readout flex 1, control-height 28, pad-x space-md, input-readonly-bg / -border, mono caption, fp-folder-path-fg, FRONT ellipsis, tooltip full path, not editable
footer row 2
hint: folder glyph icon-size-sm + micro, fp-folder-hint-fg, wraps; hidden on loading / error
footer row 3
spacer · Cancel ghost · Select folder primary, gap space-sm; never shrinks — rows 1–2 give way
Select folder
enabled: loaded or empty · disabled: loading, error (path readout in state-disabled-fg)
file rows
inert: glyph + name + size + date in fp-folder-file-fg, no hover / selection overlay, ↑↓ skip, tooltip
filter chip
hidden — the caller's filter is ignored in this mode
single click folder
selects (target = that folder)
click selected row again / empty area
clears → target = current folder
double click folder · →
enters it (selection cleared)
Enter
Select folder — confirms the target
Up · Alt+↑ · breadcrumb
unchanged
Tokens used
fp-folder-file-fg— new · → state-disabled-fgfp-folder-path-fg— new · → input-readonly-fgfp-folder-hint-fg— new · → text-mutedinput-readonly-bg— readout boxfp-footer-label-width— 64 label column

Strings (picker.folder.*) — title / confirm: Select folder · 폴더 선택 · フォルダーを選択 — label: Folder · 폴더 · フォルダー — hint.current: Using the current folder. Select a subfolder to use it instead. · 현재 폴더를 사용합니다. 하위 폴더를 고르면 그 폴더를 사용합니다. · 現在のフォルダーを使用します。サブフォルダーを選ぶとそのフォルダーを使用します。 — hint.selected: Double-click {name} to open it. · {name} 폴더를 열려면 더블클릭하세요. · {name} を開くにはダブルクリックします。 — file.tooltip: Only folders can be selected. · 폴더만 선택할 수 있습니다. · 選択できるのはフォルダーだけです。 — empty: No subfolders here · 하위 폴더가 없습니다 · サブフォルダーはありません. Width check: the longest label (フォルダー) fits 64 at caption; the longest button (フォルダーを選択) fits beside Cancel at the 320 floor; hints wrap to two lines at 320, never clip.

Don't make Enter descend in this mode: Enter presses the primary button in every mode (in open mode Open happens to descend into a selected folder); here the primary button is Select folder, so Enter confirms. Descending is double-click or →.

Preset editor window

PresetView — list → toolbar + live preview

The modeless window behind Tools › Presets. L1 tabs pick the scope (Workspace / Tab / Pane); a left list of saved presets for that scope feeds a right detail = a toolbar (rename · duplicate · delete · Edit) over a live demo-layout preview. The selected row uses the surface-fill + 2px accent left-bar (the file-handler / sidebar idiom). Click a preset, switch its mini tabs, then hit Edit — structure edits happen on the preview, a leaf's gear opens its settings screen in this same column.

Layout spec
frame
up to 680 × 452 (adaptive)
L1 tabs
40px, accent underline
list
196px, fill + 2px bar
toolbar
44px, Edit on the right
preview
flex, on --tasty-bg-app
Tokens used
bg-sidebar— L1 + listbg-panel— detailsurface-active— selected presetaccent-primary— active tab / select bar

This is the 2-depth idiom (L1 fixed scope tabs → growable L2 list → detail), reused from Settings — not a new shell. The detail column has two fillings: the demo-layout preview (below) and, while a surface is being configured, the surface settings screen (last spec). Try six › Edit › select a small cell › gear.

Demo-layout preview — the new component

An interactive miniature of a preset's tree — not a screenshot. It draws the three structural levels with distinct visual weights: pane split (upper layout) as separate bordered cards with a wide app-bg gap; tab strip as a mini 20px row whose active tab carries the accent bar; surface split (lower layout) as a thin hairline; and each surface leaf as its kind plus a summary of its configured fields (cwd / startup / file / url) so same-kind leaves are told apart. Mini tabs are live — click one to swap that pane's surface layout.

Layout spec
pane split
bordered cards · space-xs app-bg gap
tab strip
20px mini row · accent bar
surface split
1px hairline (lower layout)
leaf
icon + kind + field summary
summary
label:value rows · mono · front-trunc paths
degrade
drop summary <96×72 · icon-only <46
Tokens used
bg-app— surface fill / pane gapborder-default— pane card / surface hairlinepreset-leaf-label-fg— summary labelpreset-leaf-value-fg— summary value

Do separate the two split levels by weight: a heavy gap+border for pane (upper) splits, a hairline for surface (lower) splits — so the hierarchy reads at a glance even at thumbnail scale.

Don't render surface contents (a program's live output). A leaf shows only its kind + its configured fields (cwd / startup / file / url) — the preview is about structure & config, not runtime data. Empty fields hide their row; below ~96×72px the summary drops, below 46px the label drops too. And never draw a form inside a leaf — parameters are edited on the settings screen.

WYSIWYG edit mode — direct manipulation of STRUCTURE

The Edit button turns the same preview editable in place with the manipulation grammar of a real tasty pane — for structure. Split by edge: hover a surface's ~30% edge band (left / right / top / bottom) to preview a split there, then click to add a new surface on that side. Click the center of a surface to select it — a 2px accent outline plus two handles: settings (gear) and remove. The gear, or a double-click, opens the surface settings screen (next spec) for that leaf's parameters; the selection stays the target of the structure shortcuts, which is why a single click never leaves the preview (decision A1). Tabs: the mini strip gets a trailing + add-tab and a hover/active close × per tab (hidden on the last remaining tab). Structure changes save automatically. Try it below — edge-split, tab add/close, delete, and the gear all work.

Layout spec
split
hover 30% edge band → click
selected
2px accent inset + gear · remove handles
settings
gear or double-click → settings screen
tabs
trailing + add · hover/active × close
last tab
close × hidden (guard)
persistence
structure auto-saves · parameters via OK
Tokens used
preset-split-zone-bg— edge split bandpreset-split-zone-border— split lineaccent-primary— selection outlineaccent-danger— remove handle

Split direction follows the edge: left/top add the new surface before (left / above), right/bottom after. Below ~46px on an axis that axis's bands drop out (degrade) so tiny leaves stay selectable. Every leaf — selected or not — keeps its kind + field-value summary; nothing is ever drawn inside a cell that could clip. Empty-list scope shows "No presets saved yet." in the left list.

Surface settings screen — parameters as a draft

Editing a leaf's parameters (kind · cwd · startup · whatever the kind declares) used to open a form inside the leaf cell, which clipped in the six-surface workspace (the input and startup command fell outside the box). Now the settings screen takes over the whole detail column — toolbar and preview — as three stacked boxes: a header (kind icon in its accent + kind name · mono breadcrumb preset › pane N › tab › surface k · an unsaved dot once the draft differs), a scrolling body (one-column form, label over full-width input, capped at 460px; Kind first, then the kind's fields — changing Kind swaps the fields in place, keeping keys both kinds share), and a fixed footer (1px rule, right-aligned Cancel ghost · OK primary — the Settings-window footer grammar; product labels 취소 / 확인 via button.cancel / button.ok). The screen is a draft: OK applies the whole surface at once and auto-saves, Cancel discards. OK is disabled until something changed (decision E). While it is open the left list and L1 scope tabs dim and ignore input (decision C) — leaving would silently drop the draft; an outside selection change (context-menu “save as preset”, window close) drops it like Cancel, without a prompt. Save failure: the screen and draft stay put, the error goes to a toast. Try a frame: change Kind, hit Esc / Enter.

Layout spec
boxes
header / scroll body / fixed footer
header
44px — same as the toolbar it replaces
body
16px padding · 12px field gap · max 460
footer
52px fixed · 0 14px · Cancel ghost + OK primary
OK
disabled until draft ≠ saved
keys
Esc = Cancel · Enter in an input = OK
structure keys
inert while open (preview hidden)
after OK/Cancel
back to preview, leaf stays selected
Tokens used
preset-cfg-header-height— header (44)preset-cfg-footer-height— footer (52)preset-cfg-form-max-width— form column cap (460)preset-cfg-draft-fg— unsaved dotpreset-cfg-dim-opacity— list + scope while openbg-panel— screen fillseparator— header / footer rule

Do keep the three boxes independent: only the body scrolls; header and footer never move, so Cancel / OK sit in the same place for a two-field terminal and a twenty-field plugin.

Don't show “saved automatically” while the screen is open — it is not true for the draft. The toolbar is replaced wholesale (decision B) so the two save models never appear side by side; the header's unsaved dot is the only persistence cue on this screen.

Settings window

Settings — two-tier navigation

The largest dialog. L1 = a small, fixed set of top tabs (General / Appearance / Keybindings / Plugins). L2 = a growable, filterable left sidebar of sections (plugins contribute here). This split keeps top-level navigation stable while the content tree scales. Reuse it for any settings-like surface.

Layout spec
frame
1100 × 700 (canonical)
L1 tabs
44px bar, accent underline
L2 sidebar
200px, filter + list
footer
Cancel / Save, right
Tokens used
bg-sidebar— L1 + L2bg-panel— contentaccent-primary— active tabsurface-active— active section

L1 stays small forever; growth happens only in L2. Plugins are their own L1 tab — never crammed into another group's sidebar.

General › General — row grid, row captions, webhook row (2026-10-07 · grid 2026-10-08)

Every settings subtab uses one label column · control grid. Label column = the subtab's longest label, clamped to --tasty-settings-label-width (150) … --tasty-settings-label-max-width (240); a label longer than 240 wraps. Gap 16 (--tasty-settings-label-gap). The app's own fit-to-longest measure is canonical with this clamp; the old 12 gap moves to 16. A description that belongs to one row sits directly under that row (--tasty-settings-row-caption-gap 4), at the row's left edge, measure-md: that is where the wheel-scroll and language-restart lines go. The webhook row keeps its always-shown warning callout the same way. Terminal › TUI's OSC 52 row follows the same rule (on the grid, callout measure-md).

Layout spec
label column
longest label of the subtab, clamp 150 … 240, wraps past 240 (en: webhook label → 240, 2 lines)
gap
16 label → control
row caption
directly under its row · gap 4 · left edge · measure-md · caption 12 muted
callout
same slot as a caption · warning · alertTriangle 16 · always visible · measure-md
placement
webhook row last (after Language)
setting
[webhook] allow_external · default off · Save / Cancel · from next start
Tokens used
settings-label-width— label column floor 150settings-label-max-width— label column cap 240settings-label-gap— 16settings-row-caption-gap— row → caption 4accent-warning— callout edge + iconmeasure-md— caption / callout width

Copy is confirmed as shipped: label “Accept webhook calls from other computers:”, callout as above (settings.general.webhook_allow_external_*). Wheel / language caption copy here is placeholder for the app's existing strings — keep the app's text, move only the position.

Keybindings › Plugins — plugin picker + per-command mode (2026-10-08)interactive

Pick a plugin, then edit each of its commands on one control line: mode Select (Inherit / Custom / None) · slot (inherit-source Select, key Input, or “(Unassigned)”) · Reset. Rides the settings Row grid (title column 150 + gap 16, so the picker and every mode Select start at the same x), not the 288 Import/Export column: 288 + the line overflows the 620 cap. Every control on a line is 28 (md) — Select, Input and the ghost Reset alike. Titles wrap inside the column, never ellipsise. Inherit adds a caption under the line with the resolved key; a Custom value that fails to parse turns the Input invalid and adds an error caption. An unsaved change shows a 6px accent dot after the title. Reset is disabled when there is no override. Save / Cancel stay in the window footer.

Layout spec
picker
"Plugin:" in the 150 title column · Select 200 · plugins by name
command
padding-y 4 · 1px separator between commands, none after the last
line
min-h 32 · mode 160 · gap 8 · slot 200 · gap 8 · Reset (ghost md)
height
28 for every control on the line
caption
gap 4 · caption 12 · Inherit: muted "Inherited (Ctrl+C)" / "None" · parse error: danger
draft
6px accent-primary dot after the title
Reset
disabled when no override · tooltip "Clear the override and use the manifest default."
list gap
12 picker → first command
Tokens used
kb-plugin-title-width— 150 title columnkb-plugin-mode-width— 160kb-plugin-slot-width— 200kb-plugin-picker-width— 200kb-plugin-control-height— 28kb-plugin-draft-dot— draftkb-plugin-error-fg— parse errorkb-plugin-separator— between commands

Behaviour unchanged from the app: switching to Custom fills the previous Custom value or the manifest key; switching to Inherit fills the manifest source or the first of the four; typing writes the draft at once (commas = several keys, spaces ignored); Reset clears the override in the draft. The record-button alternative is a behaviour change and is shown for the user's decision only.

Keybindings › Plugins — Custom = record slots (2026-10-10 b12)

Custom now records, using the same record slot as the other subtabs (one slot per key, then +, or a None slot), with two local changes so the line keeps one control height: slots here are --tasty-kb-plugin-record-height 28, like the mode Select and Reset. Slots are one group (space-xs 4 between them); the group's ends keep the line's 8. The slot column is no longer a fixed 200 box: one key is 140 + 4 + 32 = 176, more keys push Reset right. When the line runs out of width it wraps as one flow: the next line starts at the mode Select x (the same control x where other subtabs wrap), and Reset stays the last item. Every key is shown; the single-button sample is retired. The Esc hint sits under the subtab as on the other subtabs.

Layout spec
slot
kb_record_slot · min kb-record-width 140 × kb-plugin-record-height 28 · + kb-record-add-width 32 · None slot 140
gaps
space-xs 4 between slots · kb-plugin-control-gap 8 around the group
width
grows with keys · Reset pushed right
wrap
one flow · next line at the mode Select x · row gap 4 · Reset last
recording
“Press key combination...” takes the + slot's place, grows to its text
unparsable config text
slot shows the raw text with the danger border + the error caption
Tokens used
kb-plugin-record-height— → kb-plugin-control-height 28kb-record-width— 140kb-record-add-width— 32kb-record-border— restkb-record-border-hover— hover

General › Overlay — toast duration

The General L1 tab gains a fourth L2 section, Overlay (after General / Notifications / Accessibility) — the umbrella term for Toast / Banner / Modifier-hint / Marker overlays. It ships with one row: Toast duration, a mono number field (the Numbers in settings shape — DragValue retired 2026-10-07) that controls how long a toast stays before auto-dismissing (today hardcoded at 2000ms). Exposed in seconds (matches the user's mental model), stored as ms. Same Grid (label + control) and hint-text pattern as the other General sections — no new interaction invented.

Layout spec
L2 position
4th — after Accessibility
label
Toast duration
unit
seconds — mono “2.0 s”
range / step
1.0–10.0 s, step 0.5
default
2.0 s (= DEFAULT_LIFETIME 2000ms)
control
mono Input (90) + static “s” — the Numbers in settings shape
commit
blur / ↵ · clamps to 1.0–10.0 and snaps to 0.5
out of range
danger border + “Between 1.0 and 10.0.” line
hint
12px muted line below the grid
Tokens used
surface-active— active L2 rowfield-width-xs— field width 90border-focus— editing borderaccent-danger— out of rangefont-mono— value text

Scope is this one row only — the tab name is the umbrella so future overlay settings (banner, marker…) can land here without inventing a new section. Existing General / Notifications / Accessibility content is untouched. Store the value in ms internally; only the display is in seconds.

Settings · General › Remote transfer — 5th L2 subtab

The General L1 tab gains a fifth L2 section, Remote transfer (after Overlay) — the umbrella for the remote (mirror) file-transfer channel, following the Overlay-subtab precedent: a topical L2 with room to grow rather than rows crammed into General. Two rows edit RemoteTransferSettings: Save folder — a mono path Input + a Browse… secondary button (opens the native folder picker, same pairing as the Scripts file row) — and Maximum size — a numeric Input with a mono MiB unit suffix. Each row keeps the settings-row grid (150px label · control), a muted description line beneath, and a separator between rows.

Layout spec
L2 position
5th — after Overlay
rows
Save folder · Maximum size
folder row
mono path Input + Browse… (secondary, folder icon)
size row
numeric Input · 88px + mono “MiB” suffix
defaults
~/.tasty/transfers/ · 500 MiB
row grid
150px label · control · desc below
Tokens used
settings-row-min-height— row heightsurface-active— active L2 rowseparator— row separatortext-muted— descriptions + unit

The unit is a static mono MiB suffix outside the field — not typed, not a Tag — mirroring how Toast duration carries its “s” unit. Exceeding Maximum size rejects new transfers before they start; the rejection surfaces as the Transfer failed popup (Overlays › Dialogs › Remote transfer).

Settings · Misc › Task pipeline — Report limits (2026-10-09)

A new Misc L2 subtab Task pipeline (after Scripts, before Tastyrc on Windows). The content column is one mono micro uppercase heading, REPORT LIMITS, then two settings rows in the row grid (label column = longest label clamped 150 … 240, gap 16), each a mono numeric Input (90) with the static unit B, its description caption directly under it (gap 4, measure-md), and a 1px separator between the rows. The two values are tied (note limit < attempt limit): each field's range narrows to the other's current value, so a crossed pair can't be committed. The relation stays in the description copy; the range line appears only when a value is out of range, like every numeric field. Values are raw bytes with no grouping (they are typed, and match the config file); the unit stays B.

Layout spec
L2 position
Misc · after Scripts · before Tastyrc (Windows)
heading
REPORT LIMITS · mono micro uppercase · muted
row grid
label [150 … 240] · gap 16 (settings-label-gap — the request's 12 is superseded)
field
mono Input · field-width-xs 90 · right-aligned · unit B (static, muted, 8 after the field)
caption
under its row · gap 4 · caption 11 · muted · measure-md
between rows
1px separator · settings-row-gap 12
ranges
note 64 … min(65536, attempt − 1) · attempt max(128, note + 1) … 131072
range line
only when out of range (danger) · clamp on commit · directly UNDER THE INPUT, in the control column (label width + label gap) — b10, every numeric field
numbers
raw bytes, no grouping, no KiB
Tokens used
settings-row-gap— → space-md 12 (2026-10-09)settings-row-caption-gap— row → caption 4field-width-xs— 90accent-danger— out of range

i18n (given): en “Note size limit” / “Attempt report limit” · ko “기록 한 건 상한” / “회차 report 상한” · ja “記録 1 件の上限” / “試行 report 上限” · heading “Report limits”. Gallery spec id: settings-task-pipeline.

Numbers in settings — one shape: mono Input + a static suffix

Three different numeric controls had appeared: a mono text Input with a static unit (remote transfer Maximum size · MiB), a drag number (plugin Default zoom · %), and a proposed stepper. The settled shape is the first, everywhere: a mono text Input, keyboard entry only, a static muted suffix outside the field, and clamp on commit (blur / ↵) — not while typing, so you can type 150 in a 25–200 field without the second keystroke fighting you. Out of range shows the danger border + one inline line naming the range. A drag surface inside a scrolling settings pane steals the scroll and hides its own range; a stepper needs two more hit targets for a field people set once.

Layout spec
control
existing Input — no new component
font
--tasty-font-mono
align
right — digits line up down a settings column
width
90 (--tasty-field-width-xs) — 88 is dropped
suffix
static text outside the field, muted
clamp
on commit (blur / ↵), never mid-typing
out of range
danger border + one inline line with the range, under the input (control column), above the description caption (b10)
scope
every numeric field in Settings and in plugin settings
Tokens used
field-width-xs— numeric field widthaccent-danger— out-of-range edge + linetext-muted— unit suffix

Maximum size keeps 90. The 88 in the earlier mock was a drawing accident, not a role — the field-width set stays 90 / 110 / 160 / 180 / 200 with no new member. Two-pixel gain, one more name to maintain: not worth it.

Don't put a drag-to-change number inside a scrollable settings pane. The gesture collides with the scroll and the range stays invisible until you overshoot it.

Settings · control metrics — shortcut rows · drag flip modifier · font combo · open Select (2026-10-09 b10 · b11)

Values that were literals in the app become tokens. Shortcut rows (Keybindings General … Scripts): the record button is the Import / Export slot size — --tasty-kb-record-width 140 × --tasty-kb-record-height 24, mono, surface-raised — and the add button is --tasty-kb-record-add-width 32 × 24; buttons in a row sit 4 apart; rows sit --tasty-kb-row-gap 8 apart with no divider (a long, scanned list — denser than the 12 of settings rows). The Explorer drag flip modifier is the last row of Keybindings › General, alone after the shortcut rows behind space-sm · separator · space-xs (b11 — the app's placement; the category switch modifier stays in Workspace and carries no caption, nor do Tab / Workspace switch modifiers). Shape: label + modifier Select (field-width-md) + caption. A combination from config.toml that is not in the list is appended at the end of the list as written. Record button, add and None slot share --tasty-kb-record-border → --tasty-kb-record-border-hover on hover (fill unchanged; Mocha “Ctrl+W” shows hover). A row with no binding shows one None slot (record width, border only, muted) that starts recording on click — no + beside it. The font search combo is the shared SearchableSelect (2026-10-19 — Components › Form controls #searchable-select); the Tab / Workspace / Category switch modifier combos are a plain Select. The font search combo list caps at --tasty-font-combo-list-max-height 300 and its search field is the list width minus --tasty-font-combo-search-inset on each side. An open Select trigger takes the existing --tasty-select-border-focus (→ border-focus), as in the components “no value” panel. Appearance › General keeps the app's order: font rows, the preview right after them (space-lg before, caption heading “Preview”), then a separator and Ligatures / Opacity.

Layout spec
record button
kb-record-width 140 × kb-record-height 24 · mono caption · surface-raised · 1px border-default
add (+)
kb-record-add-width 32 × 24 · border only (kb-record-border) · plus icon sm in text-muted (b11 — replaces the filled “+” glyph)
hover (b11)
record · add · None: border → kb-record-border-hover (border-strong) · fill unchanged
no binding (b11)
one None slot · kb-record-width × 24 · border only · kb-record-empty-fg · click = record the first binding · no +
in-row gap
space-xs 4
row gap
kb-row-gap 8 · no divider (kit KeyRow divider + paddingBottom 6 retired)
drag flip row
Keybindings › General · LAST row, alone · space-sm · separator · space-xs above (b11) · Select field-width-md · config combos not in the list appended at the end · options macOS Option / Command / Control / Shift (default Option), others Ctrl / Alt / Shift (default Ctrl)
font combo
2026-10-19: = SearchableSelect (components #searchable-select) · trigger field-width-lg 200 · popup max 300 · search field = popup − 2 × 4 · font-combo-* alias select-search-* · modifier combos = plain Select
captions (b11)
Category / Tab / Workspace switch modifier rows: no caption · drag flip keeps its caption
open Select
trigger border select-border-open → border-focus (Mocha sample shows it open)
Appearance › General
font rows → space-lg → caption “Preview” → preview (content width) → separator → Ligatures · Opacity (app order confirmed)
Tokens used
kb-record-width— → kb-ie-slot-min-width 140kb-record-height— → kb-ie-slot-height 24kb-record-add-width— → size-32kb-record-border— → border-default (b11)kb-record-border-hover— → border-strong (b11)kb-record-empty-fg— → text-muted (b11)kb-row-gap— → space-sm 8font-combo-list-max-height— → size-300font-combo-search-inset— → space-xs 4select-border-focus— existing → border-focus · now also the OPEN state

i18n: settings.keybindings.explorer_drag_flip_modifier_label “Explorer drag flip modifier:” / “탐색기 드래그 전환 키:” / “エクスプローラーのドラッグ切替キー:” · caption …_hint “Hold while dragging in the Explorer to copy instead of move, or move instead of copy.” / “탐색기에서 끌 때 누르고 있으면 이동 대신 복사, 복사 대신 이동합니다.” / “エクスプローラーでドラッグ中に押すと、移動とコピーが入れ替わります。” The same label goes in the import table and the option-migration card. Gallery id: settings-control-metrics.

Hook Handlers — origin decides whether a row can be removed

The registry re-seeds host and plugin defaults on every start, so a remove button on those rows promises something the system undoes. Rows now carry their origin as a Tag — host / you / the plugin in mauve — and only user rows get the trash affordance. Host and plugin rows show a lock glyph in the same slot (not a disabled button: nothing is pending) with the tooltip “Provided by host — can't be removed”. IpcSequence stays a one-line mono summary with an Edit button that opens the sequence editor; editing a multi-step sequence inline inside a settings row has nowhere to put the steps.

Layout spec
origin
Tag: host · you · plugin id (--tasty-accent-agent)
remove
user rows only
not removable
lock glyph, --tasty-glyph-dim, with tooltip
not a disabled button
nothing is pending — an affordance would lie
IpcSequence
mono one-line summary, steps joined by →
sequence editing (2026-10-06 b2)
Edit (ghost sm) on EVERY IpcSequence row (host / plugin edits save as a user override, like ShellCommand) → opens the inline text editor below
not text-representable
a stored method name the line format cannot carry → the row keeps caption "Edit with CLI" + copy; copies tasty hook-handler get --id <id>
registry
unchanged — defaults re-seed on start
Tokens used
glyph-dim— lock glyphaccent-agent— plugin originfont-mono— event · action · sequence

The design's earlier “remove on every row” is dropped: the registry policy wins, and the lock is the honest reading of it.

Hook Handlers — IpcSequence text editor (2026-10-06)

Edit expands the row inline: the one-line summary is replaced by a CodeArea (mono, line-number gutter) holding the sequence as text, one IPC call per line — method, a space, then optional one-line JSON params. A help line under the field states the format. The text is parsed on every change; the first error shows on its own line under the field (glyph + translated sentence with line / column, then the untranslated parser reason in mono muted), the line is marked in the gutter, and Apply is disabled while an error stands. Apply writes the tab draft; Settings Save commits it, Cancel in the footer reverts it, like every other row. An empty sequence is allowed with a muted note. Mod+Enter = Apply, Esc = Cancel; Enter is a newline. Adding a new IpcSequence handler from the Add card is out of scope this round.

Layout spec
placement
inline — the row's second line becomes the editor; one row open at a time
field
CodeArea · minRows 4 · grows to codearea-max-height 200, then scrolls · no wrap
help
text-muted caption: One call per line: method, then optional JSON params. Lines starting with # are skipped and are not kept.
parse
every change · first error only
error line
alertCircle + sentence in accent-danger caption · parser reason after it, mono caption text-muted (untranslated) · gutter number danger + tinted band
error copy
Line {line}: a method name is required before the params. · Line {line}: the method name contains a control character. · Line {line}, column {column}: invalid params JSON.
empty
allowed · note: No calls. The handler does nothing.
buttons
right-aligned · Cancel ghost sm · Apply secondary sm (disabled while an error stands)
save flow
Apply → tab draft → Settings Save
keys
Mod+Enter Apply · Esc Cancel · Enter newline
reopen
comments / blank lines are gone, JSON compact with sorted keys — the help line says so; no extra notice
Tokens used
codearea-gutter-bg— → bg-sidebarcodearea-gutter-fg— → text-mutedcodearea-error-fg— → accent-dangercodearea-max-height— → size-200

Hook Handlers — edited default: mark + Revert (2026-10-07)

Editing (or switching off) a handler that host or a plugin registered saves a user patch under the same id; the row keeps its origin Tag and padlock. The row now says so: an edited Tag right after the origin Tag, and a Revert ghost button on the action line beside Edit. Revert follows the extension-mapping pending pattern: the summary goes back to the default, the button becomes Undo in the same slot, and the Tag reads reverts on save. Settings Save drops the patch (same effect as tasty hook-handler remove --id); Cancel restores it. Any patch counts — a changed sequence, a Switch turned off, or both — and one Revert clears all of it.

Layout spec
mark
Tag (neutral) “edited” after the origin Tag — host / plugin rows with a user patch only
mark tooltip
Changed in your settings. Updates to the default no longer apply.
action
Revert · ghost Button sm · action line, left of Edit
Revert tooltip
Go back to the default from {origin}.
pending
button → Undo (same slot) · Tag “reverts on save” (disabled) · summary shows the default · Switch (at the default) and Edit disabled until Undo or Save — tooltip “Reverts on save. Undo to change it.” (batch 5)
summary line
mono term-sm · text-secondary — the Handler › Hook Handlers HookRow type on every row (batch 5)
scope
sequence edit · Switch off · both — any user patch
Save / Cancel
Save removes the patch · Cancel restores it
user rows
never marked — they are the user's own
Tokens used
tag-disabled-bg— pending Tagglyph-dim— padlockaccent-agent— plugin origin

No new tokens: the mark is the shared Tag, the pending state reuses the extension-mapping Undo + disabled Tag pair. Strings: hook_handler.edited “edited” · hook_handler.edited_tip · hook_handler.revert “Revert” · hook_handler.revert_tip · hook_handler.reverts_on_save “reverts on save” · hook_handler.pending_locked_tip “Reverts on save. Undo to change it.” · common.undo.

Appearance › colour rows — the Default hex is read-only, not disabled (2026-09-29)

With Default checked, a colour row has no override and its hex field cannot be edited. The field still carries the base value in use, the only text value on the row, so it is read-only, not disabled: the same neutral box as a disabled Input, with the value in --tasty-input-readonly-fg (text-secondary) instead of the disabled ink. The value can be selected and copied; the field takes focus (1px focus edge, no ring). Unchecking Default starts the override and the field becomes a normal Input. Applies to the Tasty colour rows, the terminal surface background row and the Colors group.

Layout spec
Default hex
Input readOnly
box
state-disabled fill + border (same as disabled)
value ink
input-readonly-fg → text-secondary-raised (2026-10-21; Latte 5.17)
select · copy
allowed; focusable
override
normal Input
disabled
reserved for an unavailable control
Tokens used
input-readonly-bg— → state-disabled-fillinput-readonly-border— → state-disabled-borderinput-readonly-fg— → text-secondary-raised

Appearance › Colors › Terminal-specific — 5 rows, active match ink after its fill (2026-10-12)

The theme key search_match_active_fg gets a row like every other palette field, so it can be changed without editing TOML. It goes right after search_match_active_bg: the ink sits on that fill, and reading the pair together is how a user judges it. The swatch stays the same plain swatch as every row. A text-on-fill sample would be the only row of its kind and would need the other row's value to draw; the live terminal is the real preview. Reset all clears it like any other override. The list is now 6 groups / 47 rows; Terminal-specific has 5.

Layout spec
group
Terminal-specific · 4 → 5 rows
order
selection_bg · vi_cursor_bg · search_match_bg · search_match_active_bg · search_match_active_fg
label
the key name, like every row (no translation)
swatch
unchanged plain swatch
Default base
Mocha #1e1e2e (neutral-200) · Latte #4c4f69 (neutral-1100)
Reset all
includes this row
total
6 groups / 47 rows
Tokens used
search-match-active-fg— the value this row editsswatch-size— swatch

The highlighted key name is only to point the new row out in this specimen; in the window every Default row uses the same muted ink.

Appearance › Font override — rows + preview below (2026-10-06)

The override grid uses the colour-override idiom already in Settings: label in --tasty-settings-label-width (150), the control at its own field width, then a trailing Checkbox "Use default" with its label always shown. Trailing means a long ko / ja label grows into free space instead of pushing the control. The Preview leaves the side column and sits below the grid at every window width, Focused and Unfocused side by side (they wrap under each other when narrow), then the one-line font summary. Nothing overlaps at 1100 or at the minimum width; the two-column egui split goes. A defaulted row keeps its control visible at the shared disabled look, filled with the default.

Layout spec
row
label 150 · control · Use default — gap space-lg, min-h settings-row 32 (same as Settings Row)
Font family
searchable combo · field-width-lg 200
Custom font file
path Input mono · field-width-lg 200
Font size · Line height
number Input · field-width-xs 90
DPI scaling
Select · field-width-md 160
Use default
Checkbox + label, trailing, never truncated
narrow
row wraps: the checkbox drops under the control (row-gap space-xs)
preview
below the grid · space-lg above · Focused / Unfocused flex 1 each
preview box (b2)
the surface's effective bg (runtime colour, not a token) + effective font & size · 4 lines: latin · hangul · digits · kana · ink = surface focused fg · edge Focused border-strong / Unfocused separator · radius
padding
font-preview-padding-y space-sm · -x space-md
line height
font-preview-line-height → line-height-ui 1.4 × effective size; box height follows (no height token)
stack
a box narrower than font-preview-min-width (→ field-width-lg 200) wraps under the other — content width < 2 × 200 + space-md
summary
mono caption · text-muted
Tokens used
settings-label-width— 150 label column (new name)field-width-lg— family · filefield-width-md— DPIfield-width-xs— numberssettings-row-min-height— row

FileHandler › File Extension Mapping — order + Add (2026-09-29)

The product's structure is the design (it replaces the earlier per-extension Select). An extension Input + Add (Button secondary sm, the same pair as the capture blacklist), then per extension an ordered detector list — first match wins. Reorder with IconButton sm chevronUp / chevronDown (the ▲ ▼ text glyphs go). Arrows are disabled, not hidden: ▲ on the top row, ▼ on the last candidate, both on a row whose detector is off (muted name + disabled Tag), so rows keep one slot layout. Add is disabled while the input is empty or no detector is installed.

Layout spec
add
Button secondary sm · disabled: empty input or no detector
order
IconButton sm chevronUp / chevronDown
top / last row
▲ / ▼ disabled
non-candidate row
both disabled · name text-disabled · Tag disabled "off"
hide instead?
no — slots stay put
Reset (2026-10-06)
ghost Button sm · header right end · only when the extension has a custom order · tooltip kept · draft only
not installed
the group header alone: .ext text-disabled · Tag disabled · Remove ghost Button sm; no detector rows; separator under it
header
min-height button-height-sm — Reset appearing never moves the rows
long copy
Tag and button never truncate; .ext label is the shrinking item
confirm
none — both edit the draft, Cancel reverts
pending (2026-10-06 b2)
after Remove / Reset, before Save: the header stays; the pressed button becomes Undo (ghost sm, same slot); a Tag disabled says what Save does — "removed on save" / "reset on save"
pending remove
.ext label line-through (text-disabled kept)
pending reset
rows already show install order; Reset → Undo
Undo
drops that one draft change; pressing it again is not a toggle back — Remove / Reset return
Save / Cancel
Save applies (removed group disappears, Reset button disappears); Cancel reverts every pending header
Tokens used
state-disabled-fg— disabled inksettings-row-min-height— row

Settings rows at narrow widths — stacked Row (2026-10-10 b12)

Row — side by side · stacked when the control doesn't fit

Below the supported minimum (a tiling WM ignoring 1100 × 700) a one-piece control can be wider than what is left beside the label. Each Row decides on its own: when its control's natural width does not fit in content − label column − gap, the label takes the full row width and the control moves under it at the row start x (not the control column), --tasty-settings-row-stack-gap 4 below. Rows whose control fits stay side by side, so a subtab can mix both. To stop flicker while resizing, a stacked row goes back only when the control fits with --tasty-settings-row-stack-hysteresis 16 to spare. Inside a stacked control the existing wraps still apply (second shortcut buttons wrap from the control x, units drop under the field). Dashed outlines are the label / control boxes.

Layout spec
switch
per Row · control natural width > content − label column − settings-label-gap
stacked
label full width · control at row start · gap settings-row-stack-gap 4
back
only when it fits with settings-row-stack-hysteresis 16 spare
row height
min settings-row-min-height 32 applies to each line
captions
unchanged — measure-md or the remaining width, whichever is smaller
inside
existing wraps (buttons from control x · unit under the field)
Tokens used
settings-row-stack-gap— → space-xs 4settings-row-stack-hysteresis— → space-lg 16settings-label-width— 150 (side by side only)settings-label-gap— 16

Handler › File Detectors · File Handlers — list + add card (2026-10-20)

Registered rows — two lines, no table

Both subtabs list every registered entry as a two-line row (the Hook Handlers row). Line 1 carries identity and state: id, origin Tag, prio (handlers), Switch and the trailing slot. Line 2 is one mono summary: the detector's rules, or the handler's detector → action. Seven columns do not fit the settings content width, and a table would truncate every column; two lines truncate only the id and the summary, both with the full value in a tooltip. Only user rows have Remove; host and plugin rows show the padlock.

Layout spec
row
2 lines · padding space-sm space-xs · gap space-xs · bottom 1px separator
line 1
min-height button-height-sm · gap space-sm · id mono body 13 semibold (ellipsis, full id in tooltip) · origin Tag · [prio N micro] · [pending Tag] · spacer · Switch · trailing slot
origin Tag(s)
host / user neutral · plugin:<id> agent variant · detectors: one Tag per source, order host → plugin → user, gap space-sm; a rule-less user patch shows “user override” · collapse → one neutral “N sources” Tag (tooltip = full list) once the Tags would ellipsize the id
trailing slot
any user contribution → Remove (ghost sm) · pending → Undo (ghost sm), same slot · no user contribution → lock glyph icon-size-sm, glyph-dim, tooltip “Provided by host, plugin:logkit”
line 2
summary mono term-sm, one line, ellipsis, full value in tooltip · detectors: every source's rules in merge order “ext md, markdown · ext mdx” · handlers “<detector> → <action>”
switched off
id text-secondary · summary text-muted (still editable)
sort
detectors: registry order · handlers: run order — priority ↑ → owner user > plugin > host → id (2026-10-21)
Tokens used
file-handler-prio-fg— → text-mutedfile-handler-summary-fg— → text-secondary

Add card — open, empty

“Add detector” / “Add handler” (secondary sm, plus glyph, right of the intro) opens the card between the intro and the list, and is disabled while it is open. Add stays disabled until the required fields have a value (Hook Handlers rule): detector = Id; handler = Short name + Detector + the action's required field. The Saved as user/… caption shows the stored id as you type.

Layout spec
card
file-handler-form-bg · 1px file-handler-form-border · radius · padding file-handler-form-padding 12 · row gap file-handler-form-row-gap 8
title
mono micro caps, file-handler-form-label-fg
field row
label column settings-label-width 150 (wraps, body 13) · gap settings-label-gap 16 · control column flex ≥ field-width-lg; wraps under the label when the card is narrower
control column
control (control-height 28) + one line under it, gap space-xs — priority error > info > caption
info line
info glyph icon-size-sm + caption 12, file-handler-form-info-fg, role status; same box as the error line; live while typing (not only after Add)
widths
text Inputs block (fill) · Detector Select file-handler-select-width 200 · Action Select file-handler-kind-width 160 · Priority Input file-handler-priority-width 90
buttons
right-aligned, gap space-sm: Cancel ghost sm · Add primary sm
error
alertCircle icon-size-sm + caption 12, file-handler-form-error-fg; replaces that field's caption; Input invalid. Cross-field error (no rule) sits above the button row
Tokens used
file-handler-form-bg— → surface-raisedfile-handler-form-border— → border-defaultfile-handler-form-label-fg— → text-secondary-raisedfile-handler-form-caption-fg— → text-secondary-raisedfile-handler-form-error-fg— → accent-danger-raised (2026-10-21)file-handler-priority-width— → field-width-xs 90file-handler-select-width— → field-width-lg 200file-handler-kind-width— → field-width-md 160

Add card — typing, validation errors

Format errors are checked when Add is pressed (Add was enabled: the required fields are filled). The error replaces the field's caption and stays until the field is valid again; Add stays clickable. Detector: id characters (lowercase, digits, -, ≤ 64) and the cross-field no extension and no glob line above the buttons. Handler: short name characters (≤ 32), priority not a whole number, IPC method not dotted. There is no duplicate error any more: an existing id is a patch (below). The detector Id caption is the character rule; the stored id equals the input, so there is no “Saved as” line. Handlers keep “Saved as user/<short>”.

Strings: file_detector.id.hint “Lowercase letters, digits and -. Up to 64.” · file_detector.error.id_chars “Use lowercase letters, digits and - only, up to 64.” · file_detector.error.reserved “Ids starting with $ are reserved. You can only change ones that already exist.” · .error.no_rules “Add at least one extension or a path glob.” · file_handler.error.short_chars “Use lowercase letters, digits and - only, up to 32.” · .error.priority_int “Enter a whole number.” · .error.method “Use a dotted name, e.g. com.example.foo.open.” Removed: .error.duplicate.

Handler add card — action kinds

The Action Select swaps the rows below it. Open surface: Surface kind (required) + Param key (empty = file). IPC: Method (required). System: no rows; the Action caption says what it does.

Several sources on one detector (2026-10-21)

A detector id merges contributions Host → Plugin → User. Line 1 shows one origin Tag per source in that order; line 2 lists every source's rules in the same order. A user patch with no rules (e.g. the user only switched a plugin detector off) adds a user override Tag. Any user contribution puts Remove in the trailing slot even when other sources exist; rows with no user contribution keep the padlock. When the Tags would push the id into an ellipsis (narrow column, long German strings) they collapse into one N sources Tag whose tooltip lists them; the id keeps its room.

Rows: markdown host + user rule · log host + plugin:logkit + user rule · notebook plugin:md-plus + user override (switched off, no user rule). Strings: file_detector.origin.user_override “user override” · .origin.sources “{n} sources”.

Remove a user contribution — the row stays (2026-10-21)

On a row with other sources, Remove takes out only the user part. The row is not struck: only the user Tag is struck (Tag disabled), the user rule pieces in line 2 are struck in summary-removed-fg, and the pending Tag says what goes: your rules removed on save, or your override removed on save for a rule-less patch. The Switch is disabled and shows the state the row will have after Save (the plugin default for notebook). Undo restores the row. A row with only a user contribution keeps the old idiom: whole id struck, removed on save.

Strings: file_detector.pending.yours_removed “your rules removed on save” · .pending.override_removed “your override removed on save” · .pending.removed “removed on save”.

Add with an existing id — patch, not an error (2026-10-21)

Typing an id that is already registered shows an info line under the field at once (before Add), and the primary button reads Update detector. No rule changes for Add being enabled. With no user contribution yet: “{id} already exists. Your rules and settings are added to it on save.” With an earlier user contribution: “You already changed {id}. This replaces your earlier rules and settings on save.” A registered reserved id ($directory) is patched the same way. A new reserved id ($foo) passes the character check but fails with the reserved error on Add. After Update the card closes and the existing row gets a user Tag and your rules added on save / your rules replaced on save; no new row appears. Undo restores the previous contribution.

Strings: file_detector.info.patch · .info.replace · .add.update “Update detector” · .pending.yours_added “your rules added on save” · .pending.yours_replaced “your rules replaced on save”.

Handler — same short name replaces (2026-10-21)

A short name whose user/<short> already exists shows the info line “user/{short} already exists. This replaces it on save.” in place of the Saved as caption, and the button reads Update handler. After Update the row shows the new values with replaced on save + Undo (back to the old values). Host / plugin handlers never collide: their ids have no user/ prefix.

Strings: file_handler.info.replace · .add.update “Update handler” · .pending.replaced “replaced on save”.

Handler run order — the list is the order handlers run (2026-10-21)

A detector with several handlers runs the first one by priority ↑, then owner user > plugin > host, then id. That contract is fixed; the list now sorts by it, so reading a detector's rows top to bottom is reading the run order. Sorting by id alone put plugin ids (com.…/) before host/ before user/ at a tie, the reverse of what runs. The intro says the rule and that the list follows it. Pending rows (added · replaced · removed) take the same slot rule.

Scene: detector markdown at priority 10 with user/my-viewer · com.example.md/preview · host/markdown-view — rows read in that order (host/image-view, another detector, sorts after them at the same priority). Strings: file_handler.intro “A handler opens a file that a detector recognized. Lower priority runs first. On a tie, user handlers run before plugin handlers, and plugin handlers before host ones. The list is in run order.” (de checked for length; ko/ja by the implementer).

Add card on a raised ground — error ink · Cancel · field edges (2026-10-21)

The add card is surface-raised. Three things on it fell under 4.5:1 or vanished in Latte: the error lines (accent-danger 3.52:1), the Cancel ghost label (text-secondary 4.05:1) and the normal box of Input and Select (border-default equals surface-raised in both themes). The card ground stays; the parts on it switch to their raised roles: a new semantic accent-danger-raised for error ink, Button ground="raised" for ghost, and ground="raised" on every Input and Select (Select gains the same variant Input has had since 2026-10-14).

Layout spec
error line
file-handler-form-error-fg → accent-danger-raised: Mocha = accent-danger 5.43:1 on surface0 · Latte = color-mix(red 60%, neutral-1100) 4.79:1 (was 3.52). Glyph takes the same ink (≥ 3:1)
invalid edge
unchanged — input-border-invalid → accent-danger (non-text, 3.52 ≥ 3 in Latte)
Cancel
Button ground="raised" → button-ghost-raised-fg → text-secondary-raised: Mocha 7.10 · Latte 5.17 (was 4.05) · hover button-ghost-raised-fg-hover → text-primary
Input / Select box
ground="raised" in every card field: bg → bg-panel, edge → border-strong (input-raised-* · select-raised-*). Focus: edge border-focus (Input keeps its ring rule). Invalid: accent-danger edge. Disabled: state-disabled-fill-raised + state-disabled-border-raised. Ink per state unchanged
scope
same fix on the Hook Handlers add card (labels text-secondary-raised, Inputs raised, Cancel raised) · card background unchanged (surface-raised)
Tokens used
accent-danger-raised— new semantic · Mocha → accent-danger · Latte → color-mix(red 60%, n1100)button-ghost-raised-fg— new · → text-secondary-raisedbutton-ghost-raised-fg-hover— new · → text-primaryselect-raised-bg— new · → bg-panelselect-raised-border— new · → border-strongselect-raised-disabled-bg— new · → state-disabled-fill-raisedselect-raised-disabled-border— new · → state-disabled-border-raised

Narrow column · long labels — pending rows keep the id (2026-10-21)

At size-360 in German, “N Quellen” plus a long pending Tag used to squeeze the id to one ellipsis character and push Switch / Undo off the row. Line 1 now gives room up in a fixed order: the origin Tags collapse first; if the id would still ellipsize, the pending Tag moves to its own line under line 1 (ellipsis + tooltip if even that line is short); only then does the id ellipsize, and never below 96 px (or its full width if shorter). Switch and the trailing slot never shrink. Wide columns keep the one-line layout.

Layout spec
line 1 fit order
1 origin Tags collapse → “N sources” · 2 pending Tag leaves line 1 · 3 id ellipsizes — each step only while the id would ellipsize; undone once the line is back to the width it had when the step was taken
id floor
min(file-handler-id-min-width 96, full id width) once the pending Tag has dropped · tooltip = full id
dropped pending Tag
own line between line 1 and the summary, gap space-xs · Tag disabled · max-width 100%, ellipsis, tooltip = full text
never shrink
Switch + trailing slot (Remove / Undo / lock) · prio
Tokens used
file-handler-id-min-width— new · → size-96

Rows: markdown host + user “eigene Regeln beim Speichern entfernt” · notebook plugin:md-plus + user override “eigene Änderung beim Speichern entfernt” · log host + plugin:logkit + user “eigene Regeln beim Speichern hinzugefügt”. No new strings.

Pending — added on save · removed on save

Nothing is written until the Settings Save; Cancel drops it. Add closes the card and puts the entry in the list at its sort position with a disabled Tag added on save; Undo (same slot as Remove) takes it out. Remove keeps the row, strikes the id, shows removed on save, disables its Switch and turns Remove into Undo. Same pair as File Extension Mapping and Hook Handlers.

Empty list

One row-height line in caption text-muted where the rows would be, with the same bottom separator. The header count reads (0); the Add button is unchanged.

Long labels · narrow column

Labels wrap inside the 150 column (German). When the card is narrower than label + gap + field-width-lg the control drops under its label. Long ids and summaries ellipsize on one line.

Keep the summary on one line and put the full value in the tooltip, so every row has the same height.

Shrink the label column below settings-label-width to fit long labels; wrap them instead.

Terminal › Performance — when saved values apply (2026-10-21)

Apply note + the three rows

The old top line said every change needs a restart, in small accent-warning text. Saved values actually reach the terminals opened afterward; terminals already open do not switch over at save time. That is a fact about timing, not a warning, so the line becomes a neutral note: info glyph + text-secondary caption, at the top of the page above the rows, one sentence pair for all three rows. It does not promise that open terminals never change (a terminal that later reconnects may pick the new polling value), and it names no internal mechanism. The rows keep their order, controls, defaults and the existing HelpHint pattern; their descriptions say what the setting does and do not repeat the timing. Disk scrollback lines keeps default 10000, -1 = no limit, stays separate from Scrollback lines (Terminal › General), and is disabled while disk swap is off. A larger valid value from the config file is shown as is and is only written back when the user commits an edit.

Layout spec
page
tab body padding space-lg · note → rows gap space-md · rows gap space-sm
note
top of the page, above the rows · info glyph icon-size-sm settings-apply-note-glyph (text-muted) · caption settings-apply-note-fg (text-secondary), line-height-ui · gap settings-apply-note-gap 8 · max-width measure-md · wraps, never ellipsises
contrast
text-secondary on bg-panel: Mocha ≈ 8.0 · Latte ≈ 5.1 (≥ 4.5) · glyph text-muted ≥ 3
rows
SettingsRow: label settings-label-width + HelpHint (?) · gap settings-label-gap 16 · min-height settings-row-min-height · wrap below the label when narrow
Targeted PTY polling
Switch · behaviour, default unchanged
Scrollback disk swap
Switch · behaviour, default unchanged
Disk scrollback lines
2026-10-22: field fits its value, unit after the field — see the next spec (was Input mono field-width-color 110 + unit addon) · default 10000 · -1 shows “no limit” after the field · disabled (state-disabled) while swap is off · large file values shown verbatim, not clamped on read
not added
no restart button · no per-terminal status · no popup · no new setting
Tokens used
settings-apply-note-fg— new · → text-secondarysettings-apply-note-glyph— new · → text-mutedsettings-apply-note-gap— new · → space-sm

i18n — retire settings.performance.restart_notice; add settings.performance.apply_notice: “Saved changes apply to terminals you open afterward. Terminals that are already open don't change right away.” · “저장한 값은 이후 새로 여는 터미널부터 적용됩니다. 이미 열린 터미널은 바로 바뀌지 않습니다.” · “保存した値は、このあと新しく開くターミナルから適用されます。すでに開いているターミナルはすぐには変わりません。” A new key, because the meaning changes. Row labels and HelpHint descriptions shown here are the specimen's reading of the three settings: keep the product's existing label / description strings, and drop any “requires restart” wording from them. “no limit” / “무제한” / “無制限” reuses the existing unlimited label if the product has one.

Say when values apply, once, as a neutral note at the top of the page.

Say a restart is required, or that open terminals never change; don't repeat the timing on every row.

Disk scrollback lines — the field fits its value, unit after the field (2026-10-22)

At 110 with the unit inside, 9 digits (100000000) lose their last digit and read ten times smaller; neither front- nor end-anchored clipping is safe for a number. So a number is never clipped: the unit word moves after the field (the pattern the remote-transfer and pipeline number fields already use), and the field fits its value — floor number-field-min-width (90, room for 9 digits + caret), growing by one character per digit while the user types and for any value read from the config file. Width is counted in ch of the field's own mono font, so a user-chosen font fits as well. Rest, hover, focus, blur and disabled draw the whole value; editing never scrolls inside the field, so Home / End / arrows only move the caret and Ctrl+A selects all. No ellipsis, no tooltip, no new string. -1 shows “no limit” in place of the unit (“-1 lines” is not a count). A value outside -1 … 100000000 is drawn as typed with the field's invalid edge and the range line under it until commit clamps it; a large config value is drawn verbatim and not clamped on read, as before. When the page is narrow the row wraps under its label and the field keeps the same width.

Layout spec
field
Input mono, no addon · width = max(number-field-min-width 90, (chars + 1) ch + 2 × input-padding-x + 2 × border) · max-width 100% of the control column · flex none
chars
length of the text in the field — typed text while editing, the stored value otherwise (sign included)
unit
after the field, gap number-field-unit-gap 8 · mono caption number-field-unit-fg (text-secondary) · disabled → state-disabled-fg · en lines · ko 줄 · ja 行 · any length (never inside the field)
-1
“no limit” (UI font, caption) in the unit's place
rest / hover / blur
whole value, left-aligned · nothing clipped, so no tooltip
focus / editing
field grows per typed char, no inner scroll · Ctrl+A selects all · Home / End move the caret only
disabled
swap off · same width rule · state-disabled
out of range
before commit: invalid edge (accent-danger) + range line under the field, caption number-field-note-fg, max-width measure-md · commit clamps as today
narrow
row wraps under the label (SettingsRow); field width unchanged; the range line follows the field
cap
longest i64 value = 20 chars ≈ 151 (D2Coding 11px) — reaches the max-width only in a column narrower than any settings page
Tokens used
number-field-min-width— new · → field-width-xs (90)number-field-unit-gap— new · → space-smnumber-field-unit-fg— new · → text-secondarynumber-field-note-fg— new · → text-secondary

Shared rule: every settings number field uses it — Disk scrollback lines, the remote-transfer and task-pipeline number fields (already 90 + unit outside; they gain only the grow-to-value rule), and any other settings Input that holds an integer. The File Handlers priority field (file-handler-priority-width) keeps its fixed 90: its range is short. Range line copy: if the product already has a range string, keep it; otherwise settings.performance.disk_lines_range “Use -1 or a number from 0 to 100000000.” / “-1 또는 0부터 100000000까지의 수를 입력하세요.” / “-1 または 0〜100000000 の数を入力してください。” The common Input addon defect noted in the request is separate and does not affect this rule (no addon here).

Let a number field fit the whole number; put the unit after it.

Clip a number at either end, or keep a unit word inside a fixed-width number field.

Settings · Misc › Anomaly detection (2026-10-27)

Default — nine values, notifications on / off / on

A new Misc L2 subtab Anomaly detection, after Task pipeline and before Tastyrc (Windows). The page opens with one info notice (the Performance apply-note recipe): these are heuristic signals that normal work can trigger, turning notifications off hides them only, and saved values apply from the next check. Then three sections in fixed order — Memory growth · Repeated calls · Call bursts. Each is a mono micro heading, the Show notifications Switch row, then the two number rows in backend order, with a 1px separator between rows. The switch row's caption says what the detector flags in plain words and ends with its internal name in mono (Detector: slow_loop.). Each number row has its own caption ending in its default. By default only Repeated calls is off. The first two frames are live: type, then Enter or click away.

Layout spec
L2 position
Misc · Scripts → Task pipeline → Anomaly detection → Tastyrc (Windows)
notice
info glyph 14 (settings-apply-note-glyph) · caption text-secondary · measure-md · gap 8
notice → section · section → section
space-xl 24
section
heading mono micro uppercase muted · rows gap settings-row-gap 12 · 1px separator between rows
row
label column settings-label-width 150 (subtab's longest en label fits) · gap 16 · min-height 32
switch row caption
what it flags · “Detector: <internal name>” (mono, text-secondary) · caption 11 muted · measure-md · gap 4
number row
settings number field (min 90, grows to value) · unit after, mono caption text-secondary · caption ends “Default N.”
order
notify switch first, then the two numbers as in [anomaly_detection]
Tokens used
settings-apply-note-fg— notice text → text-secondarysettings-apply-note-glyph— notice glyph → text-mutedspace-xl— between notice and sections (24)settings-row-gap— rows within a section (12)settings-row-caption-gap— row → its caption (4)settings-label-width— label column 150settings-label-gap— label → control 16number-field-min-width— field floor 90number-field-unit-fg— unit → text-secondaryseparator— between rows

No new tokens. Every value above is an existing settings / number-field / apply-note role.

All on · notifications off while editing a number

Turning Show notifications off changes the Switch only. The two number rows keep their normal ink and box, stay focusable and editable, and keep applying: the detector still runs and records, it just doesn't notify. That rule is stated once, in the page notice; the rows are not dimmed or disabled, so they don't read as switched off.

Layout spec
notify off
Switch off only · number rows unchanged (no dim, no disabled ink) · editable · values stay valid
meaning
notifications hidden · detection + recording continue · no history reset · no re-notify of past events (page notice)

Don't dim or disable the number rows of a section whose notifications are off, and don't describe off as “detection stopped”.

Out of range · after commit · bounds

Typing never corrects the value. While the typed number is outside the range the field gets the invalid edge and one line under it names the range and the value it will commit as. Enter or leaving the field commits: the value is clamped and the line goes away. A non-number reverts to the last committed value; there is no error popup. Values at either bound (including Minimum RSS 0) are valid and show no line.

Layout spec
while typing
no clamp · out of range → invalid edge + range line
range line
under the field, in the control column · caption 11 · number-field-note-fg · “Between 2 and 10000. Commits as 2.”
commit
Enter or blur · clamp into range · line removed
non-number
reverts to last committed value · no popup
bounds
3–20 · 0–65536 MiB · 30–3600 s · 2–10000 · 1–600 s · 10–100000
Tokens used
number-field-note-fg— range line → text-secondaryaccent-danger— invalid field edge (Input invalid)

Narrow · ko / ja

The page reuses settings_stack_row as is. Each row decides on its own: when its control does not fit beside the label column it stacks (label on its own line, control under it at the row start, gap 4) and returns side by side only with 16 px to spare. Headings, captions and the range line always wrap and are never cut; the field and its unit stay on one line. ko / ja keep the internal names untranslated.

Layout spec
switch
per Row · control natural width > content − label column − settings-label-gap → stacked
back
side by side only with settings-row-stack-hysteresis 16 spare
stacked
label full width · control at row start · gap settings-row-stack-gap 4 · caption + range line follow at the row start
wrap
heading · label · caption · range line wrap anywhere · field + unit nowrap
Tokens used
settings-row-stack-gap— → space-xs 4settings-row-stack-hysteresis— → space-lg 16

Copy keys (en source, ko / ja in the kit): settings.anomaly.notice · …show_notifications · …detector · …range ({min} {max} {value}) · …rss_surge.title / .caption · …slow_loop.title / .caption · …call_burst.title / .caption · per field …<key>.label / .unit / .caption. Detector names rss_surge · slow_loop · call_burst are literal, never translated.

Decisions (§6). 1 — Section titles are human names only; the internal name appears once, at the end of the switch row's caption, in mono, the same way in all three sections. 2 — The heuristic / false-positive notice is page-level, at the top (one place, applies to all three); the repeat-not-latency explanation for slow_loop is in its own switch caption. 3 — Number rows of a section with notifications off look and behave normally; the notice carries “keeps detecting, values still apply”. 4 — No restore-defaults button (no settings precedent, and Cancel already drops the draft); each number caption names its default instead. 5 — settings_stack_row per Row with its existing switch rule and hysteresis. 6 — No new tokens.

Settings › General › Permissions (macOS) — 2026-09-28

Status table, one action per row, one request button

macOS builds only, the last L2 under General. A three-column table: permission · status · row action. The four states are told apart by glyph + word, with colour as a third channel: Granted check / success, Not granted alertCircle / warning, Unknown helpCircle / muted, Cannot check automatically eyeOff / muted. Unknown (inference failed) and Cannot check (deliberately not looked at) share the muted ink but never the glyph, and neither can be misread as granted. The Full Disk Access shortcut moves into its own row as Secondary / Sm [Open System Settings]. What needs explaining per row now lives in HelpHints, so the two notes under the table are short. [Request all permissions] stays Primary / Md under the table. While requesting it is disabled and the line below becomes a spinner + requesting copy; the button carries no spinner. The debug-only Accessibility row carries a debug Tag.

Layout spec
columns
label (1fr) · status · row action
row
min 32 (settings row) · 1px border-default rule
status
glyph 14 + word · gap 4
FDA action
Secondary / Sm, in the FDA row
primary
Request all permissions · Primary / Md
requesting
button disabled · note line → spinner + copy
notes
caption 12 · text-muted · wrap at measure-xl
narrow
status + action wrap under each other, right-aligned; label never truncates
debug row
Tag "debug"
stale grant (2026-10-06)
no 5th state — Not granted + a caption line under the label with the remedy (remove, then add again)
revoked (2026-10-08)
same pattern — Not granted + caption line "turn it back on"; no chip, no new status for either branch
Tokens used
perm-granted-fg— checkperm-missing-fg— alertCircleperm-unknown-fg— helpCircleperm-unobservable-fg— eyeOffperm-row-height— → settings row 32

Copy changed (en final, in the specimen): FDA button → "Open System Settings"; detection_note and request_note shortened, their per-row parts moved to the FDA and Folder access HelpHints; requesting shortened to two clauses. Status words unchanged.

Don't paint Unknown or Cannot check as a blank or a dash. An empty status cell reads as "fine".

Plugins window · plugin avatar

Plugin identity mark — one component, two sizes

A plugin manifest carries no image, so its identity in the Plugins window is a square initial mark: the first letter of the plugin name, mono, bold, in a tinted square at --tasty-radius. There are exactly two sizes — sm (32) on Installed / Attention list rows and lg (46) on a detail identity block and the Add plugin manifest preview (the old one-off 42px preview copy is gone; it was the same thing at different numbers). The tint is mixed into --tasty-surface-raised, a fixed bed — not the row background — so the mark is pixel-identical on a rest, hover and selected row. The initial's size is a token per size, not a ratio of the box: 13 at sm, 14 at lg — inside the 14px UI cap, with no exception (2026-10-07; the earlier 16 is withdrawn). The two sizes differ by the box (32 / 46) and one type step. Weight is regular: no bold mono face ships, so colour and size carry the mark.

Layout spec
sizes
sm 32 · lg 46 — the whole roster
content
name initial, uppercase, mono regular
initial size
sm 13 · lg 14 — no UI-cap exception
tint bed
surface-raised — fixed, not the row bg
mix
bg 18% · border 38% of accent-primary
row states
mark unchanged — only the row bg moves
disabled row
whole row at state-disabled-opacity
Tokens used
plugin-avatar-size-sm— 32 — list rowplugin-avatar-size-lg— 46 — detail / manifest previewplugin-avatar-bg— tinted squareplugin-avatar-border— 1px edgeplugin-avatar-fg— the initialplugin-avatar-initial-font-size-sm— → font-size-body 13plugin-avatar-initial-font-size-lg— → font-size-max 14plugin-avatar-initial-weight— → font-weight-normalplugin-avatar-border-width— = --tasty-border-width

The mark carries identity, not classification. An earlier draft coloured it by a plugin category, but a manifest has no category field — every real install fell back to one colour, so the hue said nothing while implying a taxonomy. Colour is now fixed at --tasty-plugin-avatar-fg for every plugin; state (running / error / needs attention) is already carried by the row's status dot and callouts, which is where a reader looks for it. If a classification axis is ever wanted here, it needs a manifest field first — and then it is a new decision, not this token.

Don't mix the tint into the row background to "blend" on a selected row — the mark would then shift colour with row state and stop being a stable identity. And don't re-derive the initial's size from the box (round(size × 0.42)): that produced 19px at lg, off the type scale and over the UI cap with no decision behind it.

Copy fingerprint · Add plugin that can't be added (2026-09-29)

Copy fingerprint moves out of the action bar: an IconButton sm copy right after the mono fingerprint, in Attention and in the Add plugin manifest card alike. No fingerprint → no line and no button (nothing to copy, so no disabled state). Attention's action bar has no Details for signature reasons (2026-10-06): the reason panel already shows the blurb and the fingerprint. Add plugin on a verified manifest that can't be added (already installed · signed but the public key file is missing · signature error) stays in its slot disabled — the variant it would have had, drawn with the shared disabled ink — and the reason replaces "Grants N permissions" on the left of the bar.

Layout spec
copy fingerprint
IconButton sm copy · after the value · absent without a fingerprint
attention bar
no Details (2026-10-06) — the reason panel already shows the blurb + fingerprint
add — blocked
disabled Add plugin · reason on the left
reasons
Already installed (2026-10-19: missing .pub / signature error are addable now — Add anyway)
untrusted + .pub
Trust & add — Primary (agent is for AI-agent surfaces only)
grants
No permissions · Grants 1 permission · Grants N permissions
Tokens used
state-disabled-fg— disabled inktext-muted— reason

Add plugin — trust judgment, six kinds (2026-10-06 · re-cut 2026-10-19: unverified = danger + Add anyway, see next spec)

One box under the manifest card, toned by what Add will do: success — adds as is; warning — adds, and adding trusts the key (unknown key) or the new permission set (permissions changed); danger — unverified (2026-10-19: no longer blocked; explicit Add anyway, see the next spec). The fingerprint line follows the body whenever a fingerprint exists (signature errors have none). Untrusted-but-addable uses Trust & add in Primary: the agent variant is reserved for AI-agent surfaces.

Layout spec
tone
success add · warning add + trust · danger blocked
box
tint-fill + tint-border of the tone · pad space-md / 14 · radius
title
glyph 16 + 13/600 in the tone
body
term-sm · text-secondary
fingerprint
after the body · absent for signature-error
installed
trust box as judged · bar reason 'Already installed' only (no second notice)
Attention › signature invalid
no fingerprint line (the signature it would identify is the broken part)
homepage
mono caption row 'Homepage' under Source · link text, opens the default browser
homepage link (b2)
text-secondary · 1px underline always · hover text-primary · focus = focus ring · pointer cursor (no accent: the row sits on surface-raised and text-secondary already clears 4.5:1)
authors
id · first author · +N (tooltip lists all)
empty lists
Permissions / Surface kinds: 'None' in text-muted, no Tag · font-size-caption (same as the mono caption rows it replaces)
long fingerprint (b2)
colon-hex over 16 bytes → first 8 + ' … ' + last 8 bytes, one line; tooltip + copy = full value
Signature invalid (b2)
header kept · fixed note 'The signature does not match this plugin's files.' · cause (key missing / mismatch …) as a mono caption text-muted line under it
action-bar left text (b2)
font-size-caption — the kit's 12 is corrected
flow (b2)
kit structure: path input, preview card directly under it, no 'Plugin information' title / second step
Tokens used
tint-fill-alpha— box filltint-border-alpha— box edgeaccent-warning— add + trustaccent-danger— blockedaccent-success— trusted

Unverified plugin — warning + explicit Add anyway (2026-10-19)

Four signature states are now addable by an explicit choice: unknown key, public key file missing, unsigned, signature error. They share one danger trust box: the same title “Unverified plugin”, a cause line that names the actual state, and the same risk paragraph (a plugin is a program with the user's access; the permission list limits only what it can ask Tasty to do). A fingerprint line follows only when a fingerprint exists. The action bar keeps its order; the primary becomes Add anyway (danger fill) — Trust key & add anyway for an unknown key, because adding still trusts that key. Trusted and its flow are unchanged.

Layout spec
box
tint-fill + tint-border of the tone · pad 12 / 14 · radius 4 (unchanged recipe)
tone (2026-10-19)
success trusted · warning permissions changed · danger unverified (all four) — danger now means “at your own risk”, not “blocked”
title
13/600 text-primary · glyph 16 carries the tone (accent titles failed 4.5:1 in Latte)
cause
one sentence naming the state · term-sm text-secondary
risk
shared paragraph · term-sm text-secondary · wraps anywhere
fingerprint
unknown key · missing .pub only — never invented for unsigned / signature error
bar
grants left · Cancel (ghost) · Add anyway (danger) right · wraps under on narrow
only blocked state
Already installed (disabled Add plugin + reason, unchanged)
Tokens used
plugin-trust-title-fg— → text-primaryplugin-trust-body-fg— → text-secondaryplugin-trust-glyph-danger— → accent-dangerplugin-trust-glyph-warning— → accent-warning-glyph (new semantic; Latte remap)plugin-trust-glyph-success— → accent-success

Keyboard and choice. After Verify shows an unverified verdict, initial focus is Cancel; Enter activates the focused button only, so Enter cancels. There is no implicit default button. Esc = Cancel (nothing installed, the tab returns to the path field). Tab moves Cancel → Add anyway. Pointer: one click on Add anyway. Nothing is installed or registered before that click / key. Remembering the choice and re-asking rules are the implementation's, not this design's.

Contrast (WCAG relative luminance). Title text-primary on the danger tint: Mocha ≈ 11:1 · Latte ≈ 7:1. Body text-secondary: Mocha ≈ 8:1 · Latte n1000 ≈ 4.7:1. Danger glyph: Latte #d20f39 on tint ≈ 4.1:1 (≥ 3) · Mocha #f38ba8 ≈ 6:1. Warning glyph (permissions changed): Latte before #df8e1d ≈ 2.2:1 → --tasty-accent-warning-glyph (yellow 50% toward n1100) ≈ 3.6:1. Danger button label: Latte white on #d20f39 5.43:1 · Mocha crust on #f38ba8 ≈ 8:1. Trusted box: see Trusted verdict — contrast (2026-10-27) below (the old “≈ 3.3:1” figure measured neither real ground and is withdrawn).

Trusted verdict — contrast on both grounds (2026-10-27)

After Verify on a folder signed by a trusted key, the box keeps its one line, shield, copy, padding, success tint fill and border. Only the two inks move to a new success ink role: the body to --tasty-plugin-trust-body-trusted-fg, the shield to --tasty-plugin-trust-glyph-success, both → --tasty-accent-success-ink. In Mocha that role is accent-success, so Mocha is pixel-identical; Latte remaps it to green 60% toward black. Each theme is shown on mantle (the host Add tab, panel_fill) and on base (this board's AddPanel). The shield is a hover-only display element; pointing at it adds no tint. Permissions changed and unverified stay as they were (reference rows).

Layout spec
fill
accent-success × tint-fill-alpha 0.12 (egui 31/255) — unchanged
border
accent-success × tint-border-alpha 0.36 — unchanged
body
term-sm · plugin-trust-body-trusted-fg · target 4.5:1
shield
16 · plugin-trust-glyph-success · target 3:1 (same goal as the warning / danger trust glyphs)
hover
no change — the shield is Sense::hover display only
Tokens used
accent-success-ink— new semantic · Mocha → accent-success · Latte green 60% toward black ≈ #26601aplugin-trust-body-trusted-fg— new · → accent-success-inkplugin-trust-glyph-success— was accent-success → accent-success-inkaccent-success— fill + border only (unchanged, global)

Contrast (WCAG relative luminance, specified colours). Box grounds: Latte mantle+tint #d2e0d7 (CSS 0.12 and egui 31/255 byte path give the same bytes) · base+tint CSS #dae7dd / egui #dae7dc. Latte body and shield, ink #26601a: mantle+tint 5.55:1 (both methods) · base+tint 5.94:1 CSS / 5.94:1 egui. Before (#40a02b): 2.45 / 2.62. Mocha (ink = accent-success #a6e3a1, unchanged): mantle+tint 9.02:1 CSS / 8.91:1 egui · base+tint 8.26:1 both. egui path = round(src × 31/255) + dst × (1 − 31/255), rounded per channel.

Why a new role. accent-success is shared by many green consumers (status dots, Tags, fills), so it can't change; text-secondary would meet 4.5 but turns Mocha's green line grey. Mixing toward n1100 (the accent-warning-glyph move) greys the green before it clears 4.5 (30% = #486756, 4.59:1), so the Latte remap mixes toward black instead and stays visibly green.

Adding · failure · narrow window · ko / ja · Needs attention › Register anyway (2026-10-19)

While adding, both buttons are disabled and the primary shows a 14 Spinner + “Adding…” — a second click or Enter cannot start a second install. A failure uses the existing result path (toast, buttons back to rest). In a narrow window the bar wraps: reason on its own line, buttons right-aligned under it; long names and paths wrap anywhere. A plugin found in the plugins folder sits in Needs attention with the same box (no fingerprint in the box; the Signature section below keeps it) and a Register anyway danger button. Not clicking it = it stays unregistered; selecting the row never focuses the button.

Layout spec
busy
Cancel + primary disabled · Spinner 14 + Adding… / Registering…
failure
existing toast · buttons back to rest · verdict stays
narrow
bar flex-wrap · reason flex 1 · buttons margin-left auto
attention
same box (no fingerprint inside) · Signature section keeps fingerprint · Register anyway danger · no auto focus
permissions changed
copy adds the isolation limit · Re-approve flow unchanged

English is the source copy. ko / ja above are the meaning reference for translators (keys: plugins.trust.unverified_title · …unverified_risk · …cause_unknown_key / _missing_pubkey / _unsigned / _signature_error · …trust_key_note · plugins.add.add_anyway · …trust_add_anyway · …adding · plugins.attention.register_anyway · …registering).

Don't say the plugin was reviewed, certified or sandboxed, and don't let Enter on a freshly shown verdict install it.

Add plugin — dashed empty hint · manifest read error (2026-10-07)

Before Verify the slot under the path field holds the empty hint: a dashed 1px border-default box. Renderers without CSS dashes draw it with --tasty-border-dash / --tasty-border-dash-gap (4 / 4) on the straight edges, centred so each edge starts and ends on a dash; the corners stay solid arcs. When Verify cannot read tasty-plugin.toml (missing, parse error) the same box takes the slot with a solid accent-danger edge: alertTriangle + “Can't read tasty-plugin.toml”, then the reader's reason as one mono caption line (untranslated). No fill, no action — the path field above is how to fix it.

Layout spec
empty hint
dashed 1px border-default · radius · pad 14 / 16
dash
4 on / 4 off · --tasty-border-dash · --tasty-border-dash-gap · OFF-SCALE
corners
solid arc; dashes on straight edges only, centred
read error
same box · solid accent-danger edge · no fill
title
Can't read tasty-plugin.toml · body · accent-danger
reason
mono caption · text-muted · untranslated
invalid
read but fails validation (binary path · extras) — same box · title “tasty-plugin.toml is not valid” · reason = validation message (batch 5)
slot width
measure-xl (560) capped by the column — unchanged (batch 5)
action
none — fix the path above and Verify again
Tokens used
border-dash— → size-4border-dash-gap— → size-4border-default— hint edgeaccent-danger— read error

The same dash pair draws the Scripts Add trigger… control (Misc › Scripts). Strings: plugins.add.read_error “Can't read tasty-plugin.toml” · plugins.add.invalid “tasty-plugin.toml is not valid”.

Installed detail — install path · log path · Open folder (2026-10-07)

Last section of the installed detail, after Command (identity · description · error · Permissions · Command · Install path). Open folder moves to the caption row, right-aligned, so the path never competes with it for width: at the 720 minimum and the 880 default the button is always whole. The install path and the log path are mono caption muted, wrap at any character and stay selectable, so nothing is cut and no tooltip is needed.

Layout spec
order
… Permissions · Command · Install path
caption row
INSTALL PATH (mono 10 caps) · flex · Open folder
Open folder
Button secondary sm · folder icon · opens the OS file manager
path
mono caption 11 · text-muted · break-all · selectable
log
same style, “Log: ” prefix, own line
widths
left 380 ≈ 720 window · right 540 ≈ 880 window
shown
installed plugins only
Tokens used
text-muted— path textfont-size-caption— 11 pathspace-sm— 8 row gap

Installed detail — command keycaps per platform (2026-10-10 b11)

A command keycap is the manifest chord split on “+”. On Windows / Linux each part is a word keycap. On macOS the chord first goes through the keybinding mapping (alt → Command, option → Option, ctrl → Control), the modifiers sort in Apple order Ctrl · Option · Shift · Command (⌃ ⌥ ⇧ ⌘), and each one follows the user's modifier display style from Settings › General — the same as the Settings keybinding rows and the modifier hint on that machine. Ctrl stays the word “Ctrl”: it has no display style and no glyph, so no Control icon is added. Manifest ctrl + shift + h and alt + k shown below.

Layout spec
order (macOS)
Ctrl · Option · Shift · Command, then the key (⌃ ⌥ ⇧ ⌘) — kit comment fixed
style
follows Settings › General modifier display style (alt · option · shift) — never forced symbols
Ctrl
always the word “Ctrl” · no icon
other OS
word keycaps, manifest order
Tokens used
kbd-size— keycap

Attention, invalid signature — plugins.attn_desc_hidden (the design key now matches the app's flat attn_* keys; plugins.attention.desc_hidden retired): en “Description hidden — the signature is invalid.” · ko “서명이 유효하지 않아 설명을 숨겼습니다.” · ja “署名が無効なため説明を非表示にしています。”

Keybindings · Import / Export

L2 placement & entry screen

A single L2 section named Import / Export, pinned last under Keybindings (after Plugins). It moves a configuration rather than editing bindings, so it gets a separator above it — a NEW axis on the settings L2 row model (today a row carries only a label + is_plugin); the separator is suppressed while the sidebar filter is active, since filtering breaks the adjacency it describes. The section's list position is not a list: two action rows, each with a one-line description and its own trailing button. Import is primary, Export secondary — import is the path with consequences and the one people come here for; export is a single fire-and-forget write.

While we were here we also corrected the Keybindings L2 roster to the eleven real sections (General · Workspace · Pane · Tab · Surface · Clipboard · Zoom · Explorer · Scripts · Preset · Plugins) plus the new twelfth: the design carried a phantom Image section and was missing Explorer and Scripts.

Layout spec
L2 items
one — “Import / Export”
position
last, after Plugins
separator
1px above the row (new axis)
filtering
separator hidden while filtering
entry
2 action rows, not a ListCtrl
weights
Import primary · Export secondary
export feedback
the window's own toast, carrying the path
file picker
popup on the window's PopupManager
Tokens used
separator— L2 separator + row rulessurface-raised— action row bedborder-default— action row edgesettings-sidebar-width— 200 — L2 column

Why a popup for the file picker and not another drill-down step: the drill-down is already spoken for by the preview (list ⇄ detail), and the same picker serves both directions — Export needs a save target, Import an open target. The settings window already runs a PopupManager for the shortcut-conflict confirm, so this adds a case, not a mechanism.

Don't give export its own result screen. There is nothing to do after a write, so the confirmation is a toast with the resolved path and the user stays where they were.

Import preview — group headers, select column, long-table handling

The detail view keeps the Preset skeleton (back bar with Apply in the right slot, over the diff grid) and grows two axes. Group headers (NEW on this table): one row spanning all columns, carrying a select-all box, the group name in mono micro caps, a changed / total count and a collapse chevron — it does not repeat Action / Current / Imported. Four groups: general combos, quick switch, script bindings, plugin overrides. A select column (32px, leading) makes apply per row; the group box is the group's select-all. The third column header is the fixed word Imported — the file name lives in the intro line, where it can be long without breaking the grid.

Length is handled by both levers: the table opens changed-only (a Show all 73 toggle in the back bar) and every group collapses. At minimum 73 rows — 100+ once slots are expanded — neither alone is enough. Quick switch is summarised one row per axis, not per slot: slots store raw keys and the combo is composed for display, so the axis row's value is the composed range (Ctrl+1…0) with the slot count as a sub-line. That keeps 29 slot rows out of the table and matches where the edit actually happens.

Layout spec
columns
32px select · 1.6fr action · 1fr current · 1fr imported
group header
spans all columns, on surface-raised
group content
select-all · name (mono 10 caps) · counts · chevron
groups
general · quick switch · scripts · plugin overrides
default view
changed only
toggle
“Show all {n}” in the back bar
quick switch
1 row per axis (3), slot count as sub-line
plugin row
command name + agent-dot plugin name, two lines
changed cell
accent-primary, colour only (no bold)
Tokens used
surface-raised— group header bedaccent-primary— changed imported valueaccent-agent— plugin dotseparator— cell rulesletter-spacing-caps— header + group name tracking

Apply writes the draft, Save commits it — the same two-stage contract as Preset, and the intro line says so. The two never share a row: Apply sits in the back bar, Save in the window footer.

The six open values — failure, notices, conflicts, placeholder, tokens (4 · 5 belong to the removed option migration — retired 2026-10-10)

Six things the first pass left blank. The rule behind all of them: a result with nothing to do is a toast; a result with something to do is inline, where the thing to do lives.

1 · Export failure — success stays a toast carrying the resolved path. Failure is an inline danger block inside the Export row (the row that started it), with Try again and Choose another location…; the row's button goes disabled while the block is up, so there is one live retry, not two.

2 · Other bundle warnings — the dropped-override info line stays exactly as it is (muted, no tone, no glyph weight): nothing is wrong and there is nothing to do. Everything that is a warning — newer schema tag, unknown actions, empty groups — collects in one warning block, one line per notice, count in the header, in that fixed order. Three lines show; the rest fold behind Show {n} more. Never a block per notice.

3 · Parsing failure with no line number — the line clause is replaced, not dropped: the middle sentence always says why the file failed. With a position: “parsing stopped at line 1.” Without: “the file isn't TOML.” The first and last sentences never change, so the two read as one message.

4 · Several conflicts — count first, in the card intro: “3 conflicts — those shortcuts are already bound.” The per-row inline reason stays on every row (it names which binding), so the summary never repeats the list and nothing needs collapsing. The summary line appears from 2 up; at 1 the row line alone carries it.

5 · Modifier Select placeholder — Select a modifier, sentence case, UI font (not mono — it is not a key), --tasty-text-placeholder. It is a sentinel first option that leaves the list once a real combo is chosen, and the row keeps its warning-toned Not set trailer. Not an em-dashed pseudo-value like “— pick a modifier —”, which reads as a choice.

6 · Tokens — opened. The raw --tasty-size-* reads in the spec jsx were a tier violation, so they now have names (below). The off-grid 14px card inset was a slip: it snaps to --tasty-space-md (12). Drop the quoted constants and the raw-dimension ratchet and read the tokens.

Layout spec
export success
toast with the resolved path
export failure
inline danger block in the Export row
failure actions
Try again · Choose another location…
info line
dropped overrides — muted, unchanged
warning block
one block, 1 line per notice, count in header
notice order
schema → unknown actions → empty groups
fold
3 lines, then “Show {n} more”
parse copy
line clause replaced by “the file isn't TOML.”
conflicts
count-first line from 2 up; row lines always
placeholder
“Select a modifier” · text-placeholder
Tokens used
settings-content-max-width— 620 — content column cap (NEW)kb-ie-select-column-width— 32 — diff select column (NEW)kb-ie-action-column-width— 288 — action label column + sub-line indent (NEW)kb-ie-from-column-width— 120 — original shortcut column (NEW)kb-ie-slot-height— 24 — replacement slot chip (NEW)kb-ie-slot-min-width— 140 — slot chip min width (NEW)kb-ie-notice-inset— 12 — notice / card inset, both axes (NEW, was off-grid 14)accent-warning— warning block + Not setaccent-danger— failure blocks + conflict count

Why the notices are one block and the info line is not in it. Tone is the sorting key, not topic: a reader scans for "is anything wrong". Merging an unactionable fact into a warning-toned block makes the whole block unactionable-looking; splitting the four warnings into four blocks makes a wall where one thing is needed — read it, then go look at the table.

Don't toast an export failure. A toast auto-dismisses and carries no retry, so the one case where the user must act is the one case that disappears on its own.

Open values — unknown failure reason, counts of one, and the 620 cap

Three blanks left by the first round. An export failure with no known cause keeps the block exactly as decided — only the middle clause changes, from a fixed set (read-only · permission · disk full · catch-all the write didn't finish.), and the OS string is never spliced into the sentence: it gets its own muted mono line below, one line, ellipsised, full text in title. Counts of one get singular forms (1 notice, 1 unknown action was skipped) — English only; ko/ja don't inflect.

Layout spec
clause set
read-only · permission · disk full · the write didn't finish.
OS text
own line, mono 11, muted — never inside the sentence
first / last sentence
never change (same as parse failure)
singular
1 notice · 1 unknown action was skipped (…)
fold link
absent at 1 notice — nothing to fold
620 cap
the settings scroll column, every non-full-bleed subtab
prose
keeps --tasty-measure-md (reading measure, narrower)
Tokens used
settings-content-max-width— 620 — now set once on the settings content columntext-muted— OS reason lineaccent-danger— failure block

Where the 620 cap lives. On the settings content column itself — the scrolling pane inside the Settings window — so every non-full-bleed L2 subtab inherits it, UI-kit subtabs included, and no block carries its own width. Full-bleed subtabs are exempt: they replace the column with their own layout (diff tables, editors). Body prose keeps --tasty-measure-md — a line-length measure, a different axis from the column cap and narrower than it.

Don't build the OS message into the sentence (“… — os error 28: No space left on device.”). It breaks the sentence's grammar in three languages and buries the two parts the user can act on: which path, and that nothing was written.

Win · Super as option — names and notices (2026-10-10 b12)

Key names · OS-reserved caption · text-entry hint

The option token now also means Win on Windows and Super on Linux. They are shown as words, like Ctrl and Alt — no logo glyph (the Windows logo is a trademark and Linux keyboards print different marks). Linux uses Super on every desktop, KDE included, so one config reads the same everywhere. The display-style dropdown stays macOS-only (there is nothing to choose between on the other two). A shortcut typed as text (config file, bundle import, plugin shortcut field) that matches a short known list of OS-owned combos gets a warning caption — the list is a hint, not a guarantee, and the copy says “may”. A text field that receives an OS key name (cmd+k, super+k, win+k, meta+k) gets a dedicated caption saying how to write it.

Layout spec
names
macOS Option / ⌥ (display setting) · Windows Win · Linux Super (all desktops) — words, no icon asset
display dropdown
macOS only
reserved list (hint)
Windows: Win alone, Win+L · D · E · R · I · Tab · Linux: Super alone
reserved caption
warning tone · under the field / one line in the import warning block · never blocks saving
recorded
no caption (the OS key never reaches the recorder)
OS key name
error caption replacing the generic “Unrecognized key” · code runs for the tokens
hold helper
no option binding on Win / Linux → holding Win / Super shows nothing (agreed)
Tokens used
kb-os-reserved-fg— → accent-warningkb-plugin-error-fg— OS key name

i18n — keys.os_reserved.windows “Windows may use {combo} itself, so Tasty might not receive it.” / “Windows가 {combo}를 먼저 쓸 수 있어 Tasty에 전달되지 않을 수 있습니다.” / “{combo} は Windows が使うことがあり、Tasty に届かない場合があります。” · keys.os_reserved.linux “Your desktop may use {combo} itself, so Tasty might not receive it.” / “데스크톱이 {combo}를 먼저 쓸 수 있어 Tasty에 전달되지 않을 수 있습니다.” / “{combo} はデスクトップが使うことがあり、Tasty に届かない場合があります。” · keys.os_key_name “Write Win, Super and Option as `option`, and Cmd as `alt`.” / “Win·Super·Option 은 `option`, Cmd 는 `alt` 로 적으세요.” / “Win・Super・Option は `option`、Cmd は `alt` と書いてください。” (backticks = code runs). The option-migration card and its gallery sample are removed (feature deleted).

Misc · Scripts (Lua script manager)

Scripts subsection — list, states & empty

Settings › Misc › Scripts. A subsection (not a separate popup) that manages user Lua scripts run by a shortcut. Each ScriptRow: the display name, the absolute path (middle-elided — dir tail truncates, filename always shown), a bound-shortcut Kbd badge (or italic Unbound), a peach changed badge + help line when the file's SHA no longer matches the one recorded at registration (TOFU re-confirm on next run), and an Auto-run row: mono trigger chips for the host-lifecycle events the script fires on (click a chip to remove it) plus a dashed Add trigger… control offering the remaining events. Row actions: bind shortcut (→ Keybindings), rename (inline), remove (inline confirm).

Layout spec
home
Settings › Misc › Scripts (subsection)
row
name · path · shortcut · actions · auto-run
shortcut
Kbd badge or italic Unbound
changed
peach badge + help line (SHA mismatch)
auto-run
trigger chips + Add trigger… (13 lifecycle events)
Add trigger… edge
dashed · --tasty-border-dash / --tasty-border-dash-gap 4 / 4 · corners solid (2026-10-07)
trigger menu
min 200 · max 220 then scrolls · --tasty-trigger-menu-min-width / -max-height · menu-* container
all bound
control stays, DISABLED (disabled ink) · tooltip All events already bound
chip
mono event · click removes · hover 12% overlay
actions
bind · rename · remove
empty
glyph + Add script prompt
Tokens used
accent-warning— changed badge + helpborder-default— chip + add-control borderoverlay-active— chip hover / menu opentext-disabled— Unboundfont-mono— path · trigger chips

Two independent run paths: a manual shortcut (bound in the Keybindings tab, shown as the Kbd badge) and auto-run triggers (edited inline here — the script fires when the host emits a bound lifecycle event). A script can have either, both, or neither. The changed state is informational — the script still runs, but re-confirms once (TOFU) because the on-disk file drifted from the registered hash.

Git viewer

Git worktree viewer — read-only popup

Tools → Git. A 960×640 read-only popup that surveys every worktree. A header (Git + Refresh) sits over a context strip (current worktree · branch · HEAD oid · repo path). A left worktree rail (fixed 232px) lists main + linked worktrees as two-line rows — name + type badge, then oid + state badge — so nothing overflows the rail; the right column splits 50/50 into Changes (status) over Commits (log). Every badge is a token-tinted pill (the Tag visual), not colored text. No writes — only Refresh, selecting a worktree (re-binds the panes), selecting a changed file, and the diff Back button.

Layout spec
frame
960 × 640 · dismiss on outside click
header
Git + Refresh · context strip (wt · branch · oid · path)
rail
232px — 2-line rows: name+type / oid+state
right
50/50 split — Changes over Commits
badges
Tag-style pills: main · linked · current · locked · invalid
status
M·A·D·R·?·U fixed-width pill prefix
read-only
Refresh · select · Back only
Tokens used
accent-info— oid · main · refs · Raccent-success— current · Aaccent-warning— locked · Maccent-danger— invalid · D · Usurface-active— selected rowbg-sidebar— section / context strips

Do distinguish "current" worktree (green badge — where cwd lives) from the selected row (surface-active fill — what you're viewing). They can differ: you can inspect a non-current worktree without switching anything.

Diff variant — file row → unified diff

Clicking a file in Changes swaps the bottom pane (log → diff): a Back toolbar + file path, then unified hunks with an old/new line-number gutter. Hunk header in --tasty-accent-info, + in success, - in danger, context primary. Back returns to the log.

Layout spec
trigger
select a Changes row → diff
toolbar
Back + file path (muted/mono)
gutter
old / new line numbers (mono)
hunk header
--tasty-accent-info
empty diff
Back + 'No changes.'
states
non-repo · error · loading · already-open · 0 worktrees
Tokens used
accent-info— hunk headeraccent-success— added lineaccent-danger— deleted linetext-disabled— line-number gutter

Empty/edge states reuse --tasty-text-muted one-liners: "No git repository…", "No changes.", "No commits.", "No worktrees.", "Git viewer is already open." Errors are a single --tasty-accent-danger line above the (still shown) panes.

Diff toolbar — a container height, not a button height

The diff toolbar is 32px tall and holds 28px controls with 2px of air above and below. It now has its own name — --tasty-git-toolbar-height — rather than borrowing --tasty-control-height: the two are different roles and, as it happens, different numbers (32 vs 28), so aliasing them would have been wrong in value as well as in meaning. The value does not change.

Layout spec
height
32 — unchanged
token
--tasty-git-toolbar-height
holds
28px controls + 2px air
not
--tasty-control-height (28, a control's own box)
Tokens used
git-toolbar-height— toolbar containercontrol-height— the buttons inside it

Clipboard viewer

Clipboard viewer — read-only snapshot popup

Tools → Clipboard Viewer (or a toggle shortcut). A 480×360 screen-centered popup that shows what's on the system clipboard right now — a one-time snapshot, not history. Single column: a header (clipboard icon + snapshot tag), a type bar, then a recessed mono code well. The type bar treats a single type as first-class — just a Text badge + live metadata (284 chars · 6 lines · UTF-8); two-plus types expand it into a segmented switch (Text / Image / Files / HTML / Other). No rail. At 5 segments the switch compacts to icon-only (the active segment keeps its label; tooltips carry the names) so it survives 20–40% longer translations inside 480px. HTML is shown as source text only — never rendered; a Pretty print checkbox takes over the type bar's meta slot (metadata moves beside the mime in the footer) and re-indents the same source instantly (0ms). Other buckets every format that isn't text/image/files/html — one row per format: mono name + size, then its textualized content, separated by 1px rules, all of it scrolling in the same well (long content clamps with a muted +N more lines). Read-only — no copy/paste/edit/delete.

Layout spec
frame
480 × 360 · screen-center · dismiss outside
layout
single column — no rail
type bar
1 type = badge + meta · 2–4 = segmented · ≥5 = icon-only
segment
h 26 · pad 0 10 (icon-only 0 8, min-w 30)
metadata
chars · lines · encoding (mono 11, right) — moves to footer on HTML
HTML body
source only, never rendered · Pretty print = re-indent, 0ms
Other body
per format: mono 11 name + size / mono 12 content · 1px rules
body
recessed code well — bg-app · mono 12 · scroll
empty / fail
centered glyph + muted / danger one-liner
Tokens used
bg-panel— framebg-app— code wellbg-sidebar— type baraccent-primary— selected type segmentaccent-danger— read-failed

Don't add any mutate action (copy / re-copy / clear) or a master-detail type rail — this is a one-column snapshot inspector. Snapshot is taken once at open; no live polling, no history. Don't render the HTML payload (no live preview, no styled output) and don't let the type bar scroll horizontally or truncate labels with ellipsis — compact to icon-only instead.

Notifications — popup · fullscreen

Notifications popup — 352 × 400 (2026-10-17)

The existing popup, extended. Shared title bar (centred title, fullscreen fit + close). A 28 toolbar shows the same unread level pills as the LNB and Mark all read. The list is newest first. Each item: level glyph · title · elapsed time · unread dot / body (2 lines) / source · ✓ mark read · Jump. Unread = semibold title + blue dot; read = regular title, muted body. A source that has closed keeps its record, gets a · closed tag and a disabled Jump ("Source closed"). The first frame is live: click a row, ✓ or Mark all read.

Layout spec
frame
352 × 400 · title 28 · toolbar 28 · list scrolls
order
newest first
item
pad 12 / 8 · glyph 14 · title 13 · body 12 · meta mono 10 · 1px rule
unread
title semibold text-primary · blue dot (badge-dot-size)
read
title text-secondary · body text-muted · no dot
long body
clamp 2 · More / Less (only when cut) · row click toggles · expanded = full, wraps, selectable
source closed
· closed tag · Jump disabled, tooltip Source closed
empty
No notifications. · No unread · Mark all read disabled
Tokens used
notifications-popup-height— → size-400notifications-toolbar-height— → control-height 28notification-item-title-fg— → text-primary (unread)notification-item-title-fg-read— → text-secondarynotification-item-body-fg-read— → text-mutednotification-item-unread-dot— → notify-unread → accent-primarynotification-item-closed-fg— → text-placeholdernotification-item-more-fg— → accent-primarynotification-item-body-lines— 2

Fullscreen stage — same list, wide

The fit button moves the same list onto the fullscreen stage. Content, order, actions and read rules are identical; only the body clamp grows to 3 lines and titles get the width.

Layout spec
stage
fills the fullscreen stage · no shadow
body clamp
3 lines
selection
keyboard row = surface-active bed
Tokens used
notification-item-body-lines-wide— 3notification-item-bg-selected— → surface-active

Warning level glyph — Latte contrast, rest and hover (2026-10-27)

The warning glyph is the only thing in a row that says “warning” (no level word in title, body or meta), so it must clear the 3:1 non-text floor on both the resting row (bg-panel) and the hover bed (bg-panel + overlay-hover). --tasty-notification-item-warning-glyph now points at the existing --tasty-accent-warning-glyph instead of notify-warning. Mocha resolves to the same yellow as before; Latte gets the darker warning-glyph ink. Read and unread use the same colour (level is identity, not state). Error and info glyphs, the count badge and accent-warning are unchanged. Popup and fullscreen render the same NotificationItem, so both lists follow.

Layout spec
glyph
14 · alertTriangle · same ink read / unread · rest / hover
ground
bg-panel · hover = + overlay-hover (Mocha white 8% · Latte black 8%)
target
3:1 non-text, specified colours (AA edge pixels not counted)
Tokens used
notification-item-warning-glyph— was notify-warning → now accent-warning-glyph (Mocha = accent-warning · Latte yellow 50% toward n1100 ≈ #966f43)notify-warning— unchanged (accent-warning) — no longer read by the list glyphnotification-badge-warning-bg— unchanged (notify-warning-fill)notification-item-error-glyph / info-glyph— unchanged

Contrast (WCAG relative luminance). Latte, glyph #966f43 on base #eff1f5: 3.99:1 rest · hover 3.35:1 CSS (base × 0.92) / 3.35:1 egui bytes (premultiplied black (0,0,0,20): base × 235/255, rounded). Before (#df8e1d): 2.31 rest · 1.94 hover. Mocha, glyph #f9e2af (unchanged) on base #1e1e2e: 12.91:1 rest · hover 10.20:1 CSS (white 0.08) / 10.21:1 egui (premultiplied (20,20,20,20): 20 + base × 235/255).

Why reuse accent-warning-glyph. It already is “a warning glyph that must clear 3:1 on light grounds” (plugin trust box, notice column), and it clears the hover bed too. A notification-only role would hold the same value. The badge keeps its own fill role (notify-warning-fill), so it is unaffected.

Read rules and arrival

Layout spec
open / close
no read change
per item
✓ · expand
Mark all read
this window's list
attention
existing clear rule after the source's last unread is read
Jump
explicit only · disabled when source closed
arrival
no auto-open · no focus / selection / scroll change

Move & resize — popup interaction spec

Drag handles — TitleBar · Region · None

How a popup moves. A titlebar popup drags from the whole bar (the close button is excluded). A headless popup can declare a Region handle — a header band (currently the left half: icon + title) — while the header's own widgets (search, close) keep priority. None means the popup can't be moved. Affordance is the cursor only (grab → grabbing); no painted grip. Hover the bars below to feel the cursors.

Layout spec
TitleBar
whole bar drags, close excluded
Region
header left band (headless popups)
None
immovable
affordance
cursor only — no painted grip
priority
widget > close > resize edge > drag handle > content
start gate
after content render — widget pointer wins
Tokens used
bg-app— titlebar fill (mantle)surface-raised— popup bodytitlebar-height— bar / band height

Don't tint or outline the drag band over a widget — the widget always wins the press, so a visual grip there only invites mis-clicks. Let the cursor carry the affordance.

8-direction resize — handle map & cursors

Opt-in (resizable popups — today: Listening ports & Remote connections). A band just inside the border holds 8 hit zones: 4 edges + 4 corners (corners take priority). There's no visual grip — the cursor is the only affordance. Dragging an edge moves only that edge (the opposite side stays fixed); a corner moves two. Hover the outlined zones to see each cursor.

Layout spec
zones
N · S · E · W + NW · NE · SW · SE
band
inside the border (token-width)
corner
wins over edge at the crossing
edge drag
that edge moves, opposite fixed
grip
none — cursor only
persist
user size sticks until the popup closes
Tokens used
accent-info— zone labels (demo only)border-strong— popup edge

Once you resize, the size sticks (the sizer won't overwrite it); closing the popup resets it to its default size on next open.

Constraints & z-order — quiet limits, top-most wins

Limits are enforced silently. A popup can't shrink past its min size (or its default size if none is set), and no part may leave the scope rect (Window / Workspace / Pane / Tab / Surface) — drag or resize just stops at the boundary, and a shrinking scope auto-repositions the popup back inside. The scope edge is the effective max (there's no separate max size). When popups overlap, only the top-most one's handles/cursor respond; pressing a popup brings it to front.

Layout spec
min size
min_size, else default_size
max
scope rect is the cap (no max_size)
clamp
no part leaves the scope
reposition
shrinking scope pulls it back in
limit signal
none — quiet stop (no warning color)
z-order
top-most only · press = bring to front
Tokens used
border-strong— scope rect (demo)shadow-modal— popup lift

Don't flash a warning color (peach/red) when a popup hits min size or the scope edge — the limit is communicated by simply not moving. Reserve danger tones for actual errors.