Skip to main content

opensession_core/
config.rs

1//! Shared daemon/TUI configuration types.
2//!
3//! Both `opensession-daemon` and `opensession-tui` read/write `daemon.toml`
4//! using these types. Daemon-specific logic (watch-path resolution, project
5//! config merging) lives in the daemon crate; TUI-specific logic (settings
6//! layout, field editing) lives in the TUI crate.
7
8use serde::{Deserialize, Serialize};
9
10/// Top-level daemon configuration (persisted as `daemon.toml`).
11#[derive(Debug, Clone, Serialize, Deserialize, Default)]
12pub struct DaemonConfig {
13    #[serde(default)]
14    pub daemon: DaemonSettings,
15    #[serde(default)]
16    pub server: ServerSettings,
17    #[serde(default)]
18    pub identity: IdentitySettings,
19    #[serde(default)]
20    pub privacy: PrivacySettings,
21    #[serde(default)]
22    pub watchers: WatcherSettings,
23    #[serde(default)]
24    pub git_storage: GitStorageSettings,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct DaemonSettings {
29    #[serde(default = "default_false")]
30    pub auto_publish: bool,
31    #[serde(default = "default_debounce")]
32    pub debounce_secs: u64,
33    #[serde(default = "default_publish_on")]
34    pub publish_on: PublishMode,
35    #[serde(default = "default_max_retries")]
36    pub max_retries: u32,
37    #[serde(default = "default_health_check_interval")]
38    pub health_check_interval_secs: u64,
39    #[serde(default = "default_realtime_debounce_ms")]
40    pub realtime_debounce_ms: u64,
41    /// Enable realtime file preview refresh in TUI session detail.
42    #[serde(default = "default_detail_realtime_preview_enabled")]
43    pub detail_realtime_preview_enabled: bool,
44    /// Neglect-live tool rules (stream-write/PostToolUse): matching sessions skip detail live and summary.
45    #[serde(default)]
46    pub stream_write: Vec<String>,
47    /// Enable timeline summaries in TUI detail view.
48    #[serde(default = "default_summary_enabled")]
49    pub summary_enabled: bool,
50    /// Summary provider override:
51    /// auto | anthropic | openai | openai-compatible | gemini | cli:auto | cli:codex | cli:claude | cli:cursor | cli:gemini
52    #[serde(default)]
53    pub summary_provider: Option<String>,
54    /// Optional model override for summary calls (API and CLI `--model`).
55    #[serde(default)]
56    pub summary_model: Option<String>,
57    /// Summary detail mode: normal | minimal.
58    #[serde(default = "default_summary_content_mode")]
59    pub summary_content_mode: String,
60    /// Persist timeline summaries to disk and reuse by context hash.
61    #[serde(default = "default_summary_disk_cache_enabled")]
62    pub summary_disk_cache_enabled: bool,
63    /// Full OpenAI-compatible endpoint URL override.
64    #[serde(default)]
65    pub summary_openai_compat_endpoint: Option<String>,
66    /// OpenAI-compatible base URL (used when endpoint is not set).
67    #[serde(default)]
68    pub summary_openai_compat_base: Option<String>,
69    /// OpenAI-compatible path (default: /chat/completions).
70    #[serde(default)]
71    pub summary_openai_compat_path: Option<String>,
72    /// OpenAI-compatible payload style: chat | responses.
73    #[serde(default)]
74    pub summary_openai_compat_style: Option<String>,
75    /// Optional OpenAI-compatible API key.
76    #[serde(default)]
77    pub summary_openai_compat_key: Option<String>,
78    /// Optional API key header name (default: Authorization: Bearer).
79    #[serde(default)]
80    pub summary_openai_compat_key_header: Option<String>,
81    /// Number of events per summary window. `0` means auto(turn-aware).
82    #[serde(default = "default_summary_event_window")]
83    pub summary_event_window: u32,
84    /// One-shot migration guard for legacy summary window defaults.
85    #[serde(default = "default_false")]
86    pub summary_window_migrated_v2: bool,
87    /// Debounce for summary requests / realtime checks, in milliseconds.
88    #[serde(default = "default_summary_debounce_ms")]
89    pub summary_debounce_ms: u64,
90    /// Max concurrent in-flight timeline summary jobs.
91    #[serde(default = "default_summary_max_inflight")]
92    pub summary_max_inflight: u32,
93}
94
95impl Default for DaemonSettings {
96    fn default() -> Self {
97        Self {
98            auto_publish: false,
99            debounce_secs: 5,
100            publish_on: PublishMode::Manual,
101            max_retries: 3,
102            health_check_interval_secs: 300,
103            realtime_debounce_ms: 500,
104            detail_realtime_preview_enabled: false,
105            stream_write: Vec::new(),
106            summary_enabled: true,
107            summary_provider: None,
108            summary_model: None,
109            summary_content_mode: "normal".to_string(),
110            summary_disk_cache_enabled: true,
111            summary_openai_compat_endpoint: None,
112            summary_openai_compat_base: None,
113            summary_openai_compat_path: None,
114            summary_openai_compat_style: None,
115            summary_openai_compat_key: None,
116            summary_openai_compat_key_header: None,
117            summary_event_window: 0,
118            summary_window_migrated_v2: false,
119            summary_debounce_ms: 1200,
120            summary_max_inflight: 1,
121        }
122    }
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
126#[serde(rename_all = "snake_case")]
127pub enum PublishMode {
128    SessionEnd,
129    Realtime,
130    Manual,
131}
132
133impl PublishMode {
134    pub fn cycle(&self) -> Self {
135        match self {
136            Self::SessionEnd => Self::Realtime,
137            Self::Realtime => Self::Manual,
138            Self::Manual => Self::SessionEnd,
139        }
140    }
141
142    pub fn display(&self) -> &'static str {
143        match self {
144            Self::SessionEnd => "Session End",
145            Self::Realtime => "Realtime",
146            Self::Manual => "Manual",
147        }
148    }
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct ServerSettings {
153    #[serde(default = "default_server_url")]
154    pub url: String,
155    #[serde(default)]
156    pub api_key: String,
157}
158
159impl Default for ServerSettings {
160    fn default() -> Self {
161        Self {
162            url: default_server_url(),
163            api_key: String::new(),
164        }
165    }
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct IdentitySettings {
170    #[serde(default = "default_nickname")]
171    pub nickname: String,
172    /// Team ID to upload sessions to
173    #[serde(default)]
174    pub team_id: String,
175}
176
177impl Default for IdentitySettings {
178    fn default() -> Self {
179        Self {
180            nickname: default_nickname(),
181            team_id: String::new(),
182        }
183    }
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct PrivacySettings {
188    #[serde(default = "default_true")]
189    pub strip_paths: bool,
190    #[serde(default = "default_true")]
191    pub strip_env_vars: bool,
192    #[serde(default = "default_exclude_patterns")]
193    pub exclude_patterns: Vec<String>,
194    #[serde(default)]
195    pub exclude_tools: Vec<String>,
196}
197
198impl Default for PrivacySettings {
199    fn default() -> Self {
200        Self {
201            strip_paths: true,
202            strip_env_vars: true,
203            exclude_patterns: default_exclude_patterns(),
204            exclude_tools: Vec::new(),
205        }
206    }
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct WatcherSettings {
211    #[serde(default = "default_true")]
212    pub claude_code: bool,
213    #[serde(default = "default_true")]
214    pub opencode: bool,
215    #[serde(default)]
216    pub cursor: bool,
217    #[serde(default)]
218    pub custom_paths: Vec<String>,
219}
220
221impl Default for WatcherSettings {
222    fn default() -> Self {
223        Self {
224            claude_code: true,
225            opencode: true,
226            cursor: false,
227            custom_paths: Vec::new(),
228        }
229    }
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
233pub struct GitStorageSettings {
234    #[serde(default)]
235    pub method: GitStorageMethod,
236    #[serde(default)]
237    pub token: String,
238}
239
240impl Default for GitStorageSettings {
241    fn default() -> Self {
242        Self {
243            method: GitStorageMethod::None,
244            token: String::new(),
245        }
246    }
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
250#[serde(rename_all = "snake_case")]
251pub enum GitStorageMethod {
252    PlatformApi,
253    /// Store sessions as git objects on an orphan branch (no external API needed).
254    Native,
255    #[default]
256    #[serde(other)]
257    None,
258}
259
260// ── Serde default functions ─────────────────────────────────────────────
261
262fn default_true() -> bool {
263    true
264}
265fn default_false() -> bool {
266    false
267}
268fn default_debounce() -> u64 {
269    5
270}
271fn default_max_retries() -> u32 {
272    3
273}
274fn default_health_check_interval() -> u64 {
275    300
276}
277fn default_realtime_debounce_ms() -> u64 {
278    500
279}
280fn default_detail_realtime_preview_enabled() -> bool {
281    false
282}
283fn default_summary_enabled() -> bool {
284    true
285}
286fn default_summary_content_mode() -> String {
287    "normal".to_string()
288}
289fn default_summary_disk_cache_enabled() -> bool {
290    true
291}
292fn default_summary_event_window() -> u32 {
293    0
294}
295fn default_summary_debounce_ms() -> u64 {
296    1200
297}
298fn default_summary_max_inflight() -> u32 {
299    1
300}
301fn default_publish_on() -> PublishMode {
302    PublishMode::Manual
303}
304fn default_server_url() -> String {
305    "https://opensession.io".to_string()
306}
307fn default_nickname() -> String {
308    "user".to_string()
309}
310fn default_exclude_patterns() -> Vec<String> {
311    vec![
312        "*.env".to_string(),
313        "*secret*".to_string(),
314        "*credential*".to_string(),
315    ]
316}