aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-06 23:06:00 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-06 23:06:00 +0300
commit56edcbcba83b8ff7f6f69ee4231f163e97df3346 (patch)
treee6eb1c31fd8f9a317fe1d6d49b019037f14b4746
parentefceb30f0b447196615e3b2821d12ef82adcb3e2 (diff)
feat: expose pane scroll state
-rw-r--r--docs/next/CHANGELOG.md1
-rw-r--r--docs/next/api/herdr-api.schema.json136
-rw-r--r--docs/next/website/src/content/docs/cli-reference.mdx2
-rw-r--r--docs/next/website/src/content/docs/socket-api.mdx17
-rw-r--r--src/api/schema/events.rs14
-rw-r--r--src/api/schema/panes.rs9
-rw-r--r--src/api/schema/tests.rs33
-rw-r--r--src/api/server.rs1
-rw-r--r--src/api/subscriptions.rs115
-rw-r--r--src/app/api/panes.rs42
-rw-r--r--src/app/creation.rs10
11 files changed, 373 insertions, 7 deletions
diff --git a/docs/next/CHANGELOG.md b/docs/next/CHANGELOG.md
index 6d2348ae..8701dd24 100644
--- a/docs/next/CHANGELOG.md
+++ b/docs/next/CHANGELOG.md
@@ -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)
diff --git a/docs/next/api/herdr-api.schema.json b/docs/next/api/herdr-api.schema.json
index 4b33e750..ef6a6fdb 100644
--- a/docs/next/api/herdr-api.schema.json
+++ b/docs/next/api/herdr-api.schema.json
@@ -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",
@@ -3356,6 +3391,22 @@
},
{
"properties": {
+ "pane_id": {
+ "type": "string"
+ },
+ "type": {
+ "const": "pane.scroll_changed",
+ "type": "string"
+ }
+ },
+ "required": [
+ "type",
+ "pane_id"
+ ],
+ "type": "object"
+ },
+ {
+ "properties": {
"type": {
"const": "layout.updated",
"type": "string"
@@ -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",
diff --git a/docs/next/website/src/content/docs/cli-reference.mdx b/docs/next/website/src/content/docs/cli-reference.mdx
index a6870fff..81fe4dce 100644
--- a/docs/next/website/src/content/docs/cli-reference.mdx
+++ b/docs/next/website/src/content/docs/cli-reference.mdx
@@ -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
diff --git a/docs/next/website/src/content/docs/socket-api.mdx b/docs/next/website/src/content/docs/socket-api.mdx
index 64c5ac4e..600238b9 100644
--- a/docs/next/website/src/content/docs/socket-api.mdx
+++ b/docs/next/website/src/content/docs/socket-api.mdx
@@ -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
diff --git a/src/api/schema/events.rs b/src/api/schema/events.rs
index aff3d2a2..41b9907d 100644
--- a/src/api/schema/events.rs
+++ b/src/api/schema/events.rs
@@ -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 {
diff --git a/src/api/schema/panes.rs b/src/api/schema/panes.rs
index ef45a7ca..42a640ef 100644
--- a/src/api/schema/panes.rs
+++ b/src/api/schema/panes.rs
@@ -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,
diff --git a/src/api/schema/tests.rs b/src/api/schema/tests.rs
index 8bb876d0..5f603a18 100644
--- a/src/api/schema/tests.rs
+++ b/src/api/schema/tests.rs
@@ -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!(
&params.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!(
+ &params.subscriptions[2],
+ Subscription::PaneScrollChanged { pane_id } if pane_id == "p_1_1"
+ ));
}
#[test]
@@ -487,6 +495,27 @@ fn subscription_event_envelope_round_trips() {
}
#[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 {
id: "req_1".into(),
@@ -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,
},
},
diff --git a/src/api/server.rs b/src/api/server.rs
index df0f8fc3..67093c72 100644
--- a/src/api/server.rs
+++ b/src/api/server.rs
@@ -674,6 +674,7 @@ mod tests {
custom_status: None,
state_labels: HashMap::new(),
agent_session: None,
+ scroll: None,
revision: 0,
}
}
diff --git a/src/api/subscriptions.rs b/src/api/subscriptions.rs
index 73586b99..b3a369c2 100644
--- a/src/api/subscriptions.rs
+++ b/src/api/subscriptions.rs
@@ -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();
diff --git a/src/app/api/panes.rs b/src/app/api/panes.rs
index c4e6321d..60b2e93b 100644
--- a/src/app/api/panes.rs
+++ b/src/app/api/panes.rs
@@ -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,
@@ -1929,6 +1945,32 @@ mod tests {
}
#[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);
diff --git a/src/app/creation.rs b/src/app/creation.rs
index bfc4cd03..13fc0a8e 100644
--- a/src/app/creation.rs
+++ b/src/app/creation.rs
@@ -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,
})
}