Skip to content

norte v0.3.0-alpha.4

Pre-release
Pre-release

Choose a tag to compare

@compilando compilando released this 15 Sep 10:11
· 405 commits to main since this release

Pre-release. Linux x86_64 only.

Downloads

  • norte_0.3.0-alpha.4_amd64.deb, norte-0.3.0-alpha.4-1.x86_64.rpm, norte_0.3.0-alpha.4_amd64.AppImage — the window (norte-gui) together with the daemon (norte) and the terminal client (ntc), in one package.
  • norte-cli-x86_64-unknown-linux-gnu.tar.xz (norte), norte-tui-x86_64-unknown-linux-gnu.tar.xz (ntc) and their -installer.sh — without the window.

Requirements. Everything here was built on Ubuntu 22.04 and needs glibc 2.34 or newer; the window also needs WebKitGTK 4.1. Each artefact was installed and started in clean containers of Ubuntu 22.04 and 24.04, Debian 12 and 13, and Fedora 41 (ADR 0112). The AppImage expects the graphics and font libraries every desktop provides.

Checksums. SHA256SUMS covers every file; each archive also has its own .sha256. Nothing is signed yet.


Added

  • Your own themes by name. A theme saved as
    ~/.config/norte/themes/<name>.toml is offered by the theme picker, the
    first-run wizard and the settings screen in both frontends, previews live,
    and can be set as [ui] theme = "<name>". The order is fixed: a bundled
    preset first — a stale themes/nord.toml cannot change what nord
    means — then your themes directory, then the value as a path. A file that
    does not parse is left out of the list rather than breaking the picker.

  • norte theme import turns a Visual Studio Code colour theme into one
    of yours: norte theme import OneDark-Pro.json --use writes
    themes/one-dark-pro.toml and sets [ui] theme. It follows the theme's
    include chain, paints it over vscode-dark or vscode-light so what the
    theme leaves undefined is not monochrome, flattens translucent colours over
    the editor background, and refuses a name that a bundled preset would
    shadow. Comments and trailing commas in the JSON are fine;
    tokenColors is ignored. See docs/theming.md.

  • Seti file icons. The file-icons extension has a fourth style,
    seti: the icons Visual Studio Code shows by default, one per language
    where nerd has one per class, so a Python file and a Go file no longer
    look alike. They come from the same Nerd font, so the window's bundled
    subset grew from 18 to 73 glyphs (16 KB) and a Nerd-patched terminal font
    already has them. Twenty more extensions are recognised in every style
    (html, css, vue, svelte, dart, scala, tex, xml…).

  • Alt on its own opens the menu bar. In the window always (bridge 68):
    press and release Alt with nothing in between, as on any desktop; Alt+F4,
    Alt+Tab, an Alt-drag and AltGr do not count, and a dialog in front keeps
    the keyboard as it does for F9. In the terminal behind [ui] alt_menu,
    off by default: a lone modifier can only be reported under the kitty
    keyboard protocol (kitty, foot, WezTerm, Ghostty — not tmux, xterm or
    GNOME Terminal), and in that mode the terminal sends keys rather than
    text: a letter typed with a dead key (é) or a symbol typed with AltGr
    (@, #) arrives as its base key.

  • Column widths by dragging in the terminal too. Drag the separator
    that opens a column in the header; the width follows the pointer and is
    written to [ui.columns] when you let go, the same key the window writes.

  • Scrolling the window no longer flickers. Every update rebuilt the
    menu, panel and key bars, the tabs, the title, the column header and
    every visible row even when only the rows at the edge had changed; what
    paints the same now stays the same node. The overscan grew from 8 to 24
    rows, so a quick wheel gesture no longer shows blank rows at the edge
    while the new ones travel.

  • vscode-dark and vscode-light presets (spec 2026-09-11),
    transcribed from Visual Studio Code's Dark Modern and Light Modern and
    from the editor's built-in colour registry — a VSCode theme JSON is not
    a complete palette, so list.* and scrollbarSlider.* come from the
    registry, not from any file in the include chain. Each preset's header
    records where every role came from and where it diverges: on white,
    VSCode's own error (3.35:1) and warning (3.12:1) fall under the
    4.5:1 that norte requires of a signal you have to read when something
    has gone wrong, so both are darkened and the original values are named.

  • The window paints a theme's file colours (ADR 0108, bridge 66).
    [files.kind] and [files.ext] — half of what a theme file declares —
    had never reached the window: every entry came out the same colour,
    which reads as a broken theme rather than a plain one. An entry's
    colour is resolved by the host against the filename's raw bytes and
    travels in its row, because extensions are an open set and no CSS class
    could name them. Four of Style's six attributes cross; bg and
    reverse stay behind so a theme cannot hide where the cursor is. The
    scrollbar, row hover, widget surfaces and control focus rings now come
    from the theme too, and the pane footer stops painting its text with a
    border colour.

  • Entry colours follow the desktop's colour scheme (bridge 67). With
    theme_light/theme_dark set, flipping the desktop repainted the
    chrome from one variant and left the file names coloured by the other.

  • Ten chrome roles — hover, input-background, input-border,
    widget-background, widget-shadow, badge, scrollbar-slider,
    separator, focus-border and muted — for the surfaces a modern
    editor separates by elevation rather than by borders. A preset is not
    required to define them (Role::CORE is what completeness asserts):
    the window's stylesheet derives each from a colour the theme already
    has, so the eight existing presets gain nothing and change nowhere.

  • The window, polished (spec 2026-09-11). Bundled typography —
    JetBrains Mono for cells, Inter for chrome, 14 px on 22 px rows — with
    [ui] font/mono_font/font_size still in charge; column widths by
    dragging a header's edge, written to [ui.columns] width and honoured
    by both frontends, with the terminal's discard rule when the name would
    fall under ten cells; keycaps on the key bar, small-caps headers, an
    accent on the cursor, a mark checkbox on hover; breadcrumbs in the pane
    title, a toast for notices, pills for persistent warnings, a two-pixel
    gauge of the volume's usage in the footer; [ui] theme_light /
    theme_dark follow the desktop's colour scheme live, and a theme's
    [effects] backdrop = "blur" blurs what lies behind a dialog (the
    default preset asks for it). Bridge 64 and 65.

  • A thumbnail plugin kind (ADR 0107), in its own WIT package
    norte:thumbnail@0.1.0 so no installed guest needs a rebuild. A guest
    lists mimetypes like a previewer, gets the file's bytes (capped at 8 MiB)
    and the longest edge allowed, and answers a PNG/JPEG/WebP raster that
    the plugin-host verifies — magic, declared mimetype, dimensions, edge —
    before it crosses. plugin.thumbnail (protocol 0.73.0) carries it; the
    window's viewer asks for one when it has no picture of its own and paints
    it labelled «via ‹plugin›». org.norte.image-thumb is the first guest:
    a photo too big for the viewer's cap now gets a picture.

  • Plugins parametrised. file-icons paints one-cell Nerd Font glyphs
    (style = "nerd", bundled in the window as a 3.8 KB subset) and takes
    dir-icon and unknown-icon; two new columns plugins, size-bar (a
    █░ bar per file; scale, width, relative-to) and age (a glyph
    per bucket and a short figure; thresholds, glyphs, format);
    git-status takes glyphs (letters or symbols) and ignored.

  • The chrome of an orthodox manager, derived and configurable (ADR
    0106). A function-key bar on the last row of the terminal and a strip at
    the bottom of the window, read from the keymap of the screen that owns the
    keyboard; the panel bar names its buttons with the access letter
    underlined; every listing carries a footer with counts, what is marked and
    the free space of its volume; a dialog's key line is painted as clickable
    buttons; a status notice expires into the log after eight seconds and
    leaves a !n badge that opens it; the modified column prints local time
    with the precision the distance asks for (smart); the palette reads the
    human label first and keeps the last five commands on top; the focused
    cursor takes the theme's accent and the other pane's stays grey. Six
    [ui] keys (key_bar, panel_bar_style, pane_footer, date_format,
    notice_seconds, dialog_buttons), each a row on the settings screen and
    a section of the new appearance help page.

  • A first-start wizard. With no norte.toml of your own, both frontends
    ask three things once — which file manager you have in your fingers, which
    theme (previewed live), and whether your terminal shows icons — and write
    the answers as configuration. Esc keeps the defaults and never asks again;
    ntc --setup asks again; NORTE_NO_WIZARD=1 keeps it closed.

Changed

  • F9 opens the menu in six presets, as in mc, FAR, Norton Commander and
    Total Commander; the theme picker moves to Alt+9. Krusader keeps F9 as
    the terminal, as its source attests.
  • The mtime column is 12 cells (was 10), to fit 09-10 14:02.
  • A plugin may name a meaning, not a piece of chrome (ADR 0108,
    amending ADR 0037). role on a span or a decoration is validated
    against Role::REQUESTABLE — what a piece of content MEANS — rather
    than against every Role. The window's chrome and its state
    (selection, status-bar, mark…) are no longer requestable: a badge
    in the cursor's colour would lie about where the cursor is. A
    non-requestable name degrades to None, exactly as an unknown one
    already did. No WIT bump; the twelve bundled plugins are unaffected.
  • The window's bridge is version 67.

Fixed

  • A lua: key in the window says it is not available. Lua runs in the
    terminal frontend only (ADR 0110), but a lua: binding in your keymap
    layer was offered by the window's reference sheet, which-key and palette,
    and pressing it did nothing. It is now "not available here", like any
    other command the window does not have; ntc runs it as before.
  • A double click opens a directory in the window. The renderer waited
    for the engine's own dblclick, the only door into a directory with the
    mouse; it now counts two presses on the same row itself, the way the
    terminal does. And the host no longer refuses an activation that names a
    pane other than the focused one: it focuses it first, so a double click
    on the pane next door works.
  • A column a plugin contributes is named by its manifest. The header
    every manifest declares was parsed, carried over the wire and read by
    nobody, so the listing showed the column's id (acme.git/status). Both
    frontends now install the catalogue's label into the shared model, under
    the user's [ui.columns] header and above the id.
  • The terminal's extension manager answers the mouse. It was the one
    overlay where a click did nothing: no row selected, and none of the
    buttons the window has. The detail pane now opens with a row of buttons —
    enable or disable, approve or revoke, settings, uninstall, and help when
    the extension ships a page — each firing exactly the command its key
    fires. Clicking a row selects it, clicking the selected row opens its
    settings as Enter does, and the wheel moves the cursor. Choosing another
    row with a plugin's settings open closes them, so the pane never shows one
    extension and the settings of another. The keys were always there and
    still work.

Changed

  • The menu bar has ten groups, by what the reader wants to do. File
    (what reads a file: view, edit, open, properties, size, copy the path,
    quit), Operate (what writes: copy, move, rename, batch and AI rename, new
    folder, delete, permissions, pack, unpack, test, split, combine,
    checksums), Mark, Go (parent, back, forward, history, hotlist, volumes,
    connect, disconnect, refresh, command line, terminal), Panels, Tabs, Find,
    View (what the listing shows and the side panes), Tools (extensions,
    agents, settings, profiles, the palette) and Help. Every built command is
    in exactly one menu — 32 of them were in none: permissions, packing,
    checksums, the batch rename, the hotlist, the history, the agents… were
    keyboard- and palette-only. In the terminal the bar tightens to one space
    between titles when ten do not fit in the width, instead of dropping the
    last one.

  • An upper-case letter under a modifier is labelled Shift. alt+C
    printed Alt+C in the menus, the palette, the help and the reference
    sheet, and nothing said the case mattered — while alt+c is another
    command. It now prints Alt+Shift+C everywhere the chord is painted; the
    stored chord is unchanged. And mark.files moves from alt+F to alt+f
    in the orthodox, vim and cua presets, because the lower case was free: a
    Shift that buys nothing is a Shift the reader should not have to press.
    The imported presets keep their transcribed chords.

  • The terminal's extension manager has the window's detail pane. Two
    columns when the terminal is 64 cells or wider: the list on the left,
    compact — name, version, ✓ or «not approved» — and the selected extension
    on the right: version · publisher · category, its state as two facts, its
    description, its capabilities as chips, how many commands and columns it
    brings and whether it ships help, then its settings table when opened with
    Enter — inside the pane now, with the cursor and the key's description —
    and the commands it contributes. Narrower terminals keep the single list
    with the settings in their own box. Same keys as before; the decisions
    behind the two managers were already the host's and the terminal's shared
    ones, only the screen differed.

  • A daemon a frontend started stops with its last client. The window,
    ntc --daemon and a one-shot norte --daemon … start a daemon when none
    answers, and it used to outlive them by five minutes: a process nobody
    could see and nobody had asked for. They now start it with
    --idle-timeout 2, so two seconds after the last client disconnects — with
    no task running — it exits; a client that reconnects within that margin
    finds the same daemon, and a second client keeps it alive. norte daemon run by hand keeps its five minutes. The argv is built once, in the SDK
    (daemon_run_argv), instead of in four places.

  • A detached window says so quietly, and explains itself on demand. A
    terminal that started while another window held the session greeted the
    reader with «another window owns the session; this one runs on its own» in
    the message bar — jargon to anyone opening norte for what they thought was
    the first time, and gone at the next key. Now nothing is announced: the
    persistent indicator in the status bar is the whole signal, shortened to
    session not saved (plain text: the badge now drives a mouse hit-test, and
    this frontend keeps its badges ASCII for exactly that reason), and it
    appears and disappears by itself as ownership
    changes (a daemon handover no longer produces a message each way either).
    Clicking the indicator opens the help on the panes page, which gains a
    section on the session: who keeps it, the three reasons a window may not be
    the one, and that no file is at risk. The window shows the same indicator in
    its status bar — it showed nothing at all before, so a detached window closed
    and lost every panel's place in silence (ADR 0077).

  • A terminal modal can have a hierarchy now (ADR 0103), and the copy
    dialog is the first to use it.
    A modal's body was ONE string painted as a flat
    paragraph, so the editable field, the paths, the hint and the keys all came
    out in the same colour and the same weight: the last thing you found was the
    only thing you could touch. Each line now declares its ROLE — data, label or
    hint, destination, field, warning, error — and the theme decides how it is
    painted. A modal that declares nothing looks exactly as it did, so the 26 of
    them migrate one at a time.
    In TransferName: the destination stands out and the source dims, the label
    sits ABOVE the field (it was below — you read the name and then found out
    what it was), and the field is painted as a field, its background running to
    the border. The source now shows the DIRECTORY instead of repeating the file
    name, which appeared twice in a five-line body. Labels «From»/«To» replace
    the arrow: → is legitimate inside a name and is not masked, so
    docs → /home/BURN manufactured a line that reads as two paths — that is
    the corpus's arrow_join_spoof fixture, and what the host already did in
    DialogView::destination. A test finally ties the declared height to the
    body that gets composed: the module's own rustdoc had warned from the start
    that the two halves drift apart and the modal gets clipped, and nothing
    checked it.
    Two more things reviews caught. A rename now names the file it renames:
    a rename opens with to_dir = from.parent(), so showing only the directory
    made «From» and «To» identical and the name being changed vanished from the
    screen the moment you typed — confirming a mutation whose operand is not
    visible, which is what ADR 0070 forbids. And ConfirmTransfer loses its
    arrow too
    : it marked its destination with → directly above a list of
    somebody else's file names, and dropping ⟨file⟩ made that line cheaper to
    forge — slash homoglyphs (U+2215, U+2044, U+FF0F) are legal on ext4, APFS
    and NTFS, so → ∕srv∕publico is a legal file name that renders a complete
    destination line. What distinguishes it now is its ROLE, which a name cannot
    write. Both spoofs are in the corpus.

  • A modal's height is derived from its body, and modal_height — a table
    of 131 lines of hand-written formulas, one per variant — is gone. This
    module's own rustdoc had warned from the start that the two halves drift
    apart and the modal gets clipped; when a test was finally written for one
    variant, it turned out the formulas did not even agree with each other:
    some added 2 to the line count, some 3, some 4, and TrustHostKey declared
    9 fixed rows for "five lines". Deriving it makes the drift impossible —
    there are no longer two numbers that can disagree. The one modal that lets
    ratatui wrap its body still declares its height by hand, because counting
    wrapped rows needs ratatui's own rule (Paragraph::line_count knows it, but
    it is an unstable feature and is not worth turning on for one modal); a test
    now checks that its message reaches the screen.

  • tail_window budgets in CELLS, not chars. Fifty chars of CJK are a
    hundred cells, so a Japanese name overflowed its box anyway and the overflow
    ate the cursor at the end — you kept typing and the screen stopped changing.
    It affected all six free-text fields; the first snapshot of the transfer
    modal is what made it visible.

  • ⟨file⟩ stops announcing itself on local paths. It is the default case
    — this machine, this disk — so its label distinguished nothing at all, and
    it was painted on every path of every listing, header and modal, spending
    eight columns exactly where room is scarce. What informs is the scheme that
    is NOT the usual one: sftp, s3, zip and friends still say so, and a
    file WITH an authority does too — that one is another machine.

