Skip to main content

taskers_control/
protocol.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value as JsonValue;
3use uuid::Uuid;
4
5use taskers_domain::{
6    AgentTarget, AppModel, AttentionState, BrowserProfileMode, Direction,
7    NotificationDeliveryState, NotificationId, PaneContainerId, PaneId, PaneKind,
8    PaneMetadataPatch, PaneTabId, PersistedSession, ProgressState, SignalEvent, SignalKind,
9    SplitAxis, SurfaceId, WindowId, WorkspaceColumnId, WorkspaceId, WorkspaceLogEntry,
10    WorkspaceViewport, WorkspaceWindowId, WorkspaceWindowMoveTarget, WorkspaceWindowTabId,
11};
12
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(tag = "command", rename_all = "snake_case")]
15pub enum ControlCommand {
16    CreateWorkspace {
17        label: String,
18    },
19    RenameWorkspace {
20        workspace_id: WorkspaceId,
21        label: String,
22    },
23    SwitchWorkspace {
24        window_id: Option<WindowId>,
25        workspace_id: WorkspaceId,
26    },
27    SplitPane {
28        workspace_id: WorkspaceId,
29        pane_id: Option<PaneId>,
30        axis: SplitAxis,
31    },
32    SplitPaneDirection {
33        workspace_id: WorkspaceId,
34        pane_id: PaneId,
35        direction: Direction,
36    },
37    CreateWorkspaceWindow {
38        workspace_id: WorkspaceId,
39        direction: Direction,
40        #[serde(default)]
41        preferred_column_width: Option<i32>,
42        #[serde(default)]
43        preferred_window_height: Option<i32>,
44    },
45    FocusWorkspaceWindow {
46        workspace_id: WorkspaceId,
47        workspace_window_id: WorkspaceWindowId,
48    },
49    MoveWorkspaceWindow {
50        workspace_id: WorkspaceId,
51        workspace_window_id: WorkspaceWindowId,
52        target: WorkspaceWindowMoveTarget,
53    },
54    CreateWorkspaceWindowTab {
55        workspace_id: WorkspaceId,
56        workspace_window_id: WorkspaceWindowId,
57    },
58    FocusWorkspaceWindowTab {
59        workspace_id: WorkspaceId,
60        workspace_window_id: WorkspaceWindowId,
61        workspace_window_tab_id: WorkspaceWindowTabId,
62    },
63    MoveWorkspaceWindowTab {
64        workspace_id: WorkspaceId,
65        workspace_window_id: WorkspaceWindowId,
66        workspace_window_tab_id: WorkspaceWindowTabId,
67        to_index: usize,
68    },
69    TransferWorkspaceWindowTab {
70        workspace_id: WorkspaceId,
71        source_workspace_window_id: WorkspaceWindowId,
72        workspace_window_tab_id: WorkspaceWindowTabId,
73        target_workspace_window_id: WorkspaceWindowId,
74        to_index: usize,
75    },
76    ExtractWorkspaceWindowTab {
77        workspace_id: WorkspaceId,
78        source_workspace_window_id: WorkspaceWindowId,
79        workspace_window_tab_id: WorkspaceWindowTabId,
80        target: WorkspaceWindowMoveTarget,
81    },
82    CloseWorkspaceWindowTab {
83        workspace_id: WorkspaceId,
84        workspace_window_id: WorkspaceWindowId,
85        workspace_window_tab_id: WorkspaceWindowTabId,
86    },
87    CreatePaneTab {
88        workspace_id: WorkspaceId,
89        pane_container_id: PaneContainerId,
90        kind: PaneKind,
91    },
92    FocusPaneTab {
93        workspace_id: WorkspaceId,
94        pane_container_id: PaneContainerId,
95        pane_tab_id: PaneTabId,
96    },
97    MovePaneTab {
98        workspace_id: WorkspaceId,
99        pane_container_id: PaneContainerId,
100        pane_tab_id: PaneTabId,
101        to_index: usize,
102    },
103    TransferPaneTab {
104        workspace_id: WorkspaceId,
105        source_pane_container_id: PaneContainerId,
106        pane_tab_id: PaneTabId,
107        target_pane_container_id: PaneContainerId,
108        to_index: usize,
109    },
110    ClosePaneTab {
111        workspace_id: WorkspaceId,
112        pane_container_id: PaneContainerId,
113        pane_tab_id: PaneTabId,
114    },
115    FocusPane {
116        workspace_id: WorkspaceId,
117        pane_id: PaneId,
118    },
119    FocusPaneDirection {
120        workspace_id: WorkspaceId,
121        direction: Direction,
122    },
123    ResizeActiveWindow {
124        workspace_id: WorkspaceId,
125        direction: Direction,
126        amount: i32,
127    },
128    ResizeActivePaneSplit {
129        workspace_id: WorkspaceId,
130        direction: Direction,
131        amount: i32,
132    },
133    SetWorkspaceColumnWidth {
134        workspace_id: WorkspaceId,
135        workspace_column_id: WorkspaceColumnId,
136        width: i32,
137    },
138    SetWorkspaceWindowHeight {
139        workspace_id: WorkspaceId,
140        workspace_window_id: WorkspaceWindowId,
141        height: i32,
142    },
143    SetWindowSplitRatio {
144        workspace_id: WorkspaceId,
145        workspace_window_id: WorkspaceWindowId,
146        path: Vec<bool>,
147        ratio: u16,
148    },
149    SetPaneTabSplitRatio {
150        workspace_id: WorkspaceId,
151        pane_container_id: PaneContainerId,
152        pane_tab_id: PaneTabId,
153        path: Vec<bool>,
154        ratio: u16,
155    },
156    UpdatePaneMetadata {
157        pane_id: PaneId,
158        patch: PaneMetadataPatch,
159    },
160    UpdateSurfaceMetadata {
161        surface_id: SurfaceId,
162        patch: PaneMetadataPatch,
163    },
164    CreateSurface {
165        workspace_id: WorkspaceId,
166        pane_id: PaneId,
167        kind: PaneKind,
168        browser_profile_mode: Option<BrowserProfileMode>,
169    },
170    FocusSurface {
171        workspace_id: WorkspaceId,
172        pane_id: PaneId,
173        surface_id: SurfaceId,
174    },
175    StartSurfaceAgentSession {
176        workspace_id: WorkspaceId,
177        pane_id: PaneId,
178        surface_id: SurfaceId,
179        agent_kind: String,
180    },
181    StopSurfaceAgentSession {
182        workspace_id: WorkspaceId,
183        pane_id: PaneId,
184        surface_id: SurfaceId,
185        exit_status: i32,
186    },
187    MarkSurfaceCompleted {
188        workspace_id: WorkspaceId,
189        pane_id: PaneId,
190        surface_id: SurfaceId,
191    },
192    CloseSurface {
193        workspace_id: WorkspaceId,
194        pane_id: PaneId,
195        surface_id: SurfaceId,
196    },
197    MoveSurface {
198        workspace_id: WorkspaceId,
199        pane_id: PaneId,
200        surface_id: SurfaceId,
201        to_index: usize,
202    },
203    TransferSurface {
204        source_workspace_id: WorkspaceId,
205        source_pane_id: PaneId,
206        surface_id: SurfaceId,
207        target_workspace_id: WorkspaceId,
208        target_pane_id: PaneId,
209        to_index: usize,
210    },
211    MoveSurfaceToSplit {
212        source_workspace_id: WorkspaceId,
213        source_pane_id: PaneId,
214        surface_id: SurfaceId,
215        target_workspace_id: WorkspaceId,
216        target_pane_id: PaneId,
217        direction: Direction,
218    },
219    MoveSurfaceToWorkspace {
220        source_workspace_id: WorkspaceId,
221        source_pane_id: PaneId,
222        surface_id: SurfaceId,
223        target_workspace_id: WorkspaceId,
224    },
225    SetWorkspaceViewport {
226        workspace_id: WorkspaceId,
227        viewport: WorkspaceViewport,
228    },
229    ClosePane {
230        workspace_id: WorkspaceId,
231        pane_id: PaneId,
232    },
233    CloseWorkspace {
234        workspace_id: WorkspaceId,
235    },
236    ReorderWorkspaces {
237        window_id: WindowId,
238        workspace_ids: Vec<WorkspaceId>,
239    },
240    EmitSignal {
241        workspace_id: WorkspaceId,
242        pane_id: PaneId,
243        surface_id: Option<SurfaceId>,
244        event: SignalEvent,
245    },
246    AgentSetStatus {
247        workspace_id: WorkspaceId,
248        text: String,
249    },
250    AgentClearStatus {
251        workspace_id: WorkspaceId,
252    },
253    AgentSetProgress {
254        workspace_id: WorkspaceId,
255        progress: ProgressState,
256    },
257    AgentClearProgress {
258        workspace_id: WorkspaceId,
259    },
260    AgentAppendLog {
261        workspace_id: WorkspaceId,
262        entry: WorkspaceLogEntry,
263    },
264    AgentClearLog {
265        workspace_id: WorkspaceId,
266    },
267    AgentCreateNotification {
268        target: AgentTarget,
269        kind: SignalKind,
270        title: Option<String>,
271        subtitle: Option<String>,
272        external_id: Option<String>,
273        message: String,
274        state: AttentionState,
275    },
276    OpenNotification {
277        window_id: Option<WindowId>,
278        notification_id: NotificationId,
279    },
280    ClearNotification {
281        notification_id: NotificationId,
282    },
283    MarkNotificationDelivery {
284        notification_id: NotificationId,
285        delivery: NotificationDeliveryState,
286    },
287    AgentClearNotifications {
288        target: AgentTarget,
289    },
290    DismissSurfaceAlert {
291        workspace_id: WorkspaceId,
292        pane_id: PaneId,
293        surface_id: SurfaceId,
294    },
295    DismissInterruptedAgentResume {
296        workspace_id: WorkspaceId,
297        pane_id: PaneId,
298        surface_id: SurfaceId,
299    },
300    AgentTriggerFlash {
301        workspace_id: WorkspaceId,
302        pane_id: PaneId,
303        surface_id: SurfaceId,
304    },
305    AgentFocusLatestUnread {
306        window_id: Option<WindowId>,
307    },
308    Browser {
309        browser_command: BrowserControlCommand,
310    },
311    Screenshot {
312        screenshot_command: ScreenshotCommand,
313    },
314    TerminalDebug {
315        debug_command: TerminalDebugCommand,
316    },
317    Vcs {
318        vcs_command: VcsCommand,
319    },
320    QueryStatus {
321        query: ControlQuery,
322    },
323}
324
325#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
326#[serde(tag = "vcs_command", rename_all = "snake_case")]
327pub enum VcsCommand {
328    Refresh {
329        surface_id: SurfaceId,
330        diff_path: Option<String>,
331    },
332    GitCommit {
333        surface_id: SurfaceId,
334        message: String,
335    },
336    GitCreateBranch {
337        surface_id: SurfaceId,
338        name: String,
339    },
340    GitSwitchBranch {
341        surface_id: SurfaceId,
342        name: String,
343    },
344    GitFetch {
345        surface_id: SurfaceId,
346    },
347    GitPull {
348        surface_id: SurfaceId,
349    },
350    GitPush {
351        surface_id: SurfaceId,
352    },
353    JjDescribe {
354        surface_id: SurfaceId,
355        message: String,
356    },
357    JjNew {
358        surface_id: SurfaceId,
359        message: Option<String>,
360    },
361    JjCreateBookmark {
362        surface_id: SurfaceId,
363        name: String,
364    },
365    JjSwitchBookmark {
366        surface_id: SurfaceId,
367        name: String,
368    },
369    JjFetch {
370        surface_id: SurfaceId,
371    },
372    JjPush {
373        surface_id: SurfaceId,
374    },
375}
376
377#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
378#[serde(rename_all = "snake_case")]
379pub enum VcsMode {
380    Git,
381    Jj,
382}
383
384#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
385#[serde(rename_all = "snake_case")]
386pub enum VcsFileStatus {
387    Modified,
388    Added,
389    Deleted,
390    Renamed,
391    Copied,
392    Untracked,
393    Conflicted,
394    Changed,
395}
396
397#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
398pub struct VcsFileEntry {
399    pub path: String,
400    pub status: VcsFileStatus,
401    pub staged: bool,
402    #[serde(default)]
403    pub insertions: Option<u32>,
404    #[serde(default)]
405    pub deletions: Option<u32>,
406}
407
408#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
409pub struct VcsRefEntry {
410    pub name: String,
411    pub active: bool,
412}
413
414#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
415pub struct VcsCommitEntry {
416    pub id: String,
417    pub description: String,
418    pub insertions: u32,
419    pub deletions: u32,
420}
421
422#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
423pub struct VcsPullRequestInfo {
424    pub number: Option<u32>,
425    pub title: Option<String>,
426    pub url: String,
427    pub state: Option<String>,
428}
429
430#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
431pub struct VcsSnapshot {
432    pub surface_id: SurfaceId,
433    pub mode: VcsMode,
434    pub repo_root: String,
435    pub repo_name: String,
436    pub cwd: String,
437    pub headline: String,
438    pub detail: Option<String>,
439    pub summary_text: String,
440    pub files: Vec<VcsFileEntry>,
441    pub refs: Vec<VcsRefEntry>,
442    pub diff_path: Option<String>,
443    pub diff_text: Option<String>,
444    pub pull_request: Option<VcsPullRequestInfo>,
445    #[serde(default)]
446    pub total_insertions: u32,
447    #[serde(default)]
448    pub total_deletions: u32,
449    #[serde(default)]
450    pub recent_commits: Vec<VcsCommitEntry>,
451}
452
453#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
454pub struct VcsCommandResult {
455    pub snapshot: Option<VcsSnapshot>,
456    pub message: Option<String>,
457}
458
459#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
460#[serde(rename_all = "snake_case")]
461pub enum ControlErrorCode {
462    InvalidParams,
463    NotFound,
464    Timeout,
465    InvalidState,
466    NotSupported,
467    Internal,
468}
469
470#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
471pub struct ControlError {
472    pub code: ControlErrorCode,
473    pub message: String,
474}
475
476impl ControlError {
477    pub fn new(code: ControlErrorCode, message: impl Into<String>) -> Self {
478        Self {
479            code,
480            message: message.into(),
481        }
482    }
483
484    pub fn invalid_params(message: impl Into<String>) -> Self {
485        Self::new(ControlErrorCode::InvalidParams, message)
486    }
487
488    pub fn not_found(message: impl Into<String>) -> Self {
489        Self::new(ControlErrorCode::NotFound, message)
490    }
491
492    pub fn timeout(message: impl Into<String>) -> Self {
493        Self::new(ControlErrorCode::Timeout, message)
494    }
495
496    pub fn invalid_state(message: impl Into<String>) -> Self {
497        Self::new(ControlErrorCode::InvalidState, message)
498    }
499
500    pub fn not_supported(message: impl Into<String>) -> Self {
501        Self::new(ControlErrorCode::NotSupported, message)
502    }
503
504    pub fn internal(message: impl Into<String>) -> Self {
505        Self::new(ControlErrorCode::Internal, message)
506    }
507}
508
509impl std::fmt::Display for ControlError {
510    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
511        write!(f, "{:?}: {}", self.code, self.message)
512    }
513}
514
515impl std::error::Error for ControlError {}
516
517#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
518#[serde(tag = "browser_command", rename_all = "snake_case")]
519pub enum BrowserControlCommand {
520    Navigate {
521        surface_id: SurfaceId,
522        url: String,
523    },
524    Back {
525        surface_id: SurfaceId,
526    },
527    Forward {
528        surface_id: SurfaceId,
529    },
530    Reload {
531        surface_id: SurfaceId,
532    },
533    FocusWebview {
534        surface_id: SurfaceId,
535    },
536    IsWebviewFocused {
537        surface_id: SurfaceId,
538    },
539    Snapshot {
540        surface_id: SurfaceId,
541    },
542    Eval {
543        surface_id: SurfaceId,
544        script: String,
545    },
546    Wait {
547        surface_id: SurfaceId,
548        condition: BrowserWaitCondition,
549        timeout_ms: u64,
550        poll_interval_ms: u64,
551    },
552    Click {
553        surface_id: SurfaceId,
554        target: BrowserTarget,
555        snapshot_after: bool,
556    },
557    Dblclick {
558        surface_id: SurfaceId,
559        target: BrowserTarget,
560        snapshot_after: bool,
561    },
562    Type {
563        surface_id: SurfaceId,
564        target: BrowserTarget,
565        text: String,
566        snapshot_after: bool,
567    },
568    Fill {
569        surface_id: SurfaceId,
570        target: BrowserTarget,
571        text: String,
572        snapshot_after: bool,
573    },
574    Press {
575        surface_id: SurfaceId,
576        target: Option<BrowserTarget>,
577        key: String,
578        snapshot_after: bool,
579    },
580    Keydown {
581        surface_id: SurfaceId,
582        target: Option<BrowserTarget>,
583        key: String,
584        snapshot_after: bool,
585    },
586    Keyup {
587        surface_id: SurfaceId,
588        target: Option<BrowserTarget>,
589        key: String,
590        snapshot_after: bool,
591    },
592    Hover {
593        surface_id: SurfaceId,
594        target: BrowserTarget,
595        snapshot_after: bool,
596    },
597    Focus {
598        surface_id: SurfaceId,
599        target: BrowserTarget,
600        snapshot_after: bool,
601    },
602    Check {
603        surface_id: SurfaceId,
604        target: BrowserTarget,
605        snapshot_after: bool,
606    },
607    Uncheck {
608        surface_id: SurfaceId,
609        target: BrowserTarget,
610        snapshot_after: bool,
611    },
612    Select {
613        surface_id: SurfaceId,
614        target: BrowserTarget,
615        values: Vec<String>,
616        snapshot_after: bool,
617    },
618    Scroll {
619        surface_id: SurfaceId,
620        target: Option<BrowserTarget>,
621        dx: i32,
622        dy: i32,
623        snapshot_after: bool,
624    },
625    ScrollIntoView {
626        surface_id: SurfaceId,
627        target: BrowserTarget,
628        snapshot_after: bool,
629    },
630    Get {
631        surface_id: SurfaceId,
632        query: BrowserGetCommand,
633    },
634    Is {
635        surface_id: SurfaceId,
636        query: BrowserPredicateCommand,
637    },
638    Screenshot {
639        surface_id: SurfaceId,
640        path: Option<String>,
641        full_document: bool,
642    },
643    ClearData {
644        surface_id: SurfaceId,
645        origin_filter: Option<String>,
646        reload: bool,
647    },
648}
649
650#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
651#[serde(tag = "target", rename_all = "snake_case")]
652pub enum BrowserTarget {
653    Ref { value: String },
654    Selector { value: String },
655}
656
657#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
658#[serde(tag = "condition", rename_all = "snake_case")]
659pub enum BrowserWaitCondition {
660    Selector { selector: String },
661    Text { text: String },
662    UrlMatches { pattern: String },
663    LoadState { state: BrowserLoadState },
664    Function { script: String },
665    Delay { duration_ms: u64 },
666}
667
668#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
669#[serde(rename_all = "snake_case")]
670pub enum BrowserLoadState {
671    Started,
672    Redirected,
673    Committed,
674    Finished,
675}
676
677#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
678#[serde(tag = "query", rename_all = "snake_case")]
679pub enum BrowserGetCommand {
680    Url,
681    Title,
682    Text {
683        target: BrowserTarget,
684    },
685    Html {
686        target: BrowserTarget,
687    },
688    Value {
689        target: BrowserTarget,
690    },
691    Attr {
692        target: BrowserTarget,
693        name: String,
694    },
695    Count {
696        selector: String,
697    },
698    Box {
699        target: BrowserTarget,
700    },
701    Styles {
702        target: BrowserTarget,
703        properties: Vec<String>,
704    },
705}
706
707#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
708#[serde(tag = "query", rename_all = "snake_case")]
709pub enum BrowserPredicateCommand {
710    Visible { target: BrowserTarget },
711    Enabled { target: BrowserTarget },
712    Checked { target: BrowserTarget },
713}
714
715#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
716#[serde(tag = "screenshot_command", rename_all = "snake_case")]
717pub enum ScreenshotCommand {
718    Capture {
719        target: ScreenshotTarget,
720        path: Option<String>,
721    },
722}
723
724#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
725#[serde(tag = "kind", rename_all = "snake_case")]
726pub enum ScreenshotTarget {
727    Surface {
728        surface_id: SurfaceId,
729    },
730    Pane {
731        workspace_id: WorkspaceId,
732        pane_id: PaneId,
733    },
734    WorkspaceWindow {
735        workspace_id: WorkspaceId,
736    },
737    WorkspaceCanvas {
738        workspace_id: WorkspaceId,
739    },
740}
741
742#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
743pub struct ScreenshotResult {
744    pub path: String,
745    pub width: i32,
746    pub height: i32,
747    pub target: ScreenshotTargetResult,
748}
749
750#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
751#[serde(tag = "kind", rename_all = "snake_case")]
752pub enum ScreenshotTargetResult {
753    Surface {
754        workspace_id: WorkspaceId,
755        pane_id: PaneId,
756        surface_id: SurfaceId,
757    },
758    Pane {
759        workspace_id: WorkspaceId,
760        pane_id: PaneId,
761    },
762    WorkspaceWindow {
763        workspace_id: WorkspaceId,
764        workspace_window_id: WorkspaceWindowId,
765    },
766    WorkspaceCanvas {
767        workspace_id: WorkspaceId,
768    },
769}
770
771#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
772#[serde(tag = "terminal_command", rename_all = "snake_case")]
773pub enum TerminalDebugCommand {
774    IsFocused {
775        surface_id: SurfaceId,
776    },
777    ReadText {
778        surface_id: SurfaceId,
779        tail_lines: Option<usize>,
780    },
781    RenderStats {
782        surface_id: SurfaceId,
783    },
784}
785
786#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
787#[serde(tag = "result", rename_all = "snake_case")]
788pub enum TerminalDebugResult {
789    IsFocused { focused: bool },
790    ReadText { text: String },
791    RenderStats { stats: TerminalRenderStats },
792}
793
794#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
795pub struct TerminalRenderStats {
796    pub surface_id: SurfaceId,
797    pub workspace_id: WorkspaceId,
798    pub pane_id: PaneId,
799    pub mounted: bool,
800    pub visible: bool,
801    pub focused: bool,
802    pub backend: String,
803    pub cols: u16,
804    pub rows: u16,
805    pub width_px: i32,
806    pub height_px: i32,
807    pub has_selection: bool,
808}
809
810#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
811pub struct IdentifyContext {
812    pub window_id: WindowId,
813    pub workspace_id: WorkspaceId,
814    pub workspace_label: String,
815    pub workspace_window_id: Option<WorkspaceWindowId>,
816    pub pane_id: PaneId,
817    pub surface_id: SurfaceId,
818    pub surface_kind: PaneKind,
819    pub title: Option<String>,
820    pub cwd: Option<String>,
821    pub url: Option<String>,
822    pub loading: Option<bool>,
823}
824
825#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
826pub struct IdentifyResult {
827    pub focused: IdentifyContext,
828    pub caller: Option<IdentifyContext>,
829}
830
831#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
832#[serde(tag = "scope", rename_all = "snake_case")]
833pub enum ControlQuery {
834    ActiveWindow,
835    Window {
836        window_id: WindowId,
837    },
838    Workspace {
839        workspace_id: WorkspaceId,
840    },
841    Identify {
842        workspace_id: Option<WorkspaceId>,
843        pane_id: Option<PaneId>,
844        surface_id: Option<SurfaceId>,
845    },
846    All,
847}
848
849#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
850#[serde(tag = "status", rename_all = "snake_case")]
851pub enum ControlResponse {
852    Ack {
853        message: String,
854    },
855    WorkspaceCreated {
856        workspace_id: WorkspaceId,
857    },
858    PaneSplit {
859        pane_id: PaneId,
860    },
861    SurfaceMovedToSplit {
862        pane_id: PaneId,
863    },
864    SurfaceMovedToWorkspace {
865        pane_id: PaneId,
866    },
867    SurfaceCreated {
868        surface_id: SurfaceId,
869    },
870    WorkspaceWindowCreated {
871        pane_id: PaneId,
872    },
873    WorkspaceWindowTabCreated {
874        pane_id: PaneId,
875        workspace_window_tab_id: WorkspaceWindowTabId,
876    },
877    PaneTabCreated {
878        pane_id: PaneId,
879        pane_tab_id: PaneTabId,
880    },
881    Status {
882        session: PersistedSession,
883    },
884    WorkspaceState {
885        workspace_id: WorkspaceId,
886        session: PersistedSession,
887    },
888    Browser {
889        result: JsonValue,
890    },
891    Screenshot {
892        result: ScreenshotResult,
893    },
894    TerminalDebug {
895        result: TerminalDebugResult,
896    },
897    Vcs {
898        result: VcsCommandResult,
899    },
900    Identify {
901        result: IdentifyResult,
902    },
903}
904
905#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
906pub struct RequestFrame {
907    pub request_id: Uuid,
908    pub command: ControlCommand,
909}
910
911#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
912pub struct ResponseFrame {
913    pub request_id: Uuid,
914    pub response: Result<ControlResponse, ControlError>,
915}
916
917impl RequestFrame {
918    pub fn new(command: ControlCommand) -> Self {
919        Self {
920            request_id: Uuid::now_v7(),
921            command,
922        }
923    }
924}
925
926impl From<AppModel> for ControlResponse {
927    fn from(model: AppModel) -> Self {
928        Self::Status {
929            session: model.snapshot(),
930        }
931    }
932}
933
934#[cfg(test)]
935mod tests {
936    use super::{
937        ControlResponse, ScreenshotCommand, ScreenshotResult, ScreenshotTarget,
938        ScreenshotTargetResult, VcsCommandResult, VcsCommitEntry, VcsFileEntry, VcsFileStatus,
939        VcsMode, VcsSnapshot,
940    };
941    use taskers_domain::{PaneId, SurfaceId, WorkspaceId, WorkspaceWindowId};
942
943    #[test]
944    fn screenshot_commands_round_trip_through_serde() {
945        let command = ScreenshotCommand::Capture {
946            target: ScreenshotTarget::WorkspaceCanvas {
947                workspace_id: WorkspaceId::new(),
948            },
949            path: Some("/tmp/taskers-shot.png".into()),
950        };
951
952        let value = serde_json::to_value(&command).expect("serialize screenshot command");
953        let round_trip: ScreenshotCommand =
954            serde_json::from_value(value).expect("deserialize screenshot command");
955
956        assert_eq!(round_trip, command);
957    }
958
959    #[test]
960    fn screenshot_results_round_trip_through_serde() {
961        let result = ScreenshotResult {
962            path: "/tmp/taskers-shot.png".into(),
963            width: 640,
964            height: 480,
965            target: ScreenshotTargetResult::Surface {
966                workspace_id: WorkspaceId::new(),
967                pane_id: PaneId::new(),
968                surface_id: SurfaceId::new(),
969            },
970        };
971        let response = ControlResponse::Screenshot {
972            result: result.clone(),
973        };
974
975        let value = serde_json::to_value(&response).expect("serialize screenshot response");
976        let round_trip: ControlResponse =
977            serde_json::from_value(value).expect("deserialize screenshot response");
978
979        assert_eq!(round_trip, response);
980
981        let window_result = ScreenshotResult {
982            path: "/tmp/taskers-window.png".into(),
983            width: 800,
984            height: 600,
985            target: ScreenshotTargetResult::WorkspaceWindow {
986                workspace_id: WorkspaceId::new(),
987                workspace_window_id: WorkspaceWindowId::new(),
988            },
989        };
990        let value = serde_json::to_value(&window_result).expect("serialize screenshot result");
991        let round_trip: ScreenshotResult =
992            serde_json::from_value(value).expect("deserialize screenshot result");
993        assert_eq!(round_trip, window_result);
994    }
995
996    #[test]
997    fn vcs_responses_backfill_recent_stat_fields_from_older_payloads() {
998        let response = ControlResponse::Vcs {
999            result: VcsCommandResult {
1000                snapshot: Some(VcsSnapshot {
1001                    surface_id: SurfaceId::new(),
1002                    mode: VcsMode::Git,
1003                    repo_root: "/tmp/repo".into(),
1004                    repo_name: "repo".into(),
1005                    cwd: "/tmp/repo".into(),
1006                    headline: "main".into(),
1007                    detail: Some("ahead by 1".into()),
1008                    summary_text: "working tree has changes".into(),
1009                    files: vec![VcsFileEntry {
1010                        path: "src/main.rs".into(),
1011                        status: VcsFileStatus::Modified,
1012                        staged: false,
1013                        insertions: Some(7),
1014                        deletions: Some(3),
1015                    }],
1016                    refs: Vec::new(),
1017                    diff_path: Some("src/main.rs".into()),
1018                    diff_text: Some("@@ -1 +1 @@".into()),
1019                    pull_request: None,
1020                    total_insertions: 7,
1021                    total_deletions: 3,
1022                    recent_commits: vec![VcsCommitEntry {
1023                        id: "abc1234".into(),
1024                        description: "feat: add stats".into(),
1025                        insertions: 7,
1026                        deletions: 3,
1027                    }],
1028                }),
1029                message: None,
1030            },
1031        };
1032
1033        let mut value = serde_json::to_value(&response).expect("serialize vcs response");
1034        let files = value["result"]["snapshot"]["files"]
1035            .as_array_mut()
1036            .expect("files array");
1037        files[0]
1038            .as_object_mut()
1039            .expect("file object")
1040            .retain(|key, _| key != "insertions" && key != "deletions");
1041        value["result"]["snapshot"]
1042            .as_object_mut()
1043            .expect("snapshot object")
1044            .retain(|key, _| {
1045                key != "total_insertions" && key != "total_deletions" && key != "recent_commits"
1046            });
1047
1048        let round_trip: ControlResponse =
1049            serde_json::from_value(value).expect("deserialize vcs response");
1050
1051        let ControlResponse::Vcs { result } = round_trip else {
1052            panic!("expected vcs response");
1053        };
1054        let snapshot = result.snapshot.expect("snapshot");
1055        assert_eq!(snapshot.total_insertions, 0);
1056        assert_eq!(snapshot.total_deletions, 0);
1057        assert!(snapshot.recent_commits.is_empty());
1058        assert_eq!(snapshot.files.len(), 1);
1059        assert_eq!(snapshot.files[0].insertions, None);
1060        assert_eq!(snapshot.files[0].deletions, None);
1061    }
1062}