feat: v2 GPUI terminal application#2
Merged
Conversation
Replace terminal spike with proper app structure: - Window: title "PaneFlow", 1200x800 default, 800x500 minimum - Layout: 220px fixed sidebar + flexible main content area - Placeholder message when no terminal panes exist - GPU error handling: actionable message on wgpu/Vulkan failure - Vendored GPUI deps via Zed monorepo path deps - Release profile: lto=thin, codegen-units=1, strip=true Ref: PRD v2 EP-001 US-001 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add TerminalElement implementing GPUI's Element trait for cell-by-cell terminal rendering with batched text runs: - BatchAccumulator groups adjacent same-styled cells into GPU-efficient text runs via shape_line + ShapedLine::paint - Full ANSI color support: 32 named slots (Catppuccin Mocha), 24-bit true color (Color::Spec), 256-color indexed palette (6x6x6 cube + grayscale ramp) - Cell attributes: bold, italic, underline, strikethrough, inverse video - CJK wide char handling (WIDE_CHAR_SPACER skipped, 2-col width) - Background quads via fill() + paint_quad() for non-default backgrounds - FairMutex snapshot pattern: lock term, collect cells, release, then process — minimizes lock contention with PTY write thread - TerminalState + TerminalView extracted to terminal.rs module Ref: PRD v2 EP-001 US-002 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add terminal cursor rendering with all 5 DECSCUSR cursor shapes: - Block: filled rectangle in cursor color - Beam: 2px vertical bar at cell left edge - Underline: 2px horizontal bar at cell bottom - HollowBlock: 1.5px outline (used when terminal loses focus) - Hidden: skipped entirely Cursor blinking at 530ms interval via GPUI timer, resets on keystroke. Unfocused terminal always shows hollow block regardless of active shape. Wide char cursor spans 2 cell widths. Ref: PRD v2 EP-001 US-003 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Complete the PTY management formalization: - Dynamic terminal sizing: compute cols/rows from GPUI container bounds in build_layout, resize Term grid when dimensions change - ChildExit handling: drain AlacEvent channel in sync(), display "[Process exited with code N]" centered overlay on exit - Clean shutdown: Drop impl sends Msg::Shutdown to EventLoop thread - SpikeTermSize made public for cross-module resize access Ref: PRD v2 EP-002 US-004 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract dedicated keys.rs mapping module with zero-alloc static paths: - to_esc_str(keystroke, mode) returns Cow::Borrowed for all static sequences (ctrl+letter, special keys, function keys) — zero heap allocation on the hot path - TermMode-aware APP_CURSOR branching for arrow/home/end keys (SS3 vs CSI sequences) - Modifier+cursor combos via CSI 1;N encoding (one alloc, rare path) - Alt+key ESC prefix, Shift+Tab back-tab - Debug latency probe (PANEFLOW_LATENCY_PROBE=1) with 100µs threshold - Printable chars via key_char.as_bytes().to_vec() (single alloc) Ref: PRD v2 EP-002 US-005 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace unconditional 4ms repaint with dirty-flag-driven model: - TerminalState.dirty tracks whether PTY has new output - sync() sets dirty on AlacEvent::Wakeup, ChildExit, Exit - Timer only calls cx.notify() when dirty, then clears flag - Idle terminal = zero repaints, near-zero CPU - Cursor blink timer skips repaints after process exit - Bulk output coalesced: all Wakeup events batched within the 4ms polling window into a single repaint Ref: PRD v2 EP-002 US-006 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add SplitNode binary tree layout with split/close operations:
- SplitNode::Leaf(Entity<TerminalView>) for terminal panes
- SplitNode::Split { direction, first, second } for binary splits
- Ctrl+Shift+D splits horizontally (top/bottom), Ctrl+Shift+E vertically
- Each split spawns a new PTY via cx.new(TerminalView::new)
- Ctrl+Shift+W closes focused pane, promotes sibling
- Recursive flex render: flex_row/flex_col with flex_1 for equal sizing
- Min pane size 80px in both dimensions
- Max 32 panes guard against resource exhaustion
- Last pane closed shows empty state placeholder
- GPUI actions! + KeyBinding for keyboard shortcuts
Ref: PRD v2 EP-003 US-007
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add resizable split dividers with real-time ratio adjustment: - 4px visible divider between split children (Catppuccin Surface0) - cursor_col_resize / cursor_row_resize on hover - Mouse drag updates ratio via Rc<Cell<f32>> shared state - flex_basis(relative(ratio)) for proportional child sizing - Ratio clamped to [0.1, 0.9] + CSS min_w/min_h(80px) guard - on_mouse_up_out handles release outside container bounds - drag_start + drag_start_ratio for absolute position tracking Ref: PRD v2 EP-003 US-008 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add directional keyboard navigation between split panes: - Alt+Left/Right/Up/Down moves focus to adjacent pane - Tree-based navigation: recurse up from focused leaf to find compatible ancestor split, descend into opposite child - 2px Catppuccin Blue accent border on focused pane - Single pane: Alt+Arrow is a no-op (no error) - focus_last() helper for entering subtrees from the far edge Ref: PRD v2 EP-003 US-009 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add multi-workspace support with interactive sidebar: - Workspace struct with title, cwd (snapshot), split tree root - Sidebar renders scrollable workspace list with per-item display: title (bold when active), cwd basename (muted), pane count - Click-to-select with accent background on active workspace - "+" button creates new workspace with default shell PTY - Ctrl+1-9 direct workspace switching, Ctrl+Tab cycles forward - Ctrl+Shift+N creates new workspace - Max 20 workspaces guard against resource exhaustion - Default workspace auto-created at startup - Workspace switching preserves all PTY processes (no kill/respawn) - debug_assert on active_idx bounds invariant Ref: PRD v2 EP-004 US-010 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Complete workspace lifecycle management: - Ctrl+Shift+Q closes current workspace and all its PTY processes (Drop on SplitNode sends Msg::Shutdown via TerminalState::Drop) - Last workspace guard: prevents closing when only one remains - active_idx clamped after removal, sibling workspace focused - Double-click on workspace title enters rename mode (visual cue with cursor indicator, click elsewhere commits) - Closing workspace does NOT affect other workspaces' PTYs (each workspace owns its own SplitNode tree independently) Ref: PRD v2 EP-004 US-011 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Send Msg::Resize(WindowSize) to the alacritty EventLoop when terminal grid dimensions change, triggering SIGWINCH for the child process. Previously, only the Term grid was resized — the PTY never received the window size update, so shell programs didn't reflow correctly. - Notifier passed to TerminalElement for PTY channel access - WindowSize includes cell_width/cell_height from font metrics - Resize debounced: one resize per paint frame (build_layout) - Split divider drag now correctly reflows shell output Ref: PRD v2 EP-004 US-012 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add TerminalTheme struct with 30 color slots matching Zed's schema: - 5 base: background, foreground, bright/dim foreground, ansi_background - 24 ANSI: 8 hues (black-white) x 3 intensities (normal/bright/dim) - 1 cursor color - 5 bundled themes: Catppuccin Mocha, One Dark, Dracula, Gruvbox Dark, Solarized Dark — defined as Rust constants - convert_color() and named_color() refactored to use &TerminalTheme instead of hardcoded hex values - THEMES static list for theme enumeration Ref: PRD v2 EP-005 US-013 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add config-driven theme selection with hot-reload:
- Read theme name from ~/.config/paneflow/config.json ("theme" key)
- theme_by_name() matches against 5 bundled themes (case-insensitive)
- active_theme() returns configured theme or falls back to Catppuccin
- 500ms poll timer checks config file mtime for changes
- Config change triggers repaint — colors update without restart
- Invalid JSON silently preserves current theme
- Added serde_json + dirs dependencies for config reading
Usage: echo '{"theme": "Dracula"}' > ~/.config/paneflow/config.json
Ref: PRD v2 EP-005 US-014
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add Unix socket server at $XDG_RUNTIME_DIR/paneflow/paneflow.sock: - Dedicated OS thread accepts connections, reads newline-delimited JSON-RPC requests, dispatches to GPUI thread via mpsc channel - 10ms poll timer processes IPC requests on the GPUI main thread - Per-request response channel for synchronous request/response 10 methods implemented: - system.ping, system.capabilities, system.identify (stateless) - workspace.list, workspace.create, workspace.select, workspace.close, workspace.current (GPUI-dispatched) - surface.list, surface.send_text (GPUI-dispatched, PTY write) - Stale socket file removed on startup - 5s timeout for GPUI dispatch (prevents hung connections) - Multiple concurrent agents serialized via channel Ref: PRD v2 EP-006 US-015 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a `paneflow` CLI binary that communicates with the running app via JSON-RPC over Unix socket. Supports workspace list/create/select, send-text, and split commands. Also adds surface.split IPC handler with split_first_leaf method, socket permission hardening (0600), and 64 KiB size limit on send_text. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add three timing probes gated by #[cfg(debug_assertions)] and PANEFLOW_LATENCY_PROBE=1: keystroke→PTY (>1ms), paint duration (>1ms), total keystroke→pixel (>8ms with per-phase breakdown). Uses OnceLock for one-time env var check, Instant timestamps passed from TerminalView to TerminalElement. Zero overhead in release builds. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add click-drag selection using alacritty's Selection model with double-click word and triple-click line support. Selection rendered as semi-transparent blue highlight rects. Ctrl+Shift+C copies to system clipboard, Ctrl+Shift+V pastes with bracketed paste mode and C1 control character sanitization. Element origin tracked via shared Arc<Mutex> for accurate pixel-to-grid coordinate mapping. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add scroll wheel support with sub-line pixel accumulator for smooth trackpad scrolling, Shift+PageUp/Down via alacritty Scroll::PageUp/ PageDown, and a thin 4px overlay scrollbar indicator shown when scrolled up from bottom. Delta sign correctly negated for alacritty convention. New PTY output does not auto-scroll when user is scrolled up (alacritty default behavior). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Set WindowDecorations::Client in WindowOptions for CSD by default - Set appears_transparent: true in TitlebarOptions for custom title bar rendering - Migrate src-app/ from spike-gpui/ (rename, no functional changes to other modules) - Remove unused Decorations/WindowControlArea imports (re-added in US-002/US-003) Ref: tasks/prd-v2-title-bar.md US-001 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Create src-app/src/title_bar.rs with render() function - Restructure render tree: flex_col → [title_bar, flex_row(sidebar+content)] - Title bar height: (1.75 * rem_size).max(34px) per Zed formula - Display "PaneFlow" + active workspace name with em-dash separator - Truncate workspace names > 40 chars with UTF-8-safe char slicing - Background matches sidebar (Catppuccin Mantle), border-bottom separator Ref: tasks/prd-v2-title-bar.md US-002 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add close/minimize/maximize SVG buttons to title bar (CSD mode only) - Set up asset pipeline: rust-embed AssetSource + .with_assets() in main - Create 4 SVG icons: generic_close, generic_minimize, generic_maximize, generic_restore - Close calls cx.quit(), minimize calls window.minimize_window(), maximize calls window.zoom_window() - Maximize icon toggles to restore icon based on window.is_maximized() - Hide minimize button when compositor reports controls.minimize == false - Hover (Surface1) and active (Surface2) visual states on buttons - cx.stop_propagation() on button strip + individual buttons to prevent drag-through Ref: tasks/prd-v2-title-bar.md US-003 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Convert title_bar from stateless function to TitleBar Entity with Render impl - Add should_move state machine: mouse-down sets flag, first mouse-move triggers window.start_window_move(), mouse-up and mouse-down-out reset flag - Store Entity<TitleBar> in PaneFlowApp, update workspace_name each render - Window control buttons still prevent drag via cx.stop_propagation() Ref: tasks/prd-v2-title-bar.md US-004 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add on_click handler checking click_count() == 2 to call window.zoom_window() - Toggles between maximized and restored window states - No conflict with existing on_mouse_down drag handler (different event phases) - Button stop_propagation() prevents double-click on controls from triggering Ref: tasks/prd-v2-title-bar.md US-005 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Read DE button layout via cx.button_layout() each render frame - Split buttons into left/right groups per WindowButtonLayout from XDG Desktop Portal - GNOME: close on left, maximize on right; KDE: all three on right - Default fallback: right-side [minimize, maximize, close] when DE unavailable - Runtime layout changes reflected automatically (no restart needed) - Refactored render_window_controls into render_button_group + render_window_button - Side-prefixed element IDs (wc-close-l, wc-maximize-r) prevent collisions Ref: tasks/prd-v2-title-bar.md US-006 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rounds - Add title_bar_background and title_bar_inactive_background to TerminalTheme - Populate for all 5 bundled themes with palette-matching colors: Catppuccin (Mantle/Crust), One Dark, Dracula, Gruvbox Dark, Solarized Dark - Switch title bar bg based on window.is_window_active() each render - Runtime theme changes reflected via existing config mtime polling Ref: tasks/prd-v2-title-bar.md US-007 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add 10px rounded top corners when window is floating in CSD mode
- Square top-left corner when tiled to left edge (tiling.top || tiling.left)
- Square top-right corner when tiled to right edge (tiling.top || tiling.right)
- Both corners squared when maximized (all tiling edges active)
- Apply mt/mb(-1px) + border(1px) colored as bg to fill transparent corner gaps
- Read tiling state from Decorations::Client { tiling } each render
Ref: tasks/prd-v2-title-bar.md US-008
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add on_mouse_down(Right) handler calling window.show_window_menu(position) - Gated on window_controls().window_menu via .when() — no-op if unsupported - No interference with left-click drag or double-click maximize Ref: tasks/prd-v2-title-bar.md US-009 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add window_decorations: Option<String> to PaneFlowConfig schema - Read at startup: "client" (default) → CSD, "server" → SSD - Invalid values fall back to "client" with log::warn - Config re-read on each window creation (future multi-window support) Ref: tasks/prd-v2-title-bar.md US-010 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Extract SIDEBAR_WIDTH constant (220px), shared between sidebar and title bar - Split title bar into fixed-width brand section (matches sidebar) + flexible content - Brand section uses pl_3() matching sidebar's p_3() internal padding - Add overflow_x_hidden on both sections for graceful min-width truncation - Pass sidebar_width to TitleBar entity for future dynamic width support Ref: tasks/prd-v2-title-bar.md US-011 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Register WindowControlArea::Drag on title bar root div - Register WindowControlArea::Close/Min/Max on respective control buttons - Maps WindowButton variants to WindowControlArea for platform hit-testing - Prepares for future Windows WM_NCHITTEST support - No behavioral change on Linux — metadata-only annotations Ref: tasks/prd-v2-title-bar.md US-012 Completes: PRD "PaneFlow v2 — Custom Title Bar with Window Controls" (12/12 stories) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Drop the entire v1 architecture: frontend/ (React+Vite), src-tauri/, paneflow-cli, paneflow-core, paneflow-ipc, paneflow-terminal (old), .tmux-ide skills, and ide.yml. Restructure workspace to only include paneflow-config and src-app (GPUI). Decouple paneflow-config from paneflow-core and tokio dependencies. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…CloseWindow action Remove focused/unfocused border styling from split panes for cleaner visuals. Switch font to Noto Sans Mono and use text_system advance API for precise cell width measurement. Add CloseWindow action dispatched from title bar close button for proper shutdown flow. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add PRDs for GPUI terminal, native terminal, and title bar features with status tracking. Include typing latency architecture audit and v2 options audit. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ArthurDEV44
added a commit
that referenced
this pull request
Apr 21, 2026
v0.2.1 retry #2 surfaced a false-positive in the post-import sanity check added in retry #1. The check used pattern `"gpg-pubkey-${KEY_SHORT}*"` and grep'd the output for the same substring — but `rpm -qa <pattern>` matches ONLY the NAME column, and all gpg-pubkey packages share the name "gpg-pubkey". The key-id lives in the Version-Release suffix. The diagnostic dump (`rpm -qa 'gpg-pubkey*'`) ran AFTER the false-positive and correctly showed `gpg-pubkey-183f2711-69e67caa` was imported, contradicting the error message directly above it. Fix: grep the full `rpm -qa 'gpg-pubkey*'` listing for the short-key-id substring. Same information content, no pattern quirk, works across rpm versions. Adds -i to grep for case-insensitive match (safety against KEY_SHORT case drift). The `--dbpath`-based user-scoped rpm db from retry #1 IS working correctly — this commit just fixes the check that was incorrectly flagging the correct-looking db as broken. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ArthurDEV44
added a commit
that referenced
this pull request
Apr 21, 2026
feat: v2 GPUI terminal application
ArthurDEV44
added a commit
that referenced
this pull request
Apr 21, 2026
v0.2.1 retry #2 surfaced a false-positive in the post-import sanity check added in retry #1. The check used pattern `"gpg-pubkey-${KEY_SHORT}*"` and grep'd the output for the same substring — but `rpm -qa <pattern>` matches ONLY the NAME column, and all gpg-pubkey packages share the name "gpg-pubkey". The key-id lives in the Version-Release suffix. The diagnostic dump (`rpm -qa 'gpg-pubkey*'`) ran AFTER the false-positive and correctly showed `gpg-pubkey-183f2711-69e67caa` was imported, contradicting the error message directly above it. Fix: grep the full `rpm -qa 'gpg-pubkey*'` listing for the short-key-id substring. Same information content, no pattern quirk, works across rpm versions. Adds -i to grep for case-insensitive match (safety against KEY_SHORT case drift). The `--dbpath`-based user-scoped rpm db from retry #1 IS working correctly — this commit just fixes the check that was incorrectly flagging the correct-looking db as broken. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ArthurDEV44
added a commit
that referenced
this pull request
May 5, 2026
Two latent bugs in the expanded smoke matrix introduced by 1884237 ("chore(release): pin toolchain, dry-run dispatch, auto-update e2e harness"). Neither was caught at the time because the dry-run dispatch itself was broken (env.RELEASE_MODE bug, fixed in 735a094), so the matrix never ran end-to-end against a real tag. Fix #1 — RPM freetype dep on openSUSE Tumbleweed ================================================ src-app/Cargo.toml had `freetype = "*"` in the explicit RPM requires table. `freetype` is the canonical package name on Fedora / RHEL / Rocky, but does NOT exist on openSUSE Tumbleweed where the runtime ships as `libfreetype6`. Tumbleweed's package only Provides the SONAME `libfreetype.so.6()(64bit)`, not the bare `freetype` virtual. Fedora 40 smoke passed because Fedora's `freetype` package literally matches. Tumbleweed smoke failed all 3 retries with `nothing provides freetype`. Replace with the SONAME virtual provide which every RPM distro that ships freetype-as-runtime exports (Fedora, RHEL, Rocky, Mageia, openSUSE Leap + Tumbleweed). The `(64bit)` ABI tag is correct for both x86_64 and aarch64 — it indicates the 64-bit ABI, not CPU arch. Fix #2 — auto-update e2e toolchain resolution ============================================== scripts/test-update-e2e.sh phase 2 checks out v0.2.10 in a git worktree and runs `cargo build`. v0.2.10 predates the rust-toolchain .toml pin (also from 1884237), so the worktree has no toolchain file. In CI the dtolnay/rust-toolchain action installs 1.95 but does NOT set a rustup default — running cargo without a pin file fails with "rustup could not choose a version of cargo to run, because one wasn't specified explicitly". Copy the current main toolchain pin into the worktree before building. Future-proof: when main bumps 1.95 -> 2.0 the e2e auto-follows without a script edit. Guarded by `if [ -f ... ]` so the script still works against pre-pin checkouts (where there's no source file to copy). Out of scope (Windows MSI) ========================== The Build x86_64-pc-windows-msvc leg is failing on `cargo wix build --no-build` with `Error[3] (Io): The 'build' path does not exist`. Pre-existing failure (also failed on v0.2.11), and the leg is `continue-on-error: true` per the matrix - does NOT block the release. Investigated separately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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
🤖 Generated with Claude Code