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
|
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
|
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
|
interactive 20/60/20 workspace backed by six live comparison surfaces, and the
|
||||||
bounded local PTY, runtime-actor, VT engine, and capability-gated workspace
|
root workspace contains the first bounded local PTY, runtime actor, VT engine,
|
||||||
command boundaries. The GPUI slice renders one styled actor-owned VT session in
|
and capability-gated workspace command boundaries. The GPUI slice shows a
|
||||||
the primary 60% work band while five deterministic surfaces keep updating in the
|
responsive one-, three-, or five-lane work band—five on a 3440 px ultrawide—with
|
||||||
background. We are still validating selection/scrollback rendering, standalone
|
one styled actor-owned VT session and five deterministic comparison surfaces.
|
||||||
runtime IPC/durability, ACP integration,
|
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.
|
packaging, and usage-data contracts before a large implementation.
|
||||||
|
|
||||||
## Product shape
|
## Product shape
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
use alacritty_terminal::event::{Event, EventListener, WindowSize};
|
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::cell::Flags;
|
||||||
use alacritty_terminal::term::{Config, Osc52, Term, TermMode, point_to_viewport};
|
use alacritty_terminal::term::{Config, Osc52, Term, TermMode, point_to_viewport};
|
||||||
use alacritty_terminal::vte::ansi::{Color, CursorShape, NamedColor, Processor};
|
use alacritty_terminal::vte::ansi::{Color, CursorShape, NamedColor, Processor};
|
||||||
@@ -259,12 +259,23 @@ impl TerminalModes {
|
|||||||
pub struct TerminalSnapshot {
|
pub struct TerminalSnapshot {
|
||||||
pub revision: u64,
|
pub revision: u64,
|
||||||
pub dimensions: TerminalDimensions,
|
pub dimensions: TerminalDimensions,
|
||||||
|
pub display_offset: usize,
|
||||||
pub cells: Vec<TerminalCell>,
|
pub cells: Vec<TerminalCell>,
|
||||||
pub cursor: TerminalCursor,
|
pub cursor: TerminalCursor,
|
||||||
pub modes: TerminalModes,
|
pub modes: TerminalModes,
|
||||||
pub title: Option<String>,
|
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 {
|
impl TerminalSnapshot {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn cell(&self, row: u16, column: u16) -> Option<&TerminalCell> {
|
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]
|
#[must_use]
|
||||||
pub fn snapshot(&self) -> TerminalSnapshot {
|
pub fn snapshot(&self) -> TerminalSnapshot {
|
||||||
let renderable = self.terminal.renderable_content();
|
let renderable = self.terminal.renderable_content();
|
||||||
@@ -571,6 +607,7 @@ impl TerminalEngine {
|
|||||||
TerminalSnapshot {
|
TerminalSnapshot {
|
||||||
revision: self.revision,
|
revision: self.revision,
|
||||||
dimensions: self.dimensions,
|
dimensions: self.dimensions,
|
||||||
|
display_offset: renderable.display_offset,
|
||||||
cells,
|
cells,
|
||||||
cursor: TerminalCursor {
|
cursor: TerminalCursor {
|
||||||
row: u16::try_from(cursor_point.line).unwrap_or(u16::MAX),
|
row: u16::try_from(cursor_point.line).unwrap_or(u16::MAX),
|
||||||
@@ -874,6 +911,7 @@ mod tests {
|
|||||||
use super::{
|
use super::{
|
||||||
KeyModifiers, TerminalCellStyle, TerminalDimensions, TerminalEngine, TerminalEngineOptions,
|
KeyModifiers, TerminalCellStyle, TerminalDimensions, TerminalEngine, TerminalEngineOptions,
|
||||||
TerminalEvent, TerminalKey, TerminalKeyEvent, TerminalModes, TerminalNamedColor,
|
TerminalEvent, TerminalKey, TerminalKeyEvent, TerminalModes, TerminalNamedColor,
|
||||||
|
TerminalScroll,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn engine(rows: u16, columns: u16) -> TerminalEngine {
|
fn engine(rows: u16, columns: u16) -> TerminalEngine {
|
||||||
@@ -917,6 +955,23 @@ mod tests {
|
|||||||
assert_eq!(engine.snapshot().plain_rows()[0], "primary");
|
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]
|
#[test]
|
||||||
fn emits_protocol_replies_without_logging_input_content() {
|
fn emits_protocol_replies_without_logging_input_content() {
|
||||||
let mut engine = engine(4, 10);
|
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
|
through the terminal engine while five surfaces retain deterministic comparison
|
||||||
output. Its adapter groups adjacent cells into native GPUI paint runs and renders
|
output. Its adapter groups adjacent cells into native GPUI paint runs and renders
|
||||||
ANSI/indexed/RGB colors, emphasis, hyperlinks, and cursor shapes. Window geometry
|
ANSI/indexed/RGB colors, emphasis, hyperlinks, and cursor shapes. Window geometry
|
||||||
drives terminal rows and columns and resizes both the engine and PTY. Selection,
|
drives terminal rows and columns and resizes both the engine and PTY. The engine
|
||||||
scrollback navigation, mouse reporting, and a lower-level terminal canvas remain.
|
now exposes retained-history offsets and page/top/bottom viewport movement
|
||||||
This actor still runs in-process; moving the same framework-neutral
|
without writing scroll keys to the PTY. Text selection, mouse reporting, and a
|
||||||
contract behind local authenticated IPC is the next durability step. See
|
lower-level terminal canvas remain. This actor still runs in-process; moving the
|
||||||
decisions 0004 and 0005.
|
same framework-neutral contract behind local authenticated IPC is the next
|
||||||
|
durability step. See decisions 0004 and 0005.
|
||||||
|
|
||||||
## Default workspace composition
|
## Default workspace composition
|
||||||
|
|
||||||
The default desktop workspace is a vertical 3×1 composition: the top 20% holds
|
The default desktop workspace has three horizontal bands: the top 20% holds pane,
|
||||||
pane, agent, tool, context, and goal state; the middle 60% is the active terminal
|
agent, tool, context, and goal state; the middle 60% holds responsive vertical
|
||||||
or work surface; the bottom 20% is the answer, choice, chat, and approval shelf.
|
work lanes; and the bottom 20% is the answer, choice, chat, and approval shelf.
|
||||||
Six deterministic surfaces still update in the spike so performance comparisons
|
The work region shows one lane in compact windows, three at normal desktop
|
||||||
remain meaningful, but only the selected surface owns the large work region.
|
widths, and five on a 3440 px ultrawide. The selected lane alone owns keyboard
|
||||||
Users may deliberately split that region later; a dashboard grid is not the
|
input. Six surfaces continue updating so performance comparisons remain
|
||||||
calm default. See decision 0008.
|
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
|
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.
|
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
|
host, or changes which agent owns the session. Unsupported surfaces are shown as
|
||||||
unavailable rather than simulated.
|
unavailable rather than simulated.
|
||||||
|
|
||||||
The default workspace is a 3×1 vertical composition: the top 20% shows context,
|
The default workspace has three horizontal bands: the top 20% shows context,
|
||||||
pane/agent state, tools, and goal; the middle 60% is the selected terminal or
|
pane/agent state, tools, and goal; the middle 60% is a responsive set of vertical
|
||||||
work surface; the bottom 20% is the decision shelf. Multiple panes may continue
|
work lanes; and the bottom 20% is the decision shelf. The work band shows one
|
||||||
working offscreen and remain one shortcut away. Explicit split and dashboard
|
lane in compact windows, three at normal desktop widths, and up to five on an
|
||||||
layouts remain supported, but they do not displace the primary-terminal default.
|
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
|
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
|
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.
|
blocked OSC 52 behavior, key encoding, application-cursor mode, and resize.
|
||||||
- `lumbridge-core` tests capability-gated split/select/rename/close commands,
|
- `lumbridge-core` tests capability-gated split/select/rename/close commands,
|
||||||
idempotent request IDs, close-tree promotion, and atomic agentic setup plans.
|
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,
|
- The GPUI slice tests its key-event adapter, responsive one/three/five-lane
|
||||||
xterm 256-color conversion, styled-run coalescing, and cursor-run boundaries.
|
geometry, selected-pane lane windowing, 20/60/20 geometry-to-PTY sizing, xterm
|
||||||
It renders one real actor-owned VT session while five surfaces continue their
|
256-color conversion, styled-run coalescing, cursor-run boundaries, and
|
||||||
deterministic background workload.
|
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
|
- The current-GPUI probe compile-checks real AccessKit element wiring and real
|
||||||
platform input-handler installation. Unit tests cover its semantic tree and
|
platform input-handler installation. Unit tests cover its semantic tree and
|
||||||
UTF-16/UTF-8 composed-text mutations. OS screen readers, IME candidate windows,
|
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
|
The current programs establish dependency, build, launch, and interaction
|
||||||
baselines. GPUI now routes a real PTY through a VT engine and keyboard encoder,
|
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
|
paints coalesced styled cell runs and cursor shapes, and derives PTY rows/columns
|
||||||
from its 60% terminal band when the window changes. Selection, scrollback
|
from its responsive one/three/five-lane 60% terminal band when the window
|
||||||
navigation, mouse modes, a native Markdown editor, and one isolated browser child
|
changes. Retained-history page/top/bottom navigation is wired. Text selection,
|
||||||
remain.
|
mouse modes, a native Markdown editor, and one isolated browser child remain.
|
||||||
|
|
||||||
## Ubuntu baseline — metal, 2026-08-31
|
## Ubuntu baseline — metal, 2026-08-31
|
||||||
|
|
||||||
@@ -87,9 +87,9 @@ fallback.
|
|||||||
## Measurement semantics
|
## Measurement semantics
|
||||||
|
|
||||||
Both renderers retain the same all-deterministic six-surface action stream for
|
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
|
comparison. GPUI now presents a 20/60/20 workspace whose middle band shows one,
|
||||||
workspace while one terminal fixture is replaced with a real actor-owned VT
|
three, or five vertical work lanes while one terminal fixture is replaced with a
|
||||||
session and five deterministic surfaces keep running offscreen. Counters
|
real actor-owned VT session and five deterministic surfaces keep running. Counters
|
||||||
separate external PTY batches/lines from total model updates. The GPUI footer
|
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
|
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
|
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
|
## Current-run audit
|
||||||
|
|
||||||
The first 2026-08-31 GPUI baseline proved six-surface density but gave every
|
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
|
pane equal visual priority. The current slice replaces that dashboard with three
|
||||||
3×1 default: 20% context and pane selection, 60% active work surface, and 20%
|
horizontal bands: 20% context and pane selection, a 60% responsive work region,
|
||||||
decision shelf. Keyboard focus, command-palette input, live PTY input, styled VT
|
and a 20% decision shelf. The work region shows one, three, or five vertical
|
||||||
cells, cursor shapes, and geometry-driven resize are wired. Remaining visible
|
lanes according to available width. Keyboard focus, command-palette input, live
|
||||||
gaps are terminal selection/scrollback, real decision-shelf actions, usage-detail
|
PTY input, styled VT cells, cursor shapes, geometry-driven resize, and retained-
|
||||||
provenance, and end-to-end platform accessibility/IME.
|
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
|
Screenshots for the audit are stored outside Git under
|
||||||
`~/shots/2026-08/lumbridge-ui-audit/`. Accessibility and IME correctness cannot
|
`~/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;
|
cards;
|
||||||
- a quiet top tab strip for mixed terminal, Markdown, browser, and review
|
- a quiet top tab strip for mixed terminal, Markdown, browser, and review
|
||||||
surfaces;
|
surfaces;
|
||||||
- one primary work surface with optional splits, instead of forcing every
|
- a primary work region with a small responsive number of equal vertical lanes,
|
||||||
surface into an equal dashboard tile;
|
instead of either one ultrawide terminal or a dense equal-card dashboard;
|
||||||
- narrow contextual tools, such as files, review, or Buzz, that can collapse
|
- narrow contextual tools, such as files, review, or Buzz, that can collapse
|
||||||
when the terminal needs the space;
|
when the terminal needs the space;
|
||||||
- usage and agent state at the edge of the workspace rather than in modal
|
- 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.
|
the selected pane receives the accent treatment.
|
||||||
4. A waiting pane uses an amber semantic label in both the context band and
|
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`.
|
attention sidebar. The label always includes words such as `NEEDS INPUT`.
|
||||||
5. The middle 60% gives the selected terminal or work surface uninterrupted
|
5. The middle 60% gives one, three, or five terminal/work lanes uninterrupted
|
||||||
reading space and retains target, harness state, dimensions, and surface tabs.
|
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
|
6. The footer groups connection state, selected-harness identity, usage-window
|
||||||
provenance, and burn forecast into readable regions.
|
provenance, and burn forecast into readable regions.
|
||||||
7. The selected pane has a quiet surface strip for Terminal, Browser, Tools,
|
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
|
- `Alt+1` through `Alt+6`: focus a pane directly while leaving terminal digits
|
||||||
available to the PTY.
|
available to the PTY.
|
||||||
- `Cmd+K` on macOS or `Ctrl+K` on Linux: open the command palette.
|
- `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;
|
- Typing while the palette is open changes its query; arrows change the result;
|
||||||
`Enter` runs it; `Escape` closes it and restores pane focus.
|
`Enter` runs it; `Escape` closes it and restores pane focus.
|
||||||
- `Enter` on a waiting agent opens its request detail. Approval remains a
|
- `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
|
- In the first actor integration, one terminal pane consumes ordered output from
|
||||||
a real local PTY while each deterministic tick updates the other five
|
a real local PTY while each deterministic tick updates the other five
|
||||||
surfaces offscreen. The all-deterministic constructor remains available for
|
surfaces. One, three, or five are visible according to available width; the
|
||||||
framework comparison and replay tests.
|
all-deterministic constructor remains available for framework comparison and
|
||||||
|
replay tests.
|
||||||
- One pane enters and leaves `needs input` through a deterministic event.
|
- One pane enters and leaves `needs input` through a deterministic event.
|
||||||
- Markdown, browser-boundary, and review panes update counters without using a
|
- Markdown, browser-boundary, and review panes update counters without using a
|
||||||
web application shell.
|
web application shell.
|
||||||
|
|||||||
@@ -2,22 +2,21 @@
|
|||||||
|
|
||||||
Status: accepted for the native vertical slice.
|
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%
|
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,
|
is the terminal-first work region. The bottom 20% holds answers, choices, chat,
|
||||||
choices, chat, approvals, and the local-analyst boundary.
|
approvals, and the local-analyst boundary.
|
||||||
|
|
||||||
This composition favors the surface where engineers type and read for most of
|
This composition favors the region where engineers type and read for most of the
|
||||||
the day. It keeps context and decisions visible without making six equally sized
|
day. It keeps context and decisions visible without making every surface a small
|
||||||
cards compete for attention. Pane selectors and keyboard shortcuts switch the
|
dashboard card. The middle region may contain a responsive number of vertical
|
||||||
large work surface. Users may later create deliberate splits or dashboard grids,
|
work lanes under decision 0009; the selected lane alone owns keyboard input.
|
||||||
but those are workspace choices rather than the default visual hierarchy.
|
|
||||||
|
|
||||||
The framework spike retains six updating surfaces even when five are offscreen.
|
The framework spike retains six updating surfaces even when five are offscreen.
|
||||||
That preserves the earlier streaming and reducer workload for performance
|
That preserves the earlier streaming and reducer workload for performance
|
||||||
comparison. Hidden surfaces do not receive keyboard input merely because they
|
comparison. Hidden surfaces do not receive keyboard input merely because they
|
||||||
continue to update.
|
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
|
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.
|
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
|
comparison. GPUI feeds raw output through `lumbridge-terminal` and sends encoded
|
||||||
keyboard input and terminal protocol replies through the bounded runtime actor.
|
keyboard input and terminal protocol replies through the bounded runtime actor.
|
||||||
Its visual adapter coalesces VT cells into native styled runs, paints cursor
|
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,
|
shapes, and resizes the engine and PTY from a responsive one/three/five-lane
|
||||||
scrollback navigation, and mouse input remain intentionally unfinished.
|
middle 60% work band. Retained-history navigation is wired; text selection and
|
||||||
|
mouse input remain intentionally unfinished.
|
||||||
|
|
||||||
Build independently:
|
Build independently:
|
||||||
|
|
||||||
|
|||||||
+153
-17
@@ -16,7 +16,7 @@ use lumbridge_spike_model::{
|
|||||||
use lumbridge_terminal::{
|
use lumbridge_terminal::{
|
||||||
KeyModifiers, TerminalCellStyle, TerminalColor, TerminalCursorShape, TerminalDimensions,
|
KeyModifiers, TerminalCellStyle, TerminalColor, TerminalCursorShape, TerminalDimensions,
|
||||||
TerminalEngine, TerminalEngineOptions, TerminalKey, TerminalKeyEvent, TerminalNamedColor,
|
TerminalEngine, TerminalEngineOptions, TerminalKey, TerminalKeyEvent, TerminalNamedColor,
|
||||||
TerminalSnapshot,
|
TerminalScroll, TerminalSnapshot,
|
||||||
};
|
};
|
||||||
|
|
||||||
const BG: u32 = 0x090c12;
|
const BG: u32 = 0x090c12;
|
||||||
@@ -43,6 +43,7 @@ const TERMINAL_CHROME_HEIGHT: f32 = 72.0;
|
|||||||
const TERMINAL_HORIZONTAL_INSET: f32 = 32.0;
|
const TERMINAL_HORIZONTAL_INSET: f32 = 32.0;
|
||||||
const TERMINAL_CELL_WIDTH: f32 = 8.4;
|
const TERMINAL_CELL_WIDTH: f32 = 8.4;
|
||||||
const TERMINAL_CELL_HEIGHT: f32 = 18.0;
|
const TERMINAL_CELL_HEIGHT: f32 = 18.0;
|
||||||
|
const WORK_LANE_GAP: f32 = 4.0;
|
||||||
const TERMINAL_ROW_STEP: u16 = 2;
|
const TERMINAL_ROW_STEP: u16 = 2;
|
||||||
const TERMINAL_COLUMN_STEP: u16 = 10;
|
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 {
|
fn terminal_dimensions_for_window(window_size: Size<Pixels>) -> TerminalDimensions {
|
||||||
let width = f32::from(window_size.width);
|
let width = f32::from(window_size.width);
|
||||||
let height = f32::from(window_size.height);
|
let height = f32::from(window_size.height);
|
||||||
|
let lane_count = visible_lane_count(window_size);
|
||||||
let workspace_height =
|
let workspace_height =
|
||||||
(height - APP_HEADER_HEIGHT - TAB_BAR_HEIGHT - APP_FOOTER_HEIGHT).max(0.0);
|
(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_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)
|
let rows = (terminal_height / TERMINAL_CELL_HEIGHT)
|
||||||
.floor()
|
.floor()
|
||||||
.clamp(2.0, f32::from(u16::MAX));
|
.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")
|
.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 {
|
impl RenderTiming {
|
||||||
fn mark_dispatch(&mut self) {
|
fn mark_dispatch(&mut self) {
|
||||||
self.pending_dispatch = Some(Instant::now());
|
self.pending_dispatch = Some(Instant::now());
|
||||||
@@ -527,6 +551,14 @@ impl LumbridgeShell {
|
|||||||
cx.notify();
|
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>) {
|
fn select_pane(&mut self, pane: PaneId, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.dispatch(ShellAction::SelectPane(pane));
|
self.dispatch(ShellAction::SelectPane(pane));
|
||||||
window.focus(&self.root_focus);
|
window.focus(&self.root_focus);
|
||||||
@@ -615,6 +647,13 @@ impl LumbridgeShell {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let modifiers = key_modifiers(event);
|
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(
|
let Some(event) = terminal_key_from_parts(
|
||||||
event.keystroke.key.as_str(),
|
event.keystroke.key.as_str(),
|
||||||
event.keystroke.key_char.as_deref(),
|
event.keystroke.key_char.as_deref(),
|
||||||
@@ -629,6 +668,11 @@ impl LumbridgeShell {
|
|||||||
if let Err(error) = self.send_runtime_command(RuntimeCommand::Input(bytes)) {
|
if let Err(error) = self.send_runtime_command(RuntimeCommand::Input(bytes)) {
|
||||||
self.runtime_status = LiveRuntimeStatus::Fault(error.to_string());
|
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();
|
cx.notify();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -807,8 +851,13 @@ impl LumbridgeShell {
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn work_surface(&self) -> gpui::AnyElement {
|
fn work_surface(
|
||||||
let pane = self.model.pane(self.model.selected_pane());
|
&self,
|
||||||
|
pane: &PaneState,
|
||||||
|
selected: bool,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> gpui::AnyElement {
|
||||||
|
let pane_id = pane.id();
|
||||||
let external = pane.output_source() == OutputSource::External;
|
let external = pane.output_source() == OutputSource::External;
|
||||||
let status = if external {
|
let status = if external {
|
||||||
self.runtime_status.badge()
|
self.runtime_status.badge()
|
||||||
@@ -820,9 +869,17 @@ impl LumbridgeShell {
|
|||||||
} else {
|
} else {
|
||||||
pane.fixture().target.to_owned()
|
pane.fixture().target.to_owned()
|
||||||
};
|
};
|
||||||
let surface_status = if external {
|
let surface_status = if external && self.terminal_snapshot.display_offset > 0 {
|
||||||
format!(
|
format!(
|
||||||
"{} · {}×{} · auto-fit",
|
"{} · ↑{} · {}×{}",
|
||||||
|
status,
|
||||||
|
self.terminal_snapshot.display_offset,
|
||||||
|
self.terminal.dimensions().columns(),
|
||||||
|
self.terminal.dimensions().rows()
|
||||||
|
)
|
||||||
|
} else if external {
|
||||||
|
format!(
|
||||||
|
"{} · {}×{}",
|
||||||
status,
|
status,
|
||||||
self.terminal.dimensions().columns(),
|
self.terminal.dimensions().columns(),
|
||||||
self.terminal.dimensions().rows()
|
self.terminal.dimensions().rows()
|
||||||
@@ -870,14 +927,19 @@ impl LumbridgeShell {
|
|||||||
};
|
};
|
||||||
|
|
||||||
div()
|
div()
|
||||||
|
.id(("work-lane", pane_id.index()))
|
||||||
|
.on_click(cx.listener(move |shell, _, window, cx| {
|
||||||
|
shell.select_pane(pane_id, window, cx);
|
||||||
|
}))
|
||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
.size_full()
|
.size_full()
|
||||||
|
.min_w_0()
|
||||||
.min_h_0()
|
.min_h_0()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.bg(rgb(PANEL))
|
.bg(rgb(PANEL))
|
||||||
.border_y_1()
|
.border_1()
|
||||||
.border_color(rgb(BORDER))
|
.border_color(rgb(if selected { ACCENT } else { BORDER }))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
@@ -891,15 +953,23 @@ impl LumbridgeShell {
|
|||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap_3()
|
.gap_2()
|
||||||
|
.min_w_0()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.child(pane.fixture().title)
|
.child(div().flex_none().child(pane.fixture().title))
|
||||||
.child(div().text_xs().text_color(rgb(MUTED)).child(detail)),
|
.child(
|
||||||
|
div()
|
||||||
|
.truncate()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(rgb(MUTED))
|
||||||
|
.child(detail),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
|
.flex_none()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(rgb(SUCCESS))
|
.text_color(rgb(if selected { SUCCESS } else { MUTED }))
|
||||||
.child(surface_status),
|
.child(surface_status),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -1068,7 +1138,7 @@ impl LumbridgeShell {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Render for 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 attention = self.model.pane(PaneId::ClaudeUi);
|
||||||
let sidebar = div()
|
let sidebar = div()
|
||||||
.flex()
|
.flex()
|
||||||
@@ -1251,6 +1321,26 @@ impl Render for LumbridgeShell {
|
|||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let selected = self.model.pane(self.model.selected_pane());
|
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()
|
let workspace_stack = div()
|
||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
@@ -1310,9 +1400,11 @@ impl Render for LumbridgeShell {
|
|||||||
div()
|
div()
|
||||||
.h(relative(0.60))
|
.h(relative(0.60))
|
||||||
.flex_none()
|
.flex_none()
|
||||||
|
.flex()
|
||||||
|
.gap_1()
|
||||||
.min_h_0()
|
.min_h_0()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.child(self.work_surface()),
|
.children(work_lanes),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
@@ -1498,6 +1590,19 @@ fn terminal_key_from_parts(
|
|||||||
Some(TerminalKeyEvent { key, modifiers })
|
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> {
|
fn start_live_runtime(dimensions: TerminalDimensions) -> Result<RuntimeActor, RuntimeActorError> {
|
||||||
let command = CommandConfig::new("/bin/sh")
|
let command = CommandConfig::new("/bin/sh")
|
||||||
.map_err(RuntimeActorError::Start)?
|
.map_err(RuntimeActorError::Start)?
|
||||||
@@ -1551,8 +1656,9 @@ fn main() {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
KeyModifiers, TerminalEngine, TerminalEngineOptions, TerminalKey, indexed_terminal_color,
|
KeyModifiers, TerminalEngine, TerminalEngineOptions, TerminalKey, TerminalScroll,
|
||||||
terminal_dimensions_for_window, terminal_key_from_parts, terminal_paint_rows,
|
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};
|
use gpui::{px, size};
|
||||||
|
|
||||||
@@ -1582,7 +1688,37 @@ mod tests {
|
|||||||
fn terminal_geometry_tracks_the_middle_sixty_percent() {
|
fn terminal_geometry_tracks_the_middle_sixty_percent() {
|
||||||
let dimensions = terminal_dimensions_for_window(size(px(1500.0), px(960.0)));
|
let dimensions = terminal_dimensions_for_window(size(px(1500.0), px(960.0)));
|
||||||
assert_eq!(dimensions.rows(), 24);
|
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]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user