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