@@ -4,14 +4,19 @@ use anyhow::{anyhow, Result};
44use serde:: Deserialize ;
55use std:: { collections:: BTreeMap , fs, path:: PathBuf , sync:: Arc } ;
66
7- use crate :: layered_config:: { Config , ConfigManager , Scope } ;
7+ use crate :: {
8+ layered_config:: { Config , ConfigManager , Scope } ,
9+ compact:: Compactor ,
10+ todo:: { TodoStore , TodoStatus } ,
11+ } ;
812
913#[ derive( Clone ) ]
1014pub struct SlashRegistry {
1115 aliases : BTreeMap < String , String > ,
1216 macros : BTreeMap < String , Vec < String > > ,
1317 builtins : BTreeMap < String , BTreeMap < String , String > > , // name -> args
1418 cfg : Arc < ConfigManager > ,
19+ workspace_root : PathBuf ,
1520}
1621
1722#[ derive( Default , Deserialize ) ]
@@ -28,7 +33,7 @@ struct SlashTomlFile {
2833struct SlashMacro { name : String , lines : Vec < String > }
2934
3035impl SlashRegistry {
31- pub fn load_from_dirs ( cfg : Arc < ConfigManager > , dirs : & [ PathBuf ] ) -> Result < Self > {
36+ pub fn load_from_dirs_with_workspace ( cfg : Arc < ConfigManager > , workspace_root : PathBuf , dirs : & [ PathBuf ] ) -> Result < Self > {
3237 let mut aliases = BTreeMap :: new ( ) ;
3338 let mut macros: BTreeMap < String , Vec < String > > = BTreeMap :: new ( ) ;
3439 let mut builtins: BTreeMap < String , BTreeMap < String , String > > = BTreeMap :: new ( ) ;
@@ -45,7 +50,13 @@ impl SlashRegistry {
4550 }
4651 }
4752 }
48- Ok ( Self { aliases, macros, builtins, cfg } )
53+ Ok ( Self { aliases, macros, builtins, cfg, workspace_root } )
54+ }
55+
56+ // Backwards-compatible helper: default workspace is current dir
57+ pub fn load_from_dirs ( cfg : Arc < ConfigManager > , dirs : & [ PathBuf ] ) -> Result < Self > {
58+ let cwd = std:: env:: current_dir ( ) . unwrap_or_else ( |_| PathBuf :: from ( "." ) ) ;
59+ Self :: load_from_dirs_with_workspace ( cfg, cwd, dirs)
4960 }
5061
5162 pub async fn dispatch ( & self , input : & str ) -> Result < String > {
@@ -81,187 +92,97 @@ impl SlashRegistry {
8192 self . cfg . apply_runtime_overlay ( patch) ?;
8293 Ok ( "runtime overlay applied" . into ( ) )
8394 }
84- _ => Ok ( format ! ( "builtin:{} {}" , name, serde_json:: to_string( args) ?) ) ,
85- }
86- }
87- }
88-
89- // annex/src/slash.rs content below
90-
91- use anyhow:: { anyhow, Result } ;
92- use std:: { path:: PathBuf , sync:: Arc } ;
93-
94- use crate :: {
95- layered_config:: { Config , ConfigManager , Scope } ,
96- mcp_runtime:: McpRuntime ,
97- todo:: { TodoStore , TodoStatus } ,
98- compact:: { Compactor , AutoCompactStage } ,
99- } ;
100-
101- #[ derive( Clone ) ]
102- pub struct SlashRegistry {
103- cmds : Vec < Arc < dyn SlashCommand > > ,
104- }
105- impl SlashRegistry {
106- pub fn new ( ) -> Self { Self { cmds : vec ! [ ] } }
107- pub fn register ( & mut self , cmd : Arc < dyn SlashCommand > ) { self . cmds . push ( cmd) ; }
108- pub async fn dispatch ( & self , input : & str ) -> Result < String > {
109- let input = input. trim ( ) ;
110- if !input. starts_with ( '/' ) { return Err ( anyhow ! ( "not a slash command" ) ) ; }
111- let parts: Vec < & str > = input[ 1 ..] . split_whitespace ( ) . collect ( ) ;
112- if parts. is_empty ( ) { return Err ( anyhow ! ( "empty command" ) ) ; }
113- let name = parts[ 0 ] ;
114- for c in & self . cmds { if c. name ( ) == name { return c. run ( parts[ 1 ..] . join ( " " ) ) . await ; } }
115- Err ( anyhow ! ( "unknown command: {}" , name) )
116- }
117- }
118-
119- #[ async_trait:: async_trait]
120- pub trait SlashCommand : Send + Sync {
121- fn name ( & self ) -> & ' static str ;
122- async fn run ( & self , args : String ) -> Result < String > ;
123- }
124-
125- /*** Existing minimal commands from earlier snippet (not re-listed for brevity) ***/
126- pub struct AllowCommand { pub cfg : Arc < ConfigManager > }
127- #[ async_trait:: async_trait]
128- impl SlashCommand for AllowCommand {
129- fn name ( & self ) -> & ' static str { "allow" }
130- async fn run ( & self , args : String ) -> Result < String > {
131- let mut patch = Config :: default ( ) ;
132- patch. shell . allowlist_roots = vec ! [ args. trim( ) . to_string( ) ] ;
133- self . cfg . write_patch ( Scope :: Workspace , & patch) ?;
134- Ok ( format ! ( "added to allowlist (workspace): {}" , args. trim( ) ) )
135- }
136- }
137- pub struct McpAddCommand { pub cfg : Arc < ConfigManager > }
138- #[ async_trait:: async_trait]
139- impl SlashCommand for McpAddCommand {
140- fn name ( & self ) -> & ' static str { "mcp-add" }
141- async fn run ( & self , args : String ) -> Result < String > {
142- let mut patch = Config :: default ( ) ;
143- // Simple parser: JSON object {"name":"X","stdio":{"cmd": "...","args":["..."]}} or {"name":"X","tcp":{"host":"...","port":1234}}
144- let v: serde_json:: Value = serde_json:: from_str ( & args) ?;
145- let name = v. get ( "name" ) . and_then ( |x| x. as_str ( ) ) . ok_or_else ( || anyhow ! ( "missing name" ) ) ?;
146- let mut m = crate :: layered_config:: McpServer :: default ( ) ;
147- m. enabled = true ;
148- if let Some ( stdio) = v. get ( "stdio" ) {
149- m. transport = "stdio" . into ( ) ;
150- m. command = stdio. get ( "cmd" ) . and_then ( |x| x. as_str ( ) ) . map ( |s| s. into ( ) ) ;
151- if let Some ( a) = stdio. get ( "args" ) . and_then ( |x| x. as_array ( ) ) {
152- m. args = a. iter ( ) . filter_map ( |x| x. as_str ( ) . map ( |s| s. to_string ( ) ) ) . collect ( ) ;
95+ "allow" => {
96+ let root = argstr. trim ( ) ;
97+ if root. is_empty ( ) { return Err ( anyhow ! ( "usage: /allow <root-binary>" ) ) ; }
98+ let mut patch = Config :: default ( ) ;
99+ patch. shell . allowlist_roots = vec ! [ root. to_string( ) ] ;
100+ self . cfg . write_patch ( Scope :: Workspace , & patch) ?;
101+ Ok ( format ! ( "added to allowlist (workspace): {}" , root) )
153102 }
154- } else if let Some ( tcp) = v. get ( "tcp" ) {
155- m. transport = "tcp" . into ( ) ;
156- m. host = tcp. get ( "host" ) . and_then ( |x| x. as_str ( ) ) . map ( |s| s. into ( ) ) ;
157- m. port = tcp. get ( "port" ) . and_then ( |x| x. as_u64 ( ) ) . map ( |n| n as u16 ) ;
158- } else {
159- return Err ( anyhow ! ( "expect stdio or tcp" ) ) ;
160- }
161- patch. mcp . servers . insert ( name. into ( ) , m) ;
162- self . cfg . write_patch ( Scope :: Workspace , & patch) ?;
163- Ok ( "MCP server added (workspace)" . into ( ) )
164- }
165- }
166- pub struct ConfigSetCommand { pub cfg : Arc < ConfigManager > }
167- #[ async_trait:: async_trait]
168- impl SlashCommand for ConfigSetCommand {
169- fn name ( & self ) -> & ' static str { "config-set" }
170- async fn run ( & self , args : String ) -> Result < String > {
171- let parts: Vec < & str > = args. split_whitespace ( ) . collect ( ) ;
172- if parts. len ( ) < 2 { return Err ( anyhow ! ( "usage: /config-set <path> <value>" ) ) ; }
173- let path = parts[ 0 ] ; let value = parts[ 1 ..] . join ( " " ) ;
174- let mut patch = Config :: default ( ) ;
175- match path {
176- "model.name" => patch. model . name = Some ( value) ,
177- "history.persist" => patch. history . persist = Some ( value) ,
178- "sandbox.mode" => patch. sandbox . mode = Some ( value) ,
179- "sandbox.network_access" => patch. sandbox . network_access = Some ( value. parse :: < bool > ( ) ?) ,
180- _ => return Err ( anyhow ! ( "unsupported path: {}" , path) ) ,
181- }
182- self . cfg . apply_runtime_overlay ( patch) ?;
183- Ok ( "runtime overlay applied" . into ( ) )
184- }
185- }
186-
187- /*** NEW: TODO commands ***/
188- pub struct TodoCommand { pub cfg : Arc < ConfigManager > , pub workspace : PathBuf }
189- #[ async_trait:: async_trait]
190- impl SlashCommand for TodoCommand {
191- fn name ( & self ) -> & ' static str { "todo" }
192- async fn run ( & self , args : String ) -> Result < String > {
193- let cfg = self . cfg . get ( ) ;
194- let path = cfg. todo . path . clone ( ) . unwrap_or ( self . workspace . join ( ".codex" ) . join ( "todo.json" ) ) ;
195- let mut store = TodoStore :: load ( & path) ?;
196- let parts: Vec < & str > = args. split_whitespace ( ) . collect ( ) ;
197- match parts. get ( 0 ) . map ( |s| * s) . unwrap_or ( "" ) {
198- "add" => {
199- // /todo add {"title":"…","description":"…","files":["path1","path2"],"tags":["x"]}
200- let v: serde_json:: Value = serde_json:: from_str ( parts[ 1 ..] . join ( " " ) . trim ( ) ) ?;
201- let title = v. get ( "title" ) . and_then ( |x| x. as_str ( ) ) . ok_or_else ( || anyhow ! ( "title required" ) ) ?;
202- let desc = v. get ( "description" ) . and_then ( |x| x. as_str ( ) ) . map ( |s| s. to_string ( ) ) ;
203- let files: Vec < PathBuf > = v. get ( "files" ) . and_then ( |x| x. as_array ( ) ) . unwrap_or ( & vec ! [ ] )
204- . iter ( ) . filter_map ( |x| x. as_str ( ) . map ( |s| self . workspace . join ( s) ) ) . collect ( ) ;
205- let tags: Vec < String > = v. get ( "tags" ) . and_then ( |x| x. as_array ( ) ) . unwrap_or ( & vec ! [ ] )
206- . iter ( ) . filter_map ( |x| x. as_str ( ) . map ( |s| s. to_string ( ) ) ) . collect ( ) ;
207- let it = store. add ( title. to_string ( ) , desc, files, tags) ;
208- store. save ( & path) ?;
209- Ok ( format ! ( "todo added: {} ({})" , it. title, it. id) )
103+ "mcp-add" => {
104+ // JSON: {"name":"X","stdio":{...}} or {"name":"X","tcp":{...}}
105+ let v: serde_json:: Value = serde_json:: from_str ( argstr) ?;
106+ let name = v. get ( "name" ) . and_then ( |x| x. as_str ( ) ) . ok_or_else ( || anyhow ! ( "missing name" ) ) ?;
107+ let mut m = crate :: layered_config:: McpServer :: default ( ) ;
108+ m. enabled = true ;
109+ if let Some ( stdio) = v. get ( "stdio" ) {
110+ m. transport = "stdio" . into ( ) ;
111+ m. command = stdio. get ( "cmd" ) . and_then ( |x| x. as_str ( ) ) . map ( |s| s. into ( ) ) ;
112+ if let Some ( a) = stdio. get ( "args" ) . and_then ( |x| x. as_array ( ) ) {
113+ m. args = a. iter ( ) . filter_map ( |x| x. as_str ( ) . map ( |s| s. to_string ( ) ) ) . collect ( ) ;
114+ }
115+ } else if let Some ( tcp) = v. get ( "tcp" ) {
116+ m. transport = "tcp" . into ( ) ;
117+ m. host = tcp. get ( "host" ) . and_then ( |x| x. as_str ( ) ) . map ( |s| s. into ( ) ) ;
118+ m. port = tcp. get ( "port" ) . and_then ( |x| x. as_u64 ( ) ) . map ( |n| n as u16 ) ;
119+ } else { return Err ( anyhow ! ( "expect stdio or tcp" ) ) ; }
120+ let mut patch = Config :: default ( ) ;
121+ patch. mcp . servers . insert ( name. into ( ) , m) ;
122+ self . cfg . write_patch ( Scope :: Workspace , & patch) ?;
123+ Ok ( "MCP server added (workspace)" . into ( ) )
210124 }
211- "list" => {
212- let mut s = String :: new ( ) ;
213- for it in & store. items {
214- s. push_str ( & format ! ( "- [{}] {} ({}) {:?}\n " , match it. status { TodoStatus :: Open =>" " , TodoStatus :: InProgress =>">" , TodoStatus :: Done =>"x" } , it. title, it. id, it. files) ) ;
125+ "todo" => {
126+ // /todo add {json} | list | done <id> | rm <id>
127+ let cfg = self . cfg . get ( ) ;
128+ let path = cfg. todo . path . clone ( ) . unwrap_or ( self . workspace_root . join ( ".codex" ) . join ( "todo.json" ) ) ;
129+ let mut store = TodoStore :: load ( & path) ?;
130+ let parts: Vec < & str > = argstr. split_whitespace ( ) . collect ( ) ;
131+ match parts. get ( 0 ) . copied ( ) . unwrap_or ( "" ) {
132+ "add" => {
133+ let v: serde_json:: Value = serde_json:: from_str ( parts[ 1 ..] . join ( " " ) . trim ( ) ) ?;
134+ let title = v. get ( "title" ) . and_then ( |x| x. as_str ( ) ) . ok_or_else ( || anyhow ! ( "title required" ) ) ?;
135+ let desc = v. get ( "description" ) . and_then ( |x| x. as_str ( ) ) . map ( |s| s. to_string ( ) ) ;
136+ let files: Vec < PathBuf > = v. get ( "files" ) . and_then ( |x| x. as_array ( ) ) . unwrap_or ( & vec ! [ ] )
137+ . iter ( ) . filter_map ( |x| x. as_str ( ) . map ( |s| self . workspace_root . join ( s) ) ) . collect ( ) ;
138+ let tags: Vec < String > = v. get ( "tags" ) . and_then ( |x| x. as_array ( ) ) . unwrap_or ( & vec ! [ ] )
139+ . iter ( ) . filter_map ( |x| x. as_str ( ) . map ( |s| s. to_string ( ) ) ) . collect ( ) ;
140+ let it = store. add ( title. to_string ( ) , desc, files, tags) ;
141+ store. save ( & path) ?;
142+ Ok ( format ! ( "todo added: {} ({})" , it. title, it. id) )
143+ }
144+ "list" => {
145+ let mut s = String :: new ( ) ;
146+ for it in & store. items {
147+ s. push_str ( & format ! ( "- [{}] {} ({}) {:?}\n " , match it. status { TodoStatus :: Open =>" " , TodoStatus :: InProgress =>">" , TodoStatus :: Done =>"x" } , it. title, it. id, it. files) ) ;
148+ }
149+ Ok ( s)
150+ }
151+ "done" => {
152+ let id = parts. get ( 1 ) . ok_or_else ( || anyhow ! ( "usage: /todo done <id>" ) ) ?;
153+ store. set_status ( id, TodoStatus :: Done ) ?; store. save ( & path) ?;
154+ Ok ( format ! ( "todo {} marked done" , id) )
155+ }
156+ "rm" => {
157+ let id = parts. get ( 1 ) . ok_or_else ( || anyhow ! ( "usage: /todo rm <id>" ) ) ?;
158+ store. remove ( id) ?; store. save ( & path) ?;
159+ Ok ( format ! ( "todo {} removed" , id) )
160+ }
161+ _ => Err ( anyhow ! ( "usage: /todo [add|list|done|rm] …" ) ) ,
215162 }
216- Ok ( s)
217163 }
218- "done" => {
219- let id = parts. get ( 1 ) . ok_or_else ( || anyhow ! ( "usage: /todo done <id>" ) ) ?;
220- store. set_status ( id, TodoStatus :: Done ) ?;
221- store. save ( & path) ?;
222- Ok ( format ! ( "todo {} marked done" , id) )
164+ "compact" => {
165+ // args JSON: {"focus":"…","include":["glob1"],"conversation_tail":"…"}
166+ let v: serde_json:: Value = serde_json:: from_str ( argstr. trim ( ) ) ?;
167+ let focus = v. get ( "focus" ) . and_then ( |x| x. as_str ( ) ) . map ( |s| s. to_string ( ) ) ;
168+ let includes: Vec < String > = v. get ( "include" ) . and_then ( |x| x. as_array ( ) ) . unwrap_or ( & vec ! [ ] )
169+ . iter ( ) . filter_map ( |x| x. as_str ( ) . map ( |s| s. to_string ( ) ) ) . collect ( ) ;
170+ let tail = v. get ( "conversation_tail" ) . and_then ( |x| x. as_str ( ) ) . unwrap_or ( "" ) ;
171+ let comp = Compactor :: new ( self . cfg . clone ( ) , self . workspace_root . clone ( ) ) ;
172+ let res = comp. manual_compact ( focus, includes, tail) ?;
173+ Ok ( serde_json:: to_string_pretty ( & res) ?)
223174 }
224- "rm" => {
225- let id = parts. get ( 1 ) . ok_or_else ( || anyhow ! ( "usage: /todo rm <id>" ) ) ?;
226- store. remove ( id) ?;
227- store. save ( & path) ?;
228- Ok ( format ! ( "todo {} removed" , id) )
175+ "autocompact" => {
176+ let mut patch = Config :: default ( ) ;
177+ match argstr. trim ( ) {
178+ "on" => { patch. compact . auto_enable = true ; }
179+ "off" => { patch. compact . auto_enable = false ; }
180+ _ => return Err ( anyhow ! ( "usage: /autocompact on|off" ) ) ,
181+ }
182+ self . cfg . apply_runtime_overlay ( patch) ?;
183+ Ok ( format ! ( "auto-compact {}" , argstr. trim( ) ) )
229184 }
230- _ => Err ( anyhow ! ( "usage: /todo [add|list|done|rm] …" ) ) ,
231- }
232- }
233- }
234-
235- /*** NEW: compact + autocompact ***/
236- pub struct CompactCommand { pub cfg : Arc < ConfigManager > , pub workspace : PathBuf }
237- #[ async_trait:: async_trait]
238- impl SlashCommand for CompactCommand {
239- fn name ( & self ) -> & ' static str { "compact" }
240- async fn run ( & self , args : String ) -> Result < String > {
241- // JSON input: {"focus":"…","include":["glob1","glob2"],"conversation_tail":"…"}
242- let v: serde_json:: Value = serde_json:: from_str ( args. trim ( ) ) ?;
243- let focus = v. get ( "focus" ) . and_then ( |x| x. as_str ( ) ) . map ( |s| s. to_string ( ) ) ;
244- let includes: Vec < String > = v. get ( "include" ) . and_then ( |x| x. as_array ( ) ) . unwrap_or ( & vec ! [ ] )
245- . iter ( ) . filter_map ( |x| x. as_str ( ) . map ( |s| s. to_string ( ) ) ) . collect ( ) ;
246- let tail = v. get ( "conversation_tail" ) . and_then ( |x| x. as_str ( ) ) . unwrap_or ( "" ) ;
247- let comp = crate :: compact:: Compactor :: new ( self . cfg . clone ( ) , self . workspace . clone ( ) ) ;
248- let res = comp. manual_compact ( focus, includes, tail) ?;
249- Ok ( serde_json:: to_string_pretty ( & res) ?)
250- }
251- }
252-
253- pub struct AutoCompactToggle { pub cfg : Arc < ConfigManager > }
254- #[ async_trait:: async_trait]
255- impl SlashCommand for AutoCompactToggle {
256- fn name ( & self ) -> & ' static str { "autocompact" }
257- async fn run ( & self , args : String ) -> Result < String > {
258- let mut patch = Config :: default ( ) ;
259- match args. trim ( ) {
260- "on" => { patch. compact . auto_enable = true ; }
261- "off" => { patch. compact . auto_enable = false ; }
262- _ => return Err ( anyhow ! ( "usage: /autocompact on|off" ) ) ,
185+ _ => Ok ( format ! ( "builtin:{} {}" , name, serde_json:: to_string( args) ?) ) ,
263186 }
264- self . cfg . apply_runtime_overlay ( patch) ?;
265- Ok ( format ! ( "auto-compact {}" , args. trim( ) ) )
266187 }
267188}
0 commit comments