onlyne_client/backend/orca/
policy.rs1use super::resource::{TabKeys, ref_str};
5use super::session::OrcaBackend;
6use crate::backend::*;
7use chrono::{SecondsFormat, Utc};
8use serde::Serialize;
9use std::io::Write;
10use std::path::{Path, PathBuf};
11
12#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum WorktreePolicy {
20 Host,
24 Inherit,
26 Selector(String),
29}
30
31impl WorktreePolicy {
32 pub fn from_config(value: &str) -> Self {
36 match value.trim() {
37 "" | "auto" | "host" => Self::Host,
38 "inherit" => Self::Inherit,
39 selector => Self::Selector(selector.to_string()),
40 }
41 }
42}
43
44fn selector_path(selector: &str) -> Option<PathBuf> {
47 let path = selector
48 .strip_prefix("path:")
49 .or_else(|| selector.split_once("::").map(|(_, path)| path))?;
50 let path = PathBuf::from(path);
51 path.is_absolute().then_some(path)
52}
53
54pub(super) fn absolute(path: &Path) -> PathBuf {
57 if let Ok(canonical) = std::fs::canonicalize(path) {
58 return canonical;
59 }
60 if path.is_absolute() {
61 return path.to_path_buf();
62 }
63 std::env::current_dir()
64 .map(|cwd| cwd.join(path))
65 .unwrap_or_else(|_| path.to_path_buf())
66}
67
68#[derive(Debug, Clone)]
72pub(super) struct TabMemo {
73 pub(super) root: PathBuf,
74 pub(super) role: String,
75 pub(super) session_id: String,
76 pub(super) title: String,
77}
78
79#[derive(Debug, Clone, Serialize)]
87struct TabLine<'a> {
88 pane_key: &'a str,
89 handle: &'a str,
90 task_id: &'a str,
91 session_id: &'a str,
92 role: &'a str,
93 worktree_selector: &'a str,
94 title: &'a str,
95 state: &'a str,
96 updated_at: String,
97}
98
99fn append_tab_line(root: &Path, line: &TabLine<'_>) -> std::io::Result<()> {
101 let dir = root.join(".onlyne/cache");
102 std::fs::create_dir_all(&dir)?;
103 let mut text =
104 serde_json::to_string(line).map_err(|error| std::io::Error::other(error.to_string()))?;
105 text.push('\n');
106 std::fs::OpenOptions::new()
107 .create(true)
108 .append(true)
109 .open(dir.join("orca-tabs.jsonl"))?
110 .write_all(text.as_bytes())
111}
112
113pub(super) fn host_worktree_env() -> Option<String> {
116 std::env::var("ORCA_WORKTREE_ID")
117 .ok()
118 .map(|value| value.trim().to_string())
119 .filter(|value| !value.is_empty())
120}
121
122impl OrcaBackend {
123 pub(super) fn selector_for(&self) -> Option<String> {
130 match &self.policy {
131 WorktreePolicy::Host => self.host_worktree.clone(),
132 WorktreePolicy::Inherit => None,
133 WorktreePolicy::Selector(selector) => Some(selector.clone()),
134 }
135 }
136
137 pub(super) fn note(&self, session: &SessionRef, state: &str) {
142 let keys = TabKeys::from_ref(session);
143 let (Some(pane_key), Some(handle)) = (keys.pane_key.as_deref(), keys.handle.as_deref())
144 else {
145 tracing::warn!(
146 task = %session.task_id,
147 "orca terminal has no pane key or handle; the tab map stays unwritten"
148 );
149 return;
150 };
151 let memo = self.tabs.lock().get(pane_key).cloned();
152 let selector = ref_str(session, "selector");
153 let root = memo
154 .as_ref()
155 .map(|memo| memo.root.clone())
156 .or_else(|| selector.as_deref().and_then(selector_path));
157 let Some(root) = root else {
158 tracing::warn!(
159 task = %session.task_id,
160 pane_key,
161 "no workspace root for the orca tab map; the line is skipped"
162 );
163 return;
164 };
165 let line = TabLine {
166 pane_key,
167 handle,
168 task_id: &session.task_id,
169 session_id: memo
170 .as_ref()
171 .map(|memo| memo.session_id.as_str())
172 .unwrap_or_default(),
173 role: memo
174 .as_ref()
175 .map(|memo| memo.role.as_str())
176 .unwrap_or_default(),
177 worktree_selector: selector.as_deref().unwrap_or_default(),
178 title: memo
179 .as_ref()
180 .map(|memo| memo.title.as_str())
181 .unwrap_or_default(),
182 state,
183 updated_at: Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true),
184 };
185 if let Err(error) = append_tab_line(&root, &line) {
186 tracing::warn!(
187 error = %error,
188 task = %session.task_id,
189 "orca tab map line was not written"
190 );
191 }
192 }
193}