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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
“+ 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
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.
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.