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