Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:27:48.790Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

debug_format.rs

535 lines · 19.1 KB · rust
1use anyhow::{Context as _, Result};
2use collections::FxHashMap;
3use gpui::SharedString;
4use log as _;
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7use std::net::IpAddr;
8use std::path::PathBuf;
9use util::{debug_panic, schemars::add_new_subschema};
10
11use crate::{TaskTemplate, adapter_schema::AdapterSchemas};
12
13/// Represents the host information of the debug adapter
14#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
15pub struct TcpArgumentsTemplate {
16    /// The port that the debug adapter is listening on
17    ///
18    /// Default: We will try to find an open port
19    pub port: Option<u16>,
20    /// The host that the debug adapter is listening too
21    ///
22    /// Default: 127.0.0.1
23    pub host: Option<IpAddr>,
24    /// The max amount of time in milliseconds to connect to a tcp DAP before returning an error
25    ///
26    /// Default: 2000ms
27    pub timeout: Option<u64>,
28}
29
30impl TcpArgumentsTemplate {
31    /// Get the host or fallback to the default host
32    pub fn host(&self) -> IpAddr {
33        self.host
34            .unwrap_or(IpAddr::V4(std::net::Ipv4Addr::LOCALHOST))
35    }
36
37    pub fn from_proto(proto: proto::TcpHost) -> Result<Self> {
38        Ok(Self {
39            port: proto.port.map(|p| p.try_into()).transpose()?,
40            host: proto.host.map(|h| h.parse()).transpose()?,
41            timeout: proto.timeout,
42        })
43    }
44
45    pub fn to_proto(&self) -> proto::TcpHost {
46        proto::TcpHost {
47            port: self.port.map(|p| p.into()),
48            host: self.host.map(|h| h.to_string()),
49            timeout: self.timeout,
50        }
51    }
52}
53
54/// Represents the attach request information of the debug adapter
55#[derive(Default, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
56pub struct AttachRequest {
57    /// The processId to attach to, if left empty we will show a process picker
58    pub process_id: Option<u32>,
59}
60
61impl<'de> Deserialize<'de> for AttachRequest {
62    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
63    where
64        D: serde::Deserializer<'de>,
65    {
66        #[derive(Deserialize)]
67        struct Helper {
68            process_id: Option<u32>,
69        }
70
71        let helper = Helper::deserialize(deserializer)?;
72
73        // Skip creating an AttachRequest if process_id is None
74        if helper.process_id.is_none() {
75            return Err(serde::de::Error::custom("process_id is required"));
76        }
77
78        Ok(AttachRequest {
79            process_id: helper.process_id,
80        })
81    }
82}
83
84/// Represents the launch request information of the debug adapter
85#[derive(Deserialize, Serialize, Default, PartialEq, Eq, JsonSchema, Clone, Debug)]
86pub struct LaunchRequest {
87    /// The program that you trying to debug
88    pub program: String,
89    /// The current working directory of your project
90    #[serde(default)]
91    pub cwd: Option<PathBuf>,
92    /// Arguments to pass to a debuggee
93    #[serde(default)]
94    pub args: Vec<String>,
95    #[serde(default)]
96    pub env: FxHashMap<String, String>,
97}
98
99impl LaunchRequest {
100    pub fn env_json(&self) -> serde_json::Value {
101        serde_json::Value::Object(
102            self.env
103                .iter()
104                .map(|(k, v)| (k.clone(), v.to_owned().into()))
105                .collect::<serde_json::Map<String, serde_json::Value>>(),
106        )
107    }
108}
109
110/// Represents the type that will determine which request to call on the debug adapter
111#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
112#[serde(rename_all = "lowercase", tag = "request")]
113pub enum DebugRequest {
114    /// Call the `launch` request on the debug adapter
115    Launch(LaunchRequest),
116    /// Call the `attach` request on the debug adapter
117    Attach(AttachRequest),
118}
119
120impl DebugRequest {
121    pub fn to_proto(&self) -> proto::DebugRequest {
122        match self {
123            DebugRequest::Launch(launch_request) => proto::DebugRequest {
124                request: Some(proto::debug_request::Request::DebugLaunchRequest(
125                    proto::DebugLaunchRequest {
126                        program: launch_request.program.clone(),
127                        cwd: launch_request
128                            .cwd
129                            .as_ref()
130                            .map(|cwd| cwd.to_string_lossy().into_owned()),
131                        args: launch_request.args.clone(),
132                        env: launch_request
133                            .env
134                            .iter()
135                            .map(|(k, v)| (k.clone(), v.clone()))
136                            .collect(),
137                    },
138                )),
139            },
140            DebugRequest::Attach(attach_request) => proto::DebugRequest {
141                request: Some(proto::debug_request::Request::DebugAttachRequest(
142                    proto::DebugAttachRequest {
143                        process_id: attach_request
144                            .process_id
145                            .expect("The process ID to be already filled out."),
146                    },
147                )),
148            },
149        }
150    }
151
152    pub fn from_proto(val: proto::DebugRequest) -> Result<DebugRequest> {
153        let request = val.request.context("Missing debug request")?;
154        match request {
155            proto::debug_request::Request::DebugLaunchRequest(proto::DebugLaunchRequest {
156                program,
157                cwd,
158                args,
159                env,
160            }) => Ok(DebugRequest::Launch(LaunchRequest {
161                program,
162                cwd: cwd.map(From::from),
163                args,
164                env: env.into_iter().collect(),
165            })),
166
167            proto::debug_request::Request::DebugAttachRequest(proto::DebugAttachRequest {
168                process_id,
169            }) => Ok(DebugRequest::Attach(AttachRequest {
170                process_id: Some(process_id),
171            })),
172        }
173    }
174}
175
176impl From<LaunchRequest> for DebugRequest {
177    fn from(launch_config: LaunchRequest) -> Self {
178        DebugRequest::Launch(launch_config)
179    }
180}
181
182impl From<AttachRequest> for DebugRequest {
183    fn from(attach_config: AttachRequest) -> Self {
184        DebugRequest::Attach(attach_config)
185    }
186}
187
188#[derive(Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
189#[serde(untagged)]
190pub enum BuildTaskDefinition {
191    ByName(SharedString),
192    Template {
193        #[serde(flatten)]
194        task_template: TaskTemplate,
195        #[serde(skip)]
196        locator_name: Option<SharedString>,
197    },
198}
199
200impl<'de> Deserialize<'de> for BuildTaskDefinition {
201    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
202    where
203        D: serde::Deserializer<'de>,
204    {
205        #[derive(Deserialize)]
206        struct TemplateHelper {
207            #[serde(default)]
208            label: Option<String>,
209            #[serde(flatten)]
210            rest: serde_json::Value,
211        }
212
213        let value = serde_json::Value::deserialize(deserializer)?;
214
215        if let Ok(name) = serde_json::from_value::<SharedString>(value.clone()) {
216            return Ok(BuildTaskDefinition::ByName(name));
217        }
218
219        let helper: TemplateHelper =
220            serde_json::from_value(value).map_err(serde::de::Error::custom)?;
221
222        let mut template_value = helper.rest;
223        if let serde_json::Value::Object(ref mut map) = template_value {
224            map.insert(
225                "label".to_string(),
226                serde_json::to_value(helper.label.unwrap_or_else(|| "debug-build".to_owned()))
227                    .map_err(serde::de::Error::custom)?,
228            );
229        }
230
231        let task_template: TaskTemplate =
232            serde_json::from_value(template_value).map_err(serde::de::Error::custom)?;
233
234        Ok(BuildTaskDefinition::Template {
235            task_template,
236            locator_name: None,
237        })
238    }
239}
240
241#[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Debug, JsonSchema)]
242pub enum Request {
243    Launch,
244    Attach,
245}
246
247/// This struct represent a user created debug task from the new process modal
248#[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Debug, JsonSchema)]
249#[serde(rename_all = "snake_case")]
250pub struct ZedDebugConfig {
251    /// Name of the debug task
252    pub label: SharedString,
253    /// The debug adapter to use
254    pub adapter: SharedString,
255    #[serde(flatten)]
256    pub request: DebugRequest,
257    /// Whether to tell the debug adapter to stop on entry
258    #[serde(default, skip_serializing_if = "Option::is_none")]
259    pub stop_on_entry: Option<bool>,
260}
261
262/// This struct represent a user created debug task
263#[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Debug, JsonSchema)]
264#[serde(rename_all = "snake_case")]
265pub struct DebugScenario {
266    pub adapter: SharedString,
267    /// Name of the debug task
268    pub label: SharedString,
269    /// A task to run prior to spawning the debuggee.
270    #[serde(default, skip_serializing_if = "Option::is_none")]
271    pub build: Option<BuildTaskDefinition>,
272    /// The main arguments to be sent to the debug adapter
273    #[serde(default, flatten)]
274    pub config: serde_json::Value,
275    /// Optional TCP connection information
276    ///
277    /// If provided, this will be used to connect to the debug adapter instead of
278    /// spawning a new process. This is useful for connecting to a debug adapter
279    /// that is already running or is started by another process.
280    #[serde(default, skip_serializing_if = "Option::is_none")]
281    pub tcp_connection: Option<TcpArgumentsTemplate>,
282}
283
284/// A group of Debug Tasks defined in a JSON file.
285#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
286#[serde(transparent)]
287pub struct DebugTaskFile(pub Vec<DebugScenario>);
288
289impl DebugTaskFile {
290    pub fn generate_json_schema(schemas: &AdapterSchemas) -> serde_json::Value {
291        let mut generator = schemars::generate::SchemaSettings::draft2019_09().into_generator();
292
293        let mut build_task_value = BuildTaskDefinition::json_schema(&mut generator).to_value();
294
295        if let Some(template_object) = build_task_value
296            .get_mut("anyOf")
297            .and_then(|array| array.as_array_mut())
298            .and_then(|array| array.get_mut(1))
299        {
300            if let Some(properties) = template_object
301                .get_mut("properties")
302                .and_then(|value| value.as_object_mut())
303                && properties.remove("label").is_none()
304            {
305                debug_panic!(
306                    "Generated TaskTemplate json schema did not have expected 'label' field. \
307                        Schema of 2nd alternative is: {template_object:?}"
308                );
309            }
310
311            if let Some(arr) = template_object
312                .get_mut("required")
313                .and_then(|array| array.as_array_mut())
314            {
315                arr.retain(|v| v.as_str() != Some("label"));
316            }
317        } else {
318            debug_panic!(
319                "Generated TaskTemplate json schema did not match expectations. \
320                Schema is: {build_task_value:?}"
321            );
322        }
323
324        let adapter_conditions = schemas
325            .0
326            .iter()
327            .map(|adapter_schema| {
328                let adapter_name = adapter_schema.adapter.to_string();
329                add_new_subschema(
330                    &mut generator,
331                    &format!("{adapter_name}DebugSettings"),
332                    serde_json::json!({
333                        "if": {
334                            "properties": {
335                                "adapter": { "const": adapter_name }
336                            }
337                        },
338                        "then": adapter_schema.schema
339                    }),
340                )
341            })
342            .collect::<Vec<_>>();
343
344        let build_task_definition_ref = add_new_subschema(
345            &mut generator,
346            BuildTaskDefinition::schema_name().as_ref(),
347            build_task_value,
348        );
349
350        let meta_schema = generator
351            .settings()
352            .meta_schema
353            .as_ref()
354            .expect("meta_schema should be present in schemars settings")
355            .to_string();
356
357        serde_json::json!({
358            "$schema": meta_schema,
359            "title": "Debug Configurations",
360            "description": "Configuration for debug scenarios",
361            "allowTrailingCommas": true,
362            "type": "array",
363            "items": {
364                "type": "object",
365                "required": ["adapter", "label"],
366                // TODO: Uncommenting this will cause json-language-server to provide warnings for
367                // unrecognized properties. It should be enabled if/when there's an adapter JSON
368                // schema that's comprehensive. In order to not get warnings for the other schemas,
369                // `additionalProperties` or `unevaluatedProperties` (to handle "allOf" etc style
370                // schema combinations) could be set to `true` for that schema.
371                //
372                // "unevaluatedProperties": false,
373                "properties": {
374                    "adapter": {
375                        "type": "string",
376                        "description": "The name of the debug adapter"
377                    },
378                    "label": {
379                        "type": "string",
380                        "description": "The name of the debug configuration"
381                    },
382                    "build": build_task_definition_ref,
383                    "tcp_connection": {
384                        "type": "object",
385                        "description": "Optional TCP connection information for connecting to an already running debug adapter",
386                        "properties": {
387                            "port": {
388                                "type": "integer",
389                                "description": "The port that the debug adapter is listening on (default: auto-find open port)"
390                            },
391                            "host": {
392                                "type": "string",
393                                "description": "The host that the debug adapter is listening to, as an IPv4 or IPv6 address (default: 127.0.0.1)"
394                            },
395                            "timeout": {
396                                "type": "integer",
397                                "description": "The max amount of time in milliseconds to connect to a tcp DAP before returning an error (default: 2000ms)"
398                            }
399                        }
400                    }
401                },
402                "allOf": adapter_conditions
403            },
404            "$defs": generator.take_definitions(true),
405        })
406    }
407}
408
409#[cfg(test)]
410mod tests {
411    use crate::DebugScenario;
412    use serde_json::json;
413
414    #[test]
415    fn test_just_build_args() {
416        let json = r#"{
417            "label": "Build & debug rust",
418            "adapter": "CodeLLDB",
419            "build": {
420                "command": "rust",
421                "args": ["build"]
422            }
423        }"#;
424
425        let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
426        assert!(deserialized.build.is_some());
427        match deserialized.build.as_ref().unwrap() {
428            crate::BuildTaskDefinition::Template { task_template, .. } => {
429                assert_eq!("debug-build", task_template.label);
430                assert_eq!("rust", task_template.command);
431                assert_eq!(vec!["build"], task_template.args);
432            }
433            _ => panic!("Expected Template variant"),
434        }
435        assert_eq!(json!({}), deserialized.config);
436        assert_eq!("CodeLLDB", deserialized.adapter.as_ref());
437        assert_eq!("Build & debug rust", deserialized.label.as_ref());
438    }
439
440    #[test]
441    fn test_empty_scenario_has_none_request() {
442        let json = r#"{
443            "label": "Build & debug rust",
444            "build": "rust",
445            "adapter": "CodeLLDB"
446        }"#;
447
448        let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
449
450        assert_eq!(json!({}), deserialized.config);
451        assert_eq!("CodeLLDB", deserialized.adapter.as_ref());
452        assert_eq!("Build & debug rust", deserialized.label.as_ref());
453    }
454
455    #[test]
456    fn test_launch_scenario_deserialization() {
457        let json = r#"{
458            "label": "Launch program",
459            "adapter": "CodeLLDB",
460            "request": "launch",
461            "program": "target/debug/myapp",
462            "args": ["--test"]
463        }"#;
464
465        let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
466
467        assert_eq!(
468            json!({ "request": "launch", "program": "target/debug/myapp", "args": ["--test"] }),
469            deserialized.config
470        );
471        assert_eq!("CodeLLDB", deserialized.adapter.as_ref());
472        assert_eq!("Launch program", deserialized.label.as_ref());
473    }
474
475    #[test]
476    fn test_attach_scenario_deserialization() {
477        let json = r#"{
478            "label": "Attach to process",
479            "adapter": "CodeLLDB",
480            "process_id": 1234,
481            "request": "attach"
482        }"#;
483
484        let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
485
486        assert_eq!(
487            json!({ "request": "attach", "process_id": 1234 }),
488            deserialized.config
489        );
490        assert_eq!("CodeLLDB", deserialized.adapter.as_ref());
491        assert_eq!("Attach to process", deserialized.label.as_ref());
492    }
493
494    #[test]
495    fn test_build_task_definition_without_label() {
496        use crate::BuildTaskDefinition;
497
498        let json = r#""my_build_task""#;
499        let deserialized: BuildTaskDefinition = serde_json::from_str(json).unwrap();
500        match deserialized {
501            BuildTaskDefinition::ByName(name) => assert_eq!("my_build_task", name.as_ref()),
502            _ => panic!("Expected ByName variant"),
503        }
504
505        let json = r#"{
506            "command": "cargo",
507            "args": ["build", "--release"]
508        }"#;
509        let deserialized: BuildTaskDefinition = serde_json::from_str(json).unwrap();
510        match deserialized {
511            BuildTaskDefinition::Template { task_template, .. } => {
512                assert_eq!("debug-build", task_template.label);
513                assert_eq!("cargo", task_template.command);
514                assert_eq!(vec!["build", "--release"], task_template.args);
515            }
516            _ => panic!("Expected Template variant"),
517        }
518
519        let json = r#"{
520            "label": "Build Release",
521            "command": "cargo",
522            "args": ["build", "--release"]
523        }"#;
524        let deserialized: BuildTaskDefinition = serde_json::from_str(json).unwrap();
525        match deserialized {
526            BuildTaskDefinition::Template { task_template, .. } => {
527                assert_eq!("Build Release", task_template.label);
528                assert_eq!("cargo", task_template.command);
529                assert_eq!(vec!["build", "--release"], task_template.args);
530            }
531            _ => panic!("Expected Template variant"),
532        }
533    }
534}
535
Served at tenant.openagents/omega Member data and write actions are omitted.