This commit is contained in:
@@ -18,11 +18,12 @@ 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 six-surface workspace and the root workspace contains the first
|
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
|
bounded local PTY, runtime-actor, VT engine, and capability-gated workspace
|
||||||
command boundaries. The GPUI slice renders one real actor-owned VT session beside
|
command boundaries. The GPUI slice renders one styled actor-owned VT session in
|
||||||
five deterministic surfaces. We are still validating the dedicated styled
|
the primary 60% work band while five deterministic surfaces keep updating in the
|
||||||
terminal renderer, standalone runtime IPC/durability, ACP integration,
|
background. We are still validating selection/scrollback rendering, 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
|
||||||
|
|||||||
+15
-3
@@ -69,12 +69,24 @@ The first `lumbridge-runtime` actor now owns one `PtySession` on a dedicated
|
|||||||
thread. Bounded command and event queues serialize input, resize, close, and
|
thread. Bounded command and event queues serialize input, resize, close, and
|
||||||
shutdown against ordered raw-byte output. The GPUI slice feeds one actor session
|
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 current text adapter intentionally renders plain rows and does not
|
output. Its adapter groups adjacent cells into native GPUI paint runs and renders
|
||||||
yet paint cell styles, selection, or the cursor; the next rendering slice owns
|
ANSI/indexed/RGB colors, emphasis, hyperlinks, and cursor shapes. Window geometry
|
||||||
that work. This actor still runs in-process; moving the same framework-neutral
|
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
|
contract behind local authenticated IPC is the next durability step. See
|
||||||
decisions 0004 and 0005.
|
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.
|
||||||
|
|
||||||
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.
|
||||||
Remaining correctness cases include OSC 8 links, Kitty
|
Remaining correctness cases include OSC 8 links, Kitty
|
||||||
|
|||||||
@@ -66,6 +66,12 @@ 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,
|
||||||
|
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.
|
||||||
|
|
||||||
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
|
||||||
capability each choice would need. Suggestions are inert data. Choosing one may
|
capability each choice would need. Suggestions are inert data. Choosing one may
|
||||||
|
|||||||
@@ -32,6 +32,13 @@ They are shallow snapshots for study, not dependencies or vendored source.
|
|||||||
- `block/buzz`: Rust human/agent collaboration plus production ACP harness design.
|
- `block/buzz`: Rust human/agent collaboration plus production ACP harness design.
|
||||||
- `bbarit/terminal`: Rust agent terminal/IDE ideas. No top-level license file was
|
- `bbarit/terminal`: Rust agent terminal/IDE ideas. No top-level license file was
|
||||||
present in the captured snapshot, so do not reuse implementation code.
|
present in the captured snapshot, so do not reuse implementation code.
|
||||||
|
- `OnlyTerp/opengrok`: provider-wire maps, behavioral verification of reasoning
|
||||||
|
controls, local-key posture, drift detection, and fail-closed provider UX;
|
||||||
|
MIT, but its Grok Bot patching is product-specific reference work.
|
||||||
|
- `elie222/rakazo`: persistent AI teammates, delegation, routines, computer
|
||||||
|
providers, browser/terminal/desktop tools, and deterministic sandbox testing;
|
||||||
|
Apache-2.0. Its Electron/React/Postgres architecture is research-only because
|
||||||
|
Lumbridge's shell and local runtime remain native Rust and SQLite.
|
||||||
|
|
||||||
## Protocols
|
## Protocols
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ checkouts are reference material and are not part of Lumbridge's source tree.
|
|||||||
| Agent IDEs | Orca | `main` | `02a7742406a5` | 322 MB | `stablyai/orca` |
|
| Agent IDEs | Orca | `main` | `02a7742406a5` | 322 MB | `stablyai/orca` |
|
||||||
| Agent IDEs | Xum | `main` | `a4be59aade55` | 76 MB | `coder/xum` |
|
| Agent IDEs | Xum | `main` | `a4be59aade55` | 76 MB | `coder/xum` |
|
||||||
| Agent IDEs | Zed | `main` | `ce48461eaadd` | 118 MB | `zed-industries/zed` |
|
| Agent IDEs | Zed | `main` | `ce48461eaadd` | 118 MB | `zed-industries/zed` |
|
||||||
|
| Agent IDEs | OpenGrok | `main` | `2b356649cfe5` | 5.4 MB | `OnlyTerp/opengrok` |
|
||||||
|
| Agent IDEs | Rakazo | `main` | `7e1ef9ea5d5d` | 30 MB | `elie222/rakazo` |
|
||||||
| Protocols | ACP specification | `main` | `f76fe3f3d132` | 67 MB | `agentclientprotocol/agent-client-protocol` |
|
| Protocols | ACP specification | `main` | `f76fe3f3d132` | 67 MB | `agentclientprotocol/agent-client-protocol` |
|
||||||
| Protocols | ACP Rust SDK | `main` | `754d5aa1ce2c` | 4.5 MB | `agentclientprotocol/rust-sdk` |
|
| Protocols | ACP Rust SDK | `main` | `754d5aa1ce2c` | 4.5 MB | `agentclientprotocol/rust-sdk` |
|
||||||
| Harnesses | Codex | `main` | `17e8101699c5` | 103 MB | `openai/codex` |
|
| Harnesses | Codex | `main` | `17e8101699c5` | 103 MB | `openai/codex` |
|
||||||
|
|||||||
+4
-3
@@ -118,9 +118,10 @@ 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. It renders one real actor-owned VT
|
- The GPUI slice tests its key-event adapter, 20/60/20 geometry-to-PTY sizing,
|
||||||
session as plain snapshot rows and keeps five surfaces deterministic; styled
|
xterm 256-color conversion, styled-run coalescing, and cursor-run boundaries.
|
||||||
cell/cursor rendering remains a separate gate.
|
It renders one real actor-owned VT session while five surfaces continue their
|
||||||
|
deterministic background workload.
|
||||||
- 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,
|
||||||
|
|||||||
@@ -22,10 +22,11 @@ from macOS and Linux and the hard gates pass.
|
|||||||
| API clarity and maintenance burden | review | pending | pending |
|
| API clarity and maintenance burden | review | pending | pending |
|
||||||
|
|
||||||
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,
|
||||||
its renderer still flattens the immutable screen snapshot to plain rows. The
|
paints coalesced styled cell runs and cursor shapes, and derives PTY rows/columns
|
||||||
next iteration adds styled cells/cursor/selection, measured pane-driven resize,
|
from its 60% terminal band when the window changes. Selection, scrollback
|
||||||
a native Markdown editor, and one isolated browser child.
|
navigation, mouse modes, a native Markdown editor, and one isolated browser child
|
||||||
|
remain.
|
||||||
|
|
||||||
## Ubuntu baseline — metal, 2026-08-31
|
## Ubuntu baseline — metal, 2026-08-31
|
||||||
|
|
||||||
@@ -86,8 +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. The GPUI integration mode replaces one terminal fixture with a real
|
comparison. GPUI now presents the selected surface in a 20/60/20 vertical
|
||||||
actor-owned VT session and leaves five deterministic surfaces running. Counters
|
workspace while one terminal fixture is replaced with a real actor-owned VT
|
||||||
|
session and five deterministic surfaces keep running offscreen. 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
-18
@@ -9,15 +9,13 @@ framework decision gate, not a decorative dashboard.
|
|||||||
|
|
||||||
## Current-run audit
|
## Current-run audit
|
||||||
|
|
||||||
The 2026-08-31 GPUI baseline succeeds at calm density: the six surfaces are easy
|
The first 2026-08-31 GPUI baseline proved six-surface density but gave every
|
||||||
to scan, typography is consistent, and local/remote targets remain visible. It
|
pane equal visual priority. The current slice replaces that dashboard with a
|
||||||
fails the task flow in four important ways:
|
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
|
||||||
1. all panes have equal visual priority, so `NEEDS INPUT` is too easy to miss;
|
cells, cursor shapes, and geometry-driven resize are wired. Remaining visible
|
||||||
2. no pane exposes visible keyboard focus or selection;
|
gaps are terminal selection/scrollback, real decision-shelf actions, usage-detail
|
||||||
3. the command palette affordance has no interaction behind it;
|
provenance, and end-to-end platform accessibility/IME.
|
||||||
4. status and usage text in the footer is too quiet to explain provenance or
|
|
||||||
confidence at a glance.
|
|
||||||
|
|
||||||
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
|
||||||
@@ -60,11 +58,12 @@ surface rather than a hosted Lumbridge control plane.
|
|||||||
palette shortcut.
|
palette shortcut.
|
||||||
2. The sidebar starts with a compact `ATTENTION` group, followed by workspaces
|
2. The sidebar starts with a compact `ATTENTION` group, followed by workspaces
|
||||||
and remote hosts. It does not become a notification feed.
|
and remote hosts. It does not become a notification feed.
|
||||||
3. The active pane receives a two-pixel accent edge plus a quiet tinted header.
|
3. A compact top 20% context band keeps all pane identities and states visible;
|
||||||
4. A waiting pane uses an amber semantic label and remains visible when another
|
the selected pane receives the accent treatment.
|
||||||
pane is selected. The label always includes words such as `NEEDS INPUT`.
|
4. A waiting pane uses an amber semantic label in both the context band and
|
||||||
5. Pane headers expose title, execution target, harness state, and unread count
|
attention sidebar. The label always includes words such as `NEEDS INPUT`.
|
||||||
in a stable order.
|
5. The middle 60% gives the selected terminal or work surface uninterrupted
|
||||||
|
reading space and retains target, harness state, dimensions, and surface tabs.
|
||||||
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,
|
||||||
@@ -75,7 +74,7 @@ surface rather than a hosted Lumbridge control plane.
|
|||||||
|
|
||||||
## Keyboard task flow
|
## Keyboard task flow
|
||||||
|
|
||||||
- `Alt+Arrow` or `Alt+h/j/k/l`: move focus through the 2×3 pane grid while
|
- `Alt+Arrow` or `Alt+h/j/k/l`: move selection through the pane topology while
|
||||||
leaving ordinary terminal arrows and text available to the PTY.
|
leaving ordinary terminal arrows and text available to the PTY.
|
||||||
- `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.
|
||||||
@@ -93,8 +92,8 @@ 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. The all-deterministic constructor remains available for framework
|
surfaces offscreen. The all-deterministic constructor remains available for
|
||||||
comparison and replay tests.
|
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.
|
||||||
@@ -103,7 +102,8 @@ receive the same state transitions and tests.
|
|||||||
element-build timing is never presented as display-present timing.
|
element-build timing is never presented as display-present timing.
|
||||||
- The PTY actor replaces exactly one synthetic stream without changing focus or
|
- The PTY actor replaces exactly one synthetic stream without changing focus or
|
||||||
layout contracts. Synthetic streams stay available for repeatable performance
|
layout contracts. Synthetic streams stay available for repeatable performance
|
||||||
tests. Raw lines do not claim terminal-emulation fidelity.
|
tests. Live bytes are parsed into VT state and coalesced native paint runs;
|
||||||
|
deterministic fixture text makes no terminal-emulation claim.
|
||||||
|
|
||||||
## Hard gates
|
## Hard gates
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# 0008: The default workspace is a primary-terminal 3×1 composition
|
||||||
|
|
||||||
|
Status: accepted for the native vertical slice.
|
||||||
|
|
||||||
|
Lumbridge defaults to three vertical 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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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
|
||||||
|
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.
|
||||||
+3
-2
@@ -23,8 +23,9 @@ GPUI also has an integration mode with one real local PTY owned by
|
|||||||
shared model retain the all-deterministic mode for like-for-like framework
|
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 currently flattens the VT snapshot to plain rows, so styled
|
Its visual adapter coalesces VT cells into native styled runs, paints cursor
|
||||||
cells, selection, and cursor painting remain intentionally unfinished.
|
shapes, and resizes the engine and PTY from the middle 60% work band. Selection,
|
||||||
|
scrollback navigation, and mouse input remain intentionally unfinished.
|
||||||
|
|
||||||
Build independently:
|
Build independently:
|
||||||
|
|
||||||
|
|||||||
+552
-121
@@ -2,8 +2,8 @@ use std::collections::VecDeque;
|
|||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, Application, Bounds, Context, FocusHandle, KeyBinding, KeyDownEvent, Window, WindowBounds,
|
App, Application, Bounds, Context, FocusHandle, FontWeight, KeyBinding, KeyDownEvent, Pixels,
|
||||||
WindowOptions, actions, div, prelude::*, px, rgb, size,
|
Size, Window, WindowBounds, WindowOptions, actions, div, prelude::*, px, relative, rgb, size,
|
||||||
};
|
};
|
||||||
use lumbridge_runtime::{
|
use lumbridge_runtime::{
|
||||||
CommandConfig, PtyOptions, RuntimeActor, RuntimeActorError, RuntimeActorOptions,
|
CommandConfig, PtyOptions, RuntimeActor, RuntimeActorError, RuntimeActorOptions,
|
||||||
@@ -14,8 +14,9 @@ use lumbridge_spike_model::{
|
|||||||
ShellModel, SurfaceKind, WORKSPACES,
|
ShellModel, SurfaceKind, WORKSPACES,
|
||||||
};
|
};
|
||||||
use lumbridge_terminal::{
|
use lumbridge_terminal::{
|
||||||
KeyModifiers, TerminalDimensions, TerminalEngine, TerminalEngineOptions, TerminalKey,
|
KeyModifiers, TerminalCellStyle, TerminalColor, TerminalCursorShape, TerminalDimensions,
|
||||||
TerminalKeyEvent,
|
TerminalEngine, TerminalEngineOptions, TerminalKey, TerminalKeyEvent, TerminalNamedColor,
|
||||||
|
TerminalSnapshot,
|
||||||
};
|
};
|
||||||
|
|
||||||
const BG: u32 = 0x090c12;
|
const BG: u32 = 0x090c12;
|
||||||
@@ -34,6 +35,14 @@ const RUNTIME_POLL_INTERVAL: Duration = Duration::from_millis(16);
|
|||||||
const RUNTIME_DRAIN_LIMIT: usize = 64;
|
const RUNTIME_DRAIN_LIMIT: usize = 64;
|
||||||
const LIVE_PANE: PaneId = PaneId::CodexRuntime;
|
const LIVE_PANE: PaneId = PaneId::CodexRuntime;
|
||||||
const LIVE_PTY_SCRIPT: &str = "printf 'Lumbridge interactive PTY · type here\\n'; exec /bin/sh -i";
|
const LIVE_PTY_SCRIPT: &str = "printf 'Lumbridge interactive PTY · type here\\n'; exec /bin/sh -i";
|
||||||
|
const SIDEBAR_WIDTH: f32 = 248.0;
|
||||||
|
const APP_HEADER_HEIGHT: f32 = 48.0;
|
||||||
|
const TAB_BAR_HEIGHT: f32 = 38.0;
|
||||||
|
const APP_FOOTER_HEIGHT: f32 = 32.0;
|
||||||
|
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 TERMINAL_ROW_STEP: u16 = 2;
|
const TERMINAL_ROW_STEP: u16 = 2;
|
||||||
const TERMINAL_COLUMN_STEP: u16 = 10;
|
const TERMINAL_COLUMN_STEP: u16 = 10;
|
||||||
|
|
||||||
@@ -65,6 +74,7 @@ struct LumbridgeShell {
|
|||||||
runtime: Option<RuntimeActor>,
|
runtime: Option<RuntimeActor>,
|
||||||
runtime_status: LiveRuntimeStatus,
|
runtime_status: LiveRuntimeStatus,
|
||||||
terminal: TerminalEngine,
|
terminal: TerminalEngine,
|
||||||
|
terminal_snapshot: TerminalSnapshot,
|
||||||
last_runtime_sequence: u64,
|
last_runtime_sequence: u64,
|
||||||
root_focus: FocusHandle,
|
root_focus: FocusHandle,
|
||||||
pane_focus: [FocusHandle; 6],
|
pane_focus: [FocusHandle; 6],
|
||||||
@@ -123,6 +133,171 @@ struct RenderTiming {
|
|||||||
dispatch_to_element_micros: VecDeque<u128>,
|
dispatch_to_element_micros: VecDeque<u128>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Eq, PartialEq)]
|
||||||
|
struct TerminalPaintRun {
|
||||||
|
text: String,
|
||||||
|
columns: u16,
|
||||||
|
foreground: u32,
|
||||||
|
background: u32,
|
||||||
|
style: TerminalCellStyle,
|
||||||
|
cursor: Option<TerminalCursorShape>,
|
||||||
|
hyperlink: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TerminalPaintRun {
|
||||||
|
fn can_merge(&self, other: &Self) -> bool {
|
||||||
|
self.foreground == other.foreground
|
||||||
|
&& self.background == other.background
|
||||||
|
&& self.style == other.style
|
||||||
|
&& self.cursor == other.cursor
|
||||||
|
&& self.hyperlink == other.hyperlink
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn terminal_paint_rows(snapshot: &TerminalSnapshot) -> Vec<Vec<TerminalPaintRun>> {
|
||||||
|
(0..snapshot.dimensions.rows())
|
||||||
|
.map(|row| {
|
||||||
|
let mut runs: Vec<TerminalPaintRun> = Vec::new();
|
||||||
|
for column in 0..snapshot.dimensions.columns() {
|
||||||
|
let Some(cell) = snapshot.cell(row, column) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if cell.wide_spacer {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut foreground = terminal_color(cell.foreground);
|
||||||
|
let mut background = terminal_color(cell.background);
|
||||||
|
if cell.style.contains(TerminalCellStyle::INVERSE) {
|
||||||
|
std::mem::swap(&mut foreground, &mut background);
|
||||||
|
}
|
||||||
|
if cell.style.contains(TerminalCellStyle::DIM) {
|
||||||
|
foreground = dim_color(foreground);
|
||||||
|
}
|
||||||
|
if cell.style.contains(TerminalCellStyle::HIDDEN) {
|
||||||
|
foreground = background;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cursor = (snapshot.cursor.row == row
|
||||||
|
&& snapshot.cursor.column == column
|
||||||
|
&& snapshot.cursor.shape != TerminalCursorShape::Hidden)
|
||||||
|
.then_some(snapshot.cursor.shape);
|
||||||
|
let text = if cell.text.is_empty() {
|
||||||
|
" ".to_owned()
|
||||||
|
} else {
|
||||||
|
cell.text.clone()
|
||||||
|
};
|
||||||
|
let next = TerminalPaintRun {
|
||||||
|
text,
|
||||||
|
columns: if cell.wide { 2 } else { 1 },
|
||||||
|
foreground,
|
||||||
|
background,
|
||||||
|
style: cell.style,
|
||||||
|
cursor,
|
||||||
|
hyperlink: cell.hyperlink.is_some(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(current) = runs.last_mut()
|
||||||
|
&& current.can_merge(&next)
|
||||||
|
{
|
||||||
|
current.text.push_str(&next.text);
|
||||||
|
current.columns = current.columns.saturating_add(next.columns);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
runs.push(next);
|
||||||
|
}
|
||||||
|
runs
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn terminal_color(color: TerminalColor) -> u32 {
|
||||||
|
match color {
|
||||||
|
TerminalColor::Rgb { red, green, blue } => {
|
||||||
|
(u32::from(red) << 16) | (u32::from(green) << 8) | u32::from(blue)
|
||||||
|
}
|
||||||
|
TerminalColor::Indexed(index) => indexed_terminal_color(index),
|
||||||
|
TerminalColor::Named(named) => match named {
|
||||||
|
TerminalNamedColor::Black => 0x1d2430,
|
||||||
|
TerminalNamedColor::Red => 0xff6b6b,
|
||||||
|
TerminalNamedColor::Green => 0x70d6a8,
|
||||||
|
TerminalNamedColor::Yellow => 0xf1c76a,
|
||||||
|
TerminalNamedColor::Blue => 0x68b5f8,
|
||||||
|
TerminalNamedColor::Magenta => 0xc79bf2,
|
||||||
|
TerminalNamedColor::Cyan => 0x63d5da,
|
||||||
|
TerminalNamedColor::White => 0xdbe5f4,
|
||||||
|
TerminalNamedColor::BrightBlack => 0x6d7a91,
|
||||||
|
TerminalNamedColor::BrightRed => 0xff8b8b,
|
||||||
|
TerminalNamedColor::BrightGreen => 0x93e6be,
|
||||||
|
TerminalNamedColor::BrightYellow => 0xf8d98c,
|
||||||
|
TerminalNamedColor::BrightBlue => 0x8bc8ff,
|
||||||
|
TerminalNamedColor::BrightMagenta => 0xd9b4fb,
|
||||||
|
TerminalNamedColor::BrightCyan => 0x86e7eb,
|
||||||
|
TerminalNamedColor::BrightWhite | TerminalNamedColor::BrightForeground => 0xf4f8ff,
|
||||||
|
TerminalNamedColor::Foreground => TEXT,
|
||||||
|
TerminalNamedColor::Background => BG,
|
||||||
|
TerminalNamedColor::Cursor => 0xf4f8ff,
|
||||||
|
TerminalNamedColor::DimBlack => 0x121820,
|
||||||
|
TerminalNamedColor::DimRed => 0x9f4a4a,
|
||||||
|
TerminalNamedColor::DimGreen => 0x4e9878,
|
||||||
|
TerminalNamedColor::DimYellow => 0xa88a49,
|
||||||
|
TerminalNamedColor::DimBlue => 0x477fac,
|
||||||
|
TerminalNamedColor::DimMagenta => 0x896ca4,
|
||||||
|
TerminalNamedColor::DimCyan => 0x459397,
|
||||||
|
TerminalNamedColor::DimWhite | TerminalNamedColor::DimForeground => MUTED,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn indexed_terminal_color(index: u8) -> u32 {
|
||||||
|
const ANSI: [u32; 16] = [
|
||||||
|
0x1d2430, 0xff6b6b, 0x70d6a8, 0xf1c76a, 0x68b5f8, 0xc79bf2, 0x63d5da, 0xdbe5f4, 0x6d7a91,
|
||||||
|
0xff8b8b, 0x93e6be, 0xf8d98c, 0x8bc8ff, 0xd9b4fb, 0x86e7eb, 0xf4f8ff,
|
||||||
|
];
|
||||||
|
match index {
|
||||||
|
0..=15 => ANSI[usize::from(index)],
|
||||||
|
16..=231 => {
|
||||||
|
const LEVELS: [u32; 6] = [0, 95, 135, 175, 215, 255];
|
||||||
|
let offset = u32::from(index - 16);
|
||||||
|
let red = LEVELS[(offset / 36) as usize];
|
||||||
|
let green = LEVELS[((offset % 36) / 6) as usize];
|
||||||
|
let blue = LEVELS[(offset % 6) as usize];
|
||||||
|
(red << 16) | (green << 8) | blue
|
||||||
|
}
|
||||||
|
232..=255 => {
|
||||||
|
let level = 8 + 10 * u32::from(index - 232);
|
||||||
|
(level << 16) | (level << 8) | level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dim_color(color: u32) -> u32 {
|
||||||
|
let dim = |channel: u32| channel * 13 / 20;
|
||||||
|
(dim((color >> 16) & 0xff) << 16) | (dim((color >> 8) & 0xff) << 8) | dim(color & 0xff)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 rows = (terminal_height / TERMINAL_CELL_HEIGHT)
|
||||||
|
.floor()
|
||||||
|
.clamp(2.0, f32::from(u16::MAX));
|
||||||
|
let columns = (terminal_width / TERMINAL_CELL_WIDTH)
|
||||||
|
.floor()
|
||||||
|
.clamp(20.0, f32::from(u16::MAX));
|
||||||
|
TerminalDimensions::with_cell_size(
|
||||||
|
rows as u16,
|
||||||
|
columns as u16,
|
||||||
|
TERMINAL_CELL_WIDTH.round() as u16,
|
||||||
|
TERMINAL_CELL_HEIGHT.round() as u16,
|
||||||
|
)
|
||||||
|
.expect("geometry clamps terminal dimensions above zero")
|
||||||
|
}
|
||||||
|
|
||||||
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());
|
||||||
@@ -207,12 +382,23 @@ impl LumbridgeShell {
|
|||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
let terminal = TerminalEngine::new(TerminalEngineOptions::default());
|
let terminal_dimensions = terminal_dimensions_for_window(window.bounds().size);
|
||||||
|
let terminal = TerminalEngine::new(TerminalEngineOptions {
|
||||||
|
dimensions: terminal_dimensions,
|
||||||
|
..TerminalEngineOptions::default()
|
||||||
|
});
|
||||||
|
let terminal_snapshot = terminal.snapshot();
|
||||||
let (runtime, runtime_status) = match start_live_runtime(terminal.dimensions()) {
|
let (runtime, runtime_status) = match start_live_runtime(terminal.dimensions()) {
|
||||||
Ok(runtime) => (Some(runtime), LiveRuntimeStatus::Starting),
|
Ok(runtime) => (Some(runtime), LiveRuntimeStatus::Starting),
|
||||||
Err(error) => (None, LiveRuntimeStatus::Fault(error.to_string())),
|
Err(error) => (None, LiveRuntimeStatus::Fault(error.to_string())),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
cx.observe_window_bounds(window, |shell, window, cx| {
|
||||||
|
let dimensions = terminal_dimensions_for_window(window.bounds().size);
|
||||||
|
shell.resize_terminal(dimensions.rows(), dimensions.columns(), cx);
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
model: ShellModel::with_external_output(LIVE_PANE)
|
model: ShellModel::with_external_output(LIVE_PANE)
|
||||||
.expect("the live comparison pane is a terminal"),
|
.expect("the live comparison pane is a terminal"),
|
||||||
@@ -220,6 +406,7 @@ impl LumbridgeShell {
|
|||||||
runtime,
|
runtime,
|
||||||
runtime_status,
|
runtime_status,
|
||||||
terminal,
|
terminal,
|
||||||
|
terminal_snapshot,
|
||||||
last_runtime_sequence: 0,
|
last_runtime_sequence: 0,
|
||||||
root_focus,
|
root_focus,
|
||||||
pane_focus,
|
pane_focus,
|
||||||
@@ -316,7 +503,9 @@ impl LumbridgeShell {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn publish_terminal_snapshot(&mut self) {
|
fn publish_terminal_snapshot(&mut self) {
|
||||||
let lines = self.terminal.snapshot().plain_rows();
|
let snapshot = self.terminal.snapshot();
|
||||||
|
let lines = snapshot.plain_rows();
|
||||||
|
self.terminal_snapshot = snapshot;
|
||||||
self.dispatch(ShellAction::ReplaceExternalOutput {
|
self.dispatch(ShellAction::ReplaceExternalOutput {
|
||||||
pane: LIVE_PANE,
|
pane: LIVE_PANE,
|
||||||
lines,
|
lines,
|
||||||
@@ -474,11 +663,10 @@ impl LumbridgeShell {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pane_card(
|
fn pane_selector(
|
||||||
pane: &PaneState,
|
pane: &PaneState,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
runtime_status: LiveRuntimeStatus,
|
runtime_status: LiveRuntimeStatus,
|
||||||
terminal_dimensions: TerminalDimensions,
|
|
||||||
focus: FocusHandle,
|
focus: FocusHandle,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> gpui::AnyElement {
|
) -> gpui::AnyElement {
|
||||||
@@ -500,147 +688,296 @@ impl LumbridgeShell {
|
|||||||
} else {
|
} else {
|
||||||
ACCENT
|
ACCENT
|
||||||
};
|
};
|
||||||
let line_start = pane.lines().len().saturating_sub(12);
|
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.id(("pane", id.index()))
|
.id(("pane", id.index()))
|
||||||
.track_focus(&focus)
|
.track_focus(&focus)
|
||||||
.tab_index(id.index() as isize + 1)
|
.tab_index(id.index() as isize + 1)
|
||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.items_center()
|
||||||
.min_w_0()
|
.justify_between()
|
||||||
.min_h_0()
|
.gap_3()
|
||||||
.overflow_hidden()
|
.h_full()
|
||||||
.bg(rgb(if selected { PANEL_ACTIVE } else { PANEL }))
|
.min_w(px(172.0))
|
||||||
|
.px_3()
|
||||||
|
.bg(rgb(if selected { PANEL_ACTIVE } else { PANEL_ALT }))
|
||||||
.border_1()
|
.border_1()
|
||||||
.border_color(rgb(if selected || needs_input {
|
.border_color(rgb(if selected || needs_input {
|
||||||
state_color
|
state_color
|
||||||
} else {
|
} else {
|
||||||
BORDER
|
BORDER_QUIET
|
||||||
}))
|
}))
|
||||||
.rounded(px(5.0))
|
.rounded(px(5.0))
|
||||||
.when(selected, |view| view.border_2())
|
|
||||||
.on_click(cx.listener(move |shell, _, window, cx| {
|
.on_click(cx.listener(move |shell, _, window, cx| {
|
||||||
shell.select_pane(id, window, cx);
|
shell.select_pane(id, window, cx);
|
||||||
}))
|
}))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.flex_col()
|
||||||
.justify_between()
|
.min_w_0()
|
||||||
.px_3()
|
|
||||||
.h(px(36.0))
|
|
||||||
.flex_none()
|
|
||||||
.bg(rgb(if selected { PANEL_ACTIVE } else { PANEL_ALT }))
|
|
||||||
.border_b_1()
|
|
||||||
.border_color(rgb(BORDER_QUIET))
|
|
||||||
.text_sm()
|
|
||||||
.text_color(rgb(TEXT))
|
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.flex()
|
.truncate()
|
||||||
.items_center()
|
.text_sm()
|
||||||
.gap_2()
|
.text_color(rgb(TEXT))
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_xs()
|
|
||||||
.text_color(rgb(MUTED))
|
|
||||||
.child(format!("{}", id.index() + 1)),
|
|
||||||
)
|
|
||||||
.child(if external {
|
.child(if external {
|
||||||
"Shell · VT session"
|
"Terminal · local shell"
|
||||||
} else {
|
} else {
|
||||||
pane.fixture().title
|
pane.fixture().title
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.child(div().text_xs().text_color(rgb(state_color)).child(label)),
|
.child(
|
||||||
|
div()
|
||||||
|
.mt_1()
|
||||||
|
.truncate()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(rgb(MUTED))
|
||||||
|
.child(pane.fixture().target.to_owned()),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
.child(div().text_xs().text_color(rgb(state_color)).child(label))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn terminal_run(run: TerminalPaintRun) -> gpui::AnyElement {
|
||||||
|
let cursor = run.cursor;
|
||||||
|
div()
|
||||||
|
.flex_none()
|
||||||
|
.h(px(TERMINAL_CELL_HEIGHT))
|
||||||
|
.w(px(f32::from(run.columns) * TERMINAL_CELL_WIDTH))
|
||||||
|
.overflow_hidden()
|
||||||
|
.whitespace_nowrap()
|
||||||
|
.text_color(rgb(run.foreground))
|
||||||
|
.bg(rgb(run.background))
|
||||||
|
.when(run.style.contains(TerminalCellStyle::BOLD), |view| {
|
||||||
|
view.font_weight(FontWeight::BOLD)
|
||||||
|
})
|
||||||
|
.when(run.style.contains(TerminalCellStyle::ITALIC), |view| {
|
||||||
|
view.italic()
|
||||||
|
})
|
||||||
|
.when(
|
||||||
|
run.style.contains(TerminalCellStyle::UNDERLINE)
|
||||||
|
|| run.style.contains(TerminalCellStyle::DOUBLE_UNDERLINE)
|
||||||
|
|| run.style.contains(TerminalCellStyle::UNDERCURL)
|
||||||
|
|| run.style.contains(TerminalCellStyle::DOTTED_UNDERLINE)
|
||||||
|
|| run.style.contains(TerminalCellStyle::DASHED_UNDERLINE)
|
||||||
|
|| run.hyperlink,
|
||||||
|
|view| view.underline(),
|
||||||
|
)
|
||||||
|
.when(run.style.contains(TerminalCellStyle::STRIKEOUT), |view| {
|
||||||
|
view.line_through()
|
||||||
|
})
|
||||||
|
.when(cursor == Some(TerminalCursorShape::Block), |view| {
|
||||||
|
view.bg(rgb(TEXT)).text_color(rgb(BG))
|
||||||
|
})
|
||||||
|
.when(cursor == Some(TerminalCursorShape::HollowBlock), |view| {
|
||||||
|
view.border_1().border_color(rgb(TEXT))
|
||||||
|
})
|
||||||
|
.when(cursor == Some(TerminalCursorShape::Underline), |view| {
|
||||||
|
view.border_b_2().border_color(rgb(TEXT))
|
||||||
|
})
|
||||||
|
.when(cursor == Some(TerminalCursorShape::Beam), |view| {
|
||||||
|
view.border_l_2().border_color(rgb(TEXT))
|
||||||
|
})
|
||||||
|
.child(run.text)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn terminal_view(&self) -> gpui::AnyElement {
|
||||||
|
let rows = terminal_paint_rows(&self.terminal_snapshot)
|
||||||
|
.into_iter()
|
||||||
|
.map(|runs| {
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.h(px(TERMINAL_CELL_HEIGHT))
|
||||||
|
.flex_none()
|
||||||
|
.children(runs.into_iter().map(Self::terminal_run))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.size_full()
|
||||||
|
.overflow_hidden()
|
||||||
|
.bg(rgb(BG))
|
||||||
|
.font_family("monospace")
|
||||||
|
.text_size(px(13.0))
|
||||||
|
.children(rows)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn work_surface(&self) -> gpui::AnyElement {
|
||||||
|
let pane = self.model.pane(self.model.selected_pane());
|
||||||
|
let external = pane.output_source() == OutputSource::External;
|
||||||
|
let status = if external {
|
||||||
|
self.runtime_status.badge()
|
||||||
|
} else {
|
||||||
|
pane.fixture().badge
|
||||||
|
};
|
||||||
|
let detail = if external {
|
||||||
|
self.runtime_status.detail()
|
||||||
|
} else {
|
||||||
|
pane.fixture().target.to_owned()
|
||||||
|
};
|
||||||
|
let surface_status = if external {
|
||||||
|
format!(
|
||||||
|
"{} · {}×{} · auto-fit",
|
||||||
|
status,
|
||||||
|
self.terminal.dimensions().columns(),
|
||||||
|
self.terminal.dimensions().rows()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
status.to_owned()
|
||||||
|
};
|
||||||
|
let surface = match pane.kind() {
|
||||||
|
SurfaceKind::Terminal => "TERMINAL",
|
||||||
|
SurfaceKind::Markdown => "CONTEXT",
|
||||||
|
SurfaceKind::Browser => "BROWSER",
|
||||||
|
SurfaceKind::Review => "REVIEW",
|
||||||
|
};
|
||||||
|
let tabs = ["TERMINAL", "BROWSER", "TOOLS", "CONTEXT", "GOAL", "REVIEW"]
|
||||||
|
.into_iter()
|
||||||
|
.map(|label| {
|
||||||
|
div()
|
||||||
|
.h_full()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.px_3()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(rgb(if label == surface { ACCENT } else { MUTED }))
|
||||||
|
.when(label == surface, |view| {
|
||||||
|
view.border_b_2().border_color(rgb(ACCENT))
|
||||||
|
})
|
||||||
|
.child(label)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let content = if external {
|
||||||
|
self.terminal_view()
|
||||||
|
} else {
|
||||||
|
let start = pane.lines().len().saturating_sub(18);
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap_1()
|
||||||
|
.size_full()
|
||||||
|
.overflow_hidden()
|
||||||
|
.font_family("monospace")
|
||||||
|
.text_sm()
|
||||||
|
.text_color(rgb(TEXT))
|
||||||
|
.children(pane.lines()[start..].iter().cloned())
|
||||||
|
.into_any_element()
|
||||||
|
};
|
||||||
|
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.size_full()
|
||||||
|
.min_h_0()
|
||||||
|
.overflow_hidden()
|
||||||
|
.bg(rgb(PANEL))
|
||||||
|
.border_y_1()
|
||||||
|
.border_color(rgb(BORDER))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
|
.h(px(36.0))
|
||||||
|
.flex_none()
|
||||||
.px_3()
|
.px_3()
|
||||||
.py_2()
|
.bg(rgb(PANEL_ALT))
|
||||||
.text_xs()
|
.child(
|
||||||
.text_color(rgb(MUTED))
|
div()
|
||||||
.child(if external {
|
.flex()
|
||||||
format!(
|
.items_center()
|
||||||
"{} · {}×{} · Alt+Shift+arrows resize",
|
.gap_3()
|
||||||
runtime_status.detail(),
|
.text_sm()
|
||||||
terminal_dimensions.columns(),
|
.child(pane.fixture().title)
|
||||||
terminal_dimensions.rows()
|
.child(div().text_xs().text_color(rgb(MUTED)).child(detail)),
|
||||||
)
|
)
|
||||||
} else {
|
.child(
|
||||||
pane.fixture().target.to_owned()
|
div()
|
||||||
})
|
.text_xs()
|
||||||
.when(selected, |view| view.child("FOCUSED")),
|
.text_color(rgb(SUCCESS))
|
||||||
|
.child(surface_status),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.when(external && selected, |view| {
|
|
||||||
view.child(
|
|
||||||
div()
|
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.gap_3()
|
|
||||||
.h(px(26.0))
|
|
||||||
.flex_none()
|
|
||||||
.px_3()
|
|
||||||
.border_y_1()
|
|
||||||
.border_color(rgb(BORDER_QUIET))
|
|
||||||
.bg(rgb(PANEL_ALT))
|
|
||||||
.text_xs()
|
|
||||||
.child(div().text_color(rgb(ACCENT)).child("TERMINAL"))
|
|
||||||
.child(div().text_color(rgb(MUTED)).child("BROWSER"))
|
|
||||||
.child(div().text_color(rgb(MUTED)).child("TOOLS"))
|
|
||||||
.child(div().text_color(rgb(MUTED)).child("CONTEXT"))
|
|
||||||
.child(div().text_color(rgb(MUTED)).child("GOAL"))
|
|
||||||
.child(div().text_color(rgb(MUTED)).child("REVIEW")),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.h(px(28.0))
|
||||||
|
.flex_none()
|
||||||
|
.px_1()
|
||||||
|
.bg(rgb(PANEL_ALT))
|
||||||
|
.border_t_1()
|
||||||
|
.border_color(rgb(BORDER_QUIET))
|
||||||
|
.children(tabs),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.min_h_0()
|
.min_h_0()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.gap_1()
|
.p_3()
|
||||||
.px_3()
|
.child(content),
|
||||||
.pb_3()
|
)
|
||||||
.text_sm()
|
.into_any_element()
|
||||||
.text_color(rgb(TEXT))
|
}
|
||||||
.children(pane.lines()[line_start..].iter().cloned()),
|
|
||||||
|
fn decision_region(&self) -> gpui::AnyElement {
|
||||||
|
let choice = |label: &'static str, detail: &'static str| {
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.min_w(px(200.0))
|
||||||
|
.px_3()
|
||||||
|
.py_2()
|
||||||
|
.rounded(px(5.0))
|
||||||
|
.border_1()
|
||||||
|
.border_color(rgb(BORDER))
|
||||||
|
.bg(rgb(PANEL_ALT))
|
||||||
|
.text_sm()
|
||||||
|
.text_color(rgb(TEXT))
|
||||||
|
.child(label)
|
||||||
|
.child(div().mt_1().text_xs().text_color(rgb(MUTED)).child(detail))
|
||||||
|
};
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.size_full()
|
||||||
|
.min_h_0()
|
||||||
|
.overflow_hidden()
|
||||||
|
.px_3()
|
||||||
|
.py_2()
|
||||||
|
.bg(rgb(PANEL))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_between()
|
||||||
|
.text_xs()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_color(rgb(MUTED))
|
||||||
|
.child("DECISION SHELF · answer, choices, approvals"),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_color(rgb(ATTENTION))
|
||||||
|
.child("LOCAL ANALYST OFF · suggestions inert"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap_2()
|
||||||
|
.mt_2()
|
||||||
|
.child(choice("Continue", "Keep the selected agent moving"))
|
||||||
|
.child(choice("Review plan", "Inspect capability-scoped commands"))
|
||||||
|
.child(choice("Ask…", "Refine before any action")),
|
||||||
)
|
)
|
||||||
.when(external && selected, |view| {
|
|
||||||
view.child(
|
|
||||||
div()
|
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.justify_between()
|
|
||||||
.gap_2()
|
|
||||||
.h(px(38.0))
|
|
||||||
.flex_none()
|
|
||||||
.px_3()
|
|
||||||
.border_t_1()
|
|
||||||
.border_color(rgb(BORDER_QUIET))
|
|
||||||
.bg(rgb(PANEL_ALT))
|
|
||||||
.text_xs()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_color(rgb(MUTED))
|
|
||||||
.child("LOCAL ANALYST · OFF · suggestions only"),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.flex()
|
|
||||||
.gap_2()
|
|
||||||
.text_color(rgb(TEXT))
|
|
||||||
.child("Continue")
|
|
||||||
.child("Review plan")
|
|
||||||
.child("Ask…"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -899,29 +1236,91 @@ impl Render for LumbridgeShell {
|
|||||||
.child("1 interactive VT · 5 deterministic · 1 waiting"),
|
.child("1 interactive VT · 5 deterministic · 1 waiting"),
|
||||||
);
|
);
|
||||||
|
|
||||||
let pane_cards = self
|
let pane_selectors = self
|
||||||
.model
|
.model
|
||||||
.panes()
|
.panes()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|pane| {
|
.map(|pane| {
|
||||||
Self::pane_card(
|
Self::pane_selector(
|
||||||
pane,
|
pane,
|
||||||
self.model.selected_pane() == pane.id(),
|
self.model.selected_pane() == pane.id(),
|
||||||
self.runtime_status.clone(),
|
self.runtime_status.clone(),
|
||||||
self.terminal.dimensions(),
|
|
||||||
self.pane_focus[pane.id().index()].clone(),
|
self.pane_focus[pane.id().index()].clone(),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let grid = div()
|
let selected = self.model.pane(self.model.selected_pane());
|
||||||
.grid()
|
let workspace_stack = div()
|
||||||
.grid_cols(3)
|
.flex()
|
||||||
.grid_rows(2)
|
.flex_col()
|
||||||
.gap_2()
|
|
||||||
.p_2()
|
|
||||||
.size_full()
|
.size_full()
|
||||||
.children(pane_cards);
|
.min_h_0()
|
||||||
|
.overflow_hidden()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.h(relative(0.20))
|
||||||
|
.flex_none()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.min_h_0()
|
||||||
|
.overflow_hidden()
|
||||||
|
.px_3()
|
||||||
|
.py_2()
|
||||||
|
.bg(rgb(PANEL))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_between()
|
||||||
|
.text_xs()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_color(rgb(MUTED))
|
||||||
|
.child("CONTEXT · panes, agents, tools, goal"),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_color(rgb(if selected.needs_input() {
|
||||||
|
ATTENTION
|
||||||
|
} else {
|
||||||
|
SUCCESS
|
||||||
|
}))
|
||||||
|
.child(if selected.needs_input() {
|
||||||
|
"NEEDS INPUT · decision below"
|
||||||
|
} else {
|
||||||
|
"WORKSPACE HEALTHY"
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap_2()
|
||||||
|
.mt_2()
|
||||||
|
.flex_1()
|
||||||
|
.min_h_0()
|
||||||
|
.overflow_hidden()
|
||||||
|
.children(pane_selectors),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.h(relative(0.60))
|
||||||
|
.flex_none()
|
||||||
|
.min_h_0()
|
||||||
|
.overflow_hidden()
|
||||||
|
.child(self.work_surface()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.h(relative(0.20))
|
||||||
|
.flex_none()
|
||||||
|
.min_h_0()
|
||||||
|
.overflow_hidden()
|
||||||
|
.child(self.decision_region()),
|
||||||
|
);
|
||||||
|
|
||||||
let counters = self.model.counters();
|
let counters = self.model.counters();
|
||||||
let footer_left = format!(
|
let footer_left = format!(
|
||||||
@@ -1014,7 +1413,7 @@ impl Render for LumbridgeShell {
|
|||||||
.flex_1()
|
.flex_1()
|
||||||
.min_w_0()
|
.min_w_0()
|
||||||
.child(tabs)
|
.child(tabs)
|
||||||
.child(grid),
|
.child(workspace_stack),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
@@ -1141,7 +1540,11 @@ fn main() {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{KeyModifiers, TerminalKey, terminal_key_from_parts};
|
use super::{
|
||||||
|
KeyModifiers, TerminalEngine, TerminalEngineOptions, TerminalKey, indexed_terminal_color,
|
||||||
|
terminal_dimensions_for_window, terminal_key_from_parts, terminal_paint_rows,
|
||||||
|
};
|
||||||
|
use gpui::{px, size};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn maps_named_and_composed_gpui_keys_to_terminal_input() {
|
fn maps_named_and_composed_gpui_keys_to_terminal_input() {
|
||||||
@@ -1158,4 +1561,32 @@ mod tests {
|
|||||||
let control = terminal_key_from_parts("c", Some("c"), KeyModifiers::CONTROL).unwrap();
|
let control = terminal_key_from_parts("c", Some("c"), KeyModifiers::CONTROL).unwrap();
|
||||||
assert_eq!(control.key, TerminalKey::Text("c".into()));
|
assert_eq!(control.key, TerminalKey::Text("c".into()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn terminal_paint_rows_merge_runs_and_preserve_cursor_boundary() {
|
||||||
|
let mut terminal = TerminalEngine::new(TerminalEngineOptions::default());
|
||||||
|
terminal.process(b"\x1b[31mAB\x1b[0m");
|
||||||
|
let rows = terminal_paint_rows(&terminal.snapshot());
|
||||||
|
assert!(
|
||||||
|
rows[0]
|
||||||
|
.iter()
|
||||||
|
.any(|run| run.text == "AB" && run.columns == 2)
|
||||||
|
);
|
||||||
|
assert!(rows[0].iter().any(|run| run.cursor.is_some()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn xterm_color_cube_and_grayscale_are_deterministic() {
|
||||||
|
assert_eq!(indexed_terminal_color(16), 0x000000);
|
||||||
|
assert_eq!(indexed_terminal_color(231), 0xffffff);
|
||||||
|
assert_eq!(indexed_terminal_color(232), 0x080808);
|
||||||
|
assert_eq!(indexed_terminal_color(255), 0xeeeeee);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user