This commit is contained in:
@@ -18,12 +18,13 @@ as installable binaries; building from source will remain supported.
|
||||
|
||||
This repository is in architecture and vertical-slice phase. The installable
|
||||
binary is still a scaffold, while the isolated native UI spikes now exercise an
|
||||
interactive 3×1 workspace backed by six live comparison surfaces and the root workspace contains the first
|
||||
bounded local PTY, runtime-actor, VT engine, and capability-gated workspace
|
||||
command boundaries. The GPUI slice renders one styled actor-owned VT session in
|
||||
the primary 60% work band while five deterministic surfaces keep updating in the
|
||||
background. We are still validating selection/scrollback rendering, standalone
|
||||
runtime IPC/durability, ACP integration,
|
||||
interactive 20/60/20 workspace backed by six live comparison surfaces, and the
|
||||
root workspace contains the first bounded local PTY, runtime actor, VT engine,
|
||||
and capability-gated workspace command boundaries. The GPUI slice shows a
|
||||
responsive one-, three-, or five-lane work band—five on a 3440 px ultrawide—with
|
||||
one styled actor-owned VT session and five deterministic comparison surfaces.
|
||||
Retained-history navigation is wired. We are still validating terminal text
|
||||
selection and mouse input, standalone runtime IPC/durability, ACP integration,
|
||||
packaging, and usage-data contracts before a large implementation.
|
||||
|
||||
## Product shape
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use alacritty_terminal::event::{Event, EventListener, WindowSize};
|
||||
use alacritty_terminal::grid::Dimensions;
|
||||
use alacritty_terminal::grid::{Dimensions, Scroll};
|
||||
use alacritty_terminal::term::cell::Flags;
|
||||
use alacritty_terminal::term::{Config, Osc52, Term, TermMode, point_to_viewport};
|
||||
use alacritty_terminal::vte::ansi::{Color, CursorShape, NamedColor, Processor};
|
||||
@@ -259,12 +259,23 @@ impl TerminalModes {
|
||||
pub struct TerminalSnapshot {
|
||||
pub revision: u64,
|
||||
pub dimensions: TerminalDimensions,
|
||||
pub display_offset: usize,
|
||||
pub cells: Vec<TerminalCell>,
|
||||
pub cursor: TerminalCursor,
|
||||
pub modes: TerminalModes,
|
||||
pub title: Option<String>,
|
||||
}
|
||||
|
||||
/// Framework-neutral movement through the retained terminal history.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum TerminalScroll {
|
||||
Delta(i32),
|
||||
PageUp,
|
||||
PageDown,
|
||||
Top,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
impl TerminalSnapshot {
|
||||
#[must_use]
|
||||
pub fn cell(&self, row: u16, column: u16) -> Option<&TerminalCell> {
|
||||
@@ -544,6 +555,31 @@ impl TerminalEngine {
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves the visible viewport without writing bytes to the child PTY.
|
||||
///
|
||||
/// Returns `true` only when the retained-history offset changed.
|
||||
pub fn scroll_display(&mut self, scroll: TerminalScroll) -> bool {
|
||||
let previous_offset = self.terminal.grid().display_offset();
|
||||
let scroll = match scroll {
|
||||
TerminalScroll::Delta(lines) => Scroll::Delta(lines),
|
||||
TerminalScroll::PageUp => Scroll::PageUp,
|
||||
TerminalScroll::PageDown => Scroll::PageDown,
|
||||
TerminalScroll::Top => Scroll::Top,
|
||||
TerminalScroll::Bottom => Scroll::Bottom,
|
||||
};
|
||||
self.terminal.scroll_display(scroll);
|
||||
if self.terminal.grid().display_offset() == previous_offset {
|
||||
return false;
|
||||
}
|
||||
self.revision = self.revision.saturating_add(1);
|
||||
true
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn display_offset(&self) -> usize {
|
||||
self.terminal.grid().display_offset()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn snapshot(&self) -> TerminalSnapshot {
|
||||
let renderable = self.terminal.renderable_content();
|
||||
@@ -571,6 +607,7 @@ impl TerminalEngine {
|
||||
TerminalSnapshot {
|
||||
revision: self.revision,
|
||||
dimensions: self.dimensions,
|
||||
display_offset: renderable.display_offset,
|
||||
cells,
|
||||
cursor: TerminalCursor {
|
||||
row: u16::try_from(cursor_point.line).unwrap_or(u16::MAX),
|
||||
@@ -874,6 +911,7 @@ mod tests {
|
||||
use super::{
|
||||
KeyModifiers, TerminalCellStyle, TerminalDimensions, TerminalEngine, TerminalEngineOptions,
|
||||
TerminalEvent, TerminalKey, TerminalKeyEvent, TerminalModes, TerminalNamedColor,
|
||||
TerminalScroll,
|
||||
};
|
||||
|
||||
fn engine(rows: u16, columns: u16) -> TerminalEngine {
|
||||
@@ -917,6 +955,23 @@ mod tests {
|
||||
assert_eq!(engine.snapshot().plain_rows()[0], "primary");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scrolls_retained_history_without_touching_the_pty_input_path() {
|
||||
let mut engine = engine(2, 8);
|
||||
engine.process(b"one\r\ntwo\r\nthree\r\nfour");
|
||||
assert_eq!(engine.display_offset(), 0);
|
||||
|
||||
let revision = engine.revision();
|
||||
assert!(engine.scroll_display(TerminalScroll::PageUp));
|
||||
assert!(engine.display_offset() > 0);
|
||||
assert!(engine.revision() > revision);
|
||||
assert_eq!(engine.snapshot().display_offset, engine.display_offset());
|
||||
|
||||
assert!(engine.scroll_display(TerminalScroll::Bottom));
|
||||
assert_eq!(engine.display_offset(), 0);
|
||||
assert!(!engine.scroll_display(TerminalScroll::Bottom));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emits_protocol_replies_without_logging_input_content() {
|
||||
let mut engine = engine(4, 10);
|
||||
|
||||
+14
-12
@@ -71,21 +71,23 @@ shutdown against ordered raw-byte output. The GPUI slice feeds one actor session
|
||||
through the terminal engine while five surfaces retain deterministic comparison
|
||||
output. Its adapter groups adjacent cells into native GPUI paint runs and renders
|
||||
ANSI/indexed/RGB colors, emphasis, hyperlinks, and cursor shapes. Window geometry
|
||||
drives terminal rows and columns and resizes both the engine and PTY. Selection,
|
||||
scrollback navigation, mouse reporting, and a lower-level terminal canvas remain.
|
||||
This actor still runs in-process; moving the same framework-neutral
|
||||
contract behind local authenticated IPC is the next durability step. See
|
||||
decisions 0004 and 0005.
|
||||
drives terminal rows and columns and resizes both the engine and PTY. The engine
|
||||
now exposes retained-history offsets and page/top/bottom viewport movement
|
||||
without writing scroll keys to the PTY. Text selection, mouse reporting, and a
|
||||
lower-level terminal canvas remain. This actor still runs in-process; moving the
|
||||
same framework-neutral contract behind local authenticated IPC is the next
|
||||
durability step. See decisions 0004 and 0005.
|
||||
|
||||
## Default workspace composition
|
||||
|
||||
The default desktop workspace is a vertical 3×1 composition: the top 20% holds
|
||||
pane, agent, tool, context, and goal state; the middle 60% is the active terminal
|
||||
or work surface; the bottom 20% is the answer, choice, chat, and approval shelf.
|
||||
Six deterministic surfaces still update in the spike so performance comparisons
|
||||
remain meaningful, but only the selected surface owns the large work region.
|
||||
Users may deliberately split that region later; a dashboard grid is not the
|
||||
calm default. See decision 0008.
|
||||
The default desktop workspace has three horizontal bands: the top 20% holds pane,
|
||||
agent, tool, context, and goal state; the middle 60% holds responsive vertical
|
||||
work lanes; and the bottom 20% is the answer, choice, chat, and approval shelf.
|
||||
The work region shows one lane in compact windows, three at normal desktop
|
||||
widths, and five on a 3440 px ultrawide. The selected lane alone owns keyboard
|
||||
input. Six surfaces continue updating so performance comparisons remain
|
||||
meaningful, with a sliding visible window keeping the selected pane onscreen.
|
||||
See decisions 0008 and 0009.
|
||||
|
||||
We should evaluate, not blindly copy, WezTerm, Zellij, RMUX, tmux, and cmux. The
|
||||
first spike must compare a reusable terminal crate with a small first-party layer.
|
||||
|
||||
@@ -66,11 +66,13 @@ surface change never silently launches a process, moves the pane to another
|
||||
host, or changes which agent owns the session. Unsupported surfaces are shown as
|
||||
unavailable rather than simulated.
|
||||
|
||||
The default workspace is a 3×1 vertical composition: the top 20% shows context,
|
||||
pane/agent state, tools, and goal; the middle 60% is the selected terminal or
|
||||
work surface; the bottom 20% is the decision shelf. Multiple panes may continue
|
||||
working offscreen and remain one shortcut away. Explicit split and dashboard
|
||||
layouts remain supported, but they do not displace the primary-terminal default.
|
||||
The default workspace has three horizontal bands: the top 20% shows context,
|
||||
pane/agent state, tools, and goal; the middle 60% is a responsive set of vertical
|
||||
work lanes; and the bottom 20% is the decision shelf. The work band shows one
|
||||
lane in compact windows, three at normal desktop widths, and up to five on an
|
||||
ultrawide display. One lane owns keyboard input at a time, and selecting a lane
|
||||
never pauses the others. Explicit split trees and dashboard layouts remain
|
||||
supported, but they do not displace this terminal-first default.
|
||||
|
||||
An optional decision shelf sits below the active work surface. It may show an
|
||||
answer draft, two or three concrete choices, why each was suggested, and the
|
||||
|
||||
+8
-4
@@ -118,10 +118,14 @@ who already have the final cargo-watch release installed.
|
||||
blocked OSC 52 behavior, key encoding, application-cursor mode, and resize.
|
||||
- `lumbridge-core` tests capability-gated split/select/rename/close commands,
|
||||
idempotent request IDs, close-tree promotion, and atomic agentic setup plans.
|
||||
- The GPUI slice tests its key-event adapter, 20/60/20 geometry-to-PTY sizing,
|
||||
xterm 256-color conversion, styled-run coalescing, and cursor-run boundaries.
|
||||
It renders one real actor-owned VT session while five surfaces continue their
|
||||
deterministic background workload.
|
||||
- The GPUI slice tests its key-event adapter, responsive one/three/five-lane
|
||||
geometry, selected-pane lane windowing, 20/60/20 geometry-to-PTY sizing, xterm
|
||||
256-color conversion, styled-run coalescing, cursor-run boundaries, and
|
||||
scrollback shortcut routing. It renders one real actor-owned VT session while
|
||||
five surfaces continue their deterministic background workload.
|
||||
- `lumbridge-terminal` retains 10,000 history lines by default and tests
|
||||
framework-neutral page/top/bottom viewport movement, revision changes, and
|
||||
live-bottom no-ops without sending history-navigation bytes to the child PTY.
|
||||
- The current-GPUI probe compile-checks real AccessKit element wiring and real
|
||||
platform input-handler installation. Unit tests cover its semantic tree and
|
||||
UTF-16/UTF-8 composed-text mutations. OS screen readers, IME candidate windows,
|
||||
|
||||
@@ -24,9 +24,9 @@ from macOS and Linux and the hard gates pass.
|
||||
The current programs establish dependency, build, launch, and interaction
|
||||
baselines. GPUI now routes a real PTY through a VT engine and keyboard encoder,
|
||||
paints coalesced styled cell runs and cursor shapes, and derives PTY rows/columns
|
||||
from its 60% terminal band when the window changes. Selection, scrollback
|
||||
navigation, mouse modes, a native Markdown editor, and one isolated browser child
|
||||
remain.
|
||||
from its responsive one/three/five-lane 60% terminal band when the window
|
||||
changes. Retained-history page/top/bottom navigation is wired. Text selection,
|
||||
mouse modes, a native Markdown editor, and one isolated browser child remain.
|
||||
|
||||
## Ubuntu baseline — metal, 2026-08-31
|
||||
|
||||
@@ -87,9 +87,9 @@ fallback.
|
||||
## Measurement semantics
|
||||
|
||||
Both renderers retain the same all-deterministic six-surface action stream for
|
||||
comparison. GPUI now presents the selected surface in a 20/60/20 vertical
|
||||
workspace while one terminal fixture is replaced with a real actor-owned VT
|
||||
session and five deterministic surfaces keep running offscreen. Counters
|
||||
comparison. GPUI now presents a 20/60/20 workspace whose middle band shows one,
|
||||
three, or five vertical work lanes while one terminal fixture is replaced with a
|
||||
real actor-owned VT session and five deterministic surfaces keep running. Counters
|
||||
separate external PTY batches/lines from total model updates. The GPUI footer
|
||||
reports dispatch-to-element-build p50/p95 over a bounded 256-sample window. It
|
||||
is deliberately not called key-to-present or frame-present latency: neither
|
||||
|
||||
+18
-12
@@ -10,12 +10,14 @@ framework decision gate, not a decorative dashboard.
|
||||
## Current-run audit
|
||||
|
||||
The first 2026-08-31 GPUI baseline proved six-surface density but gave every
|
||||
pane equal visual priority. The current slice replaces that dashboard with a
|
||||
3×1 default: 20% context and pane selection, 60% active work surface, and 20%
|
||||
decision shelf. Keyboard focus, command-palette input, live PTY input, styled VT
|
||||
cells, cursor shapes, and geometry-driven resize are wired. Remaining visible
|
||||
gaps are terminal selection/scrollback, real decision-shelf actions, usage-detail
|
||||
provenance, and end-to-end platform accessibility/IME.
|
||||
pane equal visual priority. The current slice replaces that dashboard with three
|
||||
horizontal bands: 20% context and pane selection, a 60% responsive work region,
|
||||
and a 20% decision shelf. The work region shows one, three, or five vertical
|
||||
lanes according to available width. Keyboard focus, command-palette input, live
|
||||
PTY input, styled VT cells, cursor shapes, geometry-driven resize, and retained-
|
||||
history navigation are wired. Remaining visible gaps are terminal text selection
|
||||
and mouse modes, real decision-shelf actions, usage-detail provenance, and end-to-
|
||||
end platform accessibility/IME.
|
||||
|
||||
Screenshots for the audit are stored outside Git under
|
||||
`~/shots/2026-08/lumbridge-ui-audit/`. Accessibility and IME correctness cannot
|
||||
@@ -40,8 +42,8 @@ research, not source assets for Lumbridge. We adapt these interaction patterns:
|
||||
cards;
|
||||
- a quiet top tab strip for mixed terminal, Markdown, browser, and review
|
||||
surfaces;
|
||||
- one primary work surface with optional splits, instead of forcing every
|
||||
surface into an equal dashboard tile;
|
||||
- a primary work region with a small responsive number of equal vertical lanes,
|
||||
instead of either one ultrawide terminal or a dense equal-card dashboard;
|
||||
- narrow contextual tools, such as files, review, or Buzz, that can collapse
|
||||
when the terminal needs the space;
|
||||
- usage and agent state at the edge of the workspace rather than in modal
|
||||
@@ -62,8 +64,9 @@ surface rather than a hosted Lumbridge control plane.
|
||||
the selected pane receives the accent treatment.
|
||||
4. A waiting pane uses an amber semantic label in both the context band and
|
||||
attention sidebar. The label always includes words such as `NEEDS INPUT`.
|
||||
5. The middle 60% gives the selected terminal or work surface uninterrupted
|
||||
reading space and retains target, harness state, dimensions, and surface tabs.
|
||||
5. The middle 60% gives one, three, or five terminal/work lanes uninterrupted
|
||||
height and retains target, harness state, dimensions, and surface tabs. The
|
||||
selected lane alone owns keyboard input.
|
||||
6. The footer groups connection state, selected-harness identity, usage-window
|
||||
provenance, and burn forecast into readable regions.
|
||||
7. The selected pane has a quiet surface strip for Terminal, Browser, Tools,
|
||||
@@ -79,6 +82,8 @@ surface rather than a hosted Lumbridge control plane.
|
||||
- `Alt+1` through `Alt+6`: focus a pane directly while leaving terminal digits
|
||||
available to the PTY.
|
||||
- `Cmd+K` on macOS or `Ctrl+K` on Linux: open the command palette.
|
||||
- `Shift+PageUp/PageDown`: move the selected live terminal through retained
|
||||
history; `Shift+Home/End` jumps to the history top/live bottom.
|
||||
- Typing while the palette is open changes its query; arrows change the result;
|
||||
`Enter` runs it; `Escape` closes it and restores pane focus.
|
||||
- `Enter` on a waiting agent opens its request detail. Approval remains a
|
||||
@@ -92,8 +97,9 @@ receive the same state transitions and tests.
|
||||
|
||||
- In the first actor integration, one terminal pane consumes ordered output from
|
||||
a real local PTY while each deterministic tick updates the other five
|
||||
surfaces offscreen. The all-deterministic constructor remains available for
|
||||
framework comparison and replay tests.
|
||||
surfaces. One, three, or five are visible according to available width; the
|
||||
all-deterministic constructor remains available for framework comparison and
|
||||
replay tests.
|
||||
- One pane enters and leaves `needs input` through a deterministic event.
|
||||
- Markdown, browser-boundary, and review panes update counters without using a
|
||||
web application shell.
|
||||
|
||||
@@ -2,22 +2,21 @@
|
||||
|
||||
Status: accepted for the native vertical slice.
|
||||
|
||||
Lumbridge defaults to three vertical bands in one column. The top 20% exposes
|
||||
Lumbridge defaults to three horizontal bands in one column. The top 20% exposes
|
||||
pane, agent, tools, context, goal, target, and attention state. The middle 60%
|
||||
belongs to the active terminal or work surface. The bottom 20% holds answers,
|
||||
choices, chat, approvals, and the local-analyst boundary.
|
||||
is the terminal-first work region. The bottom 20% holds answers, choices, chat,
|
||||
approvals, and the local-analyst boundary.
|
||||
|
||||
This composition favors the surface where engineers type and read for most of
|
||||
the day. It keeps context and decisions visible without making six equally sized
|
||||
cards compete for attention. Pane selectors and keyboard shortcuts switch the
|
||||
large work surface. Users may later create deliberate splits or dashboard grids,
|
||||
but those are workspace choices rather than the default visual hierarchy.
|
||||
This composition favors the region where engineers type and read for most of the
|
||||
day. It keeps context and decisions visible without making every surface a small
|
||||
dashboard card. The middle region may contain a responsive number of vertical
|
||||
work lanes under decision 0009; the selected lane alone owns keyboard input.
|
||||
|
||||
The framework spike retains six updating surfaces even when five are offscreen.
|
||||
That preserves the earlier streaming and reducer workload for performance
|
||||
comparison. Hidden surfaces do not receive keyboard input merely because they
|
||||
continue to update.
|
||||
|
||||
Window geometry determines the middle band's terminal rows and columns. The UI
|
||||
Window and lane geometry determine each terminal's rows and columns. The UI
|
||||
resizes the terminal engine and PTY through the bounded runtime actor; it does
|
||||
not resize only the text view or infer terminal wrapping after the fact.
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# 0009: The primary work region uses responsive vertical lanes
|
||||
|
||||
Status: accepted for the native vertical slice.
|
||||
|
||||
The middle 60% work region shows one vertical lane in compact windows, three at
|
||||
normal desktop widths, and five when at least 2800 px remains after the sidebar.
|
||||
A 3440 px ultrawide therefore presents five lanes. A sliding window over the six
|
||||
comparison panes always keeps the selected pane visible.
|
||||
|
||||
This is not a return to the original 2×3 dashboard. Every lane receives the full
|
||||
work-region height, the surrounding context and decision bands remain stable,
|
||||
and only the selected lane owns keyboard input. Clicking a lane selects it. Pane
|
||||
processes and output continue independently of selection.
|
||||
|
||||
Terminal geometry is computed from the actual width of one lane, including gaps
|
||||
and insets. The selected real PTY is resized through the runtime actor when the
|
||||
responsive lane count or window bounds change. On the 3440×1440 reference
|
||||
display the current fixed-cell spike derives approximately 71 columns by 40
|
||||
rows per lane.
|
||||
|
||||
The first actor slice still owns one real PTY and five deterministic comparison
|
||||
surfaces. Promoting the runtime boundary from one actor to a pane-indexed session
|
||||
registry is required before all five visible terminal lanes can own independent
|
||||
real processes.
|
||||
+3
-2
@@ -24,8 +24,9 @@ shared model retain the all-deterministic mode for like-for-like framework
|
||||
comparison. GPUI feeds raw output through `lumbridge-terminal` and sends encoded
|
||||
keyboard input and terminal protocol replies through the bounded runtime actor.
|
||||
Its visual adapter coalesces VT cells into native styled runs, paints cursor
|
||||
shapes, and resizes the engine and PTY from the middle 60% work band. Selection,
|
||||
scrollback navigation, and mouse input remain intentionally unfinished.
|
||||
shapes, and resizes the engine and PTY from a responsive one/three/five-lane
|
||||
middle 60% work band. Retained-history navigation is wired; text selection and
|
||||
mouse input remain intentionally unfinished.
|
||||
|
||||
Build independently:
|
||||
|
||||
|
||||
+153
-17
@@ -16,7 +16,7 @@ use lumbridge_spike_model::{
|
||||
use lumbridge_terminal::{
|
||||
KeyModifiers, TerminalCellStyle, TerminalColor, TerminalCursorShape, TerminalDimensions,
|
||||
TerminalEngine, TerminalEngineOptions, TerminalKey, TerminalKeyEvent, TerminalNamedColor,
|
||||
TerminalSnapshot,
|
||||
TerminalScroll, TerminalSnapshot,
|
||||
};
|
||||
|
||||
const BG: u32 = 0x090c12;
|
||||
@@ -43,6 +43,7 @@ const TERMINAL_CHROME_HEIGHT: f32 = 72.0;
|
||||
const TERMINAL_HORIZONTAL_INSET: f32 = 32.0;
|
||||
const TERMINAL_CELL_WIDTH: f32 = 8.4;
|
||||
const TERMINAL_CELL_HEIGHT: f32 = 18.0;
|
||||
const WORK_LANE_GAP: f32 = 4.0;
|
||||
const TERMINAL_ROW_STEP: u16 = 2;
|
||||
const TERMINAL_COLUMN_STEP: u16 = 10;
|
||||
|
||||
@@ -279,10 +280,14 @@ fn dim_color(color: u32) -> u32 {
|
||||
fn terminal_dimensions_for_window(window_size: Size<Pixels>) -> TerminalDimensions {
|
||||
let width = f32::from(window_size.width);
|
||||
let height = f32::from(window_size.height);
|
||||
let lane_count = visible_lane_count(window_size);
|
||||
let workspace_height =
|
||||
(height - APP_HEADER_HEIGHT - TAB_BAR_HEIGHT - APP_FOOTER_HEIGHT).max(0.0);
|
||||
let terminal_height = (workspace_height * 0.60 - TERMINAL_CHROME_HEIGHT).max(0.0);
|
||||
let terminal_width = (width - SIDEBAR_WIDTH - TERMINAL_HORIZONTAL_INSET).max(0.0);
|
||||
let workspace_width = (width - SIDEBAR_WIDTH).max(0.0);
|
||||
let lane_gaps = WORK_LANE_GAP * lane_count.saturating_sub(1) as f32;
|
||||
let lane_width = ((workspace_width - lane_gaps).max(0.0) / lane_count as f32).max(0.0);
|
||||
let terminal_width = (lane_width - TERMINAL_HORIZONTAL_INSET).max(0.0);
|
||||
let rows = (terminal_height / TERMINAL_CELL_HEIGHT)
|
||||
.floor()
|
||||
.clamp(2.0, f32::from(u16::MAX));
|
||||
@@ -298,6 +303,25 @@ fn terminal_dimensions_for_window(window_size: Size<Pixels>) -> TerminalDimensio
|
||||
.expect("geometry clamps terminal dimensions above zero")
|
||||
}
|
||||
|
||||
fn visible_lane_count(window_size: Size<Pixels>) -> usize {
|
||||
let workspace_width = (f32::from(window_size.width) - SIDEBAR_WIDTH).max(0.0);
|
||||
if workspace_width >= 2_800.0 {
|
||||
5
|
||||
} else if workspace_width >= 1_100.0 {
|
||||
3
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
fn visible_lane_range(total: usize, selected: usize, lane_count: usize) -> std::ops::Range<usize> {
|
||||
let count = lane_count.clamp(1, total.max(1)).min(total);
|
||||
let start = selected
|
||||
.saturating_sub(count / 2)
|
||||
.min(total.saturating_sub(count));
|
||||
start..start + count
|
||||
}
|
||||
|
||||
impl RenderTiming {
|
||||
fn mark_dispatch(&mut self) {
|
||||
self.pending_dispatch = Some(Instant::now());
|
||||
@@ -527,6 +551,14 @@ impl LumbridgeShell {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn scroll_terminal(&mut self, scroll: TerminalScroll, cx: &mut Context<Self>) {
|
||||
if !self.terminal.scroll_display(scroll) {
|
||||
return;
|
||||
}
|
||||
self.publish_terminal_snapshot();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_pane(&mut self, pane: PaneId, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.dispatch(ShellAction::SelectPane(pane));
|
||||
window.focus(&self.root_focus);
|
||||
@@ -615,6 +647,13 @@ impl LumbridgeShell {
|
||||
return;
|
||||
}
|
||||
let modifiers = key_modifiers(event);
|
||||
if let Some(scroll) = terminal_scroll_from_parts(
|
||||
event.keystroke.key.as_str(),
|
||||
modifiers,
|
||||
) {
|
||||
self.scroll_terminal(scroll, cx);
|
||||
return;
|
||||
}
|
||||
let Some(event) = terminal_key_from_parts(
|
||||
event.keystroke.key.as_str(),
|
||||
event.keystroke.key_char.as_deref(),
|
||||
@@ -629,6 +668,11 @@ impl LumbridgeShell {
|
||||
if let Err(error) = self.send_runtime_command(RuntimeCommand::Input(bytes)) {
|
||||
self.runtime_status = LiveRuntimeStatus::Fault(error.to_string());
|
||||
}
|
||||
if self.terminal.display_offset() > 0
|
||||
&& self.terminal.scroll_display(TerminalScroll::Bottom)
|
||||
{
|
||||
self.publish_terminal_snapshot();
|
||||
}
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
@@ -807,8 +851,13 @@ impl LumbridgeShell {
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn work_surface(&self) -> gpui::AnyElement {
|
||||
let pane = self.model.pane(self.model.selected_pane());
|
||||
fn work_surface(
|
||||
&self,
|
||||
pane: &PaneState,
|
||||
selected: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> gpui::AnyElement {
|
||||
let pane_id = pane.id();
|
||||
let external = pane.output_source() == OutputSource::External;
|
||||
let status = if external {
|
||||
self.runtime_status.badge()
|
||||
@@ -820,9 +869,17 @@ impl LumbridgeShell {
|
||||
} else {
|
||||
pane.fixture().target.to_owned()
|
||||
};
|
||||
let surface_status = if external {
|
||||
let surface_status = if external && self.terminal_snapshot.display_offset > 0 {
|
||||
format!(
|
||||
"{} · {}×{} · auto-fit",
|
||||
"{} · ↑{} · {}×{}",
|
||||
status,
|
||||
self.terminal_snapshot.display_offset,
|
||||
self.terminal.dimensions().columns(),
|
||||
self.terminal.dimensions().rows()
|
||||
)
|
||||
} else if external {
|
||||
format!(
|
||||
"{} · {}×{}",
|
||||
status,
|
||||
self.terminal.dimensions().columns(),
|
||||
self.terminal.dimensions().rows()
|
||||
@@ -870,14 +927,19 @@ impl LumbridgeShell {
|
||||
};
|
||||
|
||||
div()
|
||||
.id(("work-lane", pane_id.index()))
|
||||
.on_click(cx.listener(move |shell, _, window, cx| {
|
||||
shell.select_pane(pane_id, window, cx);
|
||||
}))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.size_full()
|
||||
.min_w_0()
|
||||
.min_h_0()
|
||||
.overflow_hidden()
|
||||
.bg(rgb(PANEL))
|
||||
.border_y_1()
|
||||
.border_color(rgb(BORDER))
|
||||
.border_1()
|
||||
.border_color(rgb(if selected { ACCENT } else { BORDER }))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
@@ -891,15 +953,23 @@ impl LumbridgeShell {
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_3()
|
||||
.gap_2()
|
||||
.min_w_0()
|
||||
.text_sm()
|
||||
.child(pane.fixture().title)
|
||||
.child(div().text_xs().text_color(rgb(MUTED)).child(detail)),
|
||||
.child(div().flex_none().child(pane.fixture().title))
|
||||
.child(
|
||||
div()
|
||||
.truncate()
|
||||
.text_xs()
|
||||
.text_color(rgb(MUTED))
|
||||
.child(detail),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.text_xs()
|
||||
.text_color(rgb(SUCCESS))
|
||||
.text_color(rgb(if selected { SUCCESS } else { MUTED }))
|
||||
.child(surface_status),
|
||||
),
|
||||
)
|
||||
@@ -1068,7 +1138,7 @@ impl LumbridgeShell {
|
||||
}
|
||||
|
||||
impl Render for LumbridgeShell {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let attention = self.model.pane(PaneId::ClaudeUi);
|
||||
let sidebar = div()
|
||||
.flex()
|
||||
@@ -1251,6 +1321,26 @@ impl Render for LumbridgeShell {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let selected = self.model.pane(self.model.selected_pane());
|
||||
let lane_count = visible_lane_count(window.bounds().size);
|
||||
let lane_range = visible_lane_range(
|
||||
PaneId::ALL.len(),
|
||||
self.model.selected_pane().index(),
|
||||
lane_count,
|
||||
);
|
||||
let work_lanes = lane_range
|
||||
.map(|index| {
|
||||
let pane = self.model.pane(PaneId::ALL[index]);
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.min_h_0()
|
||||
.child(self.work_surface(
|
||||
pane,
|
||||
pane.id() == self.model.selected_pane(),
|
||||
cx,
|
||||
))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let workspace_stack = div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
@@ -1310,9 +1400,11 @@ impl Render for LumbridgeShell {
|
||||
div()
|
||||
.h(relative(0.60))
|
||||
.flex_none()
|
||||
.flex()
|
||||
.gap_1()
|
||||
.min_h_0()
|
||||
.overflow_hidden()
|
||||
.child(self.work_surface()),
|
||||
.children(work_lanes),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
@@ -1498,6 +1590,19 @@ fn terminal_key_from_parts(
|
||||
Some(TerminalKeyEvent { key, modifiers })
|
||||
}
|
||||
|
||||
fn terminal_scroll_from_parts(key: &str, modifiers: KeyModifiers) -> Option<TerminalScroll> {
|
||||
if !modifiers.contains(KeyModifiers::SHIFT) {
|
||||
return None;
|
||||
}
|
||||
match key {
|
||||
"pageup" => Some(TerminalScroll::PageUp),
|
||||
"pagedown" => Some(TerminalScroll::PageDown),
|
||||
"home" => Some(TerminalScroll::Top),
|
||||
"end" => Some(TerminalScroll::Bottom),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn start_live_runtime(dimensions: TerminalDimensions) -> Result<RuntimeActor, RuntimeActorError> {
|
||||
let command = CommandConfig::new("/bin/sh")
|
||||
.map_err(RuntimeActorError::Start)?
|
||||
@@ -1551,8 +1656,9 @@ fn main() {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
KeyModifiers, TerminalEngine, TerminalEngineOptions, TerminalKey, indexed_terminal_color,
|
||||
terminal_dimensions_for_window, terminal_key_from_parts, terminal_paint_rows,
|
||||
KeyModifiers, TerminalEngine, TerminalEngineOptions, TerminalKey, TerminalScroll,
|
||||
indexed_terminal_color, terminal_dimensions_for_window, terminal_key_from_parts,
|
||||
terminal_paint_rows, terminal_scroll_from_parts, visible_lane_count, visible_lane_range,
|
||||
};
|
||||
use gpui::{px, size};
|
||||
|
||||
@@ -1582,7 +1688,37 @@ mod tests {
|
||||
fn terminal_geometry_tracks_the_middle_sixty_percent() {
|
||||
let dimensions = terminal_dimensions_for_window(size(px(1500.0), px(960.0)));
|
||||
assert_eq!(dimensions.rows(), 24);
|
||||
assert_eq!(dimensions.columns(), 145);
|
||||
assert_eq!(dimensions.columns(), 45);
|
||||
|
||||
let ultrawide = size(px(3440.0), px(1440.0));
|
||||
assert_eq!(visible_lane_count(ultrawide), 5);
|
||||
let dimensions = terminal_dimensions_for_window(ultrawide);
|
||||
assert_eq!(dimensions.rows(), 40);
|
||||
assert_eq!(dimensions.columns(), 71);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lane_window_keeps_the_selected_pane_visible() {
|
||||
assert_eq!(visible_lane_range(6, 0, 5), 0..5);
|
||||
assert_eq!(visible_lane_range(6, 5, 5), 1..6);
|
||||
assert_eq!(visible_lane_range(6, 3, 3), 2..5);
|
||||
assert_eq!(visible_lane_range(6, 4, 1), 4..5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shift_navigation_controls_scrollback_without_reaching_the_pty() {
|
||||
assert_eq!(
|
||||
terminal_scroll_from_parts("pageup", KeyModifiers::SHIFT),
|
||||
Some(TerminalScroll::PageUp)
|
||||
);
|
||||
assert_eq!(
|
||||
terminal_scroll_from_parts("end", KeyModifiers::SHIFT),
|
||||
Some(TerminalScroll::Bottom)
|
||||
);
|
||||
assert_eq!(
|
||||
terminal_scroll_from_parts("pageup", KeyModifiers::default()),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user