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, Direction, NotificationDeliveryState, NotificationId,
7    PaneId, PaneKind, PaneMetadataPatch, PersistedSession, ProgressState, SignalEvent, SignalKind,
8    SplitAxis, SurfaceId, WindowId, WorkspaceColumnId, WorkspaceId, WorkspaceLogEntry,
9    WorkspaceViewport, WorkspaceWindowId, WorkspaceWindowMoveTarget, WorkspaceWindowTabId,
10};
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(tag = "command", rename_all = "snake_case")]
14pub enum ControlCommand {
15    CreateWorkspace {
16        label: String,
17    },
18    RenameWorkspace {
19        workspace_id: WorkspaceId,
20        label: String,
21    },
22    SwitchWorkspace {
23        window_id: Option<WindowId>,
24        workspace_id: WorkspaceId,
25    },
26    SplitPane {
27        workspace_id: WorkspaceId,
28        pane_id: Option<PaneId>,
29        axis: SplitAxis,
30    },
31    SplitPaneDirection {
32        workspace_id: WorkspaceId,
33        pane_id: PaneId,
34        direction: Direction,
35    },
36    CreateWorkspaceWindow {
37        workspace_id: WorkspaceId,
38        direction: Direction,
39    },
40    FocusWorkspaceWindow {
41        workspace_id: WorkspaceId,
42        workspace_window_id: WorkspaceWindowId,
43    },
44    MoveWorkspaceWindow {
45        workspace_id: WorkspaceId,
46        workspace_window_id: WorkspaceWindowId,
47        target: WorkspaceWindowMoveTarget,
48    },
49    CreateWorkspaceWindowTab {
50        workspace_id: WorkspaceId,
51        workspace_window_id: WorkspaceWindowId,
52    },
53    FocusWorkspaceWindowTab {
54        workspace_id: WorkspaceId,
55        workspace_window_id: WorkspaceWindowId,
56        workspace_window_tab_id: WorkspaceWindowTabId,
57    },
58    MoveWorkspaceWindowTab {
59        workspace_id: WorkspaceId,
60        workspace_window_id: WorkspaceWindowId,
61        workspace_window_tab_id: WorkspaceWindowTabId,
62        to_index: usize,
63    },
64    TransferWorkspaceWindowTab {
65        workspace_id: WorkspaceId,
66        source_workspace_window_id: WorkspaceWindowId,
67        workspace_window_tab_id: WorkspaceWindowTabId,
68        target_workspace_window_id: WorkspaceWindowId,
69        to_index: usize,
70    },
71    ExtractWorkspaceWindowTab {
72        workspace_id: WorkspaceId,
73        source_workspace_window_id: WorkspaceWindowId,
74        workspace_window_tab_id: WorkspaceWindowTabId,
75        target: WorkspaceWindowMoveTarget,
76    },
77    CloseWorkspaceWindowTab {
78        workspace_id: WorkspaceId,
79        workspace_window_id: WorkspaceWindowId,
80        workspace_window_tab_id: WorkspaceWindowTabId,
81    },
82    FocusPane {
83        workspace_id: WorkspaceId,
84        pane_id: PaneId,
85    },
86    FocusPaneDirection {
87        workspace_id: WorkspaceId,
88        direction: Direction,
89    },
90    ResizeActiveWindow {
91        workspace_id: WorkspaceId,
92        direction: Direction,
93        amount: i32,
94    },
95    ResizeActivePaneSplit {
96        workspace_id: WorkspaceId,
97        direction: Direction,
98        amount: i32,
99    },
100    SetWorkspaceColumnWidth {
101        workspace_id: WorkspaceId,
102        workspace_column_id: WorkspaceColumnId,
103        width: i32,
104    },
105    SetWorkspaceWindowHeight {
106        workspace_id: WorkspaceId,
107        workspace_window_id: WorkspaceWindowId,
108        height: i32,
109    },
110    SetWindowSplitRatio {
111        workspace_id: WorkspaceId,
112        workspace_window_id: WorkspaceWindowId,
113        path: Vec<bool>,
114        ratio: u16,
115    },
116    UpdatePaneMetadata {
117        pane_id: PaneId,
118        patch: PaneMetadataPatch,
119    },
120    UpdateSurfaceMetadata {
121        surface_id: SurfaceId,
122        patch: PaneMetadataPatch,
123    },
124    CreateSurface {
125        workspace_id: WorkspaceId,
126        pane_id: PaneId,
127        kind: PaneKind,
128    },
129    FocusSurface {
130        workspace_id: WorkspaceId,
131        pane_id: PaneId,
132        surface_id: SurfaceId,
133    },
134    StartSurfaceAgentSession {
135        workspace_id: WorkspaceId,
136        pane_id: PaneId,
137        surface_id: SurfaceId,
138        agent_kind: String,
139    },
140    StopSurfaceAgentSession {
141        workspace_id: WorkspaceId,
142        pane_id: PaneId,
143        surface_id: SurfaceId,
144        exit_status: i32,
145    },
146    MarkSurfaceCompleted {
147        workspace_id: WorkspaceId,
148        pane_id: PaneId,
149        surface_id: SurfaceId,
150    },
151    CloseSurface {
152        workspace_id: WorkspaceId,
153        pane_id: PaneId,
154        surface_id: SurfaceId,
155    },
156    MoveSurface {
157        workspace_id: WorkspaceId,
158        pane_id: PaneId,
159        surface_id: SurfaceId,
160        to_index: usize,
161    },
162    TransferSurface {
163        source_workspace_id: WorkspaceId,
164        source_pane_id: PaneId,
165        surface_id: SurfaceId,
166        target_workspace_id: WorkspaceId,
167        target_pane_id: PaneId,
168        to_index: usize,
169    },
170    MoveSurfaceToSplit {
171        source_workspace_id: WorkspaceId,
172        source_pane_id: PaneId,
173        surface_id: SurfaceId,
174        target_workspace_id: WorkspaceId,
175        target_pane_id: PaneId,
176        direction: Direction,
177    },
178    MoveSurfaceToWorkspace {
179        source_workspace_id: WorkspaceId,
180        source_pane_id: PaneId,
181        surface_id: SurfaceId,
182        target_workspace_id: WorkspaceId,
183    },
184    SetWorkspaceViewport {
185        workspace_id: WorkspaceId,
186        viewport: WorkspaceViewport,
187    },
188    ClosePane {
189        workspace_id: WorkspaceId,
190        pane_id: PaneId,
191    },
192    CloseWorkspace {
193        workspace_id: WorkspaceId,
194    },
195    ReorderWorkspaces {
196        window_id: WindowId,
197        workspace_ids: Vec<WorkspaceId>,
198    },
199    EmitSignal {
200        workspace_id: WorkspaceId,
201        pane_id: PaneId,
202        surface_id: Option<SurfaceId>,
203        event: SignalEvent,
204    },
205    AgentSetStatus {
206        workspace_id: WorkspaceId,
207        text: String,
208    },
209    AgentClearStatus {
210        workspace_id: WorkspaceId,
211    },
212    AgentSetProgress {
213        workspace_id: WorkspaceId,
214        progress: ProgressState,
215    },
216    AgentClearProgress {
217        workspace_id: WorkspaceId,
218    },
219    AgentAppendLog {
220        workspace_id: WorkspaceId,
221        entry: WorkspaceLogEntry,
222    },
223    AgentClearLog {
224        workspace_id: WorkspaceId,
225    },
226    AgentCreateNotification {
227        target: AgentTarget,
228        kind: SignalKind,
229        title: Option<String>,
230        subtitle: Option<String>,
231        external_id: Option<String>,
232        message: String,
233        state: AttentionState,
234    },
235    OpenNotification {
236        window_id: Option<WindowId>,
237        notification_id: NotificationId,
238    },
239    ClearNotification {
240        notification_id: NotificationId,
241    },
242    MarkNotificationDelivery {
243        notification_id: NotificationId,
244        delivery: NotificationDeliveryState,
245    },
246    AgentClearNotifications {
247        target: AgentTarget,
248    },
249    DismissSurfaceAlert {
250        workspace_id: WorkspaceId,
251        pane_id: PaneId,
252        surface_id: SurfaceId,
253    },
254    AgentTriggerFlash {
255        workspace_id: WorkspaceId,
256        pane_id: PaneId,
257        surface_id: SurfaceId,
258    },
259    AgentFocusLatestUnread {
260        window_id: Option<WindowId>,
261    },
262    Browser {
263        browser_command: BrowserControlCommand,
264    },
265    TerminalDebug {
266        debug_command: TerminalDebugCommand,
267    },
268    QueryStatus {
269        query: ControlQuery,
270    },
271}
272
273#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
274#[serde(rename_all = "snake_case")]
275pub enum ControlErrorCode {
276    InvalidParams,
277    NotFound,
278    Timeout,
279    InvalidState,
280    NotSupported,
281    Internal,
282}
283
284#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
285pub struct ControlError {
286    pub code: ControlErrorCode,
287    pub message: String,
288}
289
290impl ControlError {
291    pub fn new(code: ControlErrorCode, message: impl Into<String>) -> Self {
292        Self {
293            code,
294            message: message.into(),
295        }
296    }
297
298    pub fn invalid_params(message: impl Into<String>) -> Self {
299        Self::new(ControlErrorCode::InvalidParams, message)
300    }
301
302    pub fn not_found(message: impl Into<String>) -> Self {
303        Self::new(ControlErrorCode::NotFound, message)
304    }
305
306    pub fn timeout(message: impl Into<String>) -> Self {
307        Self::new(ControlErrorCode::Timeout, message)
308    }
309
310    pub fn invalid_state(message: impl Into<String>) -> Self {
311        Self::new(ControlErrorCode::InvalidState, message)
312    }
313
314    pub fn not_supported(message: impl Into<String>) -> Self {
315        Self::new(ControlErrorCode::NotSupported, message)
316    }
317
318    pub fn internal(message: impl Into<String>) -> Self {
319        Self::new(ControlErrorCode::Internal, message)
320    }
321}
322
323impl std::fmt::Display for ControlError {
324    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
325        write!(f, "{:?}: {}", self.code, self.message)
326    }
327}
328
329impl std::error::Error for ControlError {}
330
331#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
332#[serde(tag = "browser_command", rename_all = "snake_case")]
333pub enum BrowserControlCommand {
334    Navigate {
335        surface_id: SurfaceId,
336        url: String,
337    },
338    Back {
339        surface_id: SurfaceId,
340    },
341    Forward {
342        surface_id: SurfaceId,
343    },
344    Reload {
345        surface_id: SurfaceId,
346    },
347    FocusWebview {
348        surface_id: SurfaceId,
349    },
350    IsWebviewFocused {
351        surface_id: SurfaceId,
352    },
353    Snapshot {
354        surface_id: SurfaceId,
355    },
356    Eval {
357        surface_id: SurfaceId,
358        script: String,
359    },
360    Wait {
361        surface_id: SurfaceId,
362        condition: BrowserWaitCondition,
363        timeout_ms: u64,
364        poll_interval_ms: u64,
365    },
366    Click {
367        surface_id: SurfaceId,
368        target: BrowserTarget,
369        snapshot_after: bool,
370    },
371    Dblclick {
372        surface_id: SurfaceId,
373        target: BrowserTarget,
374        snapshot_after: bool,
375    },
376    Type {
377        surface_id: SurfaceId,
378        target: BrowserTarget,
379        text: String,
380        snapshot_after: bool,
381    },
382    Fill {
383        surface_id: SurfaceId,
384        target: BrowserTarget,
385        text: String,
386        snapshot_after: bool,
387    },
388    Press {
389        surface_id: SurfaceId,
390        target: Option<BrowserTarget>,
391        key: String,
392        snapshot_after: bool,
393    },
394    Keydown {
395        surface_id: SurfaceId,
396        target: Option<BrowserTarget>,
397        key: String,
398        snapshot_after: bool,
399    },
400    Keyup {
401        surface_id: SurfaceId,
402        target: Option<BrowserTarget>,
403        key: String,
404        snapshot_after: bool,
405    },
406    Hover {
407        surface_id: SurfaceId,
408        target: BrowserTarget,
409        snapshot_after: bool,
410    },
411    Focus {
412        surface_id: SurfaceId,
413        target: BrowserTarget,
414        snapshot_after: bool,
415    },
416    Check {
417        surface_id: SurfaceId,
418        target: BrowserTarget,
419        snapshot_after: bool,
420    },
421    Uncheck {
422        surface_id: SurfaceId,
423        target: BrowserTarget,
424        snapshot_after: bool,
425    },
426    Select {
427        surface_id: SurfaceId,
428        target: BrowserTarget,
429        values: Vec<String>,
430        snapshot_after: bool,
431    },
432    Scroll {
433        surface_id: SurfaceId,
434        target: Option<BrowserTarget>,
435        dx: i32,
436        dy: i32,
437        snapshot_after: bool,
438    },
439    ScrollIntoView {
440        surface_id: SurfaceId,
441        target: BrowserTarget,
442        snapshot_after: bool,
443    },
444    Get {
445        surface_id: SurfaceId,
446        query: BrowserGetCommand,
447    },
448    Is {
449        surface_id: SurfaceId,
450        query: BrowserPredicateCommand,
451    },
452    Screenshot {
453        surface_id: SurfaceId,
454        path: Option<String>,
455        full_document: bool,
456    },
457}
458
459#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
460#[serde(tag = "target", rename_all = "snake_case")]
461pub enum BrowserTarget {
462    Ref { value: String },
463    Selector { value: String },
464}
465
466#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
467#[serde(tag = "condition", rename_all = "snake_case")]
468pub enum BrowserWaitCondition {
469    Selector { selector: String },
470    Text { text: String },
471    UrlMatches { pattern: String },
472    LoadState { state: BrowserLoadState },
473    Function { script: String },
474    Delay { duration_ms: u64 },
475}
476
477#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
478#[serde(rename_all = "snake_case")]
479pub enum BrowserLoadState {
480    Started,
481    Redirected,
482    Committed,
483    Finished,
484}
485
486#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
487#[serde(tag = "query", rename_all = "snake_case")]
488pub enum BrowserGetCommand {
489    Url,
490    Title,
491    Text {
492        target: BrowserTarget,
493    },
494    Html {
495        target: BrowserTarget,
496    },
497    Value {
498        target: BrowserTarget,
499    },
500    Attr {
501        target: BrowserTarget,
502        name: String,
503    },
504    Count {
505        selector: String,
506    },
507    Box {
508        target: BrowserTarget,
509    },
510    Styles {
511        target: BrowserTarget,
512        properties: Vec<String>,
513    },
514}
515
516#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
517#[serde(tag = "query", rename_all = "snake_case")]
518pub enum BrowserPredicateCommand {
519    Visible { target: BrowserTarget },
520    Enabled { target: BrowserTarget },
521    Checked { target: BrowserTarget },
522}
523
524#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
525#[serde(tag = "terminal_command", rename_all = "snake_case")]
526pub enum TerminalDebugCommand {
527    IsFocused {
528        surface_id: SurfaceId,
529    },
530    ReadText {
531        surface_id: SurfaceId,
532        tail_lines: Option<usize>,
533    },
534    RenderStats {
535        surface_id: SurfaceId,
536    },
537}
538
539#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
540#[serde(tag = "result", rename_all = "snake_case")]
541pub enum TerminalDebugResult {
542    IsFocused { focused: bool },
543    ReadText { text: String },
544    RenderStats { stats: TerminalRenderStats },
545}
546
547#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
548pub struct TerminalRenderStats {
549    pub surface_id: SurfaceId,
550    pub workspace_id: WorkspaceId,
551    pub pane_id: PaneId,
552    pub mounted: bool,
553    pub visible: bool,
554    pub focused: bool,
555    pub backend: String,
556    pub cols: u16,
557    pub rows: u16,
558    pub width_px: i32,
559    pub height_px: i32,
560    pub has_selection: bool,
561}
562
563#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
564pub struct IdentifyContext {
565    pub window_id: WindowId,
566    pub workspace_id: WorkspaceId,
567    pub workspace_label: String,
568    pub workspace_window_id: Option<WorkspaceWindowId>,
569    pub pane_id: PaneId,
570    pub surface_id: SurfaceId,
571    pub surface_kind: PaneKind,
572    pub title: Option<String>,
573    pub cwd: Option<String>,
574    pub url: Option<String>,
575    pub loading: Option<bool>,
576}
577
578#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
579pub struct IdentifyResult {
580    pub focused: IdentifyContext,
581    pub caller: Option<IdentifyContext>,
582}
583
584#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
585#[serde(tag = "scope", rename_all = "snake_case")]
586pub enum ControlQuery {
587    ActiveWindow,
588    Window {
589        window_id: WindowId,
590    },
591    Workspace {
592        workspace_id: WorkspaceId,
593    },
594    Identify {
595        workspace_id: Option<WorkspaceId>,
596        pane_id: Option<PaneId>,
597        surface_id: Option<SurfaceId>,
598    },
599    All,
600}
601
602#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
603#[serde(tag = "status", rename_all = "snake_case")]
604pub enum ControlResponse {
605    Ack {
606        message: String,
607    },
608    WorkspaceCreated {
609        workspace_id: WorkspaceId,
610    },
611    PaneSplit {
612        pane_id: PaneId,
613    },
614    SurfaceMovedToSplit {
615        pane_id: PaneId,
616    },
617    SurfaceMovedToWorkspace {
618        pane_id: PaneId,
619    },
620    SurfaceCreated {
621        surface_id: SurfaceId,
622    },
623    WorkspaceWindowCreated {
624        pane_id: PaneId,
625    },
626    WorkspaceWindowTabCreated {
627        pane_id: PaneId,
628        workspace_window_tab_id: WorkspaceWindowTabId,
629    },
630    Status {
631        session: PersistedSession,
632    },
633    WorkspaceState {
634        workspace_id: WorkspaceId,
635        session: PersistedSession,
636    },
637    Browser {
638        result: JsonValue,
639    },
640    TerminalDebug {
641        result: TerminalDebugResult,
642    },
643    Identify {
644        result: IdentifyResult,
645    },
646}
647
648#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
649pub struct RequestFrame {
650    pub request_id: Uuid,
651    pub command: ControlCommand,
652}
653
654#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
655pub struct ResponseFrame {
656    pub request_id: Uuid,
657    pub response: Result<ControlResponse, ControlError>,
658}
659
660impl RequestFrame {
661    pub fn new(command: ControlCommand) -> Self {
662        Self {
663            request_id: Uuid::now_v7(),
664            command,
665        }
666    }
667}
668
669impl From<AppModel> for ControlResponse {
670    fn from(model: AppModel) -> Self {
671        Self::Status {
672            session: model.snapshot(),
673        }
674    }
675}