Skip to main content

oo_ide/
operation.rs

1use std::borrow::Cow;
2use std::collections::HashMap;
3use std::path::PathBuf;
4
5use crate::prelude::*;
6
7use app_state::ScreenKind;
8use commands::ArgValue;
9use editor::position::Position;
10use crate::issue_registry::{IssueId, NewIssue, Severity};
11use crate::persistent_issues::PersistentNewIssue;
12use crate::task_registry::{TaskId, TaskKey, TaskStatus, TaskTrigger};
13
14#[derive(Debug, Clone)]
15pub enum ExtensionConfigOp {
16    MoveUp,
17    MoveDown,
18    Select(usize),
19    SetStatus(usize),
20}
21// View-local operations for FileSelector — defined here to avoid a circular
22// dependency between operation.rs and views/file_selector.rs.
23
24#[derive(Debug, Clone)]
25pub enum FileSelectorOp {
26    FilterInput(crate::widgets::input_field::InputFieldOp),
27    SwitchPane,
28    CollapseOrLeft,
29    ExpandOrRight,
30    ToggleDir,
31    /// Delivered by the async search task with the scored+sorted results.
32    /// The `generation` field is used to discard results from superseded
33    /// queries (user typed again before this result arrived).
34    SetResults {
35        generation: u64,
36        paths: Vec<PathBuf>,
37    },
38    /// Delivered by the async directory-read task when a lazy directory
39    /// expansion completes.  Each entry is `(absolute_path, is_dir)`.
40    DirLoaded {
41        path: PathBuf,
42        entries: Vec<(PathBuf, bool)>,
43    },
44}
45
46// View-local operations for LogView.
47
48#[derive(Debug, Clone)]
49pub enum LogViewOp {
50    FilterInput(crate::widgets::input_field::InputFieldOp),
51}
52
53// View-local operations for TaskArchiveView.
54
55#[derive(Debug, Clone)]
56pub enum TaskArchiveOp {
57    FilterInput(crate::widgets::input_field::InputFieldOp),
58    /// Open the log for the highlighted task.
59    OpenSelected,
60    /// Cancel the highlighted running task.
61    CancelSelected,
62    /// Sent by app.rs when the task list changes so the view can clamp its cursor.
63    TaskUpdated { new_len: usize },
64    /// Close the inline detail pane and return to the list.
65    CloseDetail,
66    /// Populate the inline detail pane with loaded log lines.
67    ShowDetail {
68        task_id: TaskId,
69        title:   String,
70        status:  TaskStatus,
71        lines:   Vec<String>,
72    },
73}
74
75// ---------------------------------------------------------------------------
76// View-local operations for IssueView.
77// ---------------------------------------------------------------------------
78
79/// View-local operations for crate::views::issue_view::IssueView.
80#[derive(Debug, Clone)]
81pub enum IssueViewOp {
82    FilterInput(crate::widgets::input_field::InputFieldOp),
83    /// Enter — open detail for the selected issue.
84    OpenSelected,
85    /// `r` — resolve the currently selected issue.
86    ResolveSelected,
87    /// `d` — dismiss the currently selected issue.
88    DismissSelected,
89    /// `h` — toggle visibility of dismissed/resolved issues.
90    ToggleShowDismissed,
91    /// `0`–`4` — filter by severity; `None` = show all.
92    SetSeverityFilter(Option<Severity>),
93    /// Esc in Detail mode — back to List.
94    CloseDetail,
95    /// Sent when IssueAdded / IssueRemoved / IssueUpdated arrive, so the view
96    /// can clamp its cursor.
97    RegistryChanged,
98}
99
100// View-local operations for CommandRunner — same pattern as FileSelectorOp.
101
102#[derive(Debug, Clone)]
103pub enum CommandSelectorOp {
104    FilterInput(crate::widgets::input_field::InputFieldOp),
105    SwitchPane,
106    CollapseOrLeft,
107    ExpandOrRight,
108    /// Confirm selection (Enter) from the selector pane.
109    Confirm,
110    /// In the arg-form view: move focus to next field.
111    ArgNext,
112    /// In the arg-form view: move focus to previous field.
113    ArgPrev,
114    /// In the arg-form view: set the focused field's value.
115    ArgChanged(String),
116    /// Cancel the arg form and go back to selector.
117    ArgCancel,
118}
119
120// ---------------------------------------------------------------------------
121// View-local operations for CommitWindow.
122// ---------------------------------------------------------------------------
123
124#[derive(Debug, Clone)]
125pub enum CommitOp {
126    // ── UI trigger ops (view → app → spawn_blocking) ─────────────────────
127    /// Switch between Commit and Diff tabs.
128    SwitchTab,
129    // Commit tab
130    /// Cycle focus: Staged → Unstaged → Message.
131    SwitchPane,
132    /// Stage or unstage the selected file.
133    ToggleFileStage,
134    /// Set the commit message text programmatically (e.g., clear or load a draft).
135    MessageChanged(String),
136    /// Keystroke editing ops for the message field.
137    MessageInsertChar(char),
138    MessageInsertNewline,
139    MessageBackspace,
140    MessageCursorLeft,
141    MessageCursorRight,
142    MessageCursorUp,
143    MessageCursorDown,
144    /// Open the new-branch input (clears any previous value).
145    BranchInputOpen,
146    /// Cancel the branch-name input without creating a branch.
147    BranchInputCancel,
148    /// Set the branch-name input text.
149    BranchChanged(String),
150    /// Confirm new branch from branch_input.
151    BranchCreate,
152    /// Open the branch selector popup (shows existing branches).
153    BranchSelectorOpen,
154    /// Close the branch selector popup without action.
155    BranchSelectorCancel,
156    /// Checkout the given branch (handled by app.rs with git ops).
157    BranchCheckout(String),
158    /// Execute commit (guarded: message must be non-empty).
159    Confirm,
160    // Diff tab
161    /// Toggle focus between file list and diff lines panels in the diff tab.
162    DiffFocusToggle,
163    /// Toggle staged_diff_lines for the selected diff line (local, no git call).
164    /// On a hunk header row this toggles fold instead.
165    ToggleLineStage,
166    /// Revert the selected diff line.
167    RevertLine,
168    /// Request diff for a file.
169    LoadDiff {
170        path: PathBuf,
171        staged: bool,
172    },
173    /// Async result ops (spawn_blocking → op_tx → handle_operation)
174    StatusLoaded {
175        staged: Vec<crate::vcs::StatusEntry>,
176        unstaged: Vec<crate::vcs::StatusEntry>,
177        branches: Vec<String>,
178        current_branch: String,
179    },
180    DiffLoaded {
181        path: PathBuf,
182        /// Whether this diff is staged (HEAD↔index) or unstaged (index↔workdir).
183        staged: bool,
184        lines: Vec<crate::vcs::DiffLine>,
185    },
186    GitOpSuccess(String),
187    GitOpError(String),
188}
189
190// LSP-related types and view-local operations for the editor.
191
192#[derive(Debug, Clone)]
193pub struct LspCompletionItem {
194    pub label: String,
195    pub kind: Option<u8>,
196    pub detail: Option<String>,
197    pub insert_text: Option<String>,
198}
199
200#[derive(Debug, Clone)]
201pub struct LspLocation {
202    pub path: PathBuf,
203    pub row: usize,
204    pub col: usize,
205}
206
207#[derive(Debug, Clone)]
208pub enum ClipOp {
209    /// Copy active selection (or current line if empty) to clipboard + history.
210    Copy,
211    /// Cut active selection (or current line if empty) to clipboard + history.
212    Cut,
213    /// Paste from system clipboard at cursor.
214    Paste,
215    /// Open the clipboard history picker dialog.
216    OpenHistory,
217    /// Navigate history picker up.
218    HistoryUp,
219    /// Navigate history picker down.
220    HistoryDown,
221    /// Paste the selected history item and close picker.
222    HistoryConfirm,
223    /// Close the history picker without pasting.
224    HistoryClose,
225}
226
227#[derive(Debug, Clone)]
228pub enum SelectionOp {
229    /// Extend active selection head (Shift+arrow).
230    Extend { head: Position },
231    /// Select the entire buffer.
232    SelectAll,
233    /// Clear selection (Esc).
234    Clear,
235}
236
237#[derive(Debug, Clone)]
238pub enum GoToLineOp {
239    /// Open the go-to-line bar (or focus it if already open).
240    Open,
241    /// Close the bar without committing.
242    Close,
243    /// Commit the current line number and close.
244    Confirm,
245    /// Feed a key to the line-number input field.
246    Input(crate::widgets::input_field::InputFieldOp),
247}
248
249#[derive(Debug, Clone)]
250pub enum SearchOp {
251    Open {
252        replace: bool,
253    },
254    Close,
255    QueryInput(crate::widgets::input_field::InputFieldOp),
256    ReplacementInput(crate::widgets::input_field::InputFieldOp),
257    ToggleIgnoreCase,
258    ToggleRegex,
259    ToggleSmartCase,
260    /// Toggle query ↔ replacement input focus.
261    FocusSwitch,
262    NextMatch,
263    PrevMatch,
264    ReplaceOne,
265    ReplaceAll,
266    /// Stream one project-wide file result into the editor search state.
267    /// Sent by the async project-search task for every file that contains matches.
268    AddProjectResult { file: PathBuf, result: MatchSpan },
269    /// Clear all project-wide file results — sent before a new search starts.
270    ClearProjectResults,
271    /// Select a file in the project-search file list (0-based index into `files`).
272    SelectFile(usize),
273    /// Scroll the match-results panel by N rows (positive = down, negative = up).
274    ScrollMatchPanel(i8),
275    /// Keystroke routed to the include-glob filter field (Expanded mode only).
276    IncludeGlobInput(crate::widgets::input_field::InputFieldOp),
277    /// Keystroke routed to the exclude-glob filter field (Expanded mode only).
278    ExcludeGlobInput(crate::widgets::input_field::InputFieldOp),
279}
280
281#[derive(Debug, Clone)]
282pub enum LspOp {
283    CompletionResponse {
284        items: Vec<LspCompletionItem>,
285        trigger: Option<Position>,
286        version: Option<i32>,
287    },
288    HoverResponse(Option<String>),
289    CompletionMoveUp,
290    CompletionMoveDown,
291    CompletionConfirm,
292    CompletionDismiss,
293    HoverDismiss,
294}
295
296// ---------------------------------------------------------------------------
297// Project-wide search & replace
298// ---------------------------------------------------------------------------
299
300/// A single match span within a file line.
301#[derive(Debug, Clone)]
302pub struct MatchSpan {
303    /// 0-based line index.
304    pub line: usize,
305    /// Byte offset of match start within the line.
306    pub byte_start: usize,
307    /// Byte offset of match end within the line.
308    pub byte_end: usize,
309    /// Full text of the line (no newline).
310    pub line_text: String,
311}
312
313/// All matches found in one file.
314#[derive(Debug, Clone)]
315pub struct FileMatchResult {
316    pub path: PathBuf,
317    pub matches: Vec<MatchSpan>,
318}
319
320// ---------------------------------------------------------------------------
321// Git History Viewer view-local operations
322// ---------------------------------------------------------------------------
323
324#[derive(Debug, Clone)]
325pub enum GitHistoryOp {
326    /// Keystroke routed to the filter input field.
327    FilterInput(crate::widgets::input_field::InputFieldOp),
328    /// Async result: commits loaded for the current filter + generation.
329    CommitsLoaded {
330        generation: u64,
331        commits: Vec<crate::vcs::CommitInfo>,
332    },
333    /// User confirmed a revision selection (Enter on revision list).
334    /// Intercepted by app.rs which spawns file-list + diff loading.
335    SelectRevision,
336    /// Async result: file list for the selected revision.
337    FilesLoaded {
338        oid: String,
339        files: Vec<crate::vcs::FileChange>,
340    },
341    /// User navigated to or confirmed a file selection.
342    /// Intercepted by app.rs which spawns diff loading.
343    SelectFile,
344    /// Async result: diff for the selected revision + file.
345    DiffLoaded {
346        lines: Vec<crate::vcs::DiffLine>,
347    },
348    /// An async git task failed.
349    LoadError(String),
350}
351
352// ---------------------------------------------------------------------------
353// Terminal view-local operations
354// ---------------------------------------------------------------------------
355
356#[derive(Debug, Clone)]
357pub enum TerminalOp {
358    /// Raw bytes received from the PTY process (sent by the async read task).
359    Output { id: u64, data: Vec<u8> },
360    /// Completed styled lines produced from PTY output by the async read task.
361    ///
362    /// Sent alongside `Output` so the main thread can maintain a styled
363    /// scrollback buffer per tab (enabling state persistence and reuse of
364    /// `DiagnosticsExtractor::extract_from_line`).
365    AppendScrollback { id: u64, lines: Vec<crate::vt_parser::StyledLine> },
366    /// Open a new tab. `command` defaults to the configured shell if `None`.
367    NewTab { command: Option<String> },
368    /// Close a tab by ID. If it was the last tab, a new shell is spawned.
369    CloseTab { id: u64 },
370    /// Switch to the tab at the given index (clamped to valid range).
371    SwitchToTab { index: usize },
372    /// Switch to the next tab (wraps around).
373    NextTab,
374    /// Switch to the previous tab (wraps around).
375    PrevTab,
376    /// Reset scroll to the bottom of the active tab.
377    ScrollReset,
378    /// Open the context menu for the given tab.
379    OpenMenu { id: u64 },
380    /// Close the context menu without taking action.
381    CloseMenu,
382    /// Confirm the highlighted context-menu item.
383    MenuConfirm,
384    /// Open the inline rename input for the given tab directly.
385    OpenRename { id: u64 },
386    /// Clear the screen of the given tab (sends VT clear sequence to PTY).
387    ClearTab { id: u64 },
388    /// Set the rename input text.
389    RenameChanged(String),
390    /// Commit the rename.
391    RenameConfirm,
392    /// Cancel the rename.
393    RenameCancel,
394    /// Open the tab selector popup (keyboard/mouse-navigable list of all tabs).
395    OpenTabSelector,
396    /// Terminal dimensions changed — resize all PTY tabs.
397    Resize { cols: u16, rows: u16 },
398    /// The PTY child process has exited.
399    ProcessExited { id: u64 },
400}
401
402#[derive(Debug, Clone)]
403pub enum SearchReplaceOp {
404    //  Input field changes
405    QueryChanged(String),
406    ReplaceChanged(String),
407    IncludeGlobChanged(String),
408    ExcludeGlobChanged(String),
409    //  Toggle options
410    ToggleRegex,
411    ToggleCase,
412    ToggleSmartCase,
413    //  Tree navigation
414    TreeExpandOrEnter,
415    TreeCollapseOrLeave,
416    //  Replace actions
417    ReplaceCurrentFile,
418    ReplaceAll,
419    //  Undo / redo
420    Undo,
421    Redo,
422    //  Async results
423    SearchResultsReady {
424        results: Vec<FileMatchResult>,
425        generation: u64,
426    },
427    ReplaceComplete {
428        path: PathBuf,
429        replaced_count: usize,
430    },
431    AsyncError(String),
432    ProgressUpdate {
433        scanned: usize,
434        total: usize,
435    },
436}
437
438#[derive(Debug, Clone)]
439pub enum Operation {
440    // Buffer mutations
441    InsertText {
442        path: Option<PathBuf>,
443        #[allow(dead_code)]
444        cursor: Position,
445        text: String,
446    },
447    /// Bracketed paste (or fallback-heuristic detected paste) delivered to the
448    /// active editor. Unlike `InsertText`, the full multi-line string is inserted
449    /// at once via `insert_text_raw` and also pushed to the clipboard register.
450    PasteText {
451        path: Option<PathBuf>,
452        text: String,
453    },
454    DeleteText {
455        path: Option<PathBuf>,
456        #[allow(dead_code)]
457        cursor: Position,
458        #[allow(dead_code)]
459        len: usize,
460    },
461    DeleteForward {
462        path: Option<PathBuf>,
463        #[allow(dead_code)]
464        cursor: Position,
465    },
466    DeleteWordBackward {
467        path: Option<PathBuf>,
468    },
469    DeleteWordForward {
470        path: Option<PathBuf>,
471    },
472    IndentLines {
473        path: Option<PathBuf>,
474    },
475    UnindentLines {
476        path: Option<PathBuf>,
477    },
478    DeleteLine {
479        path: Option<PathBuf>,
480    },
481    /// Replace the text from `start` to `end` (same line, byte offsets) with `text`.
482    /// Used by CompletionConfirm to delete the filter prefix before inserting.
483    ReplaceRange {
484        path: Option<PathBuf>,
485        start: Position,
486        end: Position,
487        text: String,
488    },
489    MoveCursor {
490        path: Option<PathBuf>,
491        cursor: Position,
492    },
493    Undo {
494        path: Option<PathBuf>,
495    },
496    Redo {
497        path: Option<PathBuf>,
498    },
499    ToggleFold {
500        path: Option<PathBuf>,
501        line: usize,
502    },
503    ToggleMarker {
504        path: Option<PathBuf>,
505        line: usize,
506    },
507    ToggleWordWrap,
508
509    // File lifecycle
510    OpenFile {
511        path: PathBuf,
512    },
513    /// Open an external URL in the system default browser.
514    OpenUrl {
515        url: String,
516    },
517    SaveFile {
518        path: PathBuf,
519    },
520    ReloadFromDisk {
521        path: PathBuf,
522        accept_external: bool,
523    },
524
525    // App / navigation
526    SwitchScreen {
527        to: ScreenKind,
528    },
529    /// Close the currently open modal/temporary view.
530    ///
531    /// If the current view is already primary, this is a no-op.
532    CloseModal,
533    Quit,
534
535    // Focus navigation — handled by whichever view is active
536    Focus(crate::widgets::focusable::FocusOp),
537
538    // Generic list/tree/scroll navigation — dispatched to the active view.
539    // Each view interprets these according to its own focus state.
540    NavigateUp,
541    NavigateDown,
542    NavigatePageUp,
543    NavigatePageDown,
544    NavigateHome,
545    NavigateEnd,
546
547    // View-local ops that don't cross view boundaries
548    FileSelectorLocal(FileSelectorOp),
549    CommandSelectorLocal(CommandSelectorOp),
550    LogViewLocal(LogViewOp),
551    CommitLocal(CommitOp),
552    GitHistoryLocal(GitHistoryOp),
553    ExtensionConfig(ExtensionConfigOp),
554
555    // LSP trigger ops (handled in app.rs, forwarded to LspClient)
556    LspTriggerCompletion {
557        #[allow(dead_code)]
558        path: Option<PathBuf>,
559    },
560    LspTriggerHover {
561        #[allow(dead_code)]
562        path: Option<PathBuf>,
563    },
564    LspGoToDefinition {
565        #[allow(dead_code)]
566        path: Option<PathBuf>,
567    },
568    /// Result from a go-to-definition request, dispatched back into the queue.
569    LspGoToDefinitionResult(Vec<LspLocation>),
570    /// LSP editor-local ops (dropdown navigation, response delivery).
571    LspLocal(LspOp),
572    /// Search/replace bar ops (editor-local).
573    SearchLocal(SearchOp),
574    /// Go-to-line bar ops (editor-local).
575    GoToLineLocal(GoToLineOp),
576    /// Selection management ops (editor-local).
577    SelectionLocal(SelectionOp),
578    /// Clipboard ops (editor-local).
579    ClipboardLocal(ClipOp),
580    /// Project-wide search & replace ops.
581    SearchReplaceLocal(SearchReplaceOp),
582
583    /// Execute a registered command by id (with pre-filled args).
584    RunCommand {
585        id: crate::commands::CommandId,
586        args: HashMap<String, ArgValue>,
587    },
588
589    /// Open a generic context menu at `(x, y)` with the given items.
590    ///
591    /// Items are `(label, command_id)` pairs.  `app.rs` stores the menu on
592    /// [`crate::app_state::AppState`], intercepts input, and dispatches the selected command.
593    OpenContextMenu {
594        // Each item: (label, command_id, enabled_override)
595        // - enabled_override: `Some(bool)` to explicitly set enabled state,
596        //   or `None` to let the runtime compute a sensible default.
597        items: Vec<(String, crate::commands::CommandId, Option<bool>)>,
598        x: u16,
599        y: u16,
600    },
601
602    /// Close the currently open context menu without selecting any item.
603    CloseContextMenu,
604
605    // Escape hatch for plugins
606    #[allow(dead_code)]
607    Generic {
608        source: Cow<'static, str>,
609        name: Cow<'static, str>,
610        payload: HashMap<String, ArgValue>,
611    },
612
613    // Extension system
614    /// Broadcast a named event to all active extensions.
615    ExtensionEvent {
616        name: String,
617        payload: HashMap<String, String>,
618    },
619    /// Display a user-visible notification (shown in status bar).
620    ShowNotification {
621        message: String,
622        level: NotificationLevel,
623    },
624    /// Update the IDE status bar slot with custom text.
625    UpdateStatusBar {
626        text: String,
627        #[allow(unused)]
628        slot: StatusSlot,
629    },
630    /// Request a terminal be created running the given command.
631    CreateTerminal {
632        command: String,
633    },
634    /// Send raw bytes to the PTY of the terminal tab with the given ID.
635    TerminalInput {
636        id: u64,
637        data: Vec<u8>,
638    },
639    /// Terminal view-local operations.
640    TerminalLocal(TerminalOp),
641    /// Show a picker (like command palette) populated with arbitrary items.
642    ShowPicker {
643        title: String,
644        items: Vec<PickerItem>,
645    },
646
647    /// Deliver pre-computed syntax-highlight spans for an editor buffer.
648    ///
649    /// Sent by the background highlight task; handled in `app.rs`, which stores
650    /// the result in `crate::views::editor::EditorView::highlight_cache` when the version still
651    /// matches the live buffer.  The next frame then renders without blocking.
652    SetEditorHighlights {
653        path: Option<std::path::PathBuf>,
654        version: editor::buffer::Version,
655        start_line: usize,
656        generation: u64,
657        spans: Vec<Vec<editor::highlight::StyledSpan>>,
658    },
659
660    /// Incremental highlight update for multiple lines.
661    ///
662    /// Unlike SetEditorHighlights, this operation updates individual lines
663    /// without clearing existing highlights. This prevents highlights from
664    /// disappearing while typing or scrolling.
665    SetEditorHighlightsChunk {
666        path: Option<std::path::PathBuf>,
667        version: editor::buffer::Version,
668        generation: u64,
669        spans: Vec<(usize, Vec<editor::highlight::StyledSpan>)>,
670    },
671
672    // -----------------------------------------------------------------------
673    // Issue registry — ephemeral commands + startup loads
674    // (no disk I/O; produced by async tasks and the persistent-load path)
675    // -----------------------------------------------------------------------
676
677    /// Add an issue to the registry (registry-only, no disk write).
678    ///
679    /// `issue.marker = Some(m)` → ephemeral (cleared via `ClearIssuesByMarker`).
680    /// `issue.marker = None`   → used only for startup loads by the Persistent
681    ///                           Component.  User-initiated persistent issues
682    ///                           must go through `PersistentIssueLocal(Add)`.
683    AddIssue { issue: NewIssue },
684
685    /// Remove an issue by ID (registry-only).  Emits `IssueRemoved` on success.
686    RemoveIssue { id: IssueId },
687
688    /// Mark an issue as resolved (registry-only).  Emits `IssueUpdated` on success.
689    ResolveIssue { id: IssueId },
690
691    /// Mark an issue as dismissed (registry-only).  Emits `IssueUpdated` on success.
692    DismissIssue { id: IssueId },
693
694    /// Remove all ephemeral issues belonging to `marker`.  Emits one
695    /// `IssueRemoved` per removed issue.
696    ClearIssuesByMarker { marker: String },
697
698    // -----------------------------------------------------------------------
699    // Persistent issue operations — update registry AND write to disk
700    // -----------------------------------------------------------------------
701
702    /// Persistent-issue commands: each variant updates the in-memory
703    /// [`IssueRegistry`] **and** atomically rewrites `.oo/issues.yaml`.
704    ///
705    /// [`IssueRegistry`]: crate::issue_registry::IssueRegistry
706    PersistentIssueLocal(PersistentIssueOp),
707
708    // -----------------------------------------------------------------------
709    // Issue registry — events (enqueued by apply_operation after mutations)
710    // -----------------------------------------------------------------------
711
712    /// An issue was added.
713    IssueAdded { id: IssueId },
714
715    /// An issue was removed.
716    IssueRemoved { id: IssueId },
717
718    /// An issue's `resolved` or `dismissed` flag was toggled.
719    IssueUpdated { id: IssueId },
720
721    // -----------------------------------------------------------------------
722    // Task system lifecycle events
723    // -----------------------------------------------------------------------
724
725    /// A task was created and either started immediately or added to its queue.
726    TaskScheduled(TaskId),
727
728    /// A task transitioned from `Pending` to `Running`.
729    TaskStarted(TaskId),
730
731    /// A task reached a terminal status (`Success`, `Warning`, or `Error`).
732    ///
733    /// Sent by [`crate::task_executor::TaskExecutor`] when the spawned process
734    /// exits.  Handled by `apply_operation` in `app.rs`, which calls
735    /// [`crate::task_registry::TaskRegistry::mark_finished`] and starts the
736    /// next queued task if one exists.
737    TaskFinished {
738        id: TaskId,
739        status: TaskStatus,
740    },
741
742    /// A task was cancelled (running or queued).
743    TaskCancelled(TaskId),
744
745    // -----------------------------------------------------------------------
746    // Task system commands
747    // -----------------------------------------------------------------------
748
749    /// Schedule a new task.  Handled by `apply_operation`, which calls
750    /// [`crate::task_registry::TaskRegistry::schedule_task`] and immediately
751    /// spawns the executor if the queue was idle.
752    ScheduleTask {
753        key: TaskKey,
754        trigger: TaskTrigger,
755        /// The shell command to execute (passed through to the process).
756        command: String,
757    },
758
759    /// Cancel a running or queued task by its ID.  Handled by `apply_operation`,
760    /// which calls [`crate::task_registry::TaskRegistry::cancel`] and starts the
761    /// next queued task if one exists.
762    CancelTask(TaskId),
763
764    /// Open the log view and pre-populate its filter with the task's label so
765    /// the user can quickly inspect task-related entries.
766    OpenLogForTask { task_id: TaskId },
767
768    /// Open the Task Archive View.
769    OpenTaskArchive,
770
771    /// Delivered by app.rs after it asynchronously loads task output from the
772    /// log store.  Switches to `TaskOutputView` and populates it with the lines.
773    ShowTaskOutput {
774        task_id: TaskId,
775        title: String,
776        status: TaskStatus,
777        lines: Vec<String>,
778    },
779
780    /// View-local ops for `crate::views::task_archive::TaskArchiveView`.
781    TaskArchiveLocal(TaskArchiveOp),
782
783    /// Open the Issue List View.
784    OpenIssueList,
785
786    /// View-local ops for `crate::views::issue_view::IssueView`.
787    IssueViewLocal(IssueViewOp),
788}
789
790// ---------------------------------------------------------------------------
791// Persistent issue sub-operations
792// ---------------------------------------------------------------------------
793
794/// Commands that manipulate **persistent** issues.
795///
796/// Each variant is handled by `apply_operation` in `app.rs`, which:
797/// 1. Calls the corresponding [`IssueRegistry`] mutation method.
798/// 2. Enqueues the matching `IssueAdded` / `IssueRemoved` / `IssueUpdated`
799///    event operation.
800/// 3. Spawns a fire-and-forget [`crate::persistent_issues::save_atomic`] task.
801///
802/// [`IssueRegistry`]: crate::issue_registry::IssueRegistry
803#[derive(Debug, Clone)]
804pub enum PersistentIssueOp {
805    /// Add a new persistent issue and write the updated list to disk.
806    Add { issue: PersistentNewIssue },
807    /// Remove a persistent issue by ID and write the updated list to disk.
808    Remove { id: IssueId },
809    /// Mark a persistent issue as resolved and write the updated list to disk.
810    Resolve { id: IssueId },
811    /// Mark a persistent issue as dismissed and write the updated list to disk.
812    Dismiss { id: IssueId },
813}
814
815#[derive(Debug, Clone, PartialEq, Eq)]
816pub enum NotificationLevel {
817    Info,
818    Warn,
819    Error,
820}
821
822#[derive(Debug, Clone, PartialEq, Eq)]
823pub enum StatusSlot {
824    Left,
825    Center,
826    Right,
827}
828
829#[derive(Debug, Clone)]
830#[allow(unused)]
831pub struct PickerItem {
832    #[allow(dead_code)]
833    pub label: String,
834    #[allow(dead_code)]
835    pub value: String,
836}
837
838#[derive(Debug, Clone)]
839pub struct Event {
840    /// Which subsystem produced the operation: "editor", "file_selector", "plugin.git"
841    #[allow(dead_code)]
842    pub source: Cow<'static, str>,
843    #[allow(dead_code)]
844    pub kind: EventKind,
845}
846
847#[derive(Debug, Clone)]
848pub enum EventKind {
849    #[allow(dead_code)]
850    Applied(Operation),
851}
852
853impl Event {
854    pub fn applied(source: impl Into<Cow<'static, str>>, op: Operation) -> Self {
855        Self {
856            source: source.into(),
857            kind: EventKind::Applied(op),
858        }
859    }
860}