This commit is contained in:
+339
-177
@@ -1,4 +1,4 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::collections::{BTreeMap, VecDeque};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use gpui::{
|
||||
@@ -6,8 +6,8 @@ use gpui::{
|
||||
Size, Window, WindowBounds, WindowOptions, actions, div, prelude::*, px, relative, rgb, size,
|
||||
};
|
||||
use lumbridge_runtime::{
|
||||
CommandConfig, PtyOptions, RuntimeActor, RuntimeActorError, RuntimeActorOptions,
|
||||
RuntimeCommand, RuntimeEvent, TerminalSize,
|
||||
CommandConfig, PtyOptions, RuntimeActorError, RuntimeActorOptions, RuntimeCommand,
|
||||
RuntimeEvent, RuntimeRegistry, RuntimeRegistryError, TerminalSize,
|
||||
};
|
||||
use lumbridge_spike_model::{
|
||||
ActionOutcome, FOOTER_RIGHT, FocusDirection, INITIAL_ATTACHED_PANEL_COUNT, OutputSource,
|
||||
@@ -33,8 +33,7 @@ const SUCCESS: u32 = 0x70d6a8;
|
||||
const TIMING_SAMPLE_LIMIT: usize = 256;
|
||||
const RUNTIME_POLL_INTERVAL: Duration = Duration::from_millis(16);
|
||||
const RUNTIME_DRAIN_LIMIT: usize = 64;
|
||||
const LIVE_PANE: PaneId = PaneId::CodexRuntime;
|
||||
const LIVE_PTY_SCRIPT: &str = "printf 'Lumbridge interactive PTY · type here\\n'; exec /bin/sh -i";
|
||||
const LIVE_PANES: [PaneId; 3] = [PaneId::CodexRuntime, PaneId::ClaudeUi, PaneId::PiDocs];
|
||||
const SIDEBAR_WIDTH: f32 = 248.0;
|
||||
const APP_HEADER_HEIGHT: f32 = 48.0;
|
||||
const TAB_BAR_HEIGHT: f32 = 38.0;
|
||||
@@ -74,14 +73,34 @@ actions!(
|
||||
struct LumbridgeShell {
|
||||
model: ShellModel,
|
||||
timing: RenderTiming,
|
||||
runtime: Option<RuntimeActor>,
|
||||
runtime_status: LiveRuntimeStatus,
|
||||
terminal: TerminalEngine,
|
||||
terminal_snapshot: TerminalSnapshot,
|
||||
last_runtime_sequence: u64,
|
||||
runtimes: RuntimeRegistry<PaneId>,
|
||||
live_terminals: BTreeMap<PaneId, LiveTerminalState>,
|
||||
root_focus: FocusHandle,
|
||||
}
|
||||
|
||||
struct LiveTerminalState {
|
||||
status: LiveRuntimeStatus,
|
||||
terminal: TerminalEngine,
|
||||
snapshot: TerminalSnapshot,
|
||||
last_runtime_sequence: u64,
|
||||
}
|
||||
|
||||
impl LiveTerminalState {
|
||||
fn new(dimensions: TerminalDimensions) -> Self {
|
||||
let terminal = TerminalEngine::new(TerminalEngineOptions {
|
||||
dimensions,
|
||||
..TerminalEngineOptions::default()
|
||||
});
|
||||
let snapshot = terminal.snapshot();
|
||||
Self {
|
||||
status: LiveRuntimeStatus::Starting,
|
||||
terminal,
|
||||
snapshot,
|
||||
last_runtime_sequence: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
enum LiveRuntimeStatus {
|
||||
Starting,
|
||||
@@ -409,34 +428,31 @@ impl LumbridgeShell {
|
||||
|
||||
let terminal_dimensions =
|
||||
terminal_dimensions_for_window(window.bounds().size, INITIAL_ATTACHED_PANEL_COUNT);
|
||||
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()) {
|
||||
Ok(runtime) => (Some(runtime), LiveRuntimeStatus::Starting),
|
||||
Err(error) => (None, LiveRuntimeStatus::Fault(error.to_string())),
|
||||
};
|
||||
let mut runtimes = RuntimeRegistry::new();
|
||||
let mut live_terminals = BTreeMap::new();
|
||||
for pane in LIVE_PANES {
|
||||
let mut terminal = LiveTerminalState::new(terminal_dimensions);
|
||||
if let Err(error) = spawn_live_runtime(&mut runtimes, pane, terminal_dimensions) {
|
||||
terminal.status = LiveRuntimeStatus::Fault(error.to_string());
|
||||
}
|
||||
live_terminals.insert(pane, terminal);
|
||||
}
|
||||
|
||||
cx.observe_window_bounds(window, |shell, window, cx| {
|
||||
let dimensions = terminal_dimensions_for_window(
|
||||
window.bounds().size,
|
||||
shell.model.attached_panel_count(),
|
||||
);
|
||||
shell.resize_terminal(dimensions.rows(), dimensions.columns(), cx);
|
||||
shell.resize_attached_terminals(dimensions.rows(), dimensions.columns(), cx);
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
model: ShellModel::with_external_output(LIVE_PANE)
|
||||
.expect("the live comparison pane is a terminal"),
|
||||
model: ShellModel::with_external_outputs(LIVE_PANES)
|
||||
.expect("all live comparison panes are terminals"),
|
||||
timing: RenderTiming::default(),
|
||||
runtime,
|
||||
runtime_status,
|
||||
terminal,
|
||||
terminal_snapshot,
|
||||
last_runtime_sequence: 0,
|
||||
runtimes,
|
||||
live_terminals,
|
||||
root_focus,
|
||||
}
|
||||
}
|
||||
@@ -447,119 +463,179 @@ impl LumbridgeShell {
|
||||
}
|
||||
|
||||
fn drain_runtime_events(&mut self) -> bool {
|
||||
if self.runtime_status.is_terminal() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut changed = false;
|
||||
for _ in 0..RUNTIME_DRAIN_LIMIT {
|
||||
let event = match self.runtime.as_ref().map(RuntimeActor::try_recv) {
|
||||
Some(Ok(Some(event))) => event,
|
||||
Some(Ok(None)) | None => break,
|
||||
Some(Err(RuntimeActorError::Disconnected)) => {
|
||||
self.runtime_status =
|
||||
LiveRuntimeStatus::Fault("runtime actor disconnected".to_owned());
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
Some(Err(error)) => {
|
||||
self.runtime_status = LiveRuntimeStatus::Fault(error.to_string());
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
};
|
||||
for pane in LIVE_PANES {
|
||||
if self
|
||||
.live_terminals
|
||||
.get(&pane)
|
||||
.is_none_or(|terminal| terminal.status.is_terminal())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for _ in 0..RUNTIME_DRAIN_LIMIT {
|
||||
let event = match self.runtimes.try_recv(&pane) {
|
||||
Ok(Some(event)) => event,
|
||||
Ok(None) => break,
|
||||
Err(RuntimeRegistryError::Actor(RuntimeActorError::Disconnected)) => {
|
||||
self.live_terminals
|
||||
.get_mut(&pane)
|
||||
.expect("live terminal state exists")
|
||||
.status =
|
||||
LiveRuntimeStatus::Fault("runtime actor disconnected".to_owned());
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
Err(error) => {
|
||||
self.live_terminals
|
||||
.get_mut(&pane)
|
||||
.expect("live terminal state exists")
|
||||
.status = LiveRuntimeStatus::Fault(error.to_string());
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
match event {
|
||||
RuntimeEvent::Started {
|
||||
session_id,
|
||||
process_id,
|
||||
} => {
|
||||
self.runtime_status = LiveRuntimeStatus::Running {
|
||||
session_id: session_id.get(),
|
||||
match event {
|
||||
RuntimeEvent::Started {
|
||||
session_id,
|
||||
process_id,
|
||||
};
|
||||
changed = true;
|
||||
}
|
||||
RuntimeEvent::Output {
|
||||
sequence, bytes, ..
|
||||
} => {
|
||||
if sequence <= self.last_runtime_sequence {
|
||||
self.runtime_status = LiveRuntimeStatus::Fault(format!(
|
||||
"non-monotonic PTY output sequence {sequence}"
|
||||
} => {
|
||||
self.live_terminals
|
||||
.get_mut(&pane)
|
||||
.expect("live terminal state exists")
|
||||
.status = LiveRuntimeStatus::Running {
|
||||
session_id: session_id.get(),
|
||||
process_id,
|
||||
};
|
||||
changed = true;
|
||||
}
|
||||
RuntimeEvent::Output {
|
||||
sequence, bytes, ..
|
||||
} => {
|
||||
let responses = {
|
||||
let terminal = self
|
||||
.live_terminals
|
||||
.get_mut(&pane)
|
||||
.expect("live terminal state exists");
|
||||
if sequence <= terminal.last_runtime_sequence {
|
||||
terminal.status = LiveRuntimeStatus::Fault(format!(
|
||||
"non-monotonic PTY output sequence {sequence}"
|
||||
));
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
terminal.last_runtime_sequence = sequence;
|
||||
terminal.terminal.process(&bytes).outbound
|
||||
};
|
||||
for response in responses {
|
||||
if let Err(error) =
|
||||
self.send_runtime_command(pane, RuntimeCommand::Input(response))
|
||||
{
|
||||
self.live_terminals
|
||||
.get_mut(&pane)
|
||||
.expect("live terminal state exists")
|
||||
.status = LiveRuntimeStatus::Fault(error.to_string());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
self.publish_terminal_snapshot(pane);
|
||||
changed = true;
|
||||
}
|
||||
RuntimeEvent::InputClosed { .. } => {}
|
||||
RuntimeEvent::Exited { status, .. } => {
|
||||
self.live_terminals
|
||||
.get_mut(&pane)
|
||||
.expect("live terminal state exists")
|
||||
.status = LiveRuntimeStatus::Exited(format!(
|
||||
"PTY exited with code {}",
|
||||
status.code
|
||||
));
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
self.last_runtime_sequence = sequence;
|
||||
let update = self.terminal.process(&bytes);
|
||||
for response in update.outbound {
|
||||
if let Err(error) =
|
||||
self.send_runtime_command(RuntimeCommand::Input(response))
|
||||
{
|
||||
self.runtime_status = LiveRuntimeStatus::Fault(error.to_string());
|
||||
return true;
|
||||
}
|
||||
RuntimeEvent::Fault {
|
||||
operation, message, ..
|
||||
} => {
|
||||
self.live_terminals
|
||||
.get_mut(&pane)
|
||||
.expect("live terminal state exists")
|
||||
.status = LiveRuntimeStatus::Fault(format!("{operation:?}: {message}"));
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
self.publish_terminal_snapshot();
|
||||
changed = true;
|
||||
}
|
||||
RuntimeEvent::InputClosed { .. } => {}
|
||||
RuntimeEvent::Exited { status, .. } => {
|
||||
self.runtime_status =
|
||||
LiveRuntimeStatus::Exited(format!("PTY exited with code {}", status.code));
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
RuntimeEvent::Fault {
|
||||
operation, message, ..
|
||||
} => {
|
||||
self.runtime_status =
|
||||
LiveRuntimeStatus::Fault(format!("{operation:?}: {message}"));
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
changed
|
||||
}
|
||||
|
||||
fn send_runtime_command(&self, command: RuntimeCommand) -> Result<(), RuntimeActorError> {
|
||||
self.runtime
|
||||
.as_ref()
|
||||
.ok_or(RuntimeActorError::Disconnected)?
|
||||
.try_send(command)
|
||||
fn send_runtime_command(
|
||||
&self,
|
||||
pane: PaneId,
|
||||
command: RuntimeCommand,
|
||||
) -> Result<(), RuntimeRegistryError> {
|
||||
self.runtimes.try_send(&pane, command)
|
||||
}
|
||||
|
||||
fn publish_terminal_snapshot(&mut self) {
|
||||
let snapshot = self.terminal.snapshot();
|
||||
let lines = snapshot.plain_rows();
|
||||
self.terminal_snapshot = snapshot;
|
||||
self.dispatch(ShellAction::ReplaceExternalOutput {
|
||||
pane: LIVE_PANE,
|
||||
lines,
|
||||
});
|
||||
fn publish_terminal_snapshot(&mut self, pane: PaneId) {
|
||||
let lines = {
|
||||
let terminal = self
|
||||
.live_terminals
|
||||
.get_mut(&pane)
|
||||
.expect("live terminal state exists");
|
||||
let snapshot = terminal.terminal.snapshot();
|
||||
let lines = snapshot.plain_rows();
|
||||
terminal.snapshot = snapshot;
|
||||
lines
|
||||
};
|
||||
self.dispatch(ShellAction::ReplaceExternalOutput { pane, lines });
|
||||
}
|
||||
|
||||
fn resize_terminal(&mut self, rows: u16, columns: u16, cx: &mut Context<Self>) {
|
||||
fn resize_terminal(&mut self, pane: PaneId, rows: u16, columns: u16) -> bool {
|
||||
let dimensions = TerminalDimensions::new(rows, columns)
|
||||
.expect("resize actions always retain non-zero dimensions");
|
||||
if !self.terminal.resize(dimensions) {
|
||||
return;
|
||||
let terminal = self
|
||||
.live_terminals
|
||||
.get_mut(&pane)
|
||||
.expect("live terminal state exists");
|
||||
if !terminal.terminal.resize(dimensions) {
|
||||
return false;
|
||||
}
|
||||
let pty_size = TerminalSize::new(rows, columns)
|
||||
.expect("terminal engine dimensions are valid PTY dimensions");
|
||||
if let Err(error) = self.send_runtime_command(RuntimeCommand::Resize(pty_size)) {
|
||||
self.runtime_status = LiveRuntimeStatus::Fault(error.to_string());
|
||||
if let Err(error) = self.send_runtime_command(pane, RuntimeCommand::Resize(pty_size)) {
|
||||
self.live_terminals
|
||||
.get_mut(&pane)
|
||||
.expect("live terminal state exists")
|
||||
.status = LiveRuntimeStatus::Fault(error.to_string());
|
||||
}
|
||||
self.publish_terminal_snapshot();
|
||||
cx.notify();
|
||||
self.publish_terminal_snapshot(pane);
|
||||
true
|
||||
}
|
||||
|
||||
fn scroll_terminal(&mut self, scroll: TerminalScroll, cx: &mut Context<Self>) {
|
||||
if !self.terminal.scroll_display(scroll) {
|
||||
fn resize_attached_terminals(&mut self, rows: u16, columns: u16, cx: &mut Context<Self>) {
|
||||
let mut changed = false;
|
||||
for pane in LIVE_PANES {
|
||||
if self.model.is_panel_attached(pane) {
|
||||
changed |= self.resize_terminal(pane, rows, columns);
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn scroll_terminal(&mut self, pane: PaneId, scroll: TerminalScroll, cx: &mut Context<Self>) {
|
||||
if !self
|
||||
.live_terminals
|
||||
.get_mut(&pane)
|
||||
.expect("live terminal state exists")
|
||||
.terminal
|
||||
.scroll_display(scroll)
|
||||
{
|
||||
return;
|
||||
}
|
||||
self.publish_terminal_snapshot();
|
||||
self.publish_terminal_snapshot(pane);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -572,7 +648,7 @@ impl LumbridgeShell {
|
||||
fn resize_terminal_for_workspace(&mut self, window: &Window, cx: &mut Context<Self>) {
|
||||
let dimensions =
|
||||
terminal_dimensions_for_window(window.bounds().size, self.model.attached_panel_count());
|
||||
self.resize_terminal(dimensions.rows(), dimensions.columns(), cx);
|
||||
self.resize_attached_terminals(dimensions.rows(), dimensions.columns(), cx);
|
||||
}
|
||||
|
||||
fn attach_panel(&mut self, pane: PaneId, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -650,55 +726,83 @@ impl LumbridgeShell {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn selected_terminal_dimensions(&self) -> Option<(PaneId, TerminalDimensions)> {
|
||||
let pane = self.model.selected_pane();
|
||||
self.live_terminals
|
||||
.get(&pane)
|
||||
.map(|terminal| (pane, terminal.terminal.dimensions()))
|
||||
}
|
||||
|
||||
fn terminal_taller(&mut self, _: &TerminalTaller, _: &mut Window, cx: &mut Context<Self>) {
|
||||
let dimensions = self.terminal.dimensions();
|
||||
self.resize_terminal(
|
||||
let Some((pane, dimensions)) = self.selected_terminal_dimensions() else {
|
||||
return;
|
||||
};
|
||||
if self.resize_terminal(
|
||||
pane,
|
||||
dimensions.rows().saturating_add(TERMINAL_ROW_STEP),
|
||||
dimensions.columns(),
|
||||
cx,
|
||||
);
|
||||
) {
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn terminal_shorter(&mut self, _: &TerminalShorter, _: &mut Window, cx: &mut Context<Self>) {
|
||||
let dimensions = self.terminal.dimensions();
|
||||
self.resize_terminal(
|
||||
let Some((pane, dimensions)) = self.selected_terminal_dimensions() else {
|
||||
return;
|
||||
};
|
||||
if self.resize_terminal(
|
||||
pane,
|
||||
dimensions.rows().saturating_sub(TERMINAL_ROW_STEP).max(2),
|
||||
dimensions.columns(),
|
||||
cx,
|
||||
);
|
||||
) {
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn terminal_wider(&mut self, _: &TerminalWider, _: &mut Window, cx: &mut Context<Self>) {
|
||||
let dimensions = self.terminal.dimensions();
|
||||
self.resize_terminal(
|
||||
let Some((pane, dimensions)) = self.selected_terminal_dimensions() else {
|
||||
return;
|
||||
};
|
||||
if self.resize_terminal(
|
||||
pane,
|
||||
dimensions.rows(),
|
||||
dimensions.columns().saturating_add(TERMINAL_COLUMN_STEP),
|
||||
cx,
|
||||
);
|
||||
) {
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn terminal_narrower(&mut self, _: &TerminalNarrower, _: &mut Window, cx: &mut Context<Self>) {
|
||||
let dimensions = self.terminal.dimensions();
|
||||
self.resize_terminal(
|
||||
let Some((pane, dimensions)) = self.selected_terminal_dimensions() else {
|
||||
return;
|
||||
};
|
||||
if self.resize_terminal(
|
||||
pane,
|
||||
dimensions.rows(),
|
||||
dimensions
|
||||
.columns()
|
||||
.saturating_sub(TERMINAL_COLUMN_STEP)
|
||||
.max(20),
|
||||
cx,
|
||||
);
|
||||
) {
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn on_key_down(&mut self, event: &KeyDownEvent, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
if !self.model.command_palette().is_open() {
|
||||
if self.model.selected_pane() != LIVE_PANE || self.runtime_status.is_terminal() {
|
||||
let pane = self.model.selected_pane();
|
||||
if self
|
||||
.live_terminals
|
||||
.get(&pane)
|
||||
.is_none_or(|terminal| terminal.status.is_terminal())
|
||||
{
|
||||
return;
|
||||
}
|
||||
let modifiers = key_modifiers(event);
|
||||
if let Some(scroll) =
|
||||
terminal_scroll_from_parts(event.keystroke.key.as_str(), modifiers)
|
||||
{
|
||||
self.scroll_terminal(scroll, cx);
|
||||
self.scroll_terminal(pane, scroll, cx);
|
||||
return;
|
||||
}
|
||||
let Some(event) = terminal_key_from_parts(
|
||||
@@ -708,17 +812,31 @@ impl LumbridgeShell {
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
let bytes = self.terminal.encode_key(&event);
|
||||
let bytes = self
|
||||
.live_terminals
|
||||
.get(&pane)
|
||||
.expect("selected live terminal exists")
|
||||
.terminal
|
||||
.encode_key(&event);
|
||||
if bytes.is_empty() {
|
||||
return;
|
||||
}
|
||||
if let Err(error) = self.send_runtime_command(RuntimeCommand::Input(bytes)) {
|
||||
self.runtime_status = LiveRuntimeStatus::Fault(error.to_string());
|
||||
if let Err(error) = self.send_runtime_command(pane, RuntimeCommand::Input(bytes)) {
|
||||
self.live_terminals
|
||||
.get_mut(&pane)
|
||||
.expect("selected live terminal exists")
|
||||
.status = LiveRuntimeStatus::Fault(error.to_string());
|
||||
}
|
||||
if self.terminal.display_offset() > 0
|
||||
&& self.terminal.scroll_display(TerminalScroll::Bottom)
|
||||
{
|
||||
self.publish_terminal_snapshot();
|
||||
let moved_to_bottom = {
|
||||
let terminal = self
|
||||
.live_terminals
|
||||
.get_mut(&pane)
|
||||
.expect("selected live terminal exists");
|
||||
terminal.terminal.display_offset() > 0
|
||||
&& terminal.terminal.scroll_display(TerminalScroll::Bottom)
|
||||
};
|
||||
if moved_to_bottom {
|
||||
self.publish_terminal_snapshot(pane);
|
||||
}
|
||||
cx.notify();
|
||||
return;
|
||||
@@ -798,8 +916,12 @@ impl LumbridgeShell {
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn terminal_view(&self) -> gpui::AnyElement {
|
||||
let rows = terminal_paint_rows(&self.terminal_snapshot)
|
||||
fn terminal_view(&self, pane: PaneId) -> gpui::AnyElement {
|
||||
let terminal = self
|
||||
.live_terminals
|
||||
.get(&pane)
|
||||
.expect("external terminal pane has live state");
|
||||
let rows = terminal_paint_rows(&terminal.snapshot)
|
||||
.into_iter()
|
||||
.map(|runs| {
|
||||
div()
|
||||
@@ -830,34 +952,38 @@ impl LumbridgeShell {
|
||||
let pane_id = pane.id();
|
||||
let can_detach = self.model.attached_panel_count() > 1;
|
||||
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 && self.terminal_snapshot.display_offset > 0 {
|
||||
format!(
|
||||
"{} · ↑{} · {}×{}",
|
||||
status,
|
||||
self.terminal_snapshot.display_offset,
|
||||
self.terminal.dimensions().columns(),
|
||||
self.terminal.dimensions().rows()
|
||||
)
|
||||
} else if external {
|
||||
format!(
|
||||
"{} · {}×{}",
|
||||
status,
|
||||
self.terminal.dimensions().columns(),
|
||||
self.terminal.dimensions().rows()
|
||||
)
|
||||
} else {
|
||||
status.to_owned()
|
||||
};
|
||||
let live_terminal = external.then(|| {
|
||||
self.live_terminals
|
||||
.get(&pane_id)
|
||||
.expect("external terminal pane has live state")
|
||||
});
|
||||
let status = live_terminal.map_or(pane.fixture().badge, |terminal| terminal.status.badge());
|
||||
let detail = live_terminal.map_or_else(
|
||||
|| pane.fixture().target.to_owned(),
|
||||
|terminal| terminal.status.detail(),
|
||||
);
|
||||
let surface_status = live_terminal.map_or_else(
|
||||
|| status.to_owned(),
|
||||
|terminal| {
|
||||
let dimensions = terminal.terminal.dimensions();
|
||||
if terminal.snapshot.display_offset > 0 {
|
||||
format!(
|
||||
"{} · ↑{} · {}×{}",
|
||||
status,
|
||||
terminal.snapshot.display_offset,
|
||||
dimensions.columns(),
|
||||
dimensions.rows()
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{} · {}×{}",
|
||||
status,
|
||||
dimensions.columns(),
|
||||
dimensions.rows()
|
||||
)
|
||||
}
|
||||
},
|
||||
);
|
||||
let surface = match pane.kind() {
|
||||
SurfaceKind::Terminal => "TERMINAL",
|
||||
SurfaceKind::Markdown => "CONTEXT",
|
||||
@@ -993,7 +1119,7 @@ impl LumbridgeShell {
|
||||
fn pane_work_surface(&self, pane: &PaneState) -> gpui::AnyElement {
|
||||
let external = pane.output_source() == OutputSource::External;
|
||||
let content = if external {
|
||||
self.terminal_view()
|
||||
self.terminal_view(pane.id())
|
||||
} else {
|
||||
let start = pane.lines().len().saturating_sub(18);
|
||||
div()
|
||||
@@ -1261,6 +1387,15 @@ impl Render for LumbridgeShell {
|
||||
.collect::<Vec<_>>();
|
||||
let detached_count = detached_entries.len();
|
||||
let visible_count = panel_capacity.min(attached_count).max(1);
|
||||
let running_runtime_count = self
|
||||
.live_terminals
|
||||
.values()
|
||||
.filter(|terminal| matches!(terminal.status, LiveRuntimeStatus::Running { .. }))
|
||||
.count();
|
||||
let runtime_summary = format!(
|
||||
"{running_runtime_count}/{} LIVE PTYS",
|
||||
self.live_terminals.len()
|
||||
);
|
||||
let sidebar = div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
@@ -1347,7 +1482,7 @@ impl Render for LumbridgeShell {
|
||||
.py_1()
|
||||
.text_sm()
|
||||
.text_color(rgb(SUCCESS))
|
||||
.child(format!("metal · {}", self.runtime_status.badge())),
|
||||
.child(format!("metal · {runtime_summary}")),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
@@ -1496,10 +1631,16 @@ impl Render for LumbridgeShell {
|
||||
.children(workspace_panels);
|
||||
|
||||
let counters = self.model.counters();
|
||||
let terminal_revision = self
|
||||
.live_terminals
|
||||
.values()
|
||||
.map(|terminal| terminal.terminal.revision())
|
||||
.sum::<u64>();
|
||||
let footer_left = format!(
|
||||
"rev {} · VT rev {} · {} focus · {} VT snapshots · {} synthetic lines",
|
||||
"rev {} · {} PTYs · VT rev {} · {} focus · {} VT snapshots · {} synthetic lines",
|
||||
self.model.revision(),
|
||||
self.terminal.revision(),
|
||||
self.runtimes.len(),
|
||||
terminal_revision,
|
||||
counters.focus_moves,
|
||||
counters.external_snapshot_updates,
|
||||
counters.terminal_lines_appended
|
||||
@@ -1574,7 +1715,7 @@ impl Render for LumbridgeShell {
|
||||
.mr_4()
|
||||
.text_xs()
|
||||
.text_color(rgb(SUCCESS))
|
||||
.child(format!("metal · {}", self.runtime_status.badge())),
|
||||
.child(format!("metal · {runtime_summary}")),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
@@ -1685,17 +1826,38 @@ fn terminal_scroll_from_parts(key: &str, modifiers: KeyModifiers) -> Option<Term
|
||||
}
|
||||
}
|
||||
|
||||
fn start_live_runtime(dimensions: TerminalDimensions) -> Result<RuntimeActor, RuntimeActorError> {
|
||||
fn live_pty_script(pane: PaneId) -> &'static str {
|
||||
match pane {
|
||||
PaneId::CodexRuntime => {
|
||||
"printf 'Codex runtime · independent Lumbridge PTY\\n'; exec /bin/sh -i"
|
||||
}
|
||||
PaneId::ClaudeUi => {
|
||||
"printf 'Claude workspace · independent Lumbridge PTY\\n'; exec /bin/sh -i"
|
||||
}
|
||||
PaneId::PiDocs => "printf 'Pi docs · independent Lumbridge PTY\\n'; exec /bin/sh -i",
|
||||
PaneId::Architecture | PaneId::AcpPreview | PaneId::RuntimeReview => {
|
||||
unreachable!("only terminal fixtures own live PTYs")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_live_runtime(
|
||||
runtimes: &mut RuntimeRegistry<PaneId>,
|
||||
pane: PaneId,
|
||||
dimensions: TerminalDimensions,
|
||||
) -> Result<(), RuntimeRegistryError> {
|
||||
let command = CommandConfig::new("/bin/sh")
|
||||
.map_err(RuntimeActorError::Start)?
|
||||
.args(["-c", LIVE_PTY_SCRIPT]);
|
||||
.args(["-c", live_pty_script(pane)]);
|
||||
let pty_size = TerminalSize::new(dimensions.rows(), dimensions.columns())
|
||||
.map_err(RuntimeActorError::Start)?;
|
||||
RuntimeActor::spawn(
|
||||
runtimes.spawn(
|
||||
pane,
|
||||
command,
|
||||
PtyOptions::new(pty_size),
|
||||
RuntimeActorOptions::default(),
|
||||
)
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
||||
Reference in New Issue
Block a user