Added

  • File icons are a column left of the name, folders included (ADR 0105,
    protocol 0.72.0, bridge 62, WIT norte:plugin 0.10.0). A decorator's
    manifest now says which slot it fills — icon, a fixed-width column left
    of the name, or badge, the git-status place right of it — and a row can
    carry one of each from two plugins, where before the first plugin silenced
    the second. The column opens for the whole listing the moment one row has
    an icon, so names stay aligned, and the terminal's header moves with it.
    decorate receives each entry's kind along with its name, so
    file-icons gives folders the folder icon whatever they are called, links
    the link icon, and files what their name says — spreadsheets and slides
    included. The package bump means every installed plugin is rebuilt
    (just plugins force) and re-approved in the manager; a guest built
    against 0.9.0 is listed as such with both versions.
  • An extension is uninstalled from the manager, on both frontends (ADR
    0104, protocol 0.71.0). plugin.uninstall does what norte plugin uninstall did on disk — delete the directory, leave the state switched off
    and unapproved — and then what the CLI could not: the daemon forgets the
    plugin in its in-memory registry, so it stops being listed, and stops
    decorating listings, at once instead of at the next restart. Human
    connections only, like approving. The manager binds it to dialog.remove
    (d in the bundled presets; the imported four inherit the dialog block) and
    always asks first, naming the extension and saying the two things that go:
    its files, and its approval — a plugin installed later under the same id
    starts unapproved. Cancelling sends nothing.
  • The window's extension manager has a detail pane and buttons (bridge
    61). Two panes: the installed extensions on the left — name, version, a
    state pill that says both facts, publisher and category, description,
    capabilities as chips — and the selected one on the right, with Approve
    or Revoke, Enable or Disable, Help when it ships a page, and
    Uninstall, followed by its settings sheet or a hint saying how to open
    it. A header counts what is installed and what is on. The buttons carry no
    logic: each sends the row and the change, and the host walks the same path
    the key does — approving opens the consent dialog that enumerates the
    capabilities, enabling an unapproved extension is refused with the same
    message (the button is disabled and its tooltip says why), uninstalling
    asks. Help closes the manager and opens the help at that extension's page,
    as F1 over the row does in the terminal.
  • The window writes settings (bridge 60). F11 in the window was a showcase:
    it listed the shared registry with each effective value and told you it did
    not write. Now enter (or a double click) does what it does in the terminal:
    a boolean, an enumeration, the theme or the keymap preset cycle to the next
    value and are written at once; a text or a number opens the window's
    one-field prompt, prefilled with the current value, and the shared editor
    validates it — a font size outside [8, 32] is refused with the range and
    writes nothing. The write goes to the layer the window already writes
    (the active profile, else the user's), off the actor, and the configuration
    is re-read and applied through the SAME path a profile switch uses: theme,
    keymap, columns, favourites and layout change without restarting; what that
    path cannot apply (language, fonts, reduced motion, and what is fixed when a
    pane is created) keeps its "restart required" badge, and the saved message
    says so. SettingsView.read_only is gone from the bridge: it was a phase-4
    promise and no longer true. Out of scope, and said in the parity
    classification: the terminal's search filter over the settings, and the
    plugins section, which is informational in both frontends.
  • The viewer scrolls sideways (viewer.left, viewer.right, bound to
    left/right in all seven presets and to h/l in vim, both taking a
    count). The viewer does not wrap, so a minified HTML file, a wide CSV or a
    log had its right-hand half nowhere at all: painted clipped, unreachable.
    The cut is made once, in the shared model, on the already-rendered line, and
    it counts CELLS of terminal — by bytes the text jumps at the first accent,
    by characters any line with CJK misaligns against its neighbours. A wide
    character straddling the cut goes entirely and leaves its cell blank —
    dropping it without a filler slides that row one column against its
    neighbours, and the grid is the whole point. A zero-width mark that would
    open a row is dropped, as the truncators beside it already do with a tail: a
    split ZWJ cluster would otherwise paint a glyph that is not in the file. The
    stop is the longest line, measured when decoding, leaving one column always
    in view. The hex dump scrolls too, with its own 77-cell width: in a split
    pane its ASCII gutter did not fit, so refusing the axis made it unreachable.
    The status line and the window's header marks say the column in words, which
    in the docked viewer is the only thing that says the view is shifted.
  • The viewer says there is more, and the wheel moves it. Both scrollbars
    in the terminal, drawn over the frame's borders and never when everything
    fits; the coupled preview gets only the vertical one, because its bottom
    border carries the line that says WHAT is being read. The wheel reached
    neither the full-screen viewer (it sits behind the overlay cutoff) nor the
    coupled one (its slot is not a listing, so the hit test returned nothing).
    Bridge 59 carries total_cols/first_col and the viewer_scroll action.
  • A hook may write a sidecar (ADR 0101, protocol 0.70.0,
    norte:hook@0.2.0). A hook plugin can now return write-sidecar: a file
    with one of the exact names its manifest declares in
    fs-write = { sidecar = [...] } — the only form fs-write takes; the old
    reserved "scoped" is rejected — written by the core as a plugin actor
    in the parent directory of the event, through the policy engine (a rule
    actor = "plugin", action = "deny" stops it, and the reader is told once
    with the new plugin.notice kind effect-denied; an ask rule on a
    plugin is a deny) and through the journal (created; replace trashes
    the previous file first, so its content has a way back; a directory with
    the name is never touched). Rows a plugin writes never come back to any
    hook as events. Approval shows fs-write:<name> badges. org.norte.rename-log now keeps a
    .norte-renames.log next to what it renamed, carrying the previous log
    forward. A 0.69 client ignores the new notice kind.
  • Operation hooks: a plugin can observe what the journal recorded, and
    say so
    (ADR 0100, protocol 0.69.0). A new plugin kind, hook, with
    its own WIT package norte:hook@0.1.0: the manifest names the journal
    events it listens to — after-created, after-removed, after-trashed,
    after-renamed, after-mode-changed, a closed vocabulary — and the guest
    receives the committed entries in batches and may return one kind of
    effect, notify(text). There is no before-* and there is no veto: what
    decides whether a mutation happens is the policy engine, and a hook sees
    the entry after it is durable. The source is the journal's commit path,
    so a hook fires for a human's rename, an agent's, a batch and an undo
    alike, from the daemon and from an embedded ntc. The dispatcher runs off
    the critical path with a bounded queue, tells the guest how many events
    it lost, and three consecutive failures switch that plugin's hooks off and
    say so (disabling the plugin re-arms them). A hook may not declare net,
    is shown nothing under norte's own state directory, and its events appear
    at approval as hook:<event> badges. The sentence reaches both frontends
    as the new plugin.notice notification (humans only, kind ∈ {notify,
    hooks-disabled}), masked, capped, rate-limited and prefixed with the
    plugin id. Declaring a hook no longer rejects the manifest; an unknown
    event does. First hook: org.norte.rename-log (plugins/rename-log,
    just plugin-rename-log), which says how many files a rename touched. A
    0.68 client ignores the notification: the hook ran, its sentence reached
    nobody.
  • Every binary says which build it is. ntc --version, norte --version
    and ntc-gui --version print the workspace version followed by the tree's
    git describe (0.3.0-alpha.3 (v0.3.0-alpha.3-10-g674b0eb9-dirty)); the
    TUI shows the same line in the frame of the help screen (F1) and the window
    carries it in its title. The revision is fixed at compile time by
    norte-frontend's build script, falls back to NORTE_REVISION for
    packagers and to unknown without git. On a development machine just link
    points ntc at target/debug, and «0.3.0-alpha.3» was the same string ten
    commits after the tag: now the binary tells you.

Fixed

  • Switching a decorator off in the manager left its icons and badges on
    the rows
    until the next cd, on both frontends, and the reader concluded
    that switching off does not switch off. Any governance change — approve,
    revoke, enable, disable, uninstall — and any plugin setting written now
    make every open listing forget what the plugins said and ask again; a
    batch already in flight lands with an older generation, is dropped, and
    asked again. The window's rows go bare at once and fill back in; the
    terminal's event loop drains a flag the manager raises.
  • The terminal's initial listings carried no icons or badges until the
    first cd.
    Both panes are built at startup outside the path a cd
    takes, and only that path asked the decorators; a freshly opened ntc
    showed bare rows and the reader concluded the plugin did not work. The
    event loop now requests decorations for the initial panes through the same
    function every cd uses.
  • F10 and q close the window. app.quit was classified as "not
    applicable to a window — the window manager closes it", which left the quit
    key of all seven presets, and the menu's own "Quit" entry, doing nothing.
    It now asks to close through the same path as the close button, with the
    same [ui] confirm_quit question. With it, no command the orthodox preset
    binds is left dimmed in the window's key sheet.
  • A menu's dropdown hung one title too far to the right. Its left edge
    was a guess in cells — 12ch per title — while the titles are painted with
    pixel padding and do not measure the same, so the error added up towards
    the right until "Help" opened under the next title. The renderer now
    measures the painted title and hangs the list from it; the cell count stays
    only as a fallback.
  • The dialog field lost the keyboard on every key. Each key sends
    dialog_input, the host answers with a patch, the dialog box is rebuilt and
    the reused input is MOVED into the new box — and moving a node takes it out
    of the document for an instant, which drops its focus. Deleting a digit in
    "Font size" left the field unfocused and the next key went to the host as a
    chord. The renderer now gives the focus back once the new box is mounted.
  • The window's setting prompt was painted under the settings. enter on
    a text or number row opened the one-field prompt, but #dialogs came
    before #settings in the document and this window has no z-index
    anywhere: the veil darkened and no field appeared, while the invisible
    dialog kept the keyboard. Dialogs now come after every selector and panel,
    before only the help and the fatal notice, and a test pins that order
    against index.html.
  • "restart required" was on sixteen of eighteen settings rows. The window
    re-reads the whole configuration after a write, and almost everything is
    read at the moment it is used — the bars on every snapshot, the editor and
    the diff tool when launched, the search mode when searching, confirm-on-quit
    when quitting — so it changes at once. The badge now marks only what the
    host resolves once at startup (language, fonts, reduced motion) and what a
    pane fixes when it is created (hidden files, the .. row).
  • F10 and q quit from inside a side panel. With the keyboard in the
    tree, the places sidebar, the processes panel or the log, app.quit was
    not in the panel's allowlist, so the panel swallowed it and only Ctrl+C
    got out. The consequence was worse than a dead key: the reader pressed
    F10, nothing happened, closed the terminal window believing the program
    had exited, and the ntc stayed alive holding the session lock — every
    ntc opened afterwards started detached and saved nothing, for as long as
    the ghost lived (a week, in the case that surfaced this). Quitting now goes
    through the same chrome funnel as the menu key, honouring
    [ui] confirm_quit exactly as it does from a listing.
  • The window remembers its panels. Open the places sidebar, pick a layout
    template, split a pane, close the window, open it again: everything was back
    to the configured layout. The window never wrote its layout tree into the
    session and never read one from it — a comment justified that by citing
    ADR 0058 D5, which is about screen size not rewriting a stored tree, while
    D8 of the same ADR asks for exactly the opposite: close one frontend, open
    the other, carry on where you were. And the window only wrote the session
    at all on shutdown, and only when the close reached the host in time: the
    shared write policy was instantiated and never ticked. Now the window keeps
    its tree under the same session key as the terminal (default, or the
    active profile's name), applies the saved one at startup above --layout
    and the configuration — the terminal's order —, writes the session every
    second like the terminal when something changed, and writes it AT ONCE after
    any change of the tree: a panel toggled, a template chosen, a split, a
    resize. A detached window still writes nothing. The age seal of each slot is
    now remembered between writes, as in the terminal; stamping every capture
    with "now" would have made every tick a write.
  • The window's viewer header was invisible on a light status bar. The
    theme projection carried status-bg and never its foreground, so anything
    painted on top had to GUESS the text colour — the viewer header guessed
    title-fg, which on a theme whose status bar is light is light on light:
    the path, the encoding, the EOL, the lossy mark and the column all
    disappeared. Role::StatusBar is a PAIR, and the terminal has always used
    it as one. status-fg now crosses with it, and a test pins the whole key
    list — it is an agreement with a stylesheet that shares no types, so
    dropping one has to turn red rather than be found by looking at the screen.
    Found by painting the window, not by a test.
  • And the scrollbar track was a solid band across the window, for the same
    reason: it used status-bg. A track is chrome — what informs is the thumb —
    so it takes the dimmed border, the same pair the terminal uses.
  • An image in the window's viewer had lost its height, and grew bars that
    described a hex dump nobody could see.
    One branch of the image painter
    rebuilt the whole frame where the other only replaced the body, so the new
    canvas was thrown away; it replaces the body in place now, like its twin.
    No bars over an image: the counts describe the hex view underneath, and the
    picture is scaled to fit — there is nothing to scroll.
  • A double click on a file does something again, in the terminal. On a
    file nav.enter does not navigate: it resolves the desktop's program and
    leaves it armed for whoever owns the terminal to launch. The mouse arm ran
    the command and never launched what it armed, so double-clicking a .jpg
    did nothing at all and did not say why — while Enter on the same row
    worked. on_mouse's own rustdoc had promised "launch the opener a double
    click resolved" since it was written. The fix is that all three mouse arms
    now leave through the same function, so none of them can forget the third
    thing again. The window was already right: its double click goes through
    the host, which opens a local file externally.
  • The tree follows the panel that navigates (ADR 0102). Opening the tree
    and walking around left the panel pointing at the folder you were in when
    you opened it. Not a loose wire: both frontends anchored at open and never
    again, on purpose, because anchoring EMPTIES the tree and re-anchoring on
    every cd would have closed every open branch. What was missing was the
    ability to reveal without emptying — Tree::follow expands the ancestors
    and moves the cursor, and a sibling branch you opened stays open. It is
    wired into the one funnel each frontend already had for a cd, plus the
    focus change, and not into each gesture: that list went stale once already,
    which is why the funnels exist.
    The rule is about the ROOT: what has been read stays valid as long as the
    new root is an ancestor of the old one. So going UP a level (Backspace) and
    alternating between two sibling panels with Tab move the root up and keep
    every branch, where before each of them emptied the whole tree on every
    press — which made the tree useless with the two gestures it most needs to
    survive. Only another provider anchors and empties.
  • Tab reaches the third listing, and stops only at listings (ADR 0102).
    In the terminal it was focus ^= 1, a count of two: after alt+v split a
    panel the key silently did nothing from the third one, because PaneSlots
    clamps out of range instead of panicking. In the window the opposite —
    pane.switch shared an arm with layout.focus-next, so with the places
    bar, the tree and the viewer open it took five keystrokes to get back to the
    listing beside you. Now they are two rings: pane.switch is "the other
    panel" and cycles the listings, layout.focus-* walks the whole screen.
    Tab still takes you out of a side panel.