Skip to main content

zeph_config/
ui.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use serde::{Deserialize, Serialize};
5
6use crate::defaults::default_true;
7
8fn default_acp_agent_name() -> String {
9    "zeph".to_owned()
10}
11
12fn default_acp_agent_version() -> String {
13    env!("CARGO_PKG_VERSION").to_owned()
14}
15
16fn default_acp_max_sessions() -> usize {
17    4
18}
19
20fn default_acp_session_idle_timeout_secs() -> u64 {
21    1800
22}
23
24fn default_acp_broadcast_capacity() -> usize {
25    256
26}
27
28fn default_acp_transport() -> AcpTransport {
29    AcpTransport::Stdio
30}
31
32fn default_acp_http_bind() -> String {
33    "127.0.0.1:9800".to_owned()
34}
35
36fn default_acp_discovery_enabled() -> bool {
37    true
38}
39
40fn default_acp_lsp_max_diagnostics_per_file() -> usize {
41    20
42}
43
44fn default_acp_lsp_max_diagnostic_files() -> usize {
45    5
46}
47
48fn default_acp_lsp_max_references() -> usize {
49    100
50}
51
52fn default_acp_lsp_max_workspace_symbols() -> usize {
53    50
54}
55
56fn default_acp_lsp_request_timeout_secs() -> u64 {
57    10
58}
59fn default_lsp_mcp_server_id() -> String {
60    "mcpls".into()
61}
62fn default_lsp_token_budget() -> usize {
63    2000
64}
65fn default_lsp_max_per_file() -> usize {
66    20
67}
68fn default_lsp_max_symbols() -> usize {
69    5
70}
71fn default_lsp_call_timeout_secs() -> u64 {
72    5
73}
74
75#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize)]
76pub struct TuiConfig {
77    #[serde(default)]
78    pub show_source_labels: bool,
79}
80
81/// ACP server transport mode.
82#[derive(Debug, Clone, Default, Deserialize, Serialize)]
83#[serde(rename_all = "lowercase")]
84pub enum AcpTransport {
85    /// JSON-RPC over stdin/stdout (default, IDE embedding).
86    #[default]
87    Stdio,
88    /// JSON-RPC over HTTP+SSE and WebSocket.
89    Http,
90    /// Both stdio and HTTP transports active simultaneously.
91    Both,
92}
93
94#[derive(Clone, Deserialize, Serialize)]
95pub struct AcpConfig {
96    #[serde(default)]
97    pub enabled: bool,
98    #[serde(default = "default_acp_agent_name")]
99    pub agent_name: String,
100    #[serde(default = "default_acp_agent_version")]
101    pub agent_version: String,
102    #[serde(default = "default_acp_max_sessions")]
103    pub max_sessions: usize,
104    #[serde(default = "default_acp_session_idle_timeout_secs")]
105    pub session_idle_timeout_secs: u64,
106    #[serde(default = "default_acp_broadcast_capacity")]
107    pub broadcast_capacity: usize,
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub permission_file: Option<std::path::PathBuf>,
110    /// List of `{provider}:{model}` identifiers advertised to the IDE for model switching.
111    /// Example: `["claude:claude-sonnet-4-5", "ollama:llama3"]`
112    #[serde(default)]
113    pub available_models: Vec<String>,
114    /// Transport mode: "stdio" (default), "http", or "both".
115    #[serde(default = "default_acp_transport")]
116    pub transport: AcpTransport,
117    /// Bind address for the HTTP transport.
118    #[serde(default = "default_acp_http_bind")]
119    pub http_bind: String,
120    /// Bearer token for HTTP and WebSocket transport authentication.
121    /// When set, all /acp and /acp/ws requests must include `Authorization: Bearer <token>`.
122    /// Omit for local unauthenticated access. TLS termination is assumed to be handled by a
123    /// reverse proxy.
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub auth_token: Option<String>,
126    /// Whether to serve the /.well-known/acp.json agent discovery manifest.
127    /// Only effective when transport is "http" or "both". Default: true.
128    #[serde(default = "default_acp_discovery_enabled")]
129    pub discovery_enabled: bool,
130    /// LSP extension configuration (`[acp.lsp]`).
131    #[serde(default)]
132    pub lsp: AcpLspConfig,
133}
134
135impl Default for AcpConfig {
136    fn default() -> Self {
137        Self {
138            enabled: false,
139            agent_name: default_acp_agent_name(),
140            agent_version: default_acp_agent_version(),
141            max_sessions: default_acp_max_sessions(),
142            session_idle_timeout_secs: default_acp_session_idle_timeout_secs(),
143            broadcast_capacity: default_acp_broadcast_capacity(),
144            permission_file: None,
145            available_models: Vec::new(),
146            transport: default_acp_transport(),
147            http_bind: default_acp_http_bind(),
148            auth_token: None,
149            discovery_enabled: default_acp_discovery_enabled(),
150            lsp: AcpLspConfig::default(),
151        }
152    }
153}
154
155impl std::fmt::Debug for AcpConfig {
156    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157        f.debug_struct("AcpConfig")
158            .field("enabled", &self.enabled)
159            .field("agent_name", &self.agent_name)
160            .field("agent_version", &self.agent_version)
161            .field("max_sessions", &self.max_sessions)
162            .field("session_idle_timeout_secs", &self.session_idle_timeout_secs)
163            .field("broadcast_capacity", &self.broadcast_capacity)
164            .field("permission_file", &self.permission_file)
165            .field("available_models", &self.available_models)
166            .field("transport", &self.transport)
167            .field("http_bind", &self.http_bind)
168            .field(
169                "auth_token",
170                &self.auth_token.as_ref().map(|_| "[REDACTED]"),
171            )
172            .field("discovery_enabled", &self.discovery_enabled)
173            .field("lsp", &self.lsp)
174            .finish()
175    }
176}
177
178/// Configuration for the ACP LSP extension.
179///
180/// Controls LSP code intelligence features when connected to an IDE that advertises
181/// `meta["lsp"]` capability during ACP `initialize`.
182#[derive(Debug, Clone, Deserialize, Serialize)]
183pub struct AcpLspConfig {
184    /// Enable LSP extension when the IDE supports it. Default: `true`.
185    #[serde(default = "default_true")]
186    pub enabled: bool,
187    /// Automatically fetch diagnostics when `lsp/didSave` notification is received.
188    #[serde(default = "default_true")]
189    pub auto_diagnostics_on_save: bool,
190    /// Maximum diagnostics to accept per file. Default: 20.
191    #[serde(default = "default_acp_lsp_max_diagnostics_per_file")]
192    pub max_diagnostics_per_file: usize,
193    /// Maximum files in `DiagnosticsCache` (LRU eviction). Default: 5.
194    #[serde(default = "default_acp_lsp_max_diagnostic_files")]
195    pub max_diagnostic_files: usize,
196    /// Maximum reference locations returned. Default: 100.
197    #[serde(default = "default_acp_lsp_max_references")]
198    pub max_references: usize,
199    /// Maximum workspace symbol search results. Default: 50.
200    #[serde(default = "default_acp_lsp_max_workspace_symbols")]
201    pub max_workspace_symbols: usize,
202    /// Timeout in seconds for LSP `ext_method` calls. Default: 10.
203    #[serde(default = "default_acp_lsp_request_timeout_secs")]
204    pub request_timeout_secs: u64,
205}
206
207impl Default for AcpLspConfig {
208    fn default() -> Self {
209        Self {
210            enabled: true,
211            auto_diagnostics_on_save: true,
212            max_diagnostics_per_file: default_acp_lsp_max_diagnostics_per_file(),
213            max_diagnostic_files: default_acp_lsp_max_diagnostic_files(),
214            max_references: default_acp_lsp_max_references(),
215            max_workspace_symbols: default_acp_lsp_max_workspace_symbols(),
216            request_timeout_secs: default_acp_lsp_request_timeout_secs(),
217        }
218    }
219}
220
221// ── LSP context injection ─────────────────────────────────────────────────────
222
223/// Minimum diagnostic severity to include in LSP context injection.
224#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
225#[serde(rename_all = "lowercase")]
226pub enum DiagnosticSeverity {
227    #[default]
228    Error,
229    Warning,
230    Info,
231    Hint,
232}
233
234/// Configuration for the diagnostics-on-save hook (`[agent.lsp.diagnostics]`).
235///
236/// Flood control relies on `token_budget` in [`LspConfig`], not a per-file count.
237#[derive(Debug, Clone, Deserialize, Serialize)]
238#[serde(default)]
239pub struct DiagnosticsConfig {
240    /// Enable automatic diagnostics fetching after the `write` tool.
241    pub enabled: bool,
242    /// Maximum diagnostics entries per file.
243    #[serde(default = "default_lsp_max_per_file")]
244    pub max_per_file: usize,
245    /// Minimum severity to include.
246    #[serde(default)]
247    pub min_severity: DiagnosticSeverity,
248}
249impl Default for DiagnosticsConfig {
250    fn default() -> Self {
251        Self {
252            enabled: true,
253            max_per_file: default_lsp_max_per_file(),
254            min_severity: DiagnosticSeverity::default(),
255        }
256    }
257}
258
259/// Configuration for the hover-on-read hook (`[agent.lsp.hover]`).
260#[derive(Debug, Clone, Deserialize, Serialize)]
261#[serde(default)]
262pub struct HoverConfig {
263    /// Enable hover info pre-fetch after the `read` tool. Disabled by default.
264    pub enabled: bool,
265    /// Maximum hover entries per file (Rust-only for MVP).
266    #[serde(default = "default_lsp_max_symbols")]
267    pub max_symbols: usize,
268}
269impl Default for HoverConfig {
270    fn default() -> Self {
271        Self {
272            enabled: false,
273            max_symbols: default_lsp_max_symbols(),
274        }
275    }
276}
277
278/// Top-level LSP context injection configuration (`[agent.lsp]` TOML section).
279#[derive(Debug, Clone, Deserialize, Serialize)]
280#[serde(default)]
281pub struct LspConfig {
282    /// Enable LSP context injection hooks.
283    pub enabled: bool,
284    /// MCP server ID to route LSP calls through (default: "mcpls").
285    #[serde(default = "default_lsp_mcp_server_id")]
286    pub mcp_server_id: String,
287    /// Maximum tokens to spend on injected LSP context per turn.
288    #[serde(default = "default_lsp_token_budget")]
289    pub token_budget: usize,
290    /// Timeout in seconds for each MCP LSP call.
291    #[serde(default = "default_lsp_call_timeout_secs")]
292    pub call_timeout_secs: u64,
293    /// Diagnostics-on-save hook configuration.
294    #[serde(default)]
295    pub diagnostics: DiagnosticsConfig,
296    /// Hover-on-read hook configuration.
297    #[serde(default)]
298    pub hover: HoverConfig,
299}
300impl Default for LspConfig {
301    fn default() -> Self {
302        Self {
303            enabled: false,
304            mcp_server_id: default_lsp_mcp_server_id(),
305            token_budget: default_lsp_token_budget(),
306            call_timeout_secs: default_lsp_call_timeout_secs(),
307            diagnostics: DiagnosticsConfig::default(),
308            hover: HoverConfig::default(),
309        }
310    }
311}