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    /// Open a new tab. `command` defaults to the configured shell if `None`.
361    NewTab { command: Option<String> },
362    /// Close a tab by ID. If it was the last tab, a new shell is spawned.
363    CloseTab { id: u64 },
364    /// Switch to the tab at the given index (clamped to valid range).
365    SwitchToTab { index: usize },
366    /// Switch to the next tab (wraps around).
367    NextTab,
368    /// Switch to the previous tab (wraps around).
369    PrevTab,
370    /// Reset scroll to the bottom of the active tab.
371    ScrollReset,
372    /// Open the context menu for the given tab.
373    OpenMenu { id: u64 },
374    /// Close the context menu without taking action.
375    CloseMenu,
376    /// Confirm the highlighted context-menu item.
377    MenuConfirm,
378    /// Open the inline rename input for the given tab directly.
379    OpenRename { id: u64 },
380    /// Clear the screen of the given tab (sends VT clear sequence to PTY).
381    ClearTab { id: u64 },
382    /// Set the rename input text.
383    RenameChanged(String),
384    /// Commit the rename.
385    RenameConfirm,
386    /// Cancel the rename.
387    RenameCancel,
388    /// Open the tab selector popup (keyboard/mouse-navigable list of all tabs).
389    OpenTabSelector,
390    /// Terminal dimensions changed — resize all PTY tabs.
391    Resize { cols: u16, rows: u16 },
392    /// The PTY child process has exited.
393    ProcessExited { id: u64 },
394}
395
396#[derive(Debug, Clone)]
397pub enum SearchReplaceOp {
398    //  Input field changes
399    QueryChanged(String),
400    ReplaceChanged(String),
401    IncludeGlobChanged(String),
402    ExcludeGlobChanged(String),
403    //  Toggle options
404    ToggleRegex,
405    ToggleCase,
406    ToggleSmartCase,
407    //  Tree navigation
408    TreeExpandOrEnter,
409    TreeCollapseOrLeave,
410    //  Replace actions
411    ReplaceCurrentFile,
412    ReplaceAll,
413    //  Undo / redo
414    Undo,
415    Redo,
416    //  Async results
417    SearchResultsReady {
418        results: Vec<FileMatchResult>,
419        generation: u64,
420    },
421    ReplaceComplete {
422        path: PathBuf,
423        replaced_count: usize,
424    },
425    AsyncError(String),
426    ProgressUpdate {
427        scanned: usize,
428        total: usize,
429    },
430}
431
432#[derive(Debug, Clone)]
433pub enum Operation {
434    // Buffer mutations
435    InsertText {
436        path: Option<PathBuf>,
437        #[allow(dead_code)]
438        cursor: Position,
439        text: String,
440    },
441    /// Bracketed paste (or fallback-heuristic detected paste) delivered to the
442    /// active editor. Unlike `InsertText`, the full multi-line string is inserted
443    /// at once via `insert_text_raw` and also pushed to the clipboard register.
444    PasteText {
445        path: Option<PathBuf>,
446        text: String,
447    },
448    DeleteText {
449        path: Option<PathBuf>,
450        #[allow(dead_code)]
451        cursor: Position,
452        #[allow(dead_code)]
453        len: usize,
454    },
455    DeleteForward {
456        path: Option<PathBuf>,
457        #[allow(dead_code)]
458        cursor: Position,
459    },
460    DeleteWordBackward {
461        path: Option<PathBuf>,
462    },
463    DeleteWordForward {
464        path: Option<PathBuf>,
465    },
466    IndentLines {
467        path: Option<PathBuf>,
468    },
469    UnindentLines {
470        path: Option<PathBuf>,
471    },
472    DeleteLine {
473        path: Option<PathBuf>,
474    },
475    /// Replace the text from `start` to `end` (same line, byte offsets) with `text`.
476    /// Used by CompletionConfirm to delete the filter prefix before inserting.
477    ReplaceRange {
478        path: Option<PathBuf>,
479        start: Position,
480        end: Position,
481        text: String,
482    },
483    MoveCursor {
484        path: Option<PathBuf>,
485        cursor: Position,
486    },
487    Undo {
488        path: Option<PathBuf>,
489    },
490    Redo {
491        path: Option<PathBuf>,
492    },
493    ToggleFold {
494        path: Option<PathBuf>,
495        line: usize,
496    },
497    ToggleMarker {
498        path: Option<PathBuf>,
499        line: usize,
500    },
501    ToggleWordWrap,
502
503    // File lifecycle
504    OpenFile {
505        path: PathBuf,
506    },
507    SaveFile {
508        path: PathBuf,
509    },
510    ReloadFromDisk {
511        path: PathBuf,
512        accept_external: bool,
513    },
514
515    // App / navigation
516    SwitchScreen {
517        to: ScreenKind,
518    },
519    /// Close the currently open modal/temporary view.
520    ///
521    /// If the current view is already primary, this is a no-op.
522    CloseModal,
523    Quit,
524
525    // Focus navigation — handled by whichever view is active
526    Focus(crate::widgets::focusable::FocusOp),
527
528    // Generic list/tree/scroll navigation — dispatched to the active view.
529    // Each view interprets these according to its own focus state.
530    NavigateUp,
531    NavigateDown,
532    NavigatePageUp,
533    NavigatePageDown,
534    NavigateHome,
535    NavigateEnd,
536
537    // View-local ops that don't cross view boundaries
538    FileSelectorLocal(FileSelectorOp),
539    CommandSelectorLocal(CommandSelectorOp),
540    LogViewLocal(LogViewOp),
541    CommitLocal(CommitOp),
542    GitHistoryLocal(GitHistoryOp),
543    ExtensionConfig(ExtensionConfigOp),
544
545    // LSP trigger ops (handled in app.rs, forwarded to LspClient)
546    LspTriggerCompletion {
547        #[allow(dead_code)]
548        path: Option<PathBuf>,
549    },
550    LspTriggerHover {
551        #[allow(dead_code)]
552        path: Option<PathBuf>,
553    },
554    LspGoToDefinition {
555        #[allow(dead_code)]
556        path: Option<PathBuf>,
557    },
558    /// Result from a go-to-definition request, dispatched back into the queue.
559    LspGoToDefinitionResult(Vec<LspLocation>),
560    /// LSP editor-local ops (dropdown navigation, response delivery).
561    LspLocal(LspOp),
562    /// Search/replace bar ops (editor-local).
563    SearchLocal(SearchOp),
564    /// Go-to-line bar ops (editor-local).
565    GoToLineLocal(GoToLineOp),
566    /// Selection management ops (editor-local).
567    SelectionLocal(SelectionOp),
568    /// Clipboard ops (editor-local).
569    ClipboardLocal(ClipOp),
570    /// Project-wide search & replace ops.
571    SearchReplaceLocal(SearchReplaceOp),
572
573    /// Execute a registered command by id (with pre-filled args).
574    RunCommand {
575        id: crate::commands::CommandId,
576        args: HashMap<String, ArgValue>,
577    },
578
579    /// Open a generic context menu at `(x, y)` with the given items.
580    ///
581    /// Items are `(label, command_id)` pairs.  `app.rs` stores the menu on
582    /// [`crate::app_state::AppState`], intercepts input, and dispatches the selected command.
583    OpenContextMenu {
584        // Each item: (label, command_id, enabled_override)
585        // - enabled_override: `Some(bool)` to explicitly set enabled state,
586        //   or `None` to let the runtime compute a sensible default.
587        items: Vec<(String, crate::commands::CommandId, Option<bool>)>,
588        x: u16,
589        y: u16,
590    },
591
592    /// Close the currently open context menu without selecting any item.
593    CloseContextMenu,
594
595    // Escape hatch for plugins
596    #[allow(dead_code)]
597    Generic {
598        source: Cow<'static, str>,
599        name: Cow<'static, str>,
600        payload: HashMap<String, ArgValue>,
601    },
602
603    // Extension system
604    /// Broadcast a named event to all active extensions.
605    ExtensionEvent {
606        name: String,
607        payload: HashMap<String, String>,
608    },
609    /// Display a user-visible notification (shown in status bar).
610    ShowNotification {
611        message: String,
612        level: NotificationLevel,
613    },
614    /// Update the IDE status bar slot with custom text.
615    UpdateStatusBar {
616        text: String,
617        #[allow(unused)]
618        slot: StatusSlot,
619    },
620    /// Request a terminal be created running the given command.
621    CreateTerminal {
622        command: String,
623    },
624    /// Send raw bytes to the PTY of the terminal tab with the given ID.
625    TerminalInput {
626        id: u64,
627        data: Vec<u8>,
628    },
629    /// Terminal view-local operations.
630    TerminalLocal(TerminalOp),
631    /// Show a picker (like command palette) populated with arbitrary items.
632    ShowPicker {
633        title: String,
634        items: Vec<PickerItem>,
635    },
636
637    /// Deliver pre-computed syntax-highlight spans for an editor buffer.
638    ///
639    /// Sent by the background highlight task; handled in `app.rs`, which stores
640    /// the result in `crate::views::editor::EditorView::highlight_cache` when the version still
641    /// matches the live buffer.  The next frame then renders without blocking.
642    SetEditorHighlights {
643        path: Option<std::path::PathBuf>,
644        version: editor::buffer::Version,
645        start_line: usize,
646        generation: u64,
647        spans: Vec<Vec<editor::highlight::StyledSpan>>,
648    },
649
650    /// Incremental highlight update for multiple lines.
651    ///
652    /// Unlike SetEditorHighlights, this operation updates individual lines
653    /// without clearing existing highlights. This prevents highlights from
654    /// disappearing while typing or scrolling.
655    SetEditorHighlightsChunk {
656        path: Option<std::path::PathBuf>,
657        version: editor::buffer::Version,
658        generation: u64,
659        spans: Vec<(usize, Vec<editor::highlight::StyledSpan>)>,
660    },
661
662    // -----------------------------------------------------------------------
663    // Issue registry — ephemeral commands + startup loads
664    // (no disk I/O; produced by async tasks and the persistent-load path)
665    // -----------------------------------------------------------------------
666
667    /// Add an issue to the registry (registry-only, no disk write).
668    ///
669    /// `issue.marker = Some(m)` → ephemeral (cleared via `ClearIssuesByMarker`).
670    /// `issue.marker = None`   → used only for startup loads by the Persistent
671    ///                           Component.  User-initiated persistent issues
672    ///                           must go through `PersistentIssueLocal(Add)`.
673    AddIssue { issue: NewIssue },
674
675    /// Remove an issue by ID (registry-only).  Emits `IssueRemoved` on success.
676    RemoveIssue { id: IssueId },
677
678    /// Mark an issue as resolved (registry-only).  Emits `IssueUpdated` on success.
679    ResolveIssue { id: IssueId },
680
681    /// Mark an issue as dismissed (registry-only).  Emits `IssueUpdated` on success.
682    DismissIssue { id: IssueId },
683
684    /// Remove all ephemeral issues belonging to `marker`.  Emits one
685    /// `IssueRemoved` per removed issue.
686    ClearIssuesByMarker { marker: String },
687
688    // -----------------------------------------------------------------------
689    // Persistent issue operations — update registry AND write to disk
690    // -----------------------------------------------------------------------
691
692    /// Persistent-issue commands: each variant updates the in-memory
693    /// [`IssueRegistry`] **and** atomically rewrites `.oo/issues.yaml`.
694    ///
695    /// [`IssueRegistry`]: crate::issue_registry::IssueRegistry
696    PersistentIssueLocal(PersistentIssueOp),
697
698    // -----------------------------------------------------------------------
699    // Issue registry — events (enqueued by apply_operation after mutations)
700    // -----------------------------------------------------------------------
701
702    /// An issue was added.
703    IssueAdded { id: IssueId },
704
705    /// An issue was removed.
706    IssueRemoved { id: IssueId },
707
708    /// An issue's `resolved` or `dismissed` flag was toggled.
709    IssueUpdated { id: IssueId },
710
711    // -----------------------------------------------------------------------
712    // Task system lifecycle events
713    // -----------------------------------------------------------------------
714
715    /// A task was created and either started immediately or added to its queue.
716    TaskScheduled(TaskId),
717
718    /// A task transitioned from `Pending` to `Running`.
719    TaskStarted(TaskId),
720
721    /// A task reached a terminal status (`Success`, `Warning`, or `Error`).
722    ///
723    /// Sent by [`crate::task_executor::TaskExecutor`] when the spawned process
724    /// exits.  Handled by `apply_operation` in `app.rs`, which calls
725    /// [`crate::task_registry::TaskRegistry::mark_finished`] and starts the
726    /// next queued task if one exists.
727    TaskFinished {
728        id: TaskId,
729        status: TaskStatus,
730    },
731
732    /// A task was cancelled (running or queued).
733    TaskCancelled(TaskId),
734
735    // -----------------------------------------------------------------------
736    // Task system commands
737    // -----------------------------------------------------------------------
738
739    /// Schedule a new task.  Handled by `apply_operation`, which calls
740    /// [`crate::task_registry::TaskRegistry::schedule_task`] and immediately
741    /// spawns the executor if the queue was idle.
742    ScheduleTask {
743        key: TaskKey,
744        trigger: TaskTrigger,
745        /// The shell command to execute (passed through to the process).
746        command: String,
747    },
748
749    /// Cancel a running or queued task by its ID.  Handled by `apply_operation`,
750    /// which calls [`crate::task_registry::TaskRegistry::cancel`] and starts the
751    /// next queued task if one exists.
752    CancelTask(TaskId),
753
754    /// Open the log view and pre-populate its filter with the task's label so
755    /// the user can quickly inspect task-related entries.
756    OpenLogForTask { task_id: TaskId },
757
758    /// Open the Task Archive View.
759    OpenTaskArchive,
760
761    /// Delivered by app.rs after it asynchronously loads task output from the
762    /// log store.  Switches to `TaskOutputView` and populates it with the lines.
763    ShowTaskOutput {
764        task_id: TaskId,
765        title: String,
766        status: TaskStatus,
767        lines: Vec<String>,
768    },
769
770    /// View-local ops for `crate::views::task_archive::TaskArchiveView`.
771    TaskArchiveLocal(TaskArchiveOp),
772
773    /// Open the Issue List View.
774    OpenIssueList,
775
776    /// View-local ops for `crate::views::issue_view::IssueView`.
777    IssueViewLocal(IssueViewOp),
778}
779
780// ---------------------------------------------------------------------------
781// Persistent issue sub-operations
782// ---------------------------------------------------------------------------
783
784/// Commands that manipulate **persistent** issues.
785///
786/// Each variant is handled by `apply_operation` in `app.rs`, which:
787/// 1. Calls the corresponding [`IssueRegistry`] mutation method.
788/// 2. Enqueues the matching `IssueAdded` / `IssueRemoved` / `IssueUpdated`
789///    event operation.
790/// 3. Spawns a fire-and-forget [`crate::persistent_issues::save_atomic`] task.
791///
792/// [`IssueRegistry`]: crate::issue_registry::IssueRegistry
793#[derive(Debug, Clone)]
794pub enum PersistentIssueOp {
795    /// Add a new persistent issue and write the updated list to disk.
796    Add { issue: PersistentNewIssue },
797    /// Remove a persistent issue by ID and write the updated list to disk.
798    Remove { id: IssueId },
799    /// Mark a persistent issue as resolved and write the updated list to disk.
800    Resolve { id: IssueId },
801    /// Mark a persistent issue as dismissed and write the updated list to disk.
802    Dismiss { id: IssueId },
803}
804
805#[derive(Debug, Clone, PartialEq, Eq)]
806pub enum NotificationLevel {
807    Info,
808    Warn,
809    Error,
810}
811
812#[derive(Debug, Clone, PartialEq, Eq)]
813pub enum StatusSlot {
814    Left,
815    Center,
816    Right,
817}
818
819#[derive(Debug, Clone)]
820#[allow(unused)]
821pub struct PickerItem {
822    #[allow(dead_code)]
823    pub label: String,
824    #[allow(dead_code)]
825    pub value: String,
826}
827
828#[derive(Debug, Clone)]
829pub struct Event {
830    /// Which subsystem produced the operation: "editor", "file_selector", "plugin.git"
831    #[allow(dead_code)]
832    pub source: Cow<'static, str>,
833    #[allow(dead_code)]
834    pub kind: EventKind,
835}
836
837#[derive(Debug, Clone)]
838pub enum EventKind {
839    #[allow(dead_code)]
840    Applied(Operation),
841}
842
843impl Event {
844    pub fn applied(source: impl Into<Cow<'static, str>>, op: Operation) -> Self {
845        Self {
846            source: source.into(),
847            kind: EventKind::Applied(op),
848        }
849    }
850}