feat: expose pane scroll state
This commit is contained in:
parent
efceb30f0b
commit
56edcbcba8
|
|
@ -8,6 +8,7 @@
|
|||
- Added `session.snapshot` to bootstrap client runtime state in one socket API response before subscribing to events.
|
||||
- Added `herdr api schema` to inspect the bundled socket API schema, with `--json` for the full JSON Schema document and `--output PATH` for file output.
|
||||
- Added `layout.updated` socket events so protocol clients can keep tab layout snapshots current after pane split, resize, swap, move, zoom, and layout mutations.
|
||||
- Added pane scroll metrics to pane socket API responses and `pane.scroll_changed` subscriptions for clients that need to show when a pane is scrolled back.
|
||||
- Added `herdr terminal session observe` for read-only live ANSI terminal streams that bridge processes can consume as newline-delimited JSON.
|
||||
- Added `herdr terminal session control` for bridge processes that need live ANSI frames plus input, resize, scroll, release, and takeover authority.
|
||||
- Added `ui.hide_tab_bar_when_single_tab` to hide the tab row when a workspace has one tab. (#448)
|
||||
|
|
|
|||
|
|
@ -745,6 +745,16 @@
|
|||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"scroll": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/schemas/event/$defs/PaneScrollInfo"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"state_labels": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
|
|
@ -897,6 +907,31 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PaneScrollInfo": {
|
||||
"properties": {
|
||||
"max_offset_from_bottom": {
|
||||
"format": "uint64",
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"offset_from_bottom": {
|
||||
"format": "uint64",
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"viewport_rows": {
|
||||
"format": "uint64",
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"offset_from_bottom",
|
||||
"max_offset_from_bottom",
|
||||
"viewport_rows"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SplitDirection": {
|
||||
"enum": [
|
||||
"right",
|
||||
|
|
@ -3354,6 +3389,22 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"pane_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"const": "pane.scroll_changed",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"pane_id"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
|
|
@ -5093,6 +5144,50 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PaneScrollChangedEvent": {
|
||||
"properties": {
|
||||
"pane_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"scroll": {
|
||||
"$ref": "#/schemas/subscription_event/$defs/PaneScrollInfo"
|
||||
},
|
||||
"workspace_id": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"pane_id",
|
||||
"workspace_id",
|
||||
"scroll"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PaneScrollInfo": {
|
||||
"properties": {
|
||||
"max_offset_from_bottom": {
|
||||
"format": "uint64",
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"offset_from_bottom": {
|
||||
"format": "uint64",
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"viewport_rows": {
|
||||
"format": "uint64",
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"offset_from_bottom",
|
||||
"max_offset_from_bottom",
|
||||
"viewport_rows"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ReadFormat": {
|
||||
"enum": [
|
||||
"text",
|
||||
|
|
@ -5116,13 +5211,17 @@
|
|||
},
|
||||
{
|
||||
"$ref": "#/schemas/subscription_event/$defs/PaneAgentStatusChangedEvent"
|
||||
},
|
||||
{
|
||||
"$ref": "#/schemas/subscription_event/$defs/PaneScrollChangedEvent"
|
||||
}
|
||||
]
|
||||
},
|
||||
"SubscriptionEventKind": {
|
||||
"enum": [
|
||||
"pane.output_matched",
|
||||
"pane.agent_status_changed"
|
||||
"pane.agent_status_changed",
|
||||
"pane.scroll_changed"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
|
|
@ -6377,6 +6476,16 @@
|
|||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"scroll": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/schemas/success_response/$defs/PaneScrollInfo"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"state_labels": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
|
|
@ -6811,6 +6920,31 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PaneScrollInfo": {
|
||||
"properties": {
|
||||
"max_offset_from_bottom": {
|
||||
"format": "uint64",
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"offset_from_bottom": {
|
||||
"format": "uint64",
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"viewport_rows": {
|
||||
"format": "uint64",
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"offset_from_bottom",
|
||||
"max_offset_from_bottom",
|
||||
"viewport_rows"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PaneSwapReason": {
|
||||
"enum": [
|
||||
"no_neighbor",
|
||||
|
|
|
|||
|
|
@ -224,6 +224,8 @@ herdr pane report-agent <pane_id> \
|
|||
|
||||
Those commands include `foreground_cwd` when Herdr can resolve the cwd of the foreground process controlling the pane. The existing `cwd` field remains the pane/workspace cwd used for labels and follow-cwd behavior.
|
||||
|
||||
`pane get` and `pane list` include `scroll` when terminal scroll metrics are available. `scroll.offset_from_bottom == 0` means the pane is at the bottom of its scrollback.
|
||||
|
||||
Report display-only pane metadata without taking over semantic state:
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -150,6 +150,18 @@ like `ctrl+h`, `control+j`, `alt+x`, and `shift+tab`, function keys like
|
|||
Herdr returns that pane. When it is omitted, Herdr returns the active focused
|
||||
pane.
|
||||
|
||||
`PaneInfo` includes `scroll` when terminal scroll metrics are available:
|
||||
|
||||
```json
|
||||
{
|
||||
"offset_from_bottom": 12,
|
||||
"max_offset_from_bottom": 240,
|
||||
"viewport_rows": 30
|
||||
}
|
||||
```
|
||||
|
||||
Clients can treat `offset_from_bottom == 0` as at-bottom state.
|
||||
|
||||
`pane.layout` returns the tab layout snapshot with `workspace_id`, `tab_id`,
|
||||
`zoomed`, outer `area`, `focused_pane_id`, pane rects, and split rects/ratios.
|
||||
`pane.neighbor` and `pane.edges` include that same layout snapshot so clients
|
||||
|
|
@ -630,7 +642,10 @@ Tab event subscriptions include `tab.created`, `tab.closed`, `tab.focused`,
|
|||
that workspace.
|
||||
Pane event subscriptions include `pane.created`, `pane.closed`, `pane.focused`,
|
||||
`pane.moved`, `pane.exited`, `pane.agent_detected`,
|
||||
`pane.output_matched`, and `pane.agent_status_changed`.
|
||||
`pane.output_matched`, `pane.agent_status_changed`, and `pane.scroll_changed`.
|
||||
`pane.scroll_changed` is scoped to one `pane_id` and emits `pane_id`,
|
||||
`workspace_id`, and the current `scroll` metrics whenever Herdr observes a
|
||||
changed scroll snapshot.
|
||||
Layout event subscriptions include `layout.updated`. The event carries the
|
||||
updated `PaneLayoutSnapshot` for one tab. Clients that bootstrap with
|
||||
`session.snapshot` should replace the cached layout with the same
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::collections::HashMap;
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::common::{AgentStatus, ReadSource};
|
||||
use super::panes::{PaneInfo, PaneReadResult};
|
||||
use super::panes::{PaneInfo, PaneReadResult, PaneScrollInfo};
|
||||
use super::tabs::TabInfo;
|
||||
use super::workspaces::WorkspaceInfo;
|
||||
use super::worktrees::WorktreeInfo;
|
||||
|
|
@ -72,6 +72,8 @@ pub enum Subscription {
|
|||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
agent_status: Option<AgentStatus>,
|
||||
},
|
||||
#[serde(rename = "pane.scroll_changed")]
|
||||
PaneScrollChanged { pane_id: String },
|
||||
#[serde(rename = "layout.updated")]
|
||||
LayoutUpdated {},
|
||||
}
|
||||
|
|
@ -350,6 +352,8 @@ pub enum SubscriptionEventKind {
|
|||
PaneOutputMatched,
|
||||
#[serde(rename = "pane.agent_status_changed")]
|
||||
PaneAgentStatusChanged,
|
||||
#[serde(rename = "pane.scroll_changed")]
|
||||
ScrollChanged,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
|
||||
|
|
@ -363,6 +367,7 @@ pub struct SubscriptionEventEnvelope {
|
|||
pub enum SubscriptionEventData {
|
||||
PaneOutputMatched(PaneOutputMatchedEvent),
|
||||
PaneAgentStatusChanged(PaneAgentStatusChangedEvent),
|
||||
ScrollChanged(PaneScrollChangedEvent),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
|
||||
|
|
@ -389,6 +394,13 @@ pub struct PaneAgentStatusChangedEvent {
|
|||
pub state_labels: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
|
||||
pub struct PaneScrollChangedEvent {
|
||||
pub pane_id: String,
|
||||
pub workspace_id: String,
|
||||
pub scroll: PaneScrollInfo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum EventData {
|
||||
|
|
|
|||
|
|
@ -364,9 +364,18 @@ pub struct PaneInfo {
|
|||
pub state_labels: HashMap<String, String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub agent_session: Option<AgentSessionInfo>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub scroll: Option<PaneScrollInfo>,
|
||||
pub revision: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
|
||||
pub struct PaneScrollInfo {
|
||||
pub offset_from_bottom: u64,
|
||||
pub max_offset_from_bottom: u64,
|
||||
pub viewport_rows: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
|
||||
pub struct PaneProcessInfo {
|
||||
pub pane_id: String,
|
||||
|
|
|
|||
|
|
@ -430,6 +430,10 @@ fn subscribe_request_parses_parameterized_subscriptions() {
|
|||
"type": "pane.agent_status_changed",
|
||||
"pane_id": "p_1_1",
|
||||
"agent_status": "done"
|
||||
},
|
||||
{
|
||||
"type": "pane.scroll_changed",
|
||||
"pane_id": "p_1_1"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -440,7 +444,7 @@ fn subscribe_request_parses_parameterized_subscriptions() {
|
|||
let Method::EventsSubscribe(params) = request.method else {
|
||||
panic!("wrong method parsed");
|
||||
};
|
||||
assert_eq!(params.subscriptions.len(), 2);
|
||||
assert_eq!(params.subscriptions.len(), 3);
|
||||
assert!(matches!(
|
||||
¶ms.subscriptions[0],
|
||||
Subscription::PaneOutputMatched {
|
||||
|
|
@ -458,6 +462,10 @@ fn subscribe_request_parses_parameterized_subscriptions() {
|
|||
agent_status: Some(AgentStatus::Done),
|
||||
} if pane_id == "p_1_1"
|
||||
));
|
||||
assert!(matches!(
|
||||
¶ms.subscriptions[2],
|
||||
Subscription::PaneScrollChanged { pane_id } if pane_id == "p_1_1"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -486,6 +494,27 @@ fn subscription_event_envelope_round_trips() {
|
|||
assert_eq!(restored, event);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scroll_changed_subscription_event_round_trips() {
|
||||
let event = SubscriptionEventEnvelope {
|
||||
event: SubscriptionEventKind::ScrollChanged,
|
||||
data: SubscriptionEventData::ScrollChanged(PaneScrollChangedEvent {
|
||||
pane_id: "p_1_1".into(),
|
||||
workspace_id: "w_1".into(),
|
||||
scroll: PaneScrollInfo {
|
||||
offset_from_bottom: 12,
|
||||
max_offset_from_bottom: 240,
|
||||
viewport_rows: 30,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&event).unwrap();
|
||||
assert!(json.contains("\"event\":\"pane.scroll_changed\""));
|
||||
let restored: SubscriptionEventEnvelope = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(restored, event);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn success_response_round_trips() {
|
||||
let response = SuccessResponse {
|
||||
|
|
@ -600,6 +629,7 @@ fn worktree_request_and_response_round_trip() {
|
|||
custom_status: None,
|
||||
state_labels: HashMap::new(),
|
||||
agent_session: None,
|
||||
scroll: None,
|
||||
revision: 0,
|
||||
},
|
||||
worktree: WorktreeInfo {
|
||||
|
|
@ -1007,6 +1037,7 @@ fn create_response_round_trips_with_root_pane() {
|
|||
custom_status: None,
|
||||
state_labels: HashMap::new(),
|
||||
agent_session: None,
|
||||
scroll: None,
|
||||
revision: 0,
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -674,6 +674,7 @@ mod tests {
|
|||
custom_status: None,
|
||||
state_labels: HashMap::new(),
|
||||
agent_session: None,
|
||||
scroll: None,
|
||||
revision: 0,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
use regex::Regex;
|
||||
|
||||
use crate::api::schema::{
|
||||
ErrorBody, ErrorResponse, Method, PaneAgentStatusChangedEvent, PaneOutputMatchedEvent, Request,
|
||||
Subscription, SubscriptionEventData, SubscriptionEventEnvelope, SubscriptionEventKind,
|
||||
ErrorBody, ErrorResponse, Method, PaneAgentStatusChangedEvent, PaneOutputMatchedEvent,
|
||||
PaneScrollChangedEvent, PaneScrollInfo, Request, Subscription, SubscriptionEventData,
|
||||
SubscriptionEventEnvelope, SubscriptionEventKind,
|
||||
};
|
||||
use crate::api::server::{dispatch_to_app_with_timeout, APP_RESPONSE_TIMEOUT};
|
||||
use crate::api::{ApiRequestSender, EventHub};
|
||||
|
|
@ -55,6 +56,12 @@ pub(super) struct ActiveAgentStatusChangedSubscription {
|
|||
request_prefix: String,
|
||||
}
|
||||
|
||||
pub(super) struct ActiveScrollChangedSubscription {
|
||||
pane_id: String,
|
||||
last_scroll: Option<PaneScrollInfo>,
|
||||
request_prefix: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct PanePresentationSnapshot {
|
||||
title: Option<String>,
|
||||
|
|
@ -97,6 +104,7 @@ pub(super) enum ActiveSubscription {
|
|||
Event(ActiveEventSubscription),
|
||||
OutputMatched(ActiveOutputMatchedSubscription),
|
||||
AgentStatusChanged(Box<ActiveAgentStatusChangedSubscription>),
|
||||
ScrollChanged(ActiveScrollChangedSubscription),
|
||||
}
|
||||
|
||||
impl ActiveSubscription {
|
||||
|
|
@ -269,6 +277,15 @@ impl ActiveSubscription {
|
|||
},
|
||||
)))
|
||||
}
|
||||
Subscription::PaneScrollChanged { pane_id } => {
|
||||
let probe = pane_get(format!("{request_id}:sub:{index}:probe"), &pane_id, api_tx)?;
|
||||
|
||||
Ok(Self::ScrollChanged(ActiveScrollChangedSubscription {
|
||||
pane_id: probe.pane_id,
|
||||
last_scroll: probe.scroll,
|
||||
request_prefix: format!("{request_id}:sub:{index}"),
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -285,6 +302,9 @@ impl ActiveSubscription {
|
|||
Self::AgentStatusChanged(subscription) => {
|
||||
serde_json::to_value(subscription.poll(api_tx, event_hub)?).ok()
|
||||
}
|
||||
Self::ScrollChanged(subscription) => {
|
||||
serde_json::to_value(subscription.poll(api_tx)?).ok()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -466,6 +486,39 @@ impl ActiveAgentStatusChangedSubscription {
|
|||
}
|
||||
}
|
||||
|
||||
impl ActiveScrollChangedSubscription {
|
||||
fn poll(&mut self, api_tx: &ApiRequestSender) -> Option<SubscriptionEventEnvelope> {
|
||||
let pane = pane_get(
|
||||
format!("{}:pane", self.request_prefix),
|
||||
&self.pane_id,
|
||||
api_tx,
|
||||
)
|
||||
.ok()?;
|
||||
self.event_from_snapshot(pane)
|
||||
}
|
||||
|
||||
fn event_from_snapshot(
|
||||
&mut self,
|
||||
pane: crate::api::schema::PaneInfo,
|
||||
) -> Option<SubscriptionEventEnvelope> {
|
||||
let scroll = pane.scroll;
|
||||
if self.last_scroll == scroll {
|
||||
return None;
|
||||
}
|
||||
self.last_scroll = scroll;
|
||||
let scroll = scroll?;
|
||||
|
||||
Some(SubscriptionEventEnvelope {
|
||||
event: SubscriptionEventKind::ScrollChanged,
|
||||
data: SubscriptionEventData::ScrollChanged(PaneScrollChangedEvent {
|
||||
pane_id: pane.pane_id,
|
||||
workspace_id: pane.workspace_id,
|
||||
scroll,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn pane_read(
|
||||
request_id: String,
|
||||
pane_id: &str,
|
||||
|
|
@ -558,7 +611,7 @@ mod tests {
|
|||
use std::collections::HashMap;
|
||||
|
||||
use super::*;
|
||||
use crate::api::schema::{AgentStatus, EventData, EventEnvelope, EventKind};
|
||||
use crate::api::schema::{AgentStatus, EventData, EventEnvelope, EventKind, PaneInfo};
|
||||
|
||||
fn status_event(custom_status: Option<&str>) -> EventEnvelope {
|
||||
EventEnvelope {
|
||||
|
|
@ -576,6 +629,62 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
fn pane_info_with_scroll(scroll: Option<PaneScrollInfo>) -> PaneInfo {
|
||||
PaneInfo {
|
||||
pane_id: "pane_1".into(),
|
||||
terminal_id: "terminal_1".into(),
|
||||
workspace_id: "workspace_1".into(),
|
||||
tab_id: "tab_1".into(),
|
||||
focused: true,
|
||||
cwd: None,
|
||||
foreground_cwd: None,
|
||||
label: None,
|
||||
agent: None,
|
||||
title: None,
|
||||
display_agent: None,
|
||||
agent_status: AgentStatus::Unknown,
|
||||
custom_status: None,
|
||||
state_labels: HashMap::new(),
|
||||
agent_session: None,
|
||||
scroll,
|
||||
revision: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scroll_subscription_emits_when_scroll_snapshot_changes() {
|
||||
let at_bottom = PaneScrollInfo {
|
||||
offset_from_bottom: 0,
|
||||
max_offset_from_bottom: 40,
|
||||
viewport_rows: 20,
|
||||
};
|
||||
let scrolled_back = PaneScrollInfo {
|
||||
offset_from_bottom: 8,
|
||||
max_offset_from_bottom: 40,
|
||||
viewport_rows: 20,
|
||||
};
|
||||
let mut subscription = ActiveScrollChangedSubscription {
|
||||
pane_id: "pane_1".into(),
|
||||
last_scroll: Some(at_bottom),
|
||||
request_prefix: "test".into(),
|
||||
};
|
||||
|
||||
assert!(subscription
|
||||
.event_from_snapshot(pane_info_with_scroll(Some(at_bottom)))
|
||||
.is_none());
|
||||
|
||||
let event = subscription
|
||||
.event_from_snapshot(pane_info_with_scroll(Some(scrolled_back)))
|
||||
.expect("scroll event");
|
||||
assert_eq!(event.event, SubscriptionEventKind::ScrollChanged);
|
||||
let SubscriptionEventData::ScrollChanged(data) = event.data else {
|
||||
panic!("wrong event data");
|
||||
};
|
||||
assert_eq!(data.pane_id, "pane_1");
|
||||
assert_eq!(data.workspace_id, "workspace_1");
|
||||
assert_eq!(data.scroll, scrolled_back);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_status_subscription_replays_queued_metadata_set_and_expiry_events() {
|
||||
let event_hub = EventHub::default();
|
||||
|
|
|
|||
|
|
@ -1877,6 +1877,22 @@ mod tests {
|
|||
(app, public_pane_id, rx)
|
||||
}
|
||||
|
||||
fn app_with_scrollback_runtime() -> (App, String, PaneId) {
|
||||
let (mut app, public_pane_id) = app_with_test_workspace();
|
||||
let pane_id = app.state.workspaces[0].tabs[0].root_pane;
|
||||
let lines = (0..20)
|
||||
.map(|line| format!("line {line:02}\n"))
|
||||
.collect::<String>();
|
||||
let runtime = crate::terminal::TerminalRuntime::test_with_scrollback_bytes(
|
||||
20,
|
||||
5,
|
||||
1000,
|
||||
lines.as_bytes(),
|
||||
);
|
||||
app.state.insert_test_runtime(pane_id, runtime);
|
||||
(app, public_pane_id, pane_id)
|
||||
}
|
||||
|
||||
fn metadata_params(pane_id: String) -> PaneReportMetadataParams {
|
||||
PaneReportMetadataParams {
|
||||
pane_id,
|
||||
|
|
@ -1928,6 +1944,32 @@ mod tests {
|
|||
assert!(rx.try_recv().is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn api_pane_get_exposes_scroll_metrics() {
|
||||
let (mut app, public_pane_id, pane_id) = app_with_scrollback_runtime();
|
||||
let runtime = app
|
||||
.state
|
||||
.runtime_for_pane_in_workspace(&app.terminal_runtimes, 0, pane_id)
|
||||
.expect("runtime");
|
||||
runtime.scroll_up(3);
|
||||
|
||||
let response = app.handle_pane_get(
|
||||
"req".into(),
|
||||
PaneTarget {
|
||||
pane_id: public_pane_id,
|
||||
},
|
||||
);
|
||||
|
||||
let success: SuccessResponse = serde_json::from_str(&response).unwrap();
|
||||
let ResponseResult::PaneInfo { pane } = success.result else {
|
||||
panic!("expected pane info response");
|
||||
};
|
||||
let scroll = pane.scroll.expect("scroll metrics");
|
||||
assert_eq!(scroll.offset_from_bottom, 3);
|
||||
assert!(scroll.max_offset_from_bottom >= scroll.offset_from_bottom);
|
||||
assert_eq!(scroll.viewport_rows, 5);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn api_pane_send_keys_preserves_legacy_control_c_aliases() {
|
||||
let (mut app, pane_id, mut rx) = app_with_send_key_runtime(3);
|
||||
|
|
|
|||
|
|
@ -375,6 +375,15 @@ impl App {
|
|||
let pane = ws.pane_state(pane_id)?;
|
||||
let terminal = self.state.terminals.get(&pane.attached_terminal_id)?;
|
||||
let tab_idx = ws.find_tab_index_for_pane(pane_id)?;
|
||||
let scroll = self
|
||||
.state
|
||||
.runtime_for_pane_in_workspace(&self.terminal_runtimes, ws_idx, pane_id)
|
||||
.and_then(|runtime| runtime.scroll_metrics())
|
||||
.map(|metrics| crate::api::schema::PaneScrollInfo {
|
||||
offset_from_bottom: metrics.offset_from_bottom as u64,
|
||||
max_offset_from_bottom: metrics.max_offset_from_bottom as u64,
|
||||
viewport_rows: metrics.viewport_rows as u64,
|
||||
});
|
||||
let focused = self.state.active == Some(ws_idx)
|
||||
&& ws.active_tab == tab_idx
|
||||
&& ws
|
||||
|
|
@ -401,6 +410,7 @@ impl App {
|
|||
custom_status: presentation.custom_status,
|
||||
state_labels: presentation.state_labels,
|
||||
agent_session: terminal_agent_session_info(terminal),
|
||||
scroll,
|
||||
revision: terminal.revision,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue