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