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