✨ GPUI desktop UI — scope-centric views, system tray, real-time tracing logs - #38
Merged
Conversation
- Bump version to 0.6.0 (rust-version 1.95)
- Replace the entire Slint-based desktop UI with Woocraft, the GPUI
component library (git dependency)
- Port all existing functionality:
* Frameless window with custom title bar, sidebar and page stack
* Get Started page (interface selector, port, remote address, add instance)
* Connections pages per scope (accept / decline / remove, instance list
with latency and copy-on-click, close buttons)
* Network logs page streaming JSON log entries from wsrx.log
* Settings page (version/updates, language, logs export, support,
system info, footer)
* Background daemon: axum API server (pool/connect/popup/version),
latency worker with pingfall, log streaming, GitHub update check,
single-instance lock + /popup focus
* config.toml / scopes.toml persistence, light/dark theme, en_US /
zh_CN / zh_TW i18n
- Delete all Slint UI code and bundled icon assets; keep only the wsrx
base library untouched
- Drop skia/slint build inputs from the Nix flake; add vulkan-loader
- Remove the sidebar header (logo + app title) - Remove the sidebar collapse toggle button from the title bar and the related show-sidebar state entirely; the sidebar is always visible - Rebuild the sidebar as a uniform p-1 gap-1 flex column where every entry, including the controller port row, is the same full-width flat woocraft Button (port number moved into the label)
The old behavior only wrote config.toml / scopes.toml on graceful window close, so theme toggles and scope deletions were lost whenever the app was killed (or the close handler did not run). - Persist scopes.toml right after every scope mutation (API scope request/update, allow, remove) - Persist config.toml right after theme / language changes - Split persistence into async (tokio context, e.g. axum handlers) and sync (UI thread) variants: tokio panics on blocking_read inside a runtime, which crashed the API server on scope requests
Visual hierarchy now relies solely on font weight (bold) and opacity, matching the previous Slint UI. Removed all text_sm / text_lg / text_2xl modifiers so every text node renders at the theme default (16px).
The sidebar already has a right border, so the title bar now gets a 1px theme-colored divider on its bottom edge for a consistent look.
The hand-rolled log list could not scroll. Replace it with a readonly woocraft CodeEditor (plain text mode, no borders / background), filling the whole page. Incoming log entries are accumulated and pushed into the editor via set_value on every log event.
…ort button - Logs page: EditorState::set_value was a silent no-op while read_only was set, so the editor never displayed anything; flip read_only around each update - All pages now use overflow_y_scrollbar() with the wrapper sized after the call (size_full on the scrollport), which is what actually gives the scroll container a bounded height; the previous call order left the wrapper unsized and content simply overflowed - Connections page is now a single scroll container (header + instance list together) instead of a fixed header with an inner scroller - Normalize page layouts to gap-1 (flex gaps between components) - Sidebar: render the controller port number as a right-aligned flex child inside the button (flex-1 + text-right) instead of appending it to the label text
Remove the 1px divider lines and per-row vertical padding, which made the effective row spacing ~17px instead of the 4px gap-1 used on every other page. Rows are now plain gap-1 blocks.
Keep the 1px separators between sections while staying on the gap-1 layout (no per-row padding).
- Home page: restore vertical + horizontal centering (justify_center + items_center on a min-h-full content column) so the layout matches the original grid-aligned, page-centered design; min-h-full keeps it safely scrollable when the window is small - Sidebar: replace border_r_1 with a dedicated 1px divider div (same as the title bar divider) so it renders with the exact theme border color instead of a border that could look darker against the sidebar background
Button::expand(true) uses width:100%, which resolves against the row width and can collapse to the label width. Use flex_1() (flex-grow with a zero basis) so the selector always fills the remaining row space, keeping the grid alignment with the remote-address input below.
- The Button with expand(true) still would not stretch reliably; replace it with a real Input (flex-1, bordered) wrapped in a Popover whose content lists the network interfaces; clicking an entry sets the input value and dismisses the popover - The input is now the single source of truth for the local address: the send action reads it directly - Give the refresh button a border (outline) to match the bordered selector
Keep the manual tweaks (gap-8 page spacing, pt-6/pb-12 padding, success update button, 420px form width, default refresh button) and revert the local-address picker from an Input+Popover to a Button: - .default().expand(true) + w_full inside the flex-1 wrapper so it fills the row like the remote-address input below - the dropdown menu now pops downward (default TopLeft anchor instead of BottomLeft)
woocraft's dropdown builder has no access to the trigger bounds, so capture the button's laid-out width via on_prepaint into a shared cell and pin the PopupMenu width with min_w/max_w. The menu now matches the stretched button exactly and stays in sync if the layout changes.
Drop the on_prepaint width-capture machinery in favor of a simple min_w/max_w(px(282.)) on the PopupMenu.
…d subtitle - Render the bug-reporting system info in a readonly code editor (same pattern as the logs page) instead of a styled div - Pin the header motto and the two footer credit lines to English (remove them from the i18n table)
The desktop crate now embeds logo.png via a RustEmbed asset source and
serves it through woocraft's CombinedSource (woocraft built-ins first,
desktop assets second). The get-started and settings page headers render
img("logo.png") instead of IconName::GlobeStar.
Restore the two logo SVGs from history into the embedded desktop assets and drop the PNG: the get-started and settings headers render logo-stroked.svg (no background) via gpui's svg element, and the title bar uses the with-background logo.svg (colorized off) as its icon.
gpui's svg() element renders a monochrome mask and paints nothing without a text color, so the page logos were invisible. Switch them to the img() element, which rasterizes SVGs with their original colors. Also swap the variants: the page headers use the with-background logo.svg, while the title bar uses the background-less logo-stroked.svg.
Keep the manual formatting and warning fixes across daemon, i18n and the UI modules; resolve the remaining clippy warning by bundling the instance-row theme colors into an InstanceRowColors struct and dropping unused parameters, so the whole workspace is clippy-clean.
- On startup (after the single-instance check): prune log archives older than 3 days and archive any stale wsrx.log from a crashed session, so the live log (and the log viewer) only ever shows the current session - On shutdown: archive the session's wsrx.log to wsrx-<unix-ts>.log (plain integer timestamp) and remove the single-instance lock; both are also run from an on_app_quit observer as a safety net if the window-close handler is bypassed, and both are idempotent - Remove the old behavior that wiped the entire logs directory - Enlarge the get-started logo to 96px
- Render the logs in a custom readonly editor backend (LogBackend) with a LogHighlighter that colors the level token of each line using the default tracing-subscriber console palette (TRACE purple, DEBUG blue, INFO green, WARN yellow, ERROR red) - Format log lines to match the tracing console output (2026-08-22T14:07:00.123456Z INFO target: message) instead of raw JSON - The backend is a rope-based EditorBackend reusing RopeEditorSnapshot; set_value still works via the read-only flip
The editor builds text runs from highlight spans with a cumulative cursor, so they must form a contiguous partition of the visible text. Returning only the level tokens left gaps and misaligned positions, which limited the color to the first line. Now the highlighter fills the gaps with default-style spans (offsets relative to the visible range), coloring the level of every line.
- Add the feature to the woocraft git dependency (tray component ported from gpui-tray, MPL-2.0) - Enable the tray when the running-in-tray setting is on: logo icon + Show / Quit context menu, left-click pops the window back up - Closing the window now minimizes to the tray instead of quitting when the setting is enabled; tray Quit (or the tray event channel) performs a real shutdown - The settings toggle is now functional and persists the setting
The woocraft title bar's own close button called remove_window() directly, bypassing on_window_should_close, so clicking X always quit the app even with the tray enabled. Route it through the same logic as a window-manager close: minimize to the tray when running_in_tray is set, otherwise shut down. Also update the woocraft git dep for the non-blocking tray backend and the atomic D-Bus name+serve (fast startup, no zbus warning).
minimize_window() left a taskbar entry; true close-to-tray must remove the window entirely. Set QuitMode::Explicit so the app keeps running when the last window closes, close the window (WM close or the title bar button) without quitting when running_in_tray is on, and reopen the window from the tray / /popup via a shared window handle.
A log flood (e.g. naga debug output) queued thousands of Log events in the UI channel, burying Popup/Quit and freezing the main thread with a full editor set_value per line. The event pump now batch-drains pending Log events into a single UiEvent::Logs batch, handles critical events immediately, and the editor is synced once per batch (1000-line cap).
Close-to-tray removes the window but the shared window handle stays pointing at the removed window, so popup always failed silently on the stale handle and never reached the reopen branch. Treat a failed handle update as 'window missing' and reopen it.
A reopened RootView started from defaults: the Online event is only sent once at startup and logs sent while the window was hidden were dropped, so the sidebar showed no port and the log page was empty. Keep the API port, update flag and a capped log buffer in ServerState; api::serve records the port, the event pump persists logs/updates into state, and RootView::view restores all of them (and seeds the editor) on reopen.
- Highlight timestamps and targets dimmed, levels in tracing console colors - Render a 1px-per-row preview strip in the scrollbar (level-colored), windowed to the track height and following the scroll position - Bump woocraft for the scrollbar preview strip API
- scrollbar preview is now a 6px column near the track's inner edge (backend-controlled geometry), leaving room for future git/LSP status strips to share the scrollbar - bump woocraft for ScrollbarPreview geometry + borderless thumb - add the scrollbar preview design doc + python simulation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces the Slint desktop UI with a GPUI + woocraft implementation, following the scope-centric architecture (get-started form, per-scope connections, network logs, settings). Includes:
/popuprestores the window, window handle restored from the tray when stale, close removes the window instead of minimizing.tracing-subscriberconsole output (dimmed timestamps/targets, level-colored tokens), plus a scrollbar preview minimap: a 1px-per-row strip colored by log level, windowed to the track height and following the scroll position.合入后可以关闭的工作项
此 PR 合入后完成以下工作项,可一并关闭:
Supersedes the earlier draft PR #29 (
copilot/continue-refactor-gpui) and the placeholder PR #35 (master→master).Testing
cargo test -p wsrx-desktop— 10/10 passing (log formatting, span parsing, scrollbar preview windowing)cargo clippy -p wsrx-desktop— clean