1#![forbid(unsafe_code)]
7
8use core::fmt;
9
10pub mod agent;
11pub mod fs;
12pub mod git;
13pub mod input;
14pub mod lsp;
15pub mod message;
16pub mod search;
17pub mod task;
18pub mod terminal;
19
20pub use agent::{
21 AgentCapabilities, AgentEvent, AgentRequest, PermissionDecision, PromptCapabilities, StopReason,
22};
23pub use fs::{DirEntryInfo, EntryKind, FsError, FsEvent, FsRequest, FsResult};
24pub use git::{
25 GitBranch, GitBranchStatus, GitChangeKind, GitContextDiff, GitDiffTarget, GitEvent, GitFailure,
26 GitFailureKind, GitFileDiff, GitFileStatus, GitOperation, GitRepositorySnapshot, GitRequest,
27 GitResult,
28};
29pub use lsp::{
30 CodeAction, CompletionItem, Diagnostic, DiagnosticOrigin, DiagnosticSeverity, DocumentSymbol,
31 HoverText, Location, LspEvent, LspFailure, LspFailureKind, LspRequest, LspResult, SymbolKind,
32 SymbolLocation, TextChange, TextEdit, TextPosition, TextRange, WatchedFileChange,
33 WorkspaceEdit,
34};
35pub use message::AppMessage;
36pub use search::{SearchEvent, SearchMatch, SearchMode, SearchRequest};
37pub use task::{Problem, ProblemSeverity, TaskOrigin, TaskSpec, TaskStatus};
38pub use terminal::{
39 AgentTerminalOperation, AgentTerminalResponse, PtyEvent, PtyRequest, TerminalExit,
40 TerminalOwner, TerminalSize, TerminalSpec, TerminalStatus,
41};
42
43macro_rules! id_type {
44 ($(#[$m:meta])* $name:ident) => {
45 $(#[$m])*
46 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
47 pub struct $name(pub u64);
48 impl $name {
49 pub const fn new(v: u64) -> Self { Self(v) }
50 }
51 impl fmt::Display for $name {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 write!(f, concat!(stringify!($name), "({})"), self.0)
54 }
55 }
56 };
57}
58
59id_type!(
60 WorkspaceId
62);
63id_type!(PaneId);
64id_type!(
65 NodeId
68);
69id_type!(BufferId);
70id_type!(DocumentId);
71id_type!(TerminalId);
72id_type!(TerminalGeneration);
73id_type!(TaskRunId);
74id_type!(SearchRequestId);
75id_type!(PreviewRequestId);
76id_type!(LocationRequestId);
77id_type!(GitRequestId);
78id_type!(LspServerId);
79id_type!(LspRequestId);
80id_type!(AgentId);
81id_type!(SessionId);
82id_type!(TurnId);
83id_type!(ProposalId);
84id_type!(PermissionRequestId);
85id_type!(
86 AgentTerminalRequestId
88);
89id_type!(
90 ReadRequestId
97);
98
99#[non_exhaustive]
102#[derive(Debug, Clone, PartialEq, Eq)]
103pub enum Action {
104 FileOpen,
105 FileSave,
106 FileNew,
107 FolderNew,
108 FileRename,
109 FileDelete,
110 WorkspaceSearch,
111 WorkspaceRestoreDrafts,
112 PaneSplitRight,
113 FocusProject,
114 FocusEditor,
115 FocusAgent,
116 TerminalNew,
117 TerminalRun,
118 TerminalFocus,
119 TerminalNext,
120 TerminalPrevious,
121 TerminalRestart,
122 TerminalClose,
123 TerminalCopyMode,
124 GitShow,
125 GitStage,
126 GitUnstage,
127 GitCommit,
128 GitBranchCheckout,
129 GitFetch,
130 GitPull,
131 GitPush,
132 TaskRun,
133 TaskCancel,
134 ProblemsShow,
135 ProblemsNext,
136 ProblemsPrevious,
137 EditorGotoDefinition,
138 LspHover,
139 LspCompletion,
140 LspReferences,
141 LspDocumentSymbols,
142 LspWorkspaceSymbols,
143 LspRename,
144 LspCodeAction,
145 LspFormat,
146 LspRestart,
147 EditorApplyTransaction,
148 AgentSessionNew,
149 AgentPrompt,
150 AgentProposalAccept,
151 AgentProposalReject,
152 HelpShow,
153 ConfigReload,
154}
155
156impl Action {
157 pub fn id(&self) -> &'static str {
159 match self {
160 Action::FileOpen => "file.open",
161 Action::FileSave => "file.save",
162 Action::FileNew => "file.new",
163 Action::FolderNew => "file.new_folder",
164 Action::FileRename => "file.rename",
165 Action::FileDelete => "file.delete",
166 Action::WorkspaceSearch => "workspace.search",
167 Action::WorkspaceRestoreDrafts => "workspace.restore_drafts",
168 Action::PaneSplitRight => "pane.split_right",
169 Action::FocusProject => "focus.project",
170 Action::FocusEditor => "focus.editor",
171 Action::FocusAgent => "focus.agent",
172 Action::TerminalNew => "terminal.new",
173 Action::TerminalRun => "terminal.run",
174 Action::TerminalFocus => "terminal.focus",
175 Action::TerminalNext => "terminal.next",
176 Action::TerminalPrevious => "terminal.previous",
177 Action::TerminalRestart => "terminal.restart",
178 Action::TerminalClose => "terminal.close",
179 Action::TerminalCopyMode => "terminal.copy_mode",
180 Action::GitShow => "git.show",
181 Action::GitStage => "git.stage",
182 Action::GitUnstage => "git.unstage",
183 Action::GitCommit => "git.commit",
184 Action::GitBranchCheckout => "git.branch.checkout",
185 Action::GitFetch => "git.fetch",
186 Action::GitPull => "git.pull",
187 Action::GitPush => "git.push",
188 Action::TaskRun => "task.run",
189 Action::TaskCancel => "task.cancel",
190 Action::ProblemsShow => "problems.show",
191 Action::ProblemsNext => "problems.next",
192 Action::ProblemsPrevious => "problems.previous",
193 Action::EditorGotoDefinition => "editor.goto_definition",
194 Action::LspHover => "lsp.hover",
195 Action::LspCompletion => "lsp.completion",
196 Action::LspReferences => "lsp.references",
197 Action::LspDocumentSymbols => "lsp.symbols.document",
198 Action::LspWorkspaceSymbols => "lsp.symbols.workspace",
199 Action::LspRename => "lsp.rename",
200 Action::LspCodeAction => "lsp.code_action",
201 Action::LspFormat => "lsp.format",
202 Action::LspRestart => "lsp.restart",
203 Action::EditorApplyTransaction => "editor.apply_transaction",
204 Action::AgentSessionNew => "agent.session.new",
205 Action::AgentPrompt => "agent.prompt",
206 Action::AgentProposalAccept => "agent.proposal.accept",
207 Action::AgentProposalReject => "agent.proposal.reject",
208 Action::HelpShow => "help.show",
209 Action::ConfigReload => "config.reload",
210 }
211 }
212
213 pub fn title(&self) -> &'static str {
215 match self {
216 Action::FileOpen => "Open File",
217 Action::FileSave => "Save File",
218 Action::FileNew => "New File",
219 Action::FolderNew => "New Folder",
220 Action::FileRename => "Rename",
221 Action::FileDelete => "Delete",
222 Action::WorkspaceSearch => "Search in Workspace",
223 Action::WorkspaceRestoreDrafts => "Workspace: Restore Drafts",
224 Action::PaneSplitRight => "Split Pane Right",
225 Action::FocusProject => "Focus Project",
226 Action::FocusEditor => "Focus Editor",
227 Action::FocusAgent => "Focus Agent",
228 Action::TerminalNew => "New Terminal",
229 Action::TerminalRun => "Run in Terminal",
230 Action::TerminalFocus => "Focus Terminal",
231 Action::TerminalNext => "Next Terminal",
232 Action::TerminalPrevious => "Previous Terminal",
233 Action::TerminalRestart => "Restart Terminal",
234 Action::TerminalClose => "Close Terminal",
235 Action::TerminalCopyMode => "Terminal Copy Mode",
236 Action::GitShow => "Git: Show Changes",
239 Action::GitStage => "Git: Stage File",
240 Action::GitUnstage => "Git: Unstage File",
241 Action::GitCommit => "Git: Commit",
242 Action::GitBranchCheckout => "Git: Switch Branch",
243 Action::GitFetch => "Git: Fetch",
244 Action::GitPull => "Git: Pull",
245 Action::GitPush => "Git: Push",
246 Action::TaskRun => "Run Task",
247 Action::TaskCancel => "Cancel Task",
248 Action::ProblemsShow => "Show Problems",
249 Action::ProblemsNext => "Next Problem",
250 Action::ProblemsPrevious => "Previous Problem",
251 Action::EditorGotoDefinition => "Code: Go to Definition",
252 Action::LspHover => "Code: Hover",
253 Action::LspCompletion => "Code: Complete",
254 Action::LspReferences => "Code: Find References",
255 Action::LspDocumentSymbols => "Code: Document Symbols",
256 Action::LspWorkspaceSymbols => "Code: Workspace Symbols",
257 Action::LspRename => "Code: Rename Symbol",
258 Action::LspCodeAction => "Code: Quick Fix",
259 Action::LspFormat => "Code: Format Document",
260 Action::LspRestart => "Code: Restart Language Server",
261 Action::EditorApplyTransaction => "Apply Edit",
262 Action::AgentSessionNew => "New Agent Session",
263 Action::AgentPrompt => "Prompt Agent",
264 Action::AgentProposalAccept => "Accept Agent Edit",
265 Action::AgentProposalReject => "Reject Agent Edit",
266 Action::HelpShow => "Help: Keys and Actions",
267 Action::ConfigReload => "Config: Reload",
268 }
269 }
270
271 pub fn agent_needs_permission(&self) -> bool {
273 matches!(
274 self,
275 Action::FileSave
276 | Action::FileNew
277 | Action::FolderNew
278 | Action::FileRename
279 | Action::FileDelete
280 | Action::WorkspaceRestoreDrafts
281 | Action::TerminalRun
282 | Action::GitStage
283 | Action::GitUnstage
284 | Action::GitCommit
285 | Action::GitBranchCheckout
286 | Action::GitFetch
287 | Action::GitPull
288 | Action::GitPush
289 | Action::TaskRun
290 | Action::LspRename
291 | Action::LspCodeAction
292 | Action::LspFormat
293 | Action::LspRestart
294 | Action::EditorApplyTransaction
295 )
296 }
297}
298
299#[derive(Debug, Clone, PartialEq, Eq)]
308pub enum Command {
309 Action(Action),
311 FocusNext,
312 FocusPrev,
313 GrowSidebar,
314 ShrinkSidebar,
315 GrowBottom,
316 ShrinkBottom,
317 OpenPalette,
318 CloseOverlay,
319 Quit,
320 ExplorerNext,
322 ExplorerPrev,
324 ExplorerToggle,
326 ExplorerCollapseOrParent,
328
329 EditorCursorLeft,
335 EditorCursorRight,
336 EditorCursorUp,
337 EditorCursorDown,
338 EditorLineStart,
339 EditorLineEnd,
340 EditorInsertNewline,
341 EditorBackspace,
343 EditorDeleteForward,
345 EditorUndo,
346 EditorRedo,
347 EditorFind,
348 EditorFindNext,
349 EditorFindPrev,
350 EditorReplace,
351 EditorNextTab,
352 EditorPrevTab,
353 EditorCloseTab,
354
355 TerminalCopyLeft,
361 TerminalCopyRight,
362 TerminalCopyUp,
363 TerminalCopyDown,
364 TerminalCopyExtendLeft,
365 TerminalCopyExtendRight,
366 TerminalCopyExtendUp,
367 TerminalCopyExtendDown,
368 TerminalCopyPageUp,
369 TerminalCopyPageDown,
370 TerminalCopyConfirm,
371 TerminalCopyCancel,
372 TerminalScrollUp,
379 TerminalScrollDown,
380
381 AgentScrollUp,
388 AgentScrollDown,
389 AgentAllowOnce,
390 AgentAllowAlways,
391 AgentDeny,
392}
393
394impl Command {
395 pub fn is_pane_focus(&self) -> bool {
402 matches!(
403 self,
404 Command::Action(
405 Action::FocusProject
406 | Action::FocusEditor
407 | Action::FocusAgent
408 | Action::TerminalFocus
409 )
410 )
411 }
412}
413
414#[derive(Debug, Default)]
424pub struct ActionRegistry {
425 actions: Vec<Action>,
426}
427
428impl ActionRegistry {
429 pub fn with_defaults() -> Self {
430 use Action::*;
431 Self {
432 actions: vec![
433 FileOpen,
434 FileSave,
435 FileNew,
436 FolderNew,
437 FileRename,
438 FileDelete,
439 WorkspaceSearch,
440 WorkspaceRestoreDrafts,
441 PaneSplitRight,
442 FocusProject,
443 FocusEditor,
444 FocusAgent,
445 TerminalNew,
446 TerminalRun,
447 TerminalFocus,
448 TerminalNext,
449 TerminalPrevious,
450 TerminalRestart,
451 TerminalClose,
452 TerminalCopyMode,
453 GitShow,
454 GitStage,
455 GitUnstage,
456 GitCommit,
457 GitBranchCheckout,
458 GitFetch,
459 GitPull,
460 GitPush,
461 TaskRun,
462 TaskCancel,
463 ProblemsShow,
464 ProblemsNext,
465 ProblemsPrevious,
466 EditorGotoDefinition,
467 LspHover,
468 LspCompletion,
469 LspReferences,
470 LspDocumentSymbols,
471 LspWorkspaceSymbols,
472 LspRename,
473 LspCodeAction,
474 LspFormat,
475 LspRestart,
476 EditorApplyTransaction,
477 AgentSessionNew,
478 AgentPrompt,
479 AgentProposalAccept,
480 AgentProposalReject,
481 HelpShow,
482 ConfigReload,
483 ],
484 }
485 }
486 pub fn len(&self) -> usize {
487 self.actions.len()
488 }
489 pub fn is_empty(&self) -> bool {
490 self.actions.is_empty()
491 }
492 pub fn ids(&self) -> impl Iterator<Item = &'static str> + '_ {
493 self.actions.iter().map(Action::id)
494 }
495 pub fn actions(&self) -> &[Action] {
496 &self.actions
497 }
498}
499
500#[cfg(test)]
501mod tests {
502 use super::*;
503
504 #[test]
505 fn registry_exposes_stable_action_ids() {
506 let reg = ActionRegistry::with_defaults();
507 assert_eq!(reg.len(), 50);
508 assert!(reg.ids().any(|id| id == "agent.prompt"));
509 assert!(reg.ids().any(|id| id == "focus.project"));
510 assert!(reg.ids().all(|id| id.contains('.')));
511 }
512
513 #[test]
514 fn phase_10_help_action_is_stable_and_read_only() {
515 assert_eq!(Action::HelpShow.id(), "help.show");
516 assert_eq!(Action::HelpShow.title(), "Help: Keys and Actions");
517 assert!(!Action::HelpShow.agent_needs_permission());
518 assert!(ActionRegistry::with_defaults().actions().contains(&Action::HelpShow));
519 }
520
521 #[test]
522 fn phase_10_draft_restore_action_is_stable_and_permission_gated() {
523 assert_eq!(Action::WorkspaceRestoreDrafts.id(), "workspace.restore_drafts");
524 assert_eq!(Action::WorkspaceRestoreDrafts.title(), "Workspace: Restore Drafts");
525 assert!(Action::WorkspaceRestoreDrafts.agent_needs_permission());
526 assert!(ActionRegistry::with_defaults()
527 .actions()
528 .contains(&Action::WorkspaceRestoreDrafts));
529 }
530
531 #[test]
532 fn phase_07_actions_have_stable_ids_and_permissions() {
533 assert_eq!(Action::EditorGotoDefinition.id(), "editor.goto_definition");
534 assert_eq!(Action::LspHover.id(), "lsp.hover");
535 assert_eq!(Action::LspCompletion.id(), "lsp.completion");
536 assert_eq!(Action::LspReferences.id(), "lsp.references");
537 assert_eq!(Action::LspDocumentSymbols.id(), "lsp.symbols.document");
538 assert_eq!(Action::LspWorkspaceSymbols.id(), "lsp.symbols.workspace");
539 assert_eq!(Action::LspRename.id(), "lsp.rename");
540 assert_eq!(Action::LspCodeAction.id(), "lsp.code_action");
541 assert_eq!(Action::LspFormat.id(), "lsp.format");
542 assert_eq!(Action::LspRestart.id(), "lsp.restart");
543
544 for action in [
546 Action::EditorGotoDefinition,
547 Action::LspHover,
548 Action::LspCompletion,
549 Action::LspReferences,
550 Action::LspDocumentSymbols,
551 Action::LspWorkspaceSymbols,
552 Action::LspRename,
553 Action::LspCodeAction,
554 Action::LspFormat,
555 Action::LspRestart,
556 ] {
557 assert!(action.title().starts_with("Code: "), "{}", action.id());
558 }
559
560 for action in [
562 Action::EditorGotoDefinition,
563 Action::LspHover,
564 Action::LspCompletion,
565 Action::LspReferences,
566 Action::LspDocumentSymbols,
567 Action::LspWorkspaceSymbols,
568 ] {
569 assert!(!action.agent_needs_permission(), "{}", action.id());
570 }
571 for action in
572 [Action::LspRename, Action::LspCodeAction, Action::LspFormat, Action::LspRestart]
573 {
574 assert!(action.agent_needs_permission(), "{}", action.id());
575 }
576 }
577
578 #[test]
579 fn every_action_has_a_title() {
580 for a in ActionRegistry::with_defaults().actions() {
581 assert!(!a.title().is_empty());
582 }
583 }
584
585 #[test]
586 fn write_actions_are_permission_gated_for_agents() {
587 assert!(Action::GitCommit.agent_needs_permission());
588 assert!(!Action::EditorGotoDefinition.agent_needs_permission());
589 }
590
591 #[test]
592 fn terminal_actions_have_stable_ids() {
593 assert_eq!(Action::TerminalFocus.id(), "terminal.focus");
594 assert_eq!(Action::TerminalNext.id(), "terminal.next");
595 assert_eq!(Action::TerminalPrevious.id(), "terminal.previous");
596 assert_eq!(Action::TerminalRestart.id(), "terminal.restart");
597 assert_eq!(Action::TerminalClose.id(), "terminal.close");
598 assert_eq!(Action::TerminalCopyMode.id(), "terminal.copy_mode");
599 }
600
601 #[test]
602 fn agent_terminal_run_is_permission_gated() {
603 assert!(Action::TerminalRun.agent_needs_permission());
604 }
605
606 #[test]
607 fn phase_05_actions_have_stable_ids_and_permissions() {
608 assert_eq!(Action::TaskCancel.id(), "task.cancel");
609 assert_eq!(Action::ProblemsShow.id(), "problems.show");
610 assert_eq!(Action::ProblemsNext.id(), "problems.next");
611 assert_eq!(Action::ProblemsPrevious.id(), "problems.previous");
612 assert!(Action::TaskRun.agent_needs_permission());
613 assert!(!Action::TaskCancel.agent_needs_permission());
614 }
615
616 #[test]
617 fn phase_06_actions_have_stable_ids_and_permissions() {
618 assert_eq!(Action::GitShow.id(), "git.show");
619 assert_eq!(Action::GitStage.id(), "git.stage");
620 assert_eq!(Action::GitUnstage.id(), "git.unstage");
621 assert_eq!(Action::GitCommit.id(), "git.commit");
622 assert_eq!(Action::GitBranchCheckout.id(), "git.branch.checkout");
623 assert_eq!(Action::GitFetch.id(), "git.fetch");
624 assert_eq!(Action::GitPull.id(), "git.pull");
625 assert_eq!(Action::GitPush.id(), "git.push");
626 assert!(!Action::GitShow.agent_needs_permission());
627 for action in [
629 Action::GitShow,
630 Action::GitStage,
631 Action::GitUnstage,
632 Action::GitCommit,
633 Action::GitBranchCheckout,
634 Action::GitFetch,
635 Action::GitPull,
636 Action::GitPush,
637 ] {
638 assert!(action.title().starts_with("Git: "), "{}", action.title());
639 }
640 for action in [
641 Action::GitStage,
642 Action::GitUnstage,
643 Action::GitCommit,
644 Action::GitBranchCheckout,
645 Action::GitFetch,
646 Action::GitPull,
647 Action::GitPush,
648 ] {
649 assert!(action.agent_needs_permission(), "{}", action.id());
650 }
651 }
652}