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}