| //! `agents`: development instructions for whoever writes the application. | |
| //! | |
| //! `types` gives an editor the API; this gives a coding agent the rules the | |
| //! API cannot express — no DOM, state in `init`, `notify` after a change, | |
| //! single-use elements, capabilities denied by default — and the commands that | |
| //! check its work. Like `gpui-kit.d.ts` the text is generated from the runtime | |
| //! that will run the application, so the module list and the component catalog | |
| //! are the ones this host actually has. | |
| use std::path::{Path, PathBuf}; | |
| use super::HostBrand; | |
| use crate::engine::quickjs::exports; | |
| /// The file written into an application directory. `AGENTS.md` is the name | |
| /// coding agents look for without being told. | |
| pub(super) const AGENTS_FILE: &str = "AGENTS.md"; | |
| const TEMPLATE: &str = include_str!("agents.md"); | |
| /// The first line of every generated file. Only a file that starts with it is | |
| /// ever replaced, so instructions an author wrote by hand are never lost. | |
| fn marker(brand: HostBrand) -> String { | |
| format!( | |
| "<!-- Generated by `{} agents`. Edits are replaced on the next run; delete this line to keep them. -->", | |
| brand.name() | |
| ) | |
| } | |
| /// The instructions for this host, with its name, version, application | |
| /// directory and modules filled in. | |
| pub(super) fn render(brand: HostBrand, components: &crate::FrozenComponentRegistry) -> String { | |
| let body = TEMPLATE | |
| .replace("{modules}", &modules(components)) | |
| .replace("{home}", &display_home(brand)) | |
| .replace("{version}", brand.version()) | |
| .replace("{name}", brand.name()); | |
| format!("{}\n\n{body}", marker(brand)) | |
| } | |
| fn display_home(brand: HostBrand) -> String { | |
| format!("~/.{}/apps", brand.name()) | |
| } | |
| fn modules(components: &crate::FrozenComponentRegistry) -> String { | |
| let mut sections = vec![ | |
| module( | |
| "gpui-kit", | |
| "GPUI's own elements and the runtime (`\"gpui\"` is an alias)", | |
| exports::GPUI, | |
| ), | |
| module( | |
| "gpui-base", | |
| "unstyled controls, layout helpers and the theme", | |
| exports::GPUI_BASE, | |
| ), | |
| module("gpui-fps", "the native performance HUD", exports::GPUI_FPS), | |
| ]; | |
| if let Some(specifier) = components.module_specifier() { | |
| let names: Vec<&str> = components | |
| .descriptors() | |
| .map(|descriptor| descriptor.name()) | |
| .chain(components.states().map(|state| state.export())) | |
| .collect(); | |
| sections.push(module( | |
| specifier, | |
| "this host's styled components; prefer them to hand-styled base controls", | |
| &names, | |
| )); | |
| } | |
| sections.join("\n") | |
| } | |
| fn module(specifier: &str, description: &str, names: &[&str]) -> String { | |
| let names = names | |
| .iter() | |
| .map(|name| format!("`{name}`")) | |
| .collect::<Vec<_>>() | |
| .join(", "); | |
| format!("- `\"{specifier}\"` — {description}: {names}") | |
| } | |
| /// Writes the instructions into an application directory. | |
| /// | |
| /// Returns `Ok(None)` when the file already says exactly this, so a directory | |
| /// an editor watches is not touched on every run. | |
| pub(super) fn write( | |
| directory: &Path, | |
| brand: HostBrand, | |
| components: &crate::FrozenComponentRegistry, | |
| ) -> Result<Option<PathBuf>, String> { | |
| if !directory.is_dir() { | |
| return Err(format!("`{}` is not a directory", directory.display())); | |
| } | |
| let path = directory.join(AGENTS_FILE); | |
| let text = render(brand, components); | |
| match std::fs::read_to_string(&path) { | |
| Ok(existing) if existing == text => return Ok(None), | |
| Ok(existing) if !existing.starts_with(&marker(brand)) => { | |
| return Err(format!( | |
| "{} exists and was not generated by {name}; it is left alone. \ | |
| Run `{name} agents` to print the instructions instead.", | |
| path.display(), | |
| name = brand.name() | |
| )); | |
| } | |
| Ok(_) => {} | |
| Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} | |
| Err(error) => return Err(format!("cannot read {}: {error}", path.display())), | |
| } | |
| std::fs::write(&path, text) | |
| .map(|()| Some(path.clone())) | |
| .map_err(|error| format!("cannot write {}: {error}", path.display())) | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use super::*; | |
| fn brand() -> HostBrand { | |
| HostBrand::new("gpui-shell", "1.2.3") | |
| } | |
| #[test] | |
| fn the_instructions_name_this_host_and_its_modules() { | |
| let text = render(brand(), &crate::FrozenComponentRegistry::default()); | |
| assert!(text.starts_with(&marker(brand()))); | |
| assert!(text.contains("gpui-shell types")); | |
| assert!(text.contains("~/.gpui-shell/apps")); | |
| assert!(text.contains("\"shell-version\": \"1.2.3\"")); | |
| assert!(text.contains("`Button`"), "gpui-base's exports are listed"); | |
| assert!(!text.contains("{name}") && !text.contains("{modules}")); | |
| } | |
| #[test] | |
| fn only_a_generated_file_is_replaced() { | |
| let directory = | |
| std::env::temp_dir().join(format!("gpui-shell-agents-{}", std::process::id())); | |
| let _ = std::fs::remove_dir_all(&directory); | |
| std::fs::create_dir_all(&directory).expect("temporary directory"); | |
| let components = crate::FrozenComponentRegistry::default(); | |
| let written = write(&directory, brand(), &components).expect("a new file is written"); | |
| assert_eq!(written, Some(directory.join(AGENTS_FILE))); | |
| assert_eq!( | |
| write(&directory, brand(), &components).expect("unchanged"), | |
| None, | |
| "an identical file is not rewritten" | |
| ); | |
| std::fs::write(directory.join(AGENTS_FILE), "# My own notes\n").expect("hand-written"); | |
| let error = write(&directory, brand(), &components).expect_err("hand-written is kept"); | |
| assert!(error.contains("left alone"), "{error}"); | |
| assert_eq!( | |
| std::fs::read_to_string(directory.join(AGENTS_FILE)).unwrap(), | |
| "# My own notes\n" | |
| ); | |
| let _ = std::fs::remove_dir_all(&directory); | |
| } | |
| } |