Changelog
All notable changes to ashlar are documented in this file.
The format is based on Keep a Changelog, and this project follows Semantic Versioning. Before 1.0, a minor version (0.x) may contain breaking changes.
Unreleased
Added
- Documentation site (
site/, mdBook, deployed at https://ashlar.himanshuat.com): a user guide (first app, documents, commands, views, jobs, native features, agents, testing, web, a cookbook, an FAQ, building with an AI assistant), the developer docs and this changelog, plusllms.txt,llms-full.txtand each page’s markdown for AI agents.
0.2.0 - 2026-11-11
Added
-
cargo xtask release <x.y.z> [--date YYYY-MM-DD]: bumps the workspace and internal crate versions together, dates the changelog’s “Unreleased” section, updates the compare links and refreshes Cargo.lock (docs/releasing.md step 2). It never commits, tags or publishes. -
ashlar_ui::sidebar(IMP-5.14): a source list of folding sections with counts and rows with an icon, detail and badge, built onlist::Rowand virtualized. One Tab stop: arrows, Home/End, Left/Right to fold, Enter to activate, F2 to rename in place, type-ahead; click, double click and context clicks reported inSidebarResponse. Automation ids<id>,<id>.section.<s>,<id>.section.<s>.item.<i>,<id>.rename; labelsUiLabels::sidebar(ui.sidebar.rename,ui.sidebar.items, English and German).list::Rowgainsicon,detail,badgeandfocusable. The gallery has a Sidebar page. -
CodeView and DiffView follow-ups (IMP-5.20): unwrapped long lines scroll sideways (horizontal wheel or Shift+wheel, and a draggable bar along the bottom; a split diff’s sides move together); both views register the new automation role
Role::Document("document"inui.inspect). The gallery has a Code page. An open dialog no longer copies its shapes for the exit animation on frames where they did not change, and reuses the copy’s buffer when they did (perf_dialog_open_frame_allocs: per-frame overhead over a plain area 173 → 21 allocations, 24.9 → 5.5 KB). -
Controls follow-ups (IMP-5.19):
controls::Skeletonplaceholder rows (pulse while visible, static and frame-free under reduced motion, a busy progress node named by the newUiLabels::controls.loading, English and German);content::Accordionkeeps at most one section open;Segmented::disabledandRadioGroup::disableddim options that cannot be picked and that the keys skip; the⇧keycap is drawn as an outlined arrow (the default fonts lack the glyph). Visual regression adds reference-painter pairs for the progress circle and the spinner’s static poses. -
Toasts (IMP-5.17): F6 moves keyboard focus into the newest toast (its first action, else its close button) and back to where it was;
Toasts::take_actions_wheredrains only the actions a caller accepts;App::run_toast_actions(true)makes the runtime run a clicked toast action whose command is registered (other actions stay for the view). The gallery’s Overlays page demos action and progress toasts. -
ashlar-rs: thetrayandhotkeyfeatures pass through toashlar-app, so an app on the umbrella crate can useApp::trayandApp::global_shortcut. -
Windows menu shortcuts (IMP-6.17):
NativeMenuBar::attach_windowsubclasses the window and runsTranslateAcceleratorWwith the bar’s table on key-downs, so a native menu’s key equivalents fire as item activations (once; the key press doesn’t also reach the window). Compile-checked forx86_64-pc-windows-msvc; not run on Windows yet. -
Context menus and drag-out (IMP-6.15):
ashlar_native::menu::popup::show(window, &spec, at, waker)opens aMenuSpecas a native context menu (macOS: posted to the run loop so it never opens inside the UI pass; Windows: opens at once, call it between frames) and reportsSome(id)orNoneon a receiver.ashlar_native::drag_out::start(window, DragItem { files, png, text }, on_end)(featuredrag-out, infull) drags files, a PNG or text into other apps on macOS;Unsupportedelsewhere. -
ashlar_native::mac_shell(default featuremac-shell, IMP-6.14):guard_appearance()checks the window delegate’seffectiveAppearancechanges before winit casts them, so a nil appearance while the screen is captured no longer ends the app;listen_reopen(waker)reports Dock reopen clicks;quiet_requested(prefix)reads<PREFIX>_QUIET. An ashlar app installs both after its first frame, shows and focuses its window on reopen, and with<PREFIX>_QUIET=1opens without taking the focus. -
ashlar_native::hotkey(featurehotkey, IMP-6.16): system-wide shortcuts on global-hotkey (macOS, Windows, Linux on X11).GlobalShortcuts::new(waker),register(id, &chord)with the keymap’s chords (a chord without Ctrl, Alt or ⌘ is refused),unregister,drain(); dropping it releases every chord.system::bring_to_front(window)restores and focuses a window through the OS (Windows borrows the foreground thread’s input when the foreground lock refuses), and a second launch on Windows now callsAllowSetForegroundWindowfor the first (its pid travels in the endpoint file).App::global_shortcut(command, chord)(featurehotkeyofashlar-app) runs the command and brings the window forward; forwarded launches and tray clicks also bring it forward through the OS. -
ashlar_native::tray(featuretray, IMP-6.11): a tray / status-bar icon with a menu built from aMenuSpec(macOS status bar, Windows notification area, Linux StatusNotifierItem over D-Bus with no GTK).Tray::new(TraySpec { id, tooltip, icon, template, menu }, waker),events()(Activate,Menu(id)),set_tooltip/set_icon/set_menu; dropping it removes the icon. The menu bar, trays and later popups now share one process-wide menu-event router, so a tray’s items never reach the menu bar.App::tray(spec)(featuretrayofashlar-app) runs the chosen item’s command and shows the window on a click. -
cargo xtask launch --memorylaunches 5 times (--runs N) and prints the min and max idle and peak footprint and how many runs fell in the high group; one launch landed in either of the two macOS groups at random (IMP-2.19). -
ashlar_native::memoryreports the OS’s lifetime peak footprint (ProcessMemory::peak: macOS lifetime max phys footprint, Windows peak private commit) andthread_count()(macOS, Windows, Linux);app.stats→process.peak_bytesuses the OS peak, so spikes between samples count, andprocess.threadsis filled on macOS and Windows too (IMP-6.18). -
ashlar_native::display::monitors()lists the monitors before any window exists (macOS, Windows); a restored window placement is checked against them at launch instead of only on the first frame (IMP-6.18). -
Web (IMP-6.8):
App::canvas_id; when a web app cannot start (no such canvas, no WebGL) a visible error (role="alert") appears next to the canvas or at the top of the page;examples/web-hello(with anindex.htmlfor trunk) is checked for wasm32 bycargo xtask wasm. -
i18n follow-ups (IMP-6.19): plural forms for the toast “+N more” line and the keymap editor’s “N commands share a shortcut” (new key
ui.keymap.shared), throughlabels::Plurals,labels::install_i18n,labels::count_textandlabels::PLURAL_KEYS(the label structs are unchanged);ashlar_native::system::preferred_languages()(macOSNSLocale.preferredLanguages, WindowsGetUserPreferredUILanguages), whichashlar_i18n::system_locales()now appends after the environment’s languages in apps (set_native_languages,with_native). -
Record and replay (IMP-4.11):
ASHLAR_RECORD=path(orRuntime::record_to) records each frame’s time, screen and input events plus the control requests served to a JSONL ring of at most 64 MiB (written off the UI thread), andDriver::replay(path)plays it into a fresh driver; see docs/debugging-uis.md. -
Cxpolish (IMP-3.8):cx.tokens();cx.motion(id, target, curve), which animates unless the document just changed by the user’s own hand;Origin(User,Drag,Undo,Agent,Job) recorded per change (Session::origin,Session::with_origin,Cx::origin), with control requests markedAgentand job appliesJob;ashlar_ui::layout::{row, column, inset}taking atokens::Space,SpaceScale::get;Radii::{sm, md, lg}()asf32. -
Telemetry incident rules (IMP-2.13):
ashlar-traceflags 3 frames over the 120 Hz budget within 1 s (long_frames), a frame of 250 ms or more (stall) and 5 rising per-minute memory floors that grow by at least 64 MiB and 20% (memory_growth), once per rule and window, in the new optionalperf.incidentslist (Incident,IncidentRuleinashlar-trace-proto; older readers ignore it). The trace server counts them asincidents.<rule>metrics and lists them on a crash’s performance panel. -
ashlar_ui::markdownv2 (IMP-5.4): links take keyboard focus (Tab, then Enter or Space opens them) with a focus ring, have the AccessKitLinkrole and automation roleLink(new), and answer the pointer on every row they wrap onto; tables size columns to their content and wrap them down to their longest word, with a “N more rows” footer (<id>.table.<n>.more) pastMAX_TABLE_ROWS(500) instead of dropping rows silently; theMarkdownbuilder adds.on_task(|index, checked| ..)(clickable task checkboxes,<id>.task.<n>),.find(query)(highlighted matches, counted inMarkdownResponse::matches;find_rangesgives the byte ranges),.streaming(true)(an unclosed**,*,~~,`or link at the end is drawn as already styled, so markers never flash; alsoparse_streaming) and.labels(&MarkdownLabels)for “Copy”, “Copied”, “code” and the table footer, by default from the installedUiLabels(ui.markdown.copy,.copied,.code,.more_rows; English and German).MarkdownStylegainsfind_bg. -
ashlar_ui::markdown::show_with_revisionandMarkdown::revision: a caller that keeps a revision of the text skips hashing it each frame. Inline parsing is linear in the paragraph’s length (a 50 KB paragraph:bench: md_long_paragraph_parse, about 0.4 ms), and a drawing snapshot (tests/markdown_snapshot.rs) checks what markdown paints (IMP-5.18). -
Rich clipboard:
clipboard::set_rich/rich(text, HTML, PNG, file lists; read HTML is sanitized byclipboard::sanitize_html, every form size-capped),clipboard::set_secret(text, clear_after)(kept out of clipboard history where supported, cleared only if still ours) and aBackendtrait withMemoryClipboardfor tests. -
Dock ▸ Quit, logout, restart and shutdown go through the “Save changes?” check and flush settings, autosave and telemetry (
ashlar_native::session_end, featuresession-end, on by default): macOS answersapplicationShouldTerminate:later until the user chooses; Windows handlesWM_QUERYENDSESSION/WM_ENDSESSIONand, since it can’t wait for a dialog, keeps unsaved changes for recovery. The flow is the puresession_end::step. -
Gesture handoff: value fields, sliders and knobs keep going when a drag is let go while moving and ease to rest (clamped, snapped; the commit carries the resting value, so one drag is still one undo step);
.fling(false)turns it off.VirtualList::fling(true)drags the list to scroll it, with fling and rubber band past either end. A pause of 40 ms before release, or reduced motion, means no fling. -
ashlar_ui::rolling::RollingText: counters and readouts that roll instead of jumping. Only the changed characters move (prefix/suffix, then LCS for short text), up when.value(..)rises and down when it falls; the width follows;.tabular(true)gives digits one width. Reduced motion crossfades. The accessible value is the whole new text at once; nothing animates once settled. -
Windows reopen where they were:
App::remember_window(bool)(default on) saves position, size, maximized and full screen state in the settings under"window"when they change, and the next launch opens the window there, sizes bounded and a window left on no monitor moved back (ashlar_app::window_state::clamp).App::viewport(|builder| …)changes the window builder (minimum size, decorations…) andApp::icon(png_bytes)sets the window icon. -
ashlar-i18n(new crate, rank 0, builds for wasm32): translations without a full Unicode library. Flat JSON catalogs per locale, parsed on first use and layered (later catalogs override key by key); a bad catalog is reported and acts as empty, never a panic.I18n::{text, format, plural, integer, decimal, set_locale, negotiate}:{name}templates, CLDR plural categories for about 50 languages (key.one,key.few, …), per-locale group and decimal separators (including Indian grouping), fallback locale → language → source (zh-TW→zh-Hant), POSIX and BCP 47 tags (de_DE.UTF-8→de-DE),system_locales()fromLANGUAGE/LC_ALL/LANG, andI18n::check()for missing keys and mismatched placeholders. -
ashlar_ui::labels: oneUiLabelsinstalled on the context holds every built-in component string (toasts, form, chat, diff view, keymap editor, tab strip, print panel). Components use it unless the app passes its own labels, which still win. English and German catalogs ship for theui.*keys;UiLabels::from_i18nbuilds the labels for any locale. -
Perf warnings in debug builds (
ashlar_app::perf_warn): one stderr line per site when a frame waits (≥ 8 ms wall at ≥ 4× its CPU time; names the command or wake reason) or when 60 identical frames in a row are drawn for non-input wakes.ASHLAR_FRAME_BUDGET_MS=N(any build) prints at most one line a second for frames overNms with the phase split;ASHLAR_PERF_WARN=0silences. -
ASHLAR_FRAME_LOG=FILEandRuntime::log_frames(path): one JSON line per frame (wall and UI-thread CPU time, phases, wake reasons, commands run, pass number), written by a background thread and capped at 64 MiB with a final"end"line (ashlar_app::frame_log). -
app.stats(and the MCPapp_statstool) reportsprocess:footprint_bytes,peak_bytes,cpu_sandthreads(ProcessStats;nullwhere the platform cannot say). -
ashlar-nativenotifications: stable ids (Notification::id; the same id replaces a shown notification),notify::show(returns the id),notify::remove,notify::on_click(aReceiver<Click>with the notification’sinfomap),silent, andnotify::set_app_identity. Windows now shows toasts (AppUserModelID registered under HKCU for unpackaged apps, escaped XML, tags over 64 characters hashed); macOS installs a notification-center delegate so banners show while the app is frontmost and clicks are reported; Linux sends thedesktop-entryhint and reports clicks fromActionInvoked. -
ashlar_ui::chat: a chat kit over plain data.Transcriptis a virtualized, variable-height message list (heights kept per message key, revision and width; a frame costs O(visible)) that follows the newest message while it streams, stays put once the user scrolls up and then offers “Jump to latest”; it is a polite AccessKit log.Messagedraws a role and a markdown body with a streaming state,ToolCarda tool call with folded details and keyboard-reachable Approve/Deny (<id>.approve,<id>.deny), andComposera text field where Enter sends, Shift+Enter adds a line, input-method composition never sends, and@//open aPopupListof suggestions. Strings come fromChatLabels. The gallery has a Chat page. -
ashlar-diff(new crate, rank 0, no dependencies, builds for wasm32): line diffs with Myers’ algorithm under a cost cap (past the cap, a split at lines unique to both sides, then a coarse delete/insert, flaggedcoarse), word-level emphasis for paired changed lines, hunks with context and the unchanged ranges between them (FileDiff::gaps, expandable when the new text is known), andparse_patch/write_patchfor unified andgit diffpatches (renames, copies, modes, binary markers, quoted paths,\ No newline at end of file); malformed patches are an error, never a panic. About 2 ms for a 10k-line file. -
ashlar_ui::diff_view::DiffView: unified and split diffs, virtualized (about 0.4 ms per scrolled frame at 50k lines), syntax-coloured per file language, word-level emphasis, “expand N lines” on collapsed ranges, foldable files, a sticky file header, line selection,N/P(orF7) between hunks. Automation<id>.file.<f>.hunk.<h>and friends for visible rows; AccessKit document, headings and labelled lines. -
app.statsreportslaunch:to_window_ms,to_first_frame_msandlaunch_to_present_ms, counted from the first line ofrun()(FrameStats::launch,LaunchTimes). Native windows only. -
<PREFIX>_READY_FILE=path: the app writes{"pid", "ready_ms"}there atomically once its first frame is presented. -
cargo xtask launchmeasuresexamples/hellolaunch time over 9 launches (p50/p95), andcargo xtask launch --memoryits idle RSS and physical footprint (macOS). Manual, opens windows. -
App::frame_latency(frames): how many frames the GPU may queue ahead of the screen (1 to 3). -
Motion and jank gate for tests:
ashlar_ui::motion_gate::MotionRecorderrecords values animated throughashlar_ui::motionand per-frame automation rects (Driver::recorddoes both for an app), andcheck(&MotionRules)flags overshoot of opacity and colour, reversals without bounce, pops, flicker and frames requested after everything settled, with a per-id allow-list. The gallery’s dialog, toast, theme crossfade and tab switches pass it. -
ashlar_ui::overlay:Select(a button with a list of options),Combobox(a text field whose options are filtered with the command palette’s fuzzy matcher; input-method composition never chooses), andPopover(anchored, flips above at the window edge, Escape or an outside click closes, focus moves in and returns). Option lists are virtualized, fade with theenter_exitmotion token and register<id>.option.<n>with ListBox/option roles. -
ashlar_ui::help(response, text): a tooltip after the newmotion.tooltip_delaytoken, also given to screen readers as the control’s description. -
ashlar_ui::commands::menu::context(response, &spec): a context menu built from aMenuSpec. -
Automation roles
ComboBox,ListBox,ListOptionandTooltip. -
ashlar_ui::form: the settings form pattern. Sections of rows (label, description, control) with a modified marker and reset button, a filter that hides non-matching rows and empty sections, highlights matches and shows “No settings match”, andFieldwith a required marker and an announced error. Row controls get automation ids<section>.<row>; strings come fromFormLabels. The gallery has a Settings page. -
Golden snapshots:
Driver::assert_snapshot(name)and the.uilineassert screenshot <name> [target] [--tolerance channel=N,pixels=N]compare withtests/snapshots/<name>.png, exact by default; a mismatch writes-new.pngand a magenta-diff.png;ASHLAR_UPDATE_SNAPSHOTS=1records baselines with a.meta.json(adapter, backend, scale); a baseline from another adapter is skipped with a note. Screenshot replies from aDrivernow carryadapter,backendandscale(IMP-4.8). -
Named scenes:
App::scene(name, |s| ..),--scene NAME(in a window or--headless),--list-scenes,Driver::scene, theui.scenecontrol method and the MCP toolui_scene(listed only for apps with scenes) (IMP-4.9). -
App::single_instance(true): a second launch hands its arguments to the running app and exits 0; the running app opens the first file withfile.open(a dirty document asks to save first) and comes to the front. Desktop only. -
Files from Finder (macOS
odoc/GURLApple events: a double-click, a drop on the Dock icon) open withfile.openwhile the app runs and at launch. -
App::accept_drops(bool)(on by default when the app sets a format or a file extension): the first dropped file with a known extension opens withfile.open; the others reach the view asCx::dropped_files().Driver::drop_filesandDriver::forward_launchtest both paths. -
app.clockcontrol method:{pause: true},{step_ms}(one frame that much later) and{resume: true}hold the time anashlar-appwindow’s frames see, so an animation can be screenshotted or measured at an exact moment; input still runs while paused and resume does not jump.ashlar-debug clock pause|step <ms>|resume(also in.uiscripts) and the MCP toolui_clocksend it;ashlar_control::{APP_CLOCK, ClockOp}parse it. -
examples/hello, the smallest ashlar app, and acargo xtask benchstep that reports its stripped release size ashello_stripped_bytes. -
Features
accesskitandheadlessonashlar-rsandashlar-app, both on by default (no change for existing apps).default-features = falsedrops AccessKit and--headlessfor a binary about 1 MB smaller. -
A
release-smallprofile in the workspace (fat LTO, one codegen unit,opt-level = "s", stripped): the hello app goes from 13.6 MB to 9.0 MB stripped on macOS arm64. -
ashlar-ui: screen readers see every visible row ofVirtualList,VirtualTableandTreeView: list items, table rows and tree items named byrow_label(the tree’s labels), with selected state, position and set size, and level and expanded state for tree items. Only visible rows get nodes, and only while AccessKit is on. -
ashlar-ui:selection::Selection, an app-owned, serializable multi-selection (anchor, cursor, ranges).VirtualList::selection(&mut sel)andVirtualTable::selectionhandle Shift-click, Shift+arrows, ⌘/Ctrl-click and ⌘A;selectedandis_selectedkeep working without it. -
ashlar-ui:VirtualList::type_ahead(&text)andVirtualTable::type_ahead: letters typed within a second of each other jump to the next row starting with them. -
ashlar-app:--headlessfor everyApp::runbinary:--size WxH --scale S --theme light|dark --script file.ui --at SECONDS --snap out.png --tree tree.jsonwrites a screenshot (real pixels when there is a GPU) and the UI tree with no window, in a temporary folder. Exit codes 0/1/2 as inashlar-debug.ashlar-debugexportssave_png. -
ashlar-app:Driver::run_ui(script)runs a.uiscript (theashlar-debug batchlanguage) in process, with no port or thread.sleepandwait Nlet virtual time pass, so two runs give the same steps.ashlar-debuggains theTransporttrait andrun_scriptto run scripts against any target. -
ashlar-ui:cache::label(aui.labelwith its galley cached under a key: no layout and noStringon steady frames),cache::row_label(the same, not selectable, for text inside a clickable row) andcache::button_text(a button’s text as a cached galley). -
ashlar_ui::tokens::set_mode_animated: switches theme with an OKLab crossfade, surfaces over 0.4 s and text leading at half that; primary and secondary text keep 3:1 on the panel at every frame, and once settled the theme’s own visuals are back and nothing more runs.Tokens::mixblends two token sets. -
ashlar_ui::tabs::indicator: where to paint a tab strip’s selection underline or pill, sliding by its edges to the newly selected tab (paint only; tabs keep their rects for hit testing). -
ashlar_ui::tabs::TabStrip: a standalone tab strip with one Tab stop (arrows, Home and End move the selection), close by button, middle-click or ⌘W, an overflow menu for tabs that do not fit, drag or ⌘⇧ + arrow reordering (tabs::reorderapplies a move), the animated selection indicator, an AccessKit tab list with positioned tabs, and automation ids<id>.tab.<n>. The gallery has a Tabs page. -
ashlar_ui::tabs::Tab::automation: registers a tab under a dotted automation id. -
ashlar_ui::motion::presence: keeps an item drawn while it leaves (Some(progress)until its exit settles, thenNone), entering and leaving along a token pair as an opacity. -
ashlar_ui::code_view::CodeView: a read-only code view with virtualized lines (O(visible) per frame), a line-number gutter, optional wrapping, mouse selection, select all and copy, search marks and syntax colours fromSyntaxTheme::from_tokens. Line galleys are cached per line and revision; lexer states are computed lazily. Automation<id>.line.<n>for visible lines only; AccessKit document and paragraphs. About 0.06 ms per scrolled frame at 100k lines. -
ashlar_ui::markdown: fenced code blocks in a known language are syntax-coloured. -
ashlar-syntax(new crate, rank 0, no dependencies, builds for wasm32): syntax highlighting with hand-written, linear-time lexers for Rust, TypeScript/JavaScript, Go, C, C++, Java, C#, Python, shell, SQL, JSON, TOML, YAML, HTML (with script and style), CSS, Markdown (fenced code in its language) and diffs.highlightreturns ordered byte ranges that cover the input;highlight_linecarries aStatefrom line to line for incremental views;lang_for_path; aHighlightertrait so a bigger engine can plug in. About 0.5 ms per 1,000 lines of Rust. -
ashlar-app:Driver::with_renderer(runtime, Renderer::wgpu())renders real screenshots with no window, on a wgpu device with no surface, at the driver’s display scale. Without an adapter it falls back to the grey image. Screenshot replies from aDriversay which was used ("renderer": "wgpu"or"none"), and grey screenshots now have the screen’s size in pixels instead of points. -
ashlar_ui::list::Row: a cheap selectable list row for plain (non-virtual) lists: one allocation, one interact, and a cached galley only while on screen. AccessKit list item with label and selection; focusable, Space and Enter click; opt-in automation id. A 1k-row plain list scrolls in about 0.10 ms per frame with it, against about 0.7 ms withButton::selectablerows. -
ashlar_ui::list::checked: wraps a list’s content and, in debug builds, warns once per call site when it builds more than 2,000 interactive widgets in a frame, pointing toVirtualList(ASHLAR_QUIET_LISTSsilences it). -
ashlar-motion(new crate, rank 0, no dependencies, builds for wasm32): closed-form springs set by duration and bounce (Spring, never earlysettle_time),Curve(spring, cubic-bezier tween, decay, instant),Animated<T>values whose retargets keep position and velocity and that land exactly on their target,Classsettle thresholds and reduced-motion rules (opacity and colour never overshoot),MotionPrefs, motiontokens(exits shorter than enters), an OKLab/OKLCHcolormodule with gamut mapping, premultiplied mixing and WCAG contrast, andgesturehelpers (velocity tracker, fling projection, rubber band). -
ashlar-app: frame stats split the frame into phases:app.statskeeps the UI time asp50_ms/p95_ms/max_msand addspass,tessellateandframe_cpu(the whole frame on the CPU: eframe’s own figure in a window).Drivertessellates every frame like a window and takes the display scale (Driver::pixels_per_point). -
ashlar_ui::motion: animated values for egui in one call per frame (value,f32,opacity,colorin OKLab,rectby edges,set,release), kept in the context and forgotten when unused. Nothing animates on first show, frames are requested (Reason::ANIMATION) only while a value moves, and settled values are exactly their target.prefs/set_prefsset reduced motion (also turning off egui’s widget animations) and a time scale;activecounts moving values. -
ashlar_ui::tokens:motion(spring per kind of change),elevation(popup and window shadows, no longer hard-coded invisuals()) andstroke(hairline, focus) tokens;Tokens::high_contrast();contrast_ratio;TOKEN_SPEC, the OKLCH source of every built-in colour, checked against the hex values by a test. -
ashlar-appfollows the OS preferences: reduced motion (throughashlar_ui::motion::set_prefs), the accent colour (Tokens::with_accent) and increased contrast (Tokens::high_contrast), read at startup and again when the window gains focus (on Linux off the UI thread). Tokens an app installs itself win.App::follow_system_accent(bool)(default on) andApp::motion(MotionPrefs)override;Runtime::set_preferencesinjects preferences in tests, andApp::runtime()ignores the OS so test runs match on every machine.tokens::ofandtokens::apply_colorsare new.
Changed
-
File dialogs open over the app window (a sheet on macOS) and no longer block a frame: the frame that asks for one finishes, the next shows the dialog and acts on the pick. Saving an untitled document from the “Save changes?” dialog now goes on to the held-back open or quit after Save As.
ashlar_native::dialogs::FileDialog::parent(&window)parents any picker. -
ashlar-nativemacOS notifications play the default sound unlesssilentis set, and ask for sound permission only then. -
A document named on the command line opens on the frame after the first, so the window shows before the load runs (it used to load before the first paint).
-
Windows queue at most one frame ahead of the screen by default (
App::frame_latency(1)): lower input latency and one swapchain image fewer (about 10 MB less idle footprint for a 960×640 window on a Retina screen). Apps with heavy per-frame GPU work can ask for 2. -
ashlar_ui::list::Rowgainsindent,text_styleandweak. The devtools Elements list draws its rows with it (about 6.7× cheaper per row than its old selectable labels: 0.10 ms against 0.67 ms for 1k rows), and the tab strip’s overflow menu runs insidelist::checked. -
ashlar_ui::docking: group tab strips use the shared tab strip: one Tab stop per group with arrow keys, an AccessKit tab list, and the animated selection underline. -
ashlar_ui::controls: button, switch, segmented and radio hover and press fills ease over about 0.1 s, like egui’s own widgets (tokens::state_color,Tokens::interact_fill_animated); they change at once when egui’sanimation_timeis 0, which reduced motion sets. Visual regression tests now run with reduced motion. -
ashlar_ui::dialog: on open the backdrop fades in and the panel fades in while growing from 97 %; when the app stops showing a dialog, its last frame fades out a little faster without taking input. No growing under reduced motion; hit testing uses the resting layout. -
ashlar_ui::docking: the drop preview moves quickly (0.1 s) from one drop target to the next while a tab is dragged, and appears in place when a drag first shows it. -
ashlar_ui::toasts: a new toast fades and slides 8 pt in, a dismissed one slides back out and is painted until its exit settles, and the toasts behind it glide into the gap. Toasts queued before the stack is first shown appear at once; hit testing and automation rects use the resting layout; reduced motion keeps only the fade. -
Tokens::with_accentderives hover, focus, selection and selection text in OKLCH (same hue, lightness steps, capped chroma) instead of sRGB mixing, flipstext.on_accentdark when white falls below 4.5:1 and pushes the focus ring to 3:1 on every surface. The dark theme’s accent is darker (#276ee0, was#3b82f6) so white text on it reaches 4.5:1; its derived states follow. -
The status bar’s indeterminate progress holds still (and asks for no frames) under reduced motion, and its sweep follows the motion time scale.
-
Fewer allocations per frame (IMP-2.16): a frame woken only by a caret blink in the notes app makes 550 allocations instead of 958, a scroll frame 345 instead of 472, a keystroke 1,935 instead of 3,720. The menu bar and the notes list draw cached galleys, and an edit that changes no command’s enabled state no longer rebuilds every command’s info and the menus.
-
ashlar-app: a command run from the keyboard (or anything else before the view draws) no longer asks for adocumentframe: the frame that ran it already shows the change. The runtime also stops copying and comparing the view state and formatting the window title on frames that did not change them. -
ashlar-ui: variable-heightVirtualListandVirtualTablekeep row heights in a Fenwick tree, andchanged_rows(&[row])says which rows changed height: those are measured again in O(log rows) each instead of re-summing every row (1M rows: about 2 ms → 1 µs). A full rebuild (new revision or row count) costs about a third more than before. -
ashlar-ui:markdown::showkeeps its parse while the text is the same and parses streamed text again only from its last open block; each block’s layout is kept per width and style, and blocks outside the clip rect only reserve their space (a 50 KB document: about 1.9 ms → 0.02 ms a frame). Automation ids (<id>.link.<n>,<id>.code.<n>) now exist only for blocks drawn, with the same numbering.MdBlock,SpanandMarkerimplementHash. -
ashlar-ui: the galley cache (cache::galley,RevisionCache,MeshCache) hashes with a fast fixed-seed hasher (cache::hash_ofvalues differ from before), reads the shared cache under the context’s read lock, and when full drops its least recently used half instead of everything, so labels in use are not laid out again. -
ashlar-ui: a controls kit (controls):Button(primary, secondary, ghost and destructive variants, three sizes, an icon, a loading state that shows a spinner and reports busy),Switch,SegmentedandRadioGroup(one Tab stop; arrows, Home and End move the choice),Badgewith tones,Keycap(a cap per key of a shortcut),Progress::barandProgress::circle(determinate or indeterminate) andSpinner. Each registers a dotted automation id and fills its AccessKit node; indeterminate indicators ask for frames (wake::Reason::ANIMATION) only while on screen and hold a static pose withreduced_motion(true). New automation roles:switch,radio,radio_group,progress,label,group,disclosure,alert. The gallery has a Controls page, and the visual regression suite compares the kit against reference painters (222 pairs). -
ashlar-ui: dialogs move the keyboard focus inside when they open (the body’s first field, else the default button, else the cancel button;Dialog::initial_focuschooses) and give it back to the widget that had it when they close, however they close (Dialog::return_focus_tonames it for a dialog opened by a click). Screen readers get a modal dialog node named by the title, withDialog::description(also shown under the title) as its description. -
ashlar-ui: content components (content):EmptyState(icon, title, text, action),Banner(a tone, title, action and close button; a live region that screen readers announce, assertively for errors),Card(a titled group) andCollapsible(a header that shows or hides its content; reports expanded to AccessKit, arrows open and close it).IconButton::tooltipgives icon-only buttons a visible name on hover, andcontrols::Button::partregisters a component’s button as<id>.<part>. The gallery has a Content page; the visual regression suite covers the collapsible header and the banner.
Changed
-
ashlar-ui: dialog buttons arecontrols::Buttons (primary for the default, destructive for a destructive one); a disabled one is drawn at the disabled opacity instead of in the sunken colours, and its automation entry no longer reports the default button asselected. -
ashlar-ui:wake::wake,Waker::wakeand a zero-delaywake_after/rescheduleask egui for one frame instead of two (egui adds a second frame to every zero-delay repaint request). A command sent through the control port now costs 2.2 frames instead of 3.2. -
ashlar-app: the control port no longer asks for a frame after answering a request; the frame that serves it runs before the view draws and already shows the change. An engine command sent through the port now costs about 1.3 frames (was 2.2).Driver::requestwakes for thecontrolreason as the port does. -
ashlar-update: the check asks for this platform’s build (?platform=macos|windows-x64|linux-x86_64|linux-aarch64,PLATFORM,latest_url_for), so a Windows or Linux app is never offered a Mac download.
Fixed
-
Linux: copied text and images no longer vanish when the copy call returns; one clipboard lives on a worker thread for the life of the process.
-
ashlar-uilists:scroll_toread the scroll offset under the wrong id, so it never scrolled up to a row above the view and moved the list for a row already in view. It now reads the scroll area’s own id, as the fling does. -
ashlar-uiaccessibility: lists, tables and trees take a.label(text)that screen readers announce; table cells areCellnodes and the header row holdsColumnHeadernodes (with sort direction); resize handles, tree disclosure arrows and the fling drag surface no longer show up as unnamed nodes. The keymap editor’s table, its actions column and its shortcut and clear buttons have names (newKeymapLabels::tableandactions, English and German). -
ashlar-uiaccessibility: search fields and the command palette input have a name, not only a placeholder (SearchField::label, newUiLabels::searchwith English and German); banners announce their text once instead of as both name and value. -
ashlar-uidevtools: AccessKit that the debugger turned on to answer a request (ui.click,ui.snapshot, a script) goes back off once nothing needs the tree, so a driven app no longer builds a screen reader’s tree on every later frame (a caret-blink frame in the notes app: 272 allocations instead of 652). This happens only where no screen reader can exist (aDriveror--headlessrun, or a build without theaccesskitfeature); in a window it stays on, anddevtools::set_platform_accesskitsets which applies. -
Curve::Tweenstarted from rest follows its easing exactly: a linear tween is linear (it used to start at zero velocity, so a 2 s linear 0→100 tween read ≈12 at 0.525 s instead of 26.25). Retargets mid-motion still keep velocity continuous. -
Telemetry: a run whose shutdown wait ran out no longer takes back its own unsent session folder (and other leftover ones) as its worker finishes, so a courier delivers them instead of seeing them as in use until the process exits.
-
notesexample: a click on a note’s title in the list selects the note (the title was selectable text that took the click). -
native-menu: picks in the OS menu bar no longer run while a dialog (such as “Save changes?”) or the command palette is open. -
⌘Z / Ctrl+Z in a text field undoes the text, not the document. Text-editing shortcuts (undo, redo, select all, copy, cut, paste) are left to a focused text field (
works_while_typing); withnative-menuthey have no OS key equivalent, and menu picks of commands bound to them are skipped while typing. -
native-menuon Windows: the menu bar is attached to the window; until it is (or if that fails) the in-window bar shows, so the app never ends up without a menu. -
native-menuon macOS: Quit is in the application menu, and a Window menu has Minimize, Zoom and Bring All to Front. -
ashlar-debug:trace start <dir>inside--port P batch -(or--app NAME) recorded nothing, since the trace was kept only in a session file. The trace now lives in that batch run; outside a batch,tracewith--port/--appis a usage error instead of doing nothing. -
docs/control-protocol.md: the MCP tools table listedscreenshot {id?}and left out the debugger tools; it now lists every tool with its arguments, and a test keeps it in step with the code. -
ashlar-app: a focused text field no longer keeps an idle app drawing: egui’s caret blink cost about 6 frames a second for as long as the field had focus. The runtime blinks the caret itself, one frame per phase change credited to the newcaretwake reason, and leaves it solid after 10 s without input (ashlar_app::caret); input starts it again. 30 s idle with a focused field: 194 frames before, 21 after. -
ashlar-ui: a value field’s typed entry lost what was typed in the frame its editor opened (a value typed straight after the click, as the control channel does), and while its window did not have the operating system’s focus (an app driven without being activated) the editor selected all its text again every frame, so each entry replaced the last. The editor takes the keyboard before it is drawn, and keyboard handling here asks egui’s focus alone (edit::has_keyboard): value fields, sliders, trees, swatches, resize handles, virtual lists and the search field. A person typing in a focused window was not affected, except that an editor left open while they switched windows had its text selected again when they came back. -
ashlar-ui:ui.type/ui.fillwith a target no longer click a text field that has the keyboard already: two requests in a row made a double click, which selected a word for the next text to replace. -
ashlar-assistant: a saved chat reopened after a relaunch could be refused (“Invalidsignatureinthinkingblock … bound to a different conversation”): its thinking blocks were replayed even when the system prompt, the tools or the saved messages (results cut, pictures left out) differed from the request that produced them. Each Anthropic reply now records a fingerprint of its prefix (Native::binding,anthropic::request_parts,anthropic::bind), and thinking goes back only while it matches; chats saved before this replay none. A refusal anyway drops all earlier thinking (Conversation::forget_thinking) and the step is asked once more. -
ashlar-assistant: OpenAI-format requests carry at mostanthropic::MAX_IMAGESpictures (the newest), like Anthropic ones; they carried every picture of the chat. -
ashlar-ui:idle::FrameRate::fpscounts the frames of the last second (two frames 11 ms apart on an idle app read 91 fps);idle::Activity::ofdocuments that an unfocused window is the background signal that arrives (winit rarely reports a covered one). -
ashlar-native:memoryis listed as an FFI module (the deny-panics check refused it); it no longer calls macOS’smalloc_zone_pressure_relief, which released next to nothing on macOS 26, sorelease_free_memoryis a no-op there;memory::space_efficient_malloc()tells whether the process runs withMallocSpaceEfficient=1, which hands freed memory back at once.
Added
ashlar-ui: toasts v2. Each toast is an AccessKit live region (polite; assertive for errors), so screen readers announce it. New onToast:title,action(label, command)(up to three buttons, reachable astoast.<id>.action.<n>; clicks arrive fromToasts::take_actionsorshow_actions),key(pushing a toast with a queued toast’s key updates it in place), andprogress;Toasts::progress(key, text, fraction)keeps a progress toast up to date anddismiss_keyremoves it. Escape dismisses the focused or newest toast (not while a field or a popup has it), keyboard focus pauses a toast’s timer, close and action buttons show a focus ring, the close target is a full control high, toasts pastmax_visibleshow as “+N more”, and the words come fromToastLabels(English by default).Toastgained public fields, so code that builds one with a struct literal must add them (Toast::newis unchanged).ashlar-trace-proto,ashlar-trace,ashlar-trace-server: telemetry counts frames over the 120 Hz budget.DISPLAY_BUDGET_US(8.333 ms, already a bucket bound) and the optionalFrameStats::over_budgetare new within schema 1: older clients leave the field out, older servers ignore it, andFrameStats::over_budget_framescounts it from the buckets when it is missing. The SDK sends it and shows it in its local report; the server counts it from the stored buckets (no new column) and shows “Over 120 Hz budget” on the performance page and “Frames over 8.3 ms” on issue pages.ashlar-mcp:app_stats {reset?}andapp_logs {limit?, level?}tools over the runtime’sapp.statsandapp.logs, listed only when the app reports those methods.ashlar-assistant:HistoryStore::offload_imageskeeps only a chat’s newest pictures in memory (older ones stay in its history folder as file references, which saving keeps, loading reads back and requests leave out);HistoryStore::read_imageandImage::is_offloadedfor showing them.ashlar-telemetry:TelemetryConfig::build: a build identifier (such as the commit a release was packaged from) sent with every report, so a server can tell release sessions from developer ones.ashlar-assistant:Conversation::memory(): the bytes a chat holds (pictures dominate) and how many pictures, for memory readouts.ashlar-ui:idle:IdleTrimfires once per quiet spell (30 s idle, 10 s in the background) so an app can trim its caches,MemoryReport(named cache sizes plus the process footprint forperf.stats),texture_memory, andFrameRate, a frame rate that reads 0 while no frames run;wake::Reason::IDLE.ashlar-native:display(feature, on by default):check()says when this process cannot show a window at all (Windows: a service’s window station, as in session 0), where wgpu would panic configuring the window’s surface.ashlar-appchecks it before opening its window and returnsError::NoDesktopinstead of crashing.ashlar-native:memory(feature, on by default):process_memory()reads the process’s footprint and resident size the way the OS’s tools do;release_free_memory()returns the allocator’s free pages to the system (macOS keeps freed regions dirty otherwise).ashlar-assistant: saved chats (history:HistoryStore,HistorySaver; versioned, crash-safe, pictures in their own files),@mentions (mention,Block::Mention),/commands with saved prompts (slash), and/compactsummaries and chat titles (compact).ashlar-assistant:skills(Skills: skills listed by name and description in the system prompt, loaded with theload_skill/read_skill_filetools, picked as/name; from docs categories, skill folders and integrations),SlashAction::SkillandSlashAction::External, and featureintegrations(integration: an integration’s skills and MCP prompts as commands).ashlar-agents:skills::parseandskills::load_dirreadSKILL.mdskill folders back from disk.ashlar-integrations: MCP prompts (Integrations::prompts,get_prompt) and resources (skill_resources,read_resource), and skill folders in a bundle (manifestskills,skill_dirs).ashlar-ui:markdown, Markdown for chat replies (headings, lists, tables, code blocks with Copy, inline styles and links), safe for text that is still streaming in.
Security
ashlar-trace-serverand the trace examples use rusqlite 0.41 (bundled SQLite from libsqlite3-sys 0.39), fixing RUSTSEC-2026-0459 (CVE-2026-119375, stale memory read through a malicious database and-walfile pair); the yanked async-executor 1.14.0 is replaced by 1.14.1.
0.1.0 - 2026-10-27
First public release, on crates.io.
Added
- The umbrella crate, published as
ashlar-rs(imported asashlar), and theashlar-*crates: app runtime, UI components on egui, documents with undo and autosave, commands, background jobs, files and settings, logging and crash safety, native OS features, the agent control port, CLI, MCP (stdio and HTTP), agent setup, an in-app assistant harness, integrations, UI debugging, agent docs, update checks, printing, string obfuscation, and self-hosted telemetry. - Examples:
notes,gallery, and the telemetry demos. - A release workflow that publishes to crates.io only from a version tag on
main, after the full CI gate passes on that commit.
Changed
- HTTPS in
ashlar-telemetry,ashlar-update,ashlar-assistantandashlar-integrationsuses rustls with theringprovider, replacing the unmaintainedrustls-rustcrypto.
Fixed
ashlar-trace-serversendsConnection: closewith a refusal made before the request body is read (bad key, rate limit, storage full) and with every dashboard form response, so a client never reuses a connection the server is about to drop.ashlar-traceon Windows reads each spooled file’s size from the file itself, not the folder listing, which can show a stale size for a file still being written; a segment that looked empty was deleted.ashlar-tracekeeps the count of a batch the server refuses while the tracer shuts down, so the next process reports it instead of the count being lost.