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}