Skip to main content

termesh_core/
lib.rs

1//! Shared foundation: stable typed IDs, the action registry, input, commands, errors.
2//!
3//! The **action registry** here is the keystone of the whole design (ARCHITECTURE.md §3):
4//! the *same* named actions back the keymap, the command palette, and the agent's
5//! ACP tool surface. Build this well and "agent-native" mostly falls out.
6#![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    /// A single open project/workspace root.
61    WorkspaceId
62);
63id_type!(PaneId);
64id_type!(
65    /// A node in the file-explorer tree. Identity is the id, never the path — paths
66    /// move under rename and watch events (ARCHITECTURE.md §7.3).
67    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    /// One ACP terminal method awaiting a model/service response.
87    AgentTerminalRequestId
88);
89id_type!(
90    /// One `fs/read_text_file` call from the agent.
91    ///
92    /// Correlation is by id, never by path: an agent may read the same file twice in a
93    /// turn (read, edit, re-read to confirm), and keying on the path would let the second
94    /// call overwrite the first — leaving one of them unanswered and the agent blocked
95    /// forever on a reply that never comes.
96    ReadRequestId
97);
98
99/// A named, invocable action — the shared vocabulary of the keymap, command palette,
100/// plugins, and the agent tool schema exposed over ACP (§3, §6.2).
101#[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    /// The stable string id (also the tool name advertised to ACP agents).
158    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    /// Human-friendly label shown in menus and the command palette.
214    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            // One visible family: the palette is a flat list, so the shared `Git: ` prefix
237            // is what makes these eight read as a group (ADR-0010 §5).
238            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    /// Whether an agent must ask permission before this runs (write/run/commit). §9.4
272    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/// Everything the shell can be told to do. The keymap maps a [`input::KeyChord`] to one
300/// of these, and the palette dispatches [`Command::Action`] — one dispatch path for
301/// keyboard and palette alike (ARCHITECTURE.md §3, §7.1).
302///
303/// The agent does *not* dispatch these. ADR-0009 found that stable ACP has no portable
304/// way for a client to register its own tools, so the agent's reach into the workspace
305/// is the context it is given and the permission-gated operations ACP itself defines —
306/// not this enum.
307#[derive(Debug, Clone, PartialEq, Eq)]
308pub enum Command {
309    /// Invoke a registry action (feature-level; shown in the palette).
310    Action(Action),
311    FocusNext,
312    FocusPrev,
313    GrowSidebar,
314    ShrinkSidebar,
315    GrowBottom,
316    ShrinkBottom,
317    OpenPalette,
318    CloseOverlay,
319    Quit,
320    /// Move the file-explorer selection down one visible row.
321    ExplorerNext,
322    /// Move the file-explorer selection up one visible row.
323    ExplorerPrev,
324    /// Expand or collapse the selected directory.
325    ExplorerToggle,
326    /// Collapse the selection, or step to its parent when already collapsed.
327    ExplorerCollapseOrParent,
328
329    // --- editor (Phase 03) --------------------------------------------------------
330    //
331    // Cursor motion and the named text operations are commands so they stay
332    // remappable and reachable from one dispatch path. Only literal character entry
333    // is not a command — there is no finite set of "type an x" actions.
334    EditorCursorLeft,
335    EditorCursorRight,
336    EditorCursorUp,
337    EditorCursorDown,
338    EditorLineStart,
339    EditorLineEnd,
340    EditorInsertNewline,
341    /// Delete backwards from the cursor, or the selection if there is one.
342    EditorBackspace,
343    /// Delete forwards from the cursor.
344    EditorDeleteForward,
345    EditorUndo,
346    EditorRedo,
347    EditorFind,
348    EditorFindNext,
349    EditorFindPrev,
350    EditorReplace,
351    EditorNextTab,
352    EditorPrevTab,
353    EditorCloseTab,
354
355    // --- terminal copy mode (Phase 04) ------------------------------------------
356    //
357    // Literal terminal input remains data rather than a finite command vocabulary.
358    // These commands exist only for model-owned scrollback selection while copy mode
359    // is active (ADR-0008 §3).
360    TerminalCopyLeft,
361    TerminalCopyRight,
362    TerminalCopyUp,
363    TerminalCopyDown,
364    TerminalCopyExtendLeft,
365    TerminalCopyExtendRight,
366    TerminalCopyExtendUp,
367    TerminalCopyExtendDown,
368    TerminalCopyPageUp,
369    TerminalCopyPageDown,
370    TerminalCopyConfirm,
371    TerminalCopyCancel,
372    /// Move the terminal viewport back through scrollback, without entering copy mode.
373    ///
374    /// A command rather than an action, for the same reason the agent's scroll is: it
375    /// moves a viewport, it is not a standing capability. Copy mode selects text and
376    /// takes over the keyboard to do it; reading what just scrolled past should not
377    /// require either.
378    TerminalScrollUp,
379    TerminalScrollDown,
380
381    // --- agent review (Phase 03) --------------------------------------------------
382    //
383    // Accept/reject are registry *actions* (they are user-invocable features, so the
384    // palette and the agent's own tool surface reach them). Permission answers are
385    // commands: they are a response to a specific prompt, not a standing capability.
386    /// Scroll the agent transcript back through older turns.
387    AgentScrollUp,
388    AgentScrollDown,
389    AgentAllowOnce,
390    AgentAllowAlways,
391    AgentDeny,
392}
393
394impl Command {
395    /// Whether this moves focus to one named pane.
396    ///
397    /// These are the only commands a focused terminal still honours. It swallows every
398    /// other chord so the shell gets a real keyboard (ADR-0008 §3) — which is also why
399    /// the Terminal cannot sit in the `FocusNext` ring: it would capture the very Tab
400    /// that is supposed to carry you out of it, making the pane a one-way door.
401    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/// The one command surface: every user-invocable behaviour is an [`Action`] here, and
415/// both the keymap and the palette are built from this list.
416///
417/// It stayed a plain list of variants rather than growing handlers or context
418/// predicates. Dispatch lives in the application model, binding lives in
419/// `termesh-config`, and neither needs the registry to own a callback to find its way
420/// here. Dynamic registration was ruled out rather than deferred: ADR-0009 found that
421/// stable ACP has no portable client-owned custom-tool registration, so there is no
422/// caller for it.
423#[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        // One visible family in the flat palette.
545        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        // Reads are free; anything that edits a buffer or starts a process is gated.
561        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        // The palette is one flat list; a shared prefix is the only grouping it has.
628        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}