use super::edit_file_tool::EditFileTool;use super::restore_file_from_disk_tool::RestoreFileFromDiskTool;use super::save_file_tool::SaveFileTool;use super::tool_edit_parser::{ToolEditEvent, ToolEditParser};use crate::ToolInputPayload;use crate::{ AgentTool, Thread, ToolCallEventStream, ToolInput, edit_agent::{ reindent::{Reindenter, compute_indent_delta}, streaming_fuzzy_matcher::StreamingFuzzyMatcher, },};use acp_thread::Diff;use action_log::ActionLog;use agent_client_protocol::{self as acp, ToolCallLocation, ToolCallUpdateFields};use anyhow::Result;use collections::HashSet;use futures::FutureExt as _;use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity};use language::language_settings::{self, FormatOnSave};use language::{Buffer, LanguageRegistry};use language_model::LanguageModelToolResultContent;use project::lsp_store::{FormatTrigger, LspFormatTarget};use project::{AgentLocation, Project, ProjectPath};use schemars::JsonSchema;use serde::{ Deserialize, Deserializer, Serialize, de::{DeserializeOwned, Error as _},};use std::ops::Range;use std::path::PathBuf;use std::sync::Arc;use streaming_diff::{CharOperation, StreamingDiff};use text::ToOffset;use ui::SharedString;use util::rel_path::RelPath;use util::{Deferred, ResultExt}; const DEFAULT_UI_TEXT: &str = "Editing file"; /// This is a tool for creating a new file or editing an existing file. For moving or renaming files, you should generally use the `move_path` tool instead.////// Before using this tool:////// 1. Use the `read_file` tool to understand the file's contents and context////// 2. Verify the directory path is correct (only applicable when creating new files):/// - Use the `list_directory` tool to verify the parent directory exists and is the correct location#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]pub struct StreamingEditFileToolInput { /// A one-line, user-friendly markdown description of the edit. This will be shown in the UI. /// /// Be terse, but also descriptive in what you want to achieve with this edit. Avoid generic instructions. /// /// NEVER mention the file path in this description. /// /// <example>Fix API endpoint URLs</example> /// <example>Update copyright year in `page_footer`</example> /// /// Make sure to include this field before all the others in the input object so that we can display it immediately. pub display_description: String, /// The full path of the file to create or modify in the project. /// /// WARNING: When specifying which file path need changing, you MUST start each path with one of the project's root directories. /// /// The following examples assume we have two root directories in the project: /// - /a/b/backend /// - /c/d/frontend /// /// <example> /// `backend/src/main.rs` /// /// Notice how the file path starts with `backend`. Without that, the path would be ambiguous and the call would fail! /// </example> /// /// <example> /// `frontend/db.js` /// </example> pub path: PathBuf, /// The mode of operation on the file. Possible values: /// - 'write': Replace the entire contents of the file. If the file doesn't exist, it will be created. Requires 'content' field. /// - 'edit': Make granular edits to an existing file. Requires 'edits' field. /// /// When a file already exists or you just created it, prefer editing it as opposed to recreating it from scratch. pub mode: StreamingEditFileMode, /// The complete content for the new file (required for 'write' mode). /// This field should contain the entire file content. #[serde(default, skip_serializing_if = "Option::is_none")] pub content: Option<String>, /// List of edit operations to apply sequentially (required for 'edit' mode). /// Each edit finds `old_text` in the file and replaces it with `new_text`. #[serde( default, skip_serializing_if = "Option::is_none", deserialize_with = "deserialize_optional_vec_or_json_string" )] pub edits: Option<Vec<Edit>>,} #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]#[serde(rename_all = "snake_case")]pub enum StreamingEditFileMode { /// Overwrite the file with new content (replacing any existing content). /// If the file does not exist, it will be created. Write, /// Make granular edits to an existing file Edit,} /// A single edit operation that replaces old text with new text/// Properly escape all text fields as valid JSON strings./// Remember to escape special characters like newlines (`\n`) and quotes (`"`) in JSON strings.#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]pub struct Edit { /// The exact text to find in the file. This will be matched using fuzzy matching /// to handle minor differences in whitespace or formatting. /// /// Be minimal with replacements: /// - For unique lines, include only those lines /// - For non-unique lines, include enough context to identify them pub old_text: String, /// The text to replace it with pub new_text: String,} #[derive(Clone, Default, Debug, Deserialize)]struct StreamingEditFileToolPartialInput { #[serde(default)] display_description: Option<String>, #[serde(default)] path: Option<String>, #[serde(default)] mode: Option<StreamingEditFileMode>, #[serde(default)] content: Option<String>, #[serde(default, deserialize_with = "deserialize_optional_vec_or_json_string")] edits: Option<Vec<PartialEdit>>,} #[derive(Clone, Default, Debug, Deserialize)]pub struct PartialEdit { #[serde(default)] pub old_text: Option<String>, #[serde(default)] pub new_text: Option<String>,} /// Sometimes the model responds with a stringified JSON array of edits (`"[...]"`) instead of a regular array (`[...]`)fn deserialize_optional_vec_or_json_string<'de, T, D>( deserializer: D,) -> Result<Option<Vec<T>>, D::Error>where T: DeserializeOwned, D: Deserializer<'de>,{ #[derive(Deserialize)] #[serde(untagged)] enum VecOrJsonString<T> { Vec(Vec<T>), String(String), } let value = Option::<VecOrJsonString<T>>::deserialize(deserializer)?; match value { None => Ok(None), Some(VecOrJsonString::Vec(items)) => Ok(Some(items)), Some(VecOrJsonString::String(string)) => serde_json::from_str::<Vec<T>>(&string) .map(Some) .map_err(|error| { D::Error::custom(format!("failed to parse stringified edits array: {error}")) }), }} #[derive(Debug, Serialize, Deserialize)]#[serde(untagged)]pub enum StreamingEditFileToolOutput { Success { #[serde(alias = "original_path")] input_path: PathBuf, new_text: String, old_text: Arc<String>, #[serde(default)] diff: String, }, Error { error: String, #[serde(default)] input_path: Option<PathBuf>, #[serde(default)] diff: String, },} impl StreamingEditFileToolOutput { pub fn error(error: impl Into<String>) -> Self { Self::Error { error: error.into(), input_path: None, diff: String::new(), } }} impl std::fmt::Display for StreamingEditFileToolOutput { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { StreamingEditFileToolOutput::Success { diff, input_path, .. } => { if diff.is_empty() { write!(f, "No edits were made.") } else { write!( f, "Edited {}:\n\n```diff\n{diff}\n```", input_path.display() ) } } StreamingEditFileToolOutput::Error { error, diff, input_path, } => { write!(f, "{error}\n")?; if let Some(input_path) = input_path && !diff.is_empty() { write!( f, "Edited {}:\n\n```diff\n{diff}\n```", input_path.display() ) } else { write!(f, "No edits were made.") } } } }} impl From<StreamingEditFileToolOutput> for LanguageModelToolResultContent { fn from(output: StreamingEditFileToolOutput) -> Self { output.to_string().into() }} pub struct StreamingEditFileTool { project: Entity<Project>, thread: WeakEntity<Thread>, action_log: Entity<ActionLog>, language_registry: Arc<LanguageRegistry>,} enum EditSessionResult { Completed(EditSession), Failed { error: String, session: Option<EditSession>, },} impl StreamingEditFileTool { pub fn new( project: Entity<Project>, thread: WeakEntity<Thread>, action_log: Entity<ActionLog>, language_registry: Arc<LanguageRegistry>, ) -> Self { Self { project, thread, action_log, language_registry, } } fn authorize( &self, path: &PathBuf, description: &str, event_stream: &ToolCallEventStream, cx: &mut App, ) -> Task<Result<()>> { super::tool_permissions::authorize_file_edit( EditFileTool::NAME, path, description, &self.thread, event_stream, cx, ) } fn set_agent_location(&self, buffer: WeakEntity<Buffer>, position: text::Anchor, cx: &mut App) { let should_update_agent_location = self .thread .read_with(cx, |thread, _cx| !thread.is_subagent()) .unwrap_or_default(); if should_update_agent_location { self.project.update(cx, |project, cx| { project.set_agent_location(Some(AgentLocation { buffer, position }), cx); }); } } async fn ensure_buffer_saved(&self, buffer: &Entity<Buffer>, cx: &mut AsyncApp) { let format_on_save_enabled = buffer.read_with(cx, |buffer, cx| { let settings = language_settings::LanguageSettings::for_buffer(buffer, cx); settings.format_on_save != FormatOnSave::Off }); if format_on_save_enabled { self.project .update(cx, |project, cx| { project.format( HashSet::from_iter([buffer.clone()]), LspFormatTarget::Buffers, false, FormatTrigger::Save, cx, ) }) .await .log_err(); } self.project .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) .await .log_err(); self.action_log.update(cx, |log, cx| { log.buffer_edited(buffer.clone(), cx); }); } async fn process_streaming_edits( &self, input: &mut ToolInput<StreamingEditFileToolInput>, event_stream: &ToolCallEventStream, cx: &mut AsyncApp, ) -> EditSessionResult { let mut session: Option<EditSession> = None; let mut last_partial: Option<StreamingEditFileToolPartialInput> = None; loop { futures::select! { payload = input.next().fuse() => { match payload { Ok(payload) => match payload { ToolInputPayload::Partial(partial) => { if let Ok(parsed) = serde_json::from_value::<StreamingEditFileToolPartialInput>(partial) { let path_complete = parsed.path.is_some() && parsed.path.as_ref() == last_partial.as_ref().and_then(|partial| partial.path.as_ref()); last_partial = Some(parsed.clone()); if session.is_none() && path_complete && let StreamingEditFileToolPartialInput { path: Some(path), display_description: Some(display_description), mode: Some(mode), .. } = &parsed { match EditSession::new( PathBuf::from(path), display_description, *mode, self, event_stream, cx, ) .await { Ok(created_session) => session = Some(created_session), Err(error) => { log::error!("Failed to create edit session: {}", error); return EditSessionResult::Failed { error, session: None, }; } } } if let Some(current_session) = &mut session && let Err(error) = current_session.process(parsed, self, event_stream, cx) { log::error!("Failed to process edit: {}", error); return EditSessionResult::Failed { error, session }; } } } ToolInputPayload::Full(full_input) => { let mut session = if let Some(session) = session { session } else { match EditSession::new( full_input.path.clone(), &full_input.display_description, full_input.mode, self, event_stream, cx, ) .await { Ok(created_session) => created_session, Err(error) => { log::error!("Failed to create edit session: {}", error); return EditSessionResult::Failed { error, session: None, }; } } }; return match session.finalize(full_input, self, event_stream, cx).await { Ok(()) => EditSessionResult::Completed(session), Err(error) => { log::error!("Failed to finalize edit: {}", error); EditSessionResult::Failed { error, session: Some(session), } } }; } ToolInputPayload::InvalidJson { error_message } => { log::error!("Received invalid JSON: {error_message}"); return EditSessionResult::Failed { error: error_message, session, }; } }, Err(error) => { return EditSessionResult::Failed { error: format!("Failed to receive tool input: {error}"), session, }; } } } _ = event_stream.cancelled_by_user().fuse() => { return EditSessionResult::Failed { error: "Edit cancelled by user".to_string(), session, }; } } } }} impl AgentTool for StreamingEditFileTool { type Input = StreamingEditFileToolInput; type Output = StreamingEditFileToolOutput; const NAME: &'static str = "streaming_edit_file"; fn supports_input_streaming() -> bool { true } fn kind() -> acp::ToolKind { acp::ToolKind::Edit } fn initial_title( &self, input: Result<Self::Input, serde_json::Value>, cx: &mut App, ) -> SharedString { match input { Ok(input) => self .project .read(cx) .find_project_path(&input.path, cx) .and_then(|project_path| { self.project .read(cx) .short_full_path_for_project_path(&project_path, cx) }) .unwrap_or(input.path.to_string_lossy().into_owned()) .into(), Err(raw_input) => { if let Ok(input) = serde_json::from_value::<StreamingEditFileToolPartialInput>(raw_input) { let path = input.path.unwrap_or_default();Lines 1–500 of 4250. Load more
use super::edit_file_tool::EditFileTool; use super::restore_file_from_disk_tool::RestoreFileFromDiskTool; use super::save_file_tool::SaveFileTool; use super::tool_edit_parser::{ToolEditEvent, ToolEditParser}; use crate::ToolInputPayload; use crate::{ AgentTool, Thread, ToolCallEventStream, ToolInput, edit_agent::{ reindent::{Reindenter, compute_indent_delta}, streaming_fuzzy_matcher::StreamingFuzzyMatcher, }, }; use acp_thread::Diff; use action_log::ActionLog; use agent_client_protocol::{self as acp, ToolCallLocation, ToolCallUpdateFields}; use anyhow::Result; use collections::HashSet; use futures::FutureExt as _; use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity}; use language::language_settings::{self, FormatOnSave}; use language::{Buffer, LanguageRegistry}; use language_model::LanguageModelToolResultContent; use project::lsp_store::{FormatTrigger, LspFormatTarget}; use project::{AgentLocation, Project, ProjectPath}; use schemars::JsonSchema; use serde::{ Deserialize, Deserializer, Serialize, de::{DeserializeOwned, Error as _}, }; use std::ops::Range; use std::path::PathBuf; use std::sync::Arc; use streaming_diff::{CharOperation, StreamingDiff}; use text::ToOffset; use ui::SharedString; use util::rel_path::RelPath; use util::{Deferred, ResultExt}; const DEFAULT_UI_TEXT: &str = "Editing file"; /// This is a tool for creating a new file or editing an existing file. For moving or renaming files, you should generally use the `move_path` tool instead. /// /// Before using this tool: /// /// 1. Use the `read_file` tool to understand the file's contents and context /// /// 2. Verify the directory path is correct (only applicable when creating new files): /// - Use the `list_directory` tool to verify the parent directory exists and is the correct location #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct StreamingEditFileToolInput { /// A one-line, user-friendly markdown description of the edit. This will be shown in the UI. /// /// Be terse, but also descriptive in what you want to achieve with this edit. Avoid generic instructions. /// /// NEVER mention the file path in this description. /// /// <example>Fix API endpoint URLs</example> /// <example>Update copyright year in `page_footer`</example> /// /// Make sure to include this field before all the others in the input object so that we can display it immediately. pub display_description: String, /// The full path of the file to create or modify in the project. /// /// WARNING: When specifying which file path need changing, you MUST start each path with one of the project's root directories. /// /// The following examples assume we have two root directories in the project: /// - /a/b/backend /// - /c/d/frontend /// /// <example> /// `backend/src/main.rs` /// /// Notice how the file path starts with `backend`. Without that, the path would be ambiguous and the call would fail! /// </example> /// /// <example> /// `frontend/db.js` /// </example> pub path: PathBuf, /// The mode of operation on the file. Possible values: /// - 'write': Replace the entire contents of the file. If the file doesn't exist, it will be created. Requires 'content' field. /// - 'edit': Make granular edits to an existing file. Requires 'edits' field. /// /// When a file already exists or you just created it, prefer editing it as opposed to recreating it from scratch. pub mode: StreamingEditFileMode, /// The complete content for the new file (required for 'write' mode). /// This field should contain the entire file content. #[serde(default, skip_serializing_if = "Option::is_none")] pub content: Option<String>, /// List of edit operations to apply sequentially (required for 'edit' mode). /// Each edit finds `old_text` in the file and replaces it with `new_text`. #[serde( default, skip_serializing_if = "Option::is_none", deserialize_with = "deserialize_optional_vec_or_json_string" )] pub edits: Option<Vec<Edit>>, } #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum StreamingEditFileMode { /// Overwrite the file with new content (replacing any existing content). /// If the file does not exist, it will be created. Write, /// Make granular edits to an existing file Edit, } /// A single edit operation that replaces old text with new text /// Properly escape all text fields as valid JSON strings. /// Remember to escape special characters like newlines (`\n`) and quotes (`"`) in JSON strings. #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct Edit { /// The exact text to find in the file. This will be matched using fuzzy matching /// to handle minor differences in whitespace or formatting. /// /// Be minimal with replacements: /// - For unique lines, include only those lines /// - For non-unique lines, include enough context to identify them pub old_text: String, /// The text to replace it with pub new_text: String, } #[derive(Clone, Default, Debug, Deserialize)] struct StreamingEditFileToolPartialInput { #[serde(default)] display_description: Option<String>, #[serde(default)] path: Option<String>, #[serde(default)] mode: Option<StreamingEditFileMode>, #[serde(default)] content: Option<String>, #[serde(default, deserialize_with = "deserialize_optional_vec_or_json_string")] edits: Option<Vec<PartialEdit>>, } #[derive(Clone, Default, Debug, Deserialize)] pub struct PartialEdit { #[serde(default)] pub old_text: Option<String>, #[serde(default)] pub new_text: Option<String>, } /// Sometimes the model responds with a stringified JSON array of edits (`"[...]"`) instead of a regular array (`[...]`) fn deserialize_optional_vec_or_json_string<'de, T, D>( deserializer: D, ) -> Result<Option<Vec<T>>, D::Error> where T: DeserializeOwned, D: Deserializer<'de>, { #[derive(Deserialize)] #[serde(untagged)] enum VecOrJsonString<T> { Vec(Vec<T>), String(String), } let value = Option::<VecOrJsonString<T>>::deserialize(deserializer)?; match value { None => Ok(None), Some(VecOrJsonString::Vec(items)) => Ok(Some(items)), Some(VecOrJsonString::String(string)) => serde_json::from_str::<Vec<T>>(&string) .map(Some) .map_err(|error| { D::Error::custom(format!("failed to parse stringified edits array: {error}")) }), } } #[derive(Debug, Serialize, Deserialize)] #[serde(untagged)] pub enum StreamingEditFileToolOutput { Success { #[serde(alias = "original_path")] input_path: PathBuf, new_text: String, old_text: Arc<String>, #[serde(default)] diff: String, }, Error { error: String, #[serde(default)] input_path: Option<PathBuf>, #[serde(default)] diff: String, }, } impl StreamingEditFileToolOutput { pub fn error(error: impl Into<String>) -> Self { Self::Error { error: error.into(), input_path: None, diff: String::new(), } } } impl std::fmt::Display for StreamingEditFileToolOutput { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { StreamingEditFileToolOutput::Success { diff, input_path, .. } => { if diff.is_empty() { write!(f, "No edits were made.") } else { write!( f, "Edited {}:\n\n```diff\n{diff}\n```", input_path.display() ) } } StreamingEditFileToolOutput::Error { error, diff, input_path, } => { write!(f, "{error}\n")?; if let Some(input_path) = input_path && !diff.is_empty() { write!( f, "Edited {}:\n\n```diff\n{diff}\n```", input_path.display() ) } else { write!(f, "No edits were made.") } } } } } impl From<StreamingEditFileToolOutput> for LanguageModelToolResultContent { fn from(output: StreamingEditFileToolOutput) -> Self { output.to_string().into() } } pub struct StreamingEditFileTool { project: Entity<Project>, thread: WeakEntity<Thread>, action_log: Entity<ActionLog>, language_registry: Arc<LanguageRegistry>, } enum EditSessionResult { Completed(EditSession), Failed { error: String, session: Option<EditSession>, }, } impl StreamingEditFileTool { pub fn new( project: Entity<Project>, thread: WeakEntity<Thread>, action_log: Entity<ActionLog>, language_registry: Arc<LanguageRegistry>, ) -> Self { Self { project, thread, action_log, language_registry, } } fn authorize( &self, path: &PathBuf, description: &str, event_stream: &ToolCallEventStream, cx: &mut App, ) -> Task<Result<()>> { super::tool_permissions::authorize_file_edit( EditFileTool::NAME, path, description, &self.thread, event_stream, cx, ) } fn set_agent_location(&self, buffer: WeakEntity<Buffer>, position: text::Anchor, cx: &mut App) { let should_update_agent_location = self .thread .read_with(cx, |thread, _cx| !thread.is_subagent()) .unwrap_or_default(); if should_update_agent_location { self.project.update(cx, |project, cx| { project.set_agent_location(Some(AgentLocation { buffer, position }), cx); }); } } async fn ensure_buffer_saved(&self, buffer: &Entity<Buffer>, cx: &mut AsyncApp) { let format_on_save_enabled = buffer.read_with(cx, |buffer, cx| { let settings = language_settings::LanguageSettings::for_buffer(buffer, cx); settings.format_on_save != FormatOnSave::Off }); if format_on_save_enabled { self.project .update(cx, |project, cx| { project.format( HashSet::from_iter([buffer.clone()]), LspFormatTarget::Buffers, false, FormatTrigger::Save, cx, ) }) .await .log_err(); } self.project .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) .await .log_err(); self.action_log.update(cx, |log, cx| { log.buffer_edited(buffer.clone(), cx); }); } async fn process_streaming_edits( &self, input: &mut ToolInput<StreamingEditFileToolInput>, event_stream: &ToolCallEventStream, cx: &mut AsyncApp, ) -> EditSessionResult { let mut session: Option<EditSession> = None; let mut last_partial: Option<StreamingEditFileToolPartialInput> = None; loop { futures::select! { payload = input.next().fuse() => { match payload { Ok(payload) => match payload { ToolInputPayload::Partial(partial) => { if let Ok(parsed) = serde_json::from_value::<StreamingEditFileToolPartialInput>(partial) { let path_complete = parsed.path.is_some() && parsed.path.as_ref() == last_partial.as_ref().and_then(|partial| partial.path.as_ref()); last_partial = Some(parsed.clone()); if session.is_none() && path_complete && let StreamingEditFileToolPartialInput { path: Some(path), display_description: Some(display_description), mode: Some(mode), .. } = &parsed { match EditSession::new( PathBuf::from(path), display_description, *mode, self, event_stream, cx, ) .await { Ok(created_session) => session = Some(created_session), Err(error) => { log::error!("Failed to create edit session: {}", error); return EditSessionResult::Failed { error, session: None, }; } } } if let Some(current_session) = &mut session && let Err(error) = current_session.process(parsed, self, event_stream, cx) { log::error!("Failed to process edit: {}", error); return EditSessionResult::Failed { error, session }; } } } ToolInputPayload::Full(full_input) => { let mut session = if let Some(session) = session { session } else { match EditSession::new( full_input.path.clone(), &full_input.display_description, full_input.mode, self, event_stream, cx, ) .await { Ok(created_session) => created_session, Err(error) => { log::error!("Failed to create edit session: {}", error); return EditSessionResult::Failed { error, session: None, }; } } }; return match session.finalize(full_input, self, event_stream, cx).await { Ok(()) => EditSessionResult::Completed(session), Err(error) => { log::error!("Failed to finalize edit: {}", error); EditSessionResult::Failed { error, session: Some(session), } } }; } ToolInputPayload::InvalidJson { error_message } => { log::error!("Received invalid JSON: {error_message}"); return EditSessionResult::Failed { error: error_message, session, }; } }, Err(error) => { return EditSessionResult::Failed { error: format!("Failed to receive tool input: {error}"), session, }; } } } _ = event_stream.cancelled_by_user().fuse() => { return EditSessionResult::Failed { error: "Edit cancelled by user".to_string(), session, }; } } } } } impl AgentTool for StreamingEditFileTool { type Input = StreamingEditFileToolInput; type Output = StreamingEditFileToolOutput; const NAME: &'static str = "streaming_edit_file"; fn supports_input_streaming() -> bool { true } fn kind() -> acp::ToolKind { acp::ToolKind::Edit } fn initial_title( &self, input: Result<Self::Input, serde_json::Value>, cx: &mut App, ) -> SharedString { match input { Ok(input) => self .project .read(cx) .find_project_path(&input.path, cx) .and_then(|project_path| { self.project .read(cx) .short_full_path_for_project_path(&project_path, cx) }) .unwrap_or(input.path.to_string_lossy().into_owned()) .into(), Err(raw_input) => { if let Ok(input) = serde_json::from_value::<StreamingEditFileToolPartialInput>(raw_input) { let path = input.path.unwrap_or_default();