onlyne_client/backend/orca/
session.rs1use super::cli::{DEAD_STATUS, is_gone, spawn_command};
5use super::policy::{TabMemo, absolute, host_worktree_env};
6use super::resource::{TabKeys, ref_str};
7use crate::backend::*;
8use parking_lot::Mutex;
9use std::collections::BTreeMap;
10use std::sync::Arc;
11
12pub struct OrcaBackend {
13 pub(super) runner: Arc<dyn Runner>,
14 pub(super) command: String,
15 pub(super) policy: WorktreePolicy,
16 pub(super) host_worktree: Option<String>,
19 pub(super) tabs: Mutex<BTreeMap<String, TabMemo>>,
21}
22
23impl OrcaBackend {
24 pub fn new(runner: Arc<dyn Runner>) -> Self {
27 Self::with_policy(runner, WorktreePolicy::Host)
28 }
29
30 pub fn with_policy(runner: Arc<dyn Runner>, policy: WorktreePolicy) -> Self {
33 Self::with_host_worktree(runner, policy, host_worktree_env())
34 }
35
36 pub fn with_host_worktree(
40 runner: Arc<dyn Runner>,
41 policy: WorktreePolicy,
42 host_worktree: Option<String>,
43 ) -> Self {
44 Self {
45 runner,
46 command: std::env::var("ORCA_CLI_COMMAND").unwrap_or_else(|_| "orca".into()),
47 policy,
48 host_worktree,
49 tabs: Mutex::new(BTreeMap::new()),
50 }
51 }
52}
53
54impl SessionBackend for OrcaBackend {
55 fn name(&self) -> &'static str {
56 "orca"
57 }
58 fn capabilities(&self) -> Capabilities {
59 Capabilities {
60 spawn: true,
61 attach: true,
62 probe: true,
63 close: true,
64 focus: true,
65 rename: true,
66 }
67 }
68 fn available(&self) -> Result<bool> {
69 Ok(self
70 .json(vec!["terminal".into(), "list".into(), "--json".into()])
71 .is_ok())
72 }
73 fn spawn(&self, spec: SpawnSpec) -> Result<SessionRef> {
74 let title = spec
75 .rename
76 .clone()
77 .unwrap_or_else(|| format!("onlyne:{}", spec.task_id));
78 let selector = self.selector_for();
79 let shell = spawn_command(&spec)?;
80 let mut args = vec!["terminal".into(), "create".into()];
81 if let Some(selector) = &selector {
82 args.push("--worktree".into());
83 args.push(selector.clone());
84 }
85 args.push("--title".into());
86 args.push(title.clone());
87 args.push("--command".into());
88 args.push(shell);
89 if spec.focus.unwrap_or(false) {
90 args.push("--focus".into());
91 }
92 args.push("--json".into());
93 let value = self.json(args)?;
94 let keys = TabKeys::read(&value);
95 if keys.handle.is_none() {
96 return Err(self.roll_back_create(&keys, &title, &value));
100 }
101 let mut backend_ref = keys.to_ref();
102 if let Some(selector) = &selector {
103 backend_ref["selector"] = Value::String(selector.clone());
104 }
105 let session = SessionRef {
106 task_id: spec.task_id.clone(),
107 backend: self.name().into(),
108 backend_ref,
109 generation: 1,
110 };
111 if let Some(pane_key) = keys.pane_key.clone() {
112 self.tabs.lock().insert(
113 pane_key,
114 TabMemo {
115 root: absolute(&spec.cwd),
116 role: spec.env.get("ONLYNE_ROLE").cloned().unwrap_or_default(),
117 session_id: spec
118 .env
119 .get("ONLYNE_SESSION_ID")
120 .cloned()
121 .unwrap_or_default(),
122 title,
123 },
124 );
125 }
126 self.note(&session, "spawned");
127 Ok(session)
128 }
129 fn attach(&self, session: &SessionRef) -> Result<SessionRef> {
130 self.current(session).map(|(refreshed, _)| refreshed)
131 }
132 fn probe(&self, session: &SessionRef) -> Result<ResourceProbe> {
133 let (session, value) = match self.current(session) {
134 Ok(found) => found,
135 Err(error) if is_gone(&error) => {
139 return Ok(ResourceProbe {
140 alive: false,
141 attached: false,
142 detail: Some(serde_json::json!({"error": error.to_string()})),
143 });
144 }
145 Err(error) => return Err(error),
146 };
147 let row = value.pointer("/terminal").unwrap_or(&value);
148 let status = row.get("status").and_then(Value::as_str);
149 let exit_cause = row.pointer("/exitCause/kind").and_then(Value::as_str);
150 let connected = row.get("connected").and_then(Value::as_bool);
151 let writable = row.get("writable").and_then(Value::as_bool);
152 let ended = exit_cause.is_some() || status.is_some_and(|s| DEAD_STATUS.contains(&s));
158 let alive = !ended
159 && match status {
160 Some(_) => true,
161 None => connected.unwrap_or(false) && writable.unwrap_or(false),
162 };
163 Ok(ResourceProbe {
164 alive,
165 attached: connected.unwrap_or(alive),
166 detail: Some(serde_json::json!({
167 "handle": ref_str(&session, "handle"),
168 "pane_key": ref_str(&session, "pane_key"),
169 "status": status,
170 "exit_cause": exit_cause,
171 "last_output_at": row.get("lastOutputAt"),
172 })),
173 })
174 }
175 fn close(&self, session: &SessionRef, reason: CloseReason, force: bool) -> Result<()> {
176 tracing::debug!(task = %session.task_id, ?reason, force, "closing orca terminal");
179 let current = match self.current(session) {
180 Ok((current, _)) => current,
181 Err(error) if is_gone(&error) => {
184 self.note(session, "closed");
185 return Ok(());
186 }
187 Err(error) => return Err(error),
188 };
189 self.close_handle(&Self::ref_handle(¤t)?)?;
190 self.note(¤t, "closed");
191 if let Some(pane_key) = ref_str(¤t, "pane_key") {
192 self.tabs.lock().remove(&pane_key);
193 }
194 Ok(())
195 }
196 fn rename(&self, session: &SessionRef, title: &str) -> Result<()> {
197 self.json(vec![
198 "terminal".into(),
199 "rename".into(),
200 "--terminal".into(),
201 Self::ref_handle(session)?,
202 "--title".into(),
203 title.into(),
204 "--json".into(),
205 ])
206 .map(|_| ())
207 }
208 fn focus(&self, session: &SessionRef) -> Result<()> {
209 self.json(vec![
210 "terminal".into(),
211 "switch".into(),
212 "--terminal".into(),
213 Self::ref_handle(session)?,
214 "--json".into(),
215 ])
216 .map(|_| ())
217 }
218}