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}