1mod backups;
18mod blank;
19mod bridge;
20mod builds;
21mod busy;
22mod desktop;
23mod files;
24mod freeze;
25mod freezing;
26mod history;
27mod keep;
28mod panel;
29mod plan;
30mod relay;
31mod rename;
32pub(crate) mod shared;
33mod sound;
34mod strip;
35mod tab;
36mod viewer;
37
38#[cfg(test)]
39mod live;
40#[cfg(test)]
41mod tests;
42
43pub(crate) use backups::bytes;
44pub use backups::{BackupOf, BackupTrouble, Farewell, Leaving, Part, leaving};
45pub use blank::Choice;
46#[cfg(test)]
47pub(crate) use bridge::type_in;
48pub use bridge::{Letter, Letters, Undelivered};
49pub use desktop::Opening;
50pub use files::DocumentTrouble;
51pub use freezing::Read;
52pub use freezing::waking_on_the_way_out;
53pub use history::HistoryKey;
54pub use keep::{BASE_LABEL as WORKSPACE_BASE_LABEL, enter_admin, image as workspace_image};
55pub use panel::{Panel, PanelWidget};
56#[cfg(test)]
57pub(crate) use plan::open_window as plan_open_window;
58#[cfg(test)]
59pub(crate) use plan::prepare_window as plan_prepare_window;
60pub use plan::{
61 ASSETS_DIR, Bridge, CODE_DIR, ContainerPlan, HOME_DIR, KEEP_ALIVE, LaunchFailure, MCP_DIR, PLAN_LABEL, SHELL,
62 ensure_running, ensure_running_noting, give_window_login, open_page,
63};
64pub use tab::{Back, Pages, Shown, Tab, TabKey, TabKind, TabState};
65pub use viewer::{MOST_TEXT, Taken};
66
67use std::collections::{HashMap, HashSet};
68use std::ffi::OsString;
69
70use qframe::date::Date;
71use qframe::icons::Icons;
72use qframe::keymap::{Key, KeyChord};
73use qframe::prelude::*;
74use qframe::runtime::Task;
75use qframe::widgets::{
76 CollapsedMarker, ContextItem, EmptyState, FileManagerMsg, FileManagerState, LogView, Markdown, RailTab, ScrollView,
77 ShimmerText, Side, SidePanel, Spinner, TabEdit, TabRail, TabWidth, Terminal, TerminalEvent, TerminalSession, Toast,
78};
79
80use std::path::{Path, PathBuf};
81
82use crate::backup::conversations::Brought;
83use crate::backup::{BackupEvery, Entry, Snapshot, SnapshotId};
84use crate::base::apps::{self, Editor, FileKind, Quiet, Sound};
85use crate::bridge::config::{self as bridge_config, Unregistered};
86use crate::bridge::rules::Rules;
87use crate::bridge::socket::Call;
88use crate::engine::{Container, Engine, EngineCommand, EngineKind, Exec, HostUser};
89use crate::profile::guidance::Unguided;
90use crate::profile::history::Conversation;
91use crate::profile::unattended::{self, Asks};
92use crate::profile::{HarnessKind, Pick, Profile, ProviderChoice};
93use crate::store::{
94 Registry, Session, SessionTab, SessionTabKind, SessionWorkspace, Store, WorkspaceFile, WorkspaceId, WorkspacePaths,
95 add_profile,
96};
97use crate::ui::page;
98use crate::ui::profiles::work::Problem;
99use crate::ui::settings::engine::{Help, help, name as engine_name};
100use crate::ui::stalling;
101
102const RAIL_WIDTH: u16 = 4;
106
107const RAIL_ROWS: u16 = 3;
109
110const PANEL_WIDTH: u16 = 32;
113const PANEL_MIN: u16 = 18;
115const PANEL_MAX: u16 = 52;
117
118#[derive(Debug, Clone)]
120pub enum Msg {
121 OpenWorkspace(usize),
123 CloseWorkspace(usize),
125 AddWorkspace,
128 Freezable(bool),
131 Look,
134 Looked(usize, String, String, freezing::Read, std::time::Instant),
140 Frozen(String, bool),
143 Thawed(String, bool),
146 OpenTab(usize),
148 CloseTab(usize),
150 CloseTabAnyway(TabKey),
153 TabEnded,
155 MoveTab {
157 from: usize,
159 to: usize,
161 },
162 NewTab,
164 RenameTab(usize),
166 RenameOpenTab,
168 TabName(String),
170 NameTab,
172 CancelRename,
174 LeaveTerminal,
176 EnterTerminal,
178 Walking(bool),
181 EnterTab,
183 NextTab(bool),
185 GoToTab(usize),
187 Choose(TabKey, Choice),
189 HighlightChoice(usize),
191 ShowAll(HistoryKey),
193 HistoryRead(HistoryKey, u64, Result<Vec<Conversation>, String>),
196 HistorySlow(HistoryKey, u64),
198 HistorySettled(HistoryKey),
200 ManageProfiles,
204 Profiles(Vec<Profile>),
206 ProfileAdded(String, Result<WorkspaceFile, String>),
208 Ready(TabKey, u64, Result<(), LaunchFailure>),
210 BuildImage(TabKey),
212 ImageLine(TabKey, String),
214 ImageBuilt(TabKey, Result<(), Option<String>>),
217 StopBuild(TabKey),
219 BuildLooked(std::time::Instant),
222 CancelOpen(TabKey),
225 Woken(TabKey, u64, Result<(), LaunchFailure>, Option<Vec<Conversation>>, Option<String>),
229 Output(TabKey, u64, TerminalEvent),
231 Checked(TabKey, u64, bool),
233 Restart(TabKey),
235 TogglePanel(bool),
237 ResizePanel(u16),
239 ToggleWidget(usize, bool),
241 MoveWidget {
243 from: usize,
245 to: usize,
247 },
248 ShowWidgets(bool),
250 HighlightWidget(usize),
252 AddWidget(PanelWidget),
254 RemoveWidget(PanelWidget),
256 Files(String, FileManagerMsg),
259 LeaveOut(String, bool),
262 OpenFile(String),
264 EditFile(String),
266 Missing(TabKey, u64),
268 DocumentRead(TabKey, u64, Result<String, DocumentTrouble>),
270 TextRead(TabKey, u64, Result<Taken, LaunchFailure>),
272 Page(TabKey, Option<usize>),
275 Playable(TabKey, u64, Result<PathBuf, LaunchFailure>),
278 Described(TabKey, u64, Quiet, Result<Taken, LaunchFailure>),
281 Silenced,
283 WindowOpened(TabKey, u64, Result<Opening, LaunchFailure>),
285 WindowEnded(TabKey, u64, Option<u32>),
287 SignInWanted(TabKey, u64, Vec<String>),
289 SignInOpened(TabKey, u64, String, Shown),
291 SignInBack(TabKey, u64, u64, crate::desktop::callback::Ending),
293 SignInHere(TabKey),
295 RaiseWindow(TabKey),
297 WindowRaised(Result<(), LaunchFailure>),
299 CloseWindow(TabKey),
301 WindowsClosed(Option<LaunchFailure>),
304 SelectContainer(usize),
306 RefreshContainers,
308 ContainersRead(String, Result<Vec<Container>, LaunchFailure>),
310 StopContainer(String),
312 RestartContainer(String),
314 ContainerActed(Result<(), LaunchFailure>),
316 Noted(Noting, Box<Msg>),
319 BackupDue(String, u64),
322 BackedUp {
324 workspace: String,
326 name: String,
328 made: Result<Snapshot, BackupTrouble>,
330 missed: Vec<(Part, BackupTrouble)>,
332 },
333 NewestBackup(String, Option<i64>),
336 BackupEvery(BackupEvery),
338 SkipWritten(String, Result<Vec<String>, String>),
341 BackupSize(String, u64),
343 BackupAssets(bool),
345 AssetsWritten(String, Result<bool, String>),
348 ShowBackups(Option<String>),
351 BackupsRead(u64, Result<Vec<Entry>, BackupTrouble>),
354 BackupsSlow(u64),
356 BackupsSettled(u64),
358 HighlightBackup(usize),
360 ChooseBackup(usize),
362 BackupsOf(usize),
365 RestoreBackup(SnapshotId),
367 StopAndRestore(SnapshotId),
370 KeepBackup,
372 CloseBackups,
374 Bridge(String, u64, Option<Call>),
377 Allow {
379 from: TabKey,
381 to: TabKey,
383 allow: bool,
385 },
386 StillAsking {
389 from: TabKey,
391 to: TabKey,
393 },
394 Letters(TabKey, Letters),
396 Deliver,
398 Return(TabKey),
400 Turn(std::time::Instant),
402 Reported(crate::provider::RelayEvent),
405 Silence(String, u64),
408 Asked(HarnessKind, Asks, Box<Msg>),
411 Unapproved(String, Box<Msg>),
414 Unbridged(HarnessKind, Unregistered, Box<Msg>),
417 Unguided(HarnessKind, Unguided, Box<Msg>),
420 Behind(String, String, Box<Msg>),
425 OpenAdmin(String),
427 Restored {
429 workspace: String,
431 name: String,
433 of: BackupOf,
435 file: Option<String>,
437 done: Result<Brought, BackupTrouble>,
439 },
440}
441
442#[derive(Debug, Clone, PartialEq, Eq)]
445pub enum Noting {
446 Unwritten(String),
449 Repaired(String),
452}
453
454#[derive(Debug)]
456pub struct OpenWorkspace {
457 id: WorkspaceId,
458 name: String,
459 paths: WorkspacePaths,
460 profiles: Vec<Profile>,
461 carried: Vec<String>,
462 tabs: Vec<Tab>,
463 active_tab: usize,
464 history: HashMap<String, history::Shelf>,
467 files: FileManagerState,
469 containers: Vec<Container>,
470 container_row: usize,
471 container_error: Option<LaunchFailure>,
472 busy: bool,
473 backup: backups::State,
475 skip: Vec<String>,
477 assets: bool,
479 link: bridge::Link,
481 relay: relay::Link,
484 servers: shared::ServerTokens,
487 next_number: u32,
490}
491
492impl OpenWorkspace {
493 #[must_use]
501 pub fn new(file: &WorkspaceFile, paths: WorkspacePaths, profiles: Vec<Profile>) -> Self {
502 let files = FileManagerState::new(paths.code.clone()).confined().showing_hidden(true);
505 let carried = file.profiles.iter().map(|profile| profile.name.clone()).collect();
506 let mut workspace = Self {
507 id: file.id.clone(),
508 name: file.name.clone(),
509 paths,
510 profiles: Vec::new(),
511 carried,
512 tabs: Vec::new(),
513 active_tab: 0,
514 history: HashMap::new(),
515 files,
516 containers: Vec::new(),
517 container_row: 0,
518 container_error: None,
519 busy: false,
520 backup: backups::State::default(),
521 skip: file.backup_skip.clone(),
522 assets: file.backup_assets,
523 link: bridge::Link::default(),
524 relay: relay::Link::default(),
525 servers: shared::ServerTokens::default(),
526 next_number: 1,
527 };
528 workspace.set_profiles(profiles);
529 workspace
530 }
531
532 fn set_profiles(&mut self, mut profiles: Vec<Profile>) {
535 profiles.sort_by_key(|profile| {
536 self.carried.iter().position(|name| name == profile.name.as_str()).unwrap_or(usize::MAX)
537 });
538 self.profiles = profiles;
539 }
540
541 #[must_use]
543 pub fn id(&self) -> &str {
544 self.id.as_str()
545 }
546
547 #[must_use]
549 pub fn name(&self) -> &str {
550 &self.name
551 }
552
553 #[must_use]
555 pub fn paths(&self) -> &WorkspacePaths {
556 &self.paths
557 }
558
559 #[must_use]
562 pub fn profiles(&self) -> &[Profile] {
563 &self.profiles
564 }
565
566 #[must_use]
568 pub fn carries(&self, name: &str) -> bool {
569 self.carried.iter().any(|carried| carried == name)
570 }
571
572 #[must_use]
574 pub fn tabs(&self) -> &[Tab] {
575 &self.tabs
576 }
577
578 #[must_use]
580 pub fn active_tab(&self) -> Option<&Tab> {
581 self.tabs.get(self.active_tab)
582 }
583
584 #[must_use]
586 pub fn files(&self) -> &FileManagerState {
587 &self.files
588 }
589
590 #[must_use]
592 pub fn backup_skip(&self) -> &[String] {
593 &self.skip
594 }
595
596 #[must_use]
598 pub fn backs_up_assets(&self) -> bool {
599 self.assets
600 }
601
602 #[must_use]
604 pub fn containers(&self) -> &[Container] {
605 &self.containers
606 }
607
608 #[must_use]
613 pub fn plan(&self, kind: &TabKind) -> Option<ContainerPlan> {
614 match kind {
615 TabKind::Shell | TabKind::Image(_) | TabKind::Editor(_) | TabKind::Pdf(_) | TabKind::Office(_) => {
616 Some(ContainerPlan::base(self.id.as_str(), &self.paths))
617 }
618 TabKind::Markdown(_) | TabKind::Sound(_) => None,
619 TabKind::Profile(name) | TabKind::Admin(name) => self
621 .profiles
622 .iter()
623 .find(|profile| profile.name.as_str() == name)
624 .map(|profile| ContainerPlan::profile(&self.id, &self.paths, profile)),
625 TabKind::Desktop(name) => self
628 .profiles
629 .iter()
630 .find(|profile| profile.name.as_str() == name)
631 .and_then(|profile| ContainerPlan::window(&self.id, &self.paths, profile)),
632 TabKind::New => None,
633 }
634 }
635
636 #[must_use]
641 pub fn program(&self, tab: &Tab, editor: Editor) -> Option<Vec<String>> {
642 match tab.kind() {
643 TabKind::Shell => Some(plan::SHELL.iter().map(|part| (*part).to_owned()).collect()),
644 TabKind::Admin(_) => Some(vec!["bash".to_owned()]),
647 TabKind::Profile(name) => {
648 let profile = self.profiles.iter().find(|profile| profile.name.as_str() == name)?;
649 if shared::shares(profile) {
652 return Some(shared::tab_program(tab.conversation()));
653 }
654 let mut command = profile.harness.command_line(tab.conversation());
655 Some(match &profile.provider {
661 Some(provider) => {
662 command.splice(1..1, provider.arguments(profile.harness));
663 crate::provider::relay::wrapping(&command)
664 }
665 None => command,
666 })
667 }
668 TabKind::Image(file) => Some(apps::picture(&inside_container(file))),
669 TabKind::Editor(file) => Some(editor.command(&inside_container(file))),
670 TabKind::Pdf(file) if tab.pages().drawn => Some(apps::pdf_page(&inside_container(file), tab.pages().page)),
671 TabKind::New
674 | TabKind::Markdown(_)
675 | TabKind::Pdf(_)
676 | TabKind::Office(_)
677 | TabKind::Sound(_)
678 | TabKind::Desktop(_) => None,
679 }
680 }
681
682 fn join(&mut self, mut tab: Tab) -> usize {
684 tab.numbered(self.next_number);
685 self.next_number += 1;
686 self.tabs.push(tab);
687 self.tabs.len() - 1
688 }
689
690 fn tab_label(&self, index: usize) -> String {
694 let Some(tab) = self.tabs.get(index) else { return String::new() };
695 if let Some(name) = tab.name() {
696 return name.to_owned();
697 }
698 let label = self.automatic_label(index);
699 let alike = tab.kind().runs_agent()
700 && self.tabs.iter().enumerate().any(|(other, shown)| {
701 other != index
702 && shown.kind().runs_agent()
703 && shown.name().map_or_else(|| self.automatic_label(other), str::to_owned) == label
704 });
705 if alike { format!("{label} {}", tab.number()) } else { label }
706 }
707
708 fn automatic_label(&self, index: usize) -> String {
713 let Some(tab) = self.tabs.get(index) else { return String::new() };
714 match tab.kind() {
715 TabKind::Profile(name) => tab
716 .conversation()
717 .and_then(|id| self.history.get(name)?.title(id))
718 .map(history::short_title)
719 .filter(|title| !title.is_empty())
720 .unwrap_or_else(|| name.clone()),
721 TabKind::Admin(name) => t!("workspace.tab.admin", profile = name.clone()),
722 TabKind::Desktop(name) => t!("workspace.tab.window", profile = name.clone()),
725 TabKind::New => t!("workspace.new-tab"),
726 TabKind::Image(file)
727 | TabKind::Markdown(file)
728 | TabKind::Editor(file)
729 | TabKind::Pdf(file)
730 | TabKind::Office(file)
731 | TabKind::Sound(file) => files::name(file).to_owned(),
732 TabKind::Shell => {
733 let shell = t!("workspace.tab.shell");
734 let position = self.tabs[..index].iter().filter(|tab| tab.kind() == &TabKind::Shell).count() + 1;
735 let total = self.tabs.iter().filter(|tab| tab.kind() == &TabKind::Shell).count();
736 if total > 1 { format!("{shell} {position}") } else { shell }
737 }
738 }
739 }
740
741 #[must_use]
743 pub fn is_bridged(&self) -> bool {
744 matches!(self.link, bridge::Link::On { .. })
745 }
746
747 fn find(&mut self, key: TabKey) -> Option<(usize, &mut Tab)> {
749 self.tabs.iter_mut().enumerate().find(|(_, tab)| tab.key() == key)
750 }
751}
752
753fn inside_container(key: &str) -> String {
756 format!("{CODE_DIR}/{key}")
757}
758
759#[derive(Debug)]
761pub struct WorkspaceScreen {
762 engine: Option<Engine>,
763 user: HostUser,
764 workspaces: Vec<OpenWorkspace>,
765 active: usize,
766 panel: Panel,
767 blank_row: usize,
769 expanded: HashSet<HistoryKey>,
771 shown_blank: Option<TabKey>,
774 shown_document: Option<TabKey>,
777 editor: Editor,
779 sound: Sound,
781 runtime: Option<PathBuf>,
783 display: Result<crate::desktop::Display, crate::desktop::NoDisplay>,
786 next_key: u64,
787 live: bool,
789 patience: Option<std::time::Duration>,
791 registry: Option<PathBuf>,
794 providers_path: Option<PathBuf>,
799 upstream: crate::provider::Upstream,
803 registry_told: bool,
805 backup_every: BackupEvery,
807 last_timer: u64,
809 listing: Option<backups::Listing>,
811 last_listing: u64,
813 bridging: bool,
815 last_link: u64,
817 rules: Rules,
819 asking: Vec<bridge::Asking>,
821 delivering: bool,
824 ask_first: bool,
826 freeze_idle: bool,
829 freezing: freezing::Freezing,
832 where_: freezing::Where,
836 stall: stalling::Lengths,
839 watching: bool,
841 renaming: Option<rename::Renaming>,
843 owed_focus: Option<TabKey>,
848 walking: bool,
851 looked: std::time::Instant,
854 turning: bool,
856}
857
858impl WorkspaceScreen {
859 #[must_use]
865 pub fn new(engine: Option<Engine>, user: HostUser, workspaces: Vec<OpenWorkspace>) -> Self {
866 Self {
867 engine,
868 user,
869 workspaces,
870 active: 0,
871 panel: Panel::default(),
872 blank_row: 0,
873 expanded: HashSet::new(),
874 shown_blank: None,
875 shown_document: None,
876 editor: Editor::default(),
877 sound: Sound::default(),
878 runtime: std::env::var_os("XDG_RUNTIME_DIR").map(PathBuf::from),
879 display: desktop::display(),
880 next_key: 0,
881 live: false,
882 patience: None,
883 registry: None,
884 providers_path: crate::provider::Providers::file(),
885 upstream: crate::provider::Upstream::network(),
886 registry_told: false,
887 backup_every: BackupEvery::default(),
888 last_timer: 0,
889 listing: None,
890 last_listing: 0,
891 bridging: false,
892 last_link: 0,
893 rules: Rules::default(),
894 asking: Vec::new(),
895 delivering: false,
896 ask_first: false,
897 freeze_idle: true,
898 freezing: freezing::Freezing::default(),
899 where_: freezing::Where::default(),
900 stall: stalling::Lengths::default(),
901 watching: false,
902 renaming: None,
903 owed_focus: None,
904 walking: false,
905 looked: std::time::Instant::now(),
906 turning: false,
907 }
908 }
909
910 #[must_use]
917 pub fn watching(mut self, live: bool) -> Self {
918 self.live = live;
919 self
920 }
921
922 #[cfg(test)]
925 #[must_use]
926 pub fn watching_within(mut self, bound: std::time::Duration) -> Self {
927 self.live = true;
928 self.patience = Some(bound);
929 self
930 }
931
932 #[must_use]
939 pub fn bridging(mut self, bridging: bool) -> Self {
940 self.bridging = bridging;
941 self
942 }
943
944 #[must_use]
947 pub fn with_registry(mut self, path: Option<PathBuf>) -> Self {
948 self.registry = path;
949 self
950 }
951
952 #[must_use]
956 pub fn with_providers_path(mut self, path: Option<PathBuf>) -> Self {
957 self.providers_path = path;
958 self
959 }
960
961 #[must_use]
967 pub fn with_upstream(mut self, upstream: crate::provider::Upstream) -> Self {
968 self.upstream = upstream;
969 self
970 }
971
972 #[must_use]
974 pub fn backing_up(mut self, every: BackupEvery) -> Self {
975 self.backup_every = every;
976 self
977 }
978
979 #[must_use]
981 pub fn backup_every(&self) -> BackupEvery {
982 self.backup_every
983 }
984
985 #[must_use]
987 pub fn workspace(&self) -> Option<&OpenWorkspace> {
988 self.workspaces.get(self.active)
989 }
990
991 #[must_use]
993 pub fn workspaces(&self) -> &[OpenWorkspace] {
994 &self.workspaces
995 }
996
997 pub fn set_editor(&mut self, editor: Editor) {
1000 self.editor = editor;
1001 }
1002
1003 #[must_use]
1005 pub fn editor(&self) -> Editor {
1006 self.editor
1007 }
1008
1009 pub fn set_sound(&mut self, sound: Sound) {
1012 self.sound = sound;
1013 }
1014
1015 #[must_use]
1017 pub fn sound(&self) -> Sound {
1018 self.sound
1019 }
1020
1021 pub fn set_ask_first(&mut self, ask: bool) {
1024 self.ask_first = ask;
1025 }
1026
1027 #[must_use]
1029 pub fn ask_first(&self) -> bool {
1030 self.ask_first
1031 }
1032
1033 pub fn set_freeze_idle(&mut self, freeze: bool) {
1036 self.freeze_idle = freeze;
1037 }
1038
1039 #[must_use]
1041 pub fn freezes_idle(&self) -> bool {
1042 self.freeze_idle
1043 }
1044
1045 pub fn settled(&mut self) -> Command<Msg> {
1050 let asking = match self.engine.clone() {
1051 Some(engine) if self.freezing.supported.is_none() => freezing::ask(engine),
1052 _ => Command::none(),
1053 };
1054 Command::batch([asking, freezing::again(self)])
1055 }
1056
1057 #[cfg(test)]
1065 #[must_use]
1066 pub fn looking(
1067 mut self,
1068 lengths: freeze::Lengths,
1069 every: std::time::Duration,
1070 cgroups: std::path::PathBuf,
1071 ) -> Self {
1072 self.where_ = freezing::Where { quiet: lengths.quiet, window: lengths.window, every, cgroups };
1073 self
1074 }
1075
1076 #[cfg(test)]
1082 #[must_use]
1083 pub fn stalling(mut self, lengths: stalling::Lengths) -> Self {
1084 self.stall = lengths;
1085 self
1086 }
1087
1088 #[must_use]
1091 pub fn hearing_in(mut self, runtime: Option<PathBuf>) -> Self {
1092 self.runtime = runtime;
1093 self
1094 }
1095
1096 #[must_use]
1103 pub fn showing_on(mut self, display: Result<crate::desktop::Display, crate::desktop::NoDisplay>) -> Self {
1104 self.display = display;
1105 self
1106 }
1107
1108 pub fn set_active(&mut self, index: usize) {
1110 if index < self.workspaces.len() {
1111 self.active = index;
1112 }
1113 }
1114
1115 pub fn restore_tabs(&mut self, index: usize, record: &SessionWorkspace) {
1127 let Some(workspace) = self.workspaces.get_mut(index) else { return };
1128 let mut active = 0;
1129 let highest = record.tabs.iter().filter_map(|saved| saved.number).max().unwrap_or(0);
1130 workspace.next_number = workspace.next_number.max(highest.saturating_add(1));
1131 for (position, saved) in record.tabs.iter().enumerate() {
1132 let kind = match &saved.kind {
1133 SessionTabKind::Shell => TabKind::Shell,
1134 SessionTabKind::Profile(name) => TabKind::Profile(name.clone()),
1135 SessionTabKind::New => TabKind::New,
1136 SessionTabKind::Image(file) => TabKind::Image(file.clone()),
1137 SessionTabKind::Markdown(file) => TabKind::Markdown(file.clone()),
1138 SessionTabKind::Editor(file) => TabKind::Editor(file.clone()),
1139 SessionTabKind::Pdf(file) => TabKind::Pdf(file.clone()),
1140 SessionTabKind::Office(file) => TabKind::Office(file.clone()),
1141 SessionTabKind::Sound(file) => TabKind::Sound(file.clone()),
1142 SessionTabKind::Desktop(name) => TabKind::Desktop(name.clone()),
1143 };
1144 let usable = match &kind {
1145 TabKind::New => true,
1146 TabKind::Image(file)
1147 | TabKind::Markdown(file)
1148 | TabKind::Editor(file)
1149 | TabKind::Pdf(file)
1150 | TabKind::Office(file)
1151 | TabKind::Sound(file) => files::is_inside(file),
1152 TabKind::Shell | TabKind::Profile(_) | TabKind::Desktop(_) | TabKind::Admin(_) => {
1153 workspace.plan(&kind).is_some()
1154 }
1155 };
1156 if !usable {
1157 continue;
1158 }
1159 if position <= record.active_tab {
1160 active = workspace.tabs.len();
1161 }
1162 let key = TabKey(self.next_key);
1163 self.next_key += 1;
1164 let mut tab = Tab::restored(key, kind, saved.opened, saved.conversation.clone());
1165 tab.hold(matches!(tab.kind(), TabKind::Sound(_) | TabKind::Desktop(_)));
1168 tab.rename(saved.name.clone());
1169 match saved.number.filter(|number| workspace.tabs.iter().all(|tab| tab.number() != *number)) {
1170 Some(number) => {
1171 tab.numbered(number);
1172 workspace.tabs.push(tab);
1173 }
1174 None => {
1175 workspace.join(tab);
1176 }
1177 }
1178 }
1179 workspace.active_tab = active;
1180 }
1181
1182 #[must_use]
1185 pub fn session(&self) -> Session {
1186 let workspaces = self
1187 .workspaces
1188 .iter()
1189 .map(|workspace| SessionWorkspace {
1190 id: workspace.id.clone(),
1191 active_tab: workspace.active_tab,
1192 tabs: workspace
1193 .tabs
1194 .iter()
1195 .filter_map(|tab| {
1196 Some(SessionTab {
1197 kind: match tab.kind() {
1198 TabKind::Admin(_) => return None,
1201 TabKind::Shell => SessionTabKind::Shell,
1202 TabKind::Profile(name) => SessionTabKind::Profile(name.clone()),
1203 TabKind::New => SessionTabKind::New,
1204 TabKind::Image(file) => SessionTabKind::Image(file.clone()),
1205 TabKind::Markdown(file) => SessionTabKind::Markdown(file.clone()),
1206 TabKind::Editor(file) => SessionTabKind::Editor(file.clone()),
1207 TabKind::Pdf(file) => SessionTabKind::Pdf(file.clone()),
1208 TabKind::Office(file) => SessionTabKind::Office(file.clone()),
1209 TabKind::Sound(file) => SessionTabKind::Sound(file.clone()),
1210 TabKind::Desktop(name) => SessionTabKind::Desktop(name.clone()),
1211 },
1212 conversation: tab.conversation().map(str::to_owned),
1213 opened: tab.opened(),
1214 number: Some(tab.number()),
1215 name: tab.name().map(str::to_owned),
1216 })
1217 })
1218 .collect(),
1219 })
1220 .collect();
1221 Session { active: self.workspace().map(|workspace| workspace.id.clone()), workspaces }
1222 }
1223
1224 #[must_use]
1226 pub fn engine(&self) -> Option<&Engine> {
1227 self.engine.as_ref()
1228 }
1229
1230 #[must_use]
1232 pub fn panel(&self) -> &Panel {
1233 &self.panel
1234 }
1235
1236 #[must_use]
1241 pub fn launch_command(&self, key: TabKey) -> Option<EngineCommand> {
1242 let engine = self.engine.as_ref()?;
1243 let workspace = self.workspaces.iter().find(|workspace| workspace.tabs.iter().any(|tab| tab.key() == key))?;
1244 let tab = workspace.tabs.iter().find(|tab| tab.key() == key)?;
1245 if let TabKind::Sound(_) = tab.kind() {
1246 return sound::play_command(engine, self.user, workspace, tab, tab.socket()?);
1247 }
1248 let plan = workspace.plan(tab.kind())?;
1249 let program = workspace.program(tab, self.editor)?;
1250 let parts: Vec<&str> = program.iter().map(String::as_str).collect();
1251 match tab.kind() {
1256 TabKind::Profile(_) => {
1257 let mut env = vec![(crate::bridge::TOKEN_VARIABLE.to_owned(), tab.token().to_owned())];
1258 let name = tab.kind().profile();
1259 let profile = workspace.profiles.iter().find(|profile| Some(profile.name.as_str()) == name);
1260 if let Some(profile) = profile
1261 && shared::shares(profile)
1262 {
1263 env.extend(self.server_environment(workspace, profile));
1266 } else if let Some(profile) = profile
1267 && let Some(provider) = &profile.provider
1268 {
1269 env.extend(provider.environment(profile.harness, tab.token(), self.measured_window(provider)));
1272 }
1273 let env: Vec<(&str, &str)> = env.iter().map(|(name, value)| (name.as_str(), value.as_str())).collect();
1274 Some(plan.enter_with(engine, &parts, &env))
1275 }
1276 TabKind::Admin(_) => Some(keep::enter_admin(engine, &plan)),
1277 _ => Some(plan.enter(engine, &parts)),
1278 }
1279 }
1280
1281 #[must_use]
1285 pub fn read_command(&self, key: TabKey) -> Option<EngineCommand> {
1286 let engine = self.engine.as_ref()?;
1287 let workspace = self.workspaces.iter().find(|workspace| workspace.tabs.iter().any(|tab| tab.key() == key))?;
1288 let tab = workspace.tabs.iter().find(|tab| tab.key() == key)?;
1289 let plan = workspace.plan(tab.kind())?;
1290 let program = viewer::text_command(tab.kind())?;
1291 let parts: Vec<&str> = program.iter().map(String::as_str).collect();
1292 Some(engine.exec_without_terminal(&Exec { container: &plan.name, command: &parts }).unbounded())
1294 }
1295
1296 fn workspace_mut(&mut self, id: &str) -> Option<&mut OpenWorkspace> {
1298 self.workspaces.iter_mut().find(|workspace| workspace.id.as_str() == id)
1299 }
1300
1301 fn shelf(&mut self, key: &HistoryKey) -> Option<&mut history::Shelf> {
1303 Some(self.workspace_mut(&key.workspace)?.history.entry(key.profile.clone()).or_default())
1304 }
1305
1306 fn owner(&self, key: TabKey) -> Option<&OpenWorkspace> {
1308 self.workspaces.iter().find(|workspace| workspace.tabs.iter().any(|tab| tab.key() == key))
1309 }
1310
1311 fn owner_mut(&mut self, key: TabKey) -> Option<&mut OpenWorkspace> {
1313 self.workspaces.iter_mut().find(|workspace| workspace.tabs.iter().any(|tab| tab.key() == key))
1314 }
1315}
1316
1317impl WorkspaceScreen {
1318 pub fn attach(&mut self, key: TabKey, session: TerminalSession) {
1322 if let Some((_, tab)) = self.owner_mut(key).and_then(|workspace| workspace.find(key)) {
1323 tab.attached(session);
1324 }
1325 }
1326
1327 #[must_use]
1330 pub fn reading(&self, profile: &str) -> u64 {
1331 self.workspace().and_then(|workspace| workspace.history.get(profile)).map_or(0, history::Shelf::generation)
1332 }
1333
1334 #[must_use]
1337 pub fn backups_reading(&self) -> u64 {
1338 self.listing.as_ref().map_or(0, |_| self.last_listing)
1339 }
1340}
1341
1342pub fn opened(screen: &mut WorkspaceScreen) -> Command<Msg> {
1352 let relay = relay::follow(screen);
1353 let command = Command::batch([load_root(screen), list_containers(screen), wake(screen), show_page(screen, true)]);
1354 let settled = screen.settled();
1355 follow_disk(screen);
1356 Command::batch([command, settled, backups::keep(screen), bridge::follow(screen), relay])
1357}
1358
1359pub fn add(screen: &mut WorkspaceScreen, workspace: OpenWorkspace) -> Command<Msg> {
1362 match screen.workspaces.iter().position(|open| open.id == workspace.id) {
1363 Some(index) => screen.active = index,
1364 None => {
1365 screen.workspaces.push(workspace);
1366 screen.active = screen.workspaces.len() - 1;
1367 }
1368 }
1369 opened(screen)
1370}
1371
1372pub fn update(screen: &mut WorkspaceScreen, message: Msg) -> Command<Msg> {
1378 let relay = relay::follow(screen);
1381 let profiles = matches!(message, Msg::Profiles(_));
1383 let command = apply(screen, message);
1384 follow_disk(screen);
1385 let kept = Command::batch([reread(screen), backups::keep(screen), bridge::follow(screen), busy::follow(screen)]);
1386 let settled = freezing::again(screen);
1389 Command::batch([command, relay, wake(screen), show_page(screen, profiles), kept, settled])
1390}
1391
1392fn follow_disk(screen: &mut WorkspaceScreen) {
1396 for (index, workspace) in screen.workspaces.iter_mut().enumerate() {
1397 if index != screen.active && workspace.files.follows_changes() {
1398 workspace.files.set_following(false);
1399 }
1400 }
1401}
1402
1403fn apply(screen: &mut WorkspaceScreen, message: Msg) -> Command<Msg> {
1405 let walking = screen.walking;
1406 match message {
1407 Msg::OpenWorkspace(index) => {
1408 screen.set_active(index);
1409 Command::batch([
1410 load_root(screen),
1411 list_containers(screen),
1412 settle(screen, walking),
1413 freezing::shown(screen),
1414 ])
1415 }
1416 Msg::CloseWorkspace(index) => {
1417 let backed = backups::closing(screen, index);
1418 freezing::left(screen, index);
1421 let (silenced, shut) = match screen.workspaces.get(index) {
1422 Some(workspace) => {
1423 let tabs: Vec<&Tab> = workspace.tabs.iter().collect();
1424 (sound::silence(screen, workspace, &tabs), desktop::close_all(screen, workspace, &tabs))
1425 }
1426 None => (Command::none(), Command::none()),
1427 };
1428 let silenced = Command::batch([silenced, shut]);
1429 let Some(workspace) = screen.workspaces.get_mut(index) else { return Command::none() };
1430 let keys: Vec<TabKey> = workspace.tabs.iter().map(Tab::key).collect();
1433 for key in &keys {
1434 bridge::returning_on_close(screen, *key);
1435 }
1436 let Some(workspace) = screen.workspaces.get_mut(index) else { return Command::none() };
1437 for tab in &mut workspace.tabs {
1438 tab.close_session();
1439 }
1440 let tokens: Vec<String> = workspace.tabs.iter().map(|tab| tab.token().to_owned()).collect();
1441 relay::closed(workspace, &tokens);
1442 bridge::closed(screen, &keys);
1443 rename::closed(screen, &keys);
1444 TabEdit::Close(index).apply(&mut screen.workspaces, &mut screen.active);
1445 Command::batch([
1446 backed,
1447 silenced,
1448 load_root(screen),
1449 list_containers(screen),
1450 settle(screen, walking),
1451 freezing::shown(screen),
1452 ])
1453 }
1454 Msg::AddWorkspace => Command::none(),
1457 Msg::Freezable(can) => {
1460 screen.freezing.supported = Some(can);
1461 freezing::again(screen)
1462 }
1463 Msg::Look => {
1466 screen.freezing.timed = false;
1467 let looked = freezing::look(screen);
1468 Command::batch([looked, freezing::again(screen)])
1469 }
1470 Msg::Looked(index, id, name, read, at) => freezing::looked(screen, index, &id, &name, read, at),
1471 Msg::Frozen(container, frozen) => {
1476 screen.freezing.note(&container, frozen);
1477 Command::batch([freezing::shown(screen), list_containers(screen)])
1478 }
1479 Msg::Thawed(container, awake) => {
1482 screen.freezing.woke(&container);
1483 screen.freezing.note(&container, !awake);
1484 if awake {
1485 screen.freezing.forgot(&container);
1486 }
1487 list_containers(screen)
1489 }
1490 Msg::OpenTab(index) => {
1491 if let Some(workspace) = screen.workspaces.get_mut(screen.active)
1492 && index < workspace.tabs.len()
1493 {
1494 workspace.active_tab = index;
1495 }
1496 Command::batch([settle(screen, walking), freezing::shown(screen)])
1497 }
1498 Msg::CloseTab(index) => {
1499 if let Some(tab) = screen.workspace().and_then(|workspace| workspace.tabs.get(index))
1504 && (!tab.letters().is_empty() || tab.owed().is_some())
1505 {
1506 let name = screen.workspace().map(|workspace| workspace.tab_label(index)).unwrap_or_default();
1507 return bridge::ask_close(tab.key(), &name, tab.letters().len(), tab.owed().is_some());
1508 }
1509 if let Some((key, harness)) = screen.workspace().and_then(|workspace| agent_running(workspace, index)) {
1512 return ask_end(key, &harness);
1513 }
1514 let closed = close_tab(screen, index);
1515 Command::batch([closed, settle(screen, walking)])
1516 }
1517 Msg::TabEnded => Command::none(),
1518 Msg::CloseTabAnyway(key) => {
1519 let index = screen.workspace().and_then(|workspace| workspace.tabs.iter().position(|tab| tab.key() == key));
1520 match index {
1521 Some(index) => {
1522 let closed = close_tab(screen, index);
1523 Command::batch([closed, settle(screen, walking)])
1524 }
1525 None => Command::none(),
1526 }
1527 }
1528 Msg::MoveTab { from, to } => {
1529 if let Some(workspace) = screen.workspaces.get_mut(screen.active) {
1530 TabEdit::Move { from, to }.apply(&mut workspace.tabs, &mut workspace.active_tab);
1531 }
1532 freezing::shown(screen)
1533 }
1534 Msg::NewTab => open_blank(screen),
1535 Msg::RenameTab(index) => rename::open(screen, index),
1536 Msg::RenameOpenTab => match screen.workspace().filter(|workspace| !workspace.tabs.is_empty()) {
1537 Some(workspace) => rename::open(screen, workspace.active_tab),
1538 None => Command::none(),
1539 },
1540 Msg::TabName(value) => {
1541 rename::typed(screen, value);
1542 Command::none()
1543 }
1544 Msg::NameTab => rename::submit(screen),
1545 Msg::CancelRename => rename::cancel(screen),
1546 Msg::LeaveTerminal => {
1549 screen.owed_focus = None;
1550 Command::focus(TABS_ID)
1551 }
1552 Msg::EnterTerminal => {
1555 let terminal = screen.workspace().and_then(OpenWorkspace::active_tab).and_then(Tab::session).is_some();
1556 if terminal { Command::focus(TERMINAL_ID) } else { Command::none() }
1557 }
1558 Msg::Walking(walking) => {
1559 screen.walking = walking;
1560 Command::none()
1561 }
1562 Msg::EnterTab => land(screen),
1563 Msg::NextTab(forward) => {
1564 let Some(workspace) = screen.workspaces.get_mut(screen.active) else { return Command::none() };
1565 let count = workspace.tabs.len();
1566 if count == 0 {
1567 return Command::none();
1568 }
1569 workspace.active_tab =
1570 if forward { (workspace.active_tab + 1) % count } else { (workspace.active_tab + count - 1) % count };
1571 Command::batch([land(screen), freezing::shown(screen)])
1572 }
1573 Msg::GoToTab(index) => match screen.workspaces.get_mut(screen.active) {
1574 Some(workspace) if index < workspace.tabs.len() => {
1575 workspace.active_tab = index;
1576 Command::batch([land(screen), freezing::shown(screen)])
1577 }
1578 _ => Command::none(),
1579 },
1580 Msg::Choose(key, choice) => choose(screen, key, choice),
1581 Msg::HighlightChoice(row) => {
1582 screen.blank_row = row;
1583 Command::none()
1584 }
1585 Msg::ShowAll(key) => {
1586 screen.expanded.insert(key);
1587 Command::none()
1588 }
1589 Msg::HistoryRead(key, generation, answer) => {
1590 blank::keep_row(screen, |screen| {
1591 if let Some(shelf) = screen.shelf(&key) {
1592 shelf.answered(generation, answer);
1593 }
1594 });
1595 Command::none()
1596 }
1597 Msg::HistorySlow(key, generation) => {
1598 let mut shown = false;
1599 blank::keep_row(screen, |screen| {
1600 shown = screen.shelf(&key).is_some_and(|shelf| shelf.slow(generation));
1601 });
1602 if shown { after(history::SHOW_AT_LEAST, Msg::HistorySettled(key)) } else { Command::none() }
1603 }
1604 Msg::HistorySettled(key) => {
1605 blank::keep_row(screen, |screen| {
1606 if let Some(shelf) = screen.shelf(&key) {
1607 shelf.settled();
1608 }
1609 });
1610 Command::none()
1611 }
1612 Msg::ManageProfiles => Command::none(),
1614 Msg::Profiles(profiles) => {
1615 for workspace in &mut screen.workspaces {
1616 workspace.set_profiles(profiles.clone());
1617 }
1618 Command::none()
1619 }
1620 Msg::ProfileAdded(id, result) => match result {
1621 Ok(file) => {
1622 if let Some(workspace) = screen.workspace_mut(&id) {
1623 workspace.carried = file.profiles.into_iter().map(|profile| profile.name).collect();
1624 }
1625 Command::none()
1626 }
1627 Err(reason) => Command::toast(Toast::warning(t!("workspace.add-failed")).body(reason)),
1630 },
1631 Msg::Ready(key, run, result) => {
1634 if result.is_ok() {
1635 freezing::up(screen, key);
1636 }
1637 ready(screen, key, run, &result)
1638 }
1639 Msg::BuildImage(key) => {
1640 let building = build_image(screen, key);
1641 Command::batch([building, builds::follow(screen)])
1642 }
1643 Msg::ImageLine(key, text) => {
1644 if let Some((_, tab)) = screen.owner_mut(key).and_then(|workspace| workspace.find(key)) {
1645 tab.build_line(&text, false);
1646 }
1647 Command::none()
1648 }
1649 Msg::ImageBuilt(key, result) => image_built(screen, key, result),
1650 Msg::BuildLooked(now) => builds::looked(screen, now),
1653 Msg::StopBuild(key) => {
1654 let Some((_, tab)) = screen.owner_mut(key).and_then(|workspace| workspace.find(key)) else {
1655 return Command::none();
1656 };
1657 let TabState::Building(task) = *tab.state() else { return Command::none() };
1658 tab.settled(TabState::NoImage);
1661 Command::cancel_task(task)
1662 }
1663 Msg::CancelOpen(key) => {
1664 let index = screen.workspace().and_then(|workspace| workspace.tabs.iter().position(|tab| tab.key() == key));
1665 match index {
1666 Some(index) => update(screen, Msg::CloseTab(index)),
1667 None => Command::none(),
1668 }
1669 }
1670 Msg::Woken(key, run, result, found, shown) => {
1671 if let Some(found) = found {
1672 resume_newest(screen, key, run, &found);
1673 }
1674 if let Some(conversation) = shown {
1675 show(screen, key, run, conversation);
1676 }
1677 if result.is_ok() {
1678 freezing::up(screen, key);
1679 }
1680 ready(screen, key, run, &result)
1681 }
1682 Msg::Output(key, run, event) => output(screen, key, run, event),
1683 Msg::Checked(key, run, running) => {
1684 if let Some(workspace) = screen.owner_mut(key)
1685 && let Some((_, tab)) = workspace.find(key)
1686 && tab.run() == run
1687 && let TabState::Ended { .. } = tab.state()
1688 && !running
1689 {
1690 tab.settled(TabState::Stopped);
1691 let id = workspace.id.as_str().to_owned();
1693 return containers_of(screen, &id);
1694 }
1695 Command::none()
1696 }
1697 Msg::Restart(key) => restart_tab(screen, key),
1698 Msg::TogglePanel(open) => {
1699 screen.panel.set_open(open);
1700 Command::none()
1701 }
1702 Msg::ResizePanel(width) => {
1703 screen.panel.set_width(width);
1704 Command::none()
1705 }
1706 Msg::ToggleWidget(index, open) => {
1707 screen.panel.toggle(index, open);
1708 Command::none()
1709 }
1710 Msg::MoveWidget { from, to } => {
1711 screen.panel.move_widget(from, to);
1712 Command::none()
1713 }
1714 Msg::ShowWidgets(show) => {
1715 screen.panel.show_chooser(show);
1716 if show {
1719 screen.owed_focus = None;
1720 Command::focus(panel::CHOOSER_ID)
1721 } else {
1722 Command::none()
1723 }
1724 }
1725 Msg::HighlightWidget(row) => {
1726 screen.panel.highlight(row);
1727 Command::none()
1728 }
1729 Msg::AddWidget(widget) => {
1730 screen.panel.add(widget);
1731 Command::none()
1732 }
1733 Msg::RemoveWidget(widget) => {
1734 screen.panel.remove(widget);
1735 Command::none()
1736 }
1737 Msg::Files(id, message) => files::update(screen, &id, message),
1738 Msg::LeaveOut(key, out) => {
1739 let Some(workspace) = screen.workspaces.get(screen.active) else { return Command::none() };
1740 backups::leave_out(workspace, workspace.files.targets(&key), out)
1741 }
1742 Msg::OpenFile(key) => open_file(screen, &key),
1743 Msg::EditFile(key) => {
1744 if !files::is_inside(&key) {
1745 return Command::none();
1746 }
1747 open_tab(screen, TabKind::Editor(key))
1748 }
1749 Msg::Missing(key, run) => {
1750 if let Some((_, tab)) = screen.owner_mut(key).and_then(|workspace| workspace.find(key))
1751 && tab.run() == run
1752 {
1753 tab.settled(TabState::Missing);
1754 }
1755 Command::none()
1756 }
1757 Msg::DocumentRead(key, run, answer) => {
1758 document_read(screen, key, run, answer);
1759 Command::none()
1760 }
1761 Msg::TextRead(key, run, answer) => viewer::text_read(screen, key, run, answer),
1762 Msg::Page(key, page) => viewer::show_page(screen, key, page),
1763 Msg::Playable(key, run, answer) => sound::playable(screen, key, run, answer),
1764 Msg::Described(key, run, why, answer) => sound::described(screen, key, run, why, answer),
1765 Msg::Silenced => Command::none(),
1766 Msg::WindowOpened(key, run, answer) => desktop::opened(screen, key, run, answer),
1767 Msg::WindowEnded(key, run, code) => desktop::ended(screen, key, run, code),
1768 Msg::SignInWanted(key, run, addresses) => desktop::sign_in_wanted(screen, key, run, &addresses),
1769 Msg::SignInOpened(key, run, address, opened) => desktop::sign_in_opened(screen, key, run, &address, opened),
1770 Msg::SignInBack(key, run, id, ending) => desktop::sign_in_back(screen, key, run, id, ending),
1771 Msg::SignInHere(key) => desktop::sign_in_here(screen, key),
1772 Msg::RaiseWindow(key) => desktop::raise(screen, key),
1773 Msg::WindowRaised(result) => match result {
1776 Ok(()) => Command::none(),
1777 Err(failure) => Command::toast(Toast::warning(t!("workspace.window.raise-failed")).body(failure.output)),
1778 },
1779 Msg::CloseWindow(key) => desktop::close(screen, key),
1780 Msg::WindowsClosed(failure) => match failure {
1781 None => Command::none(),
1782 Some(failure) => Command::toast(Toast::danger(t!("workspace.window.close-failed")).body(failure.output)),
1783 },
1784 Msg::SelectContainer(index) => {
1785 if let Some(workspace) = screen.workspaces.get_mut(screen.active) {
1786 workspace.container_row = index;
1787 }
1788 Command::none()
1789 }
1790 Msg::RefreshContainers => list_containers(screen),
1791 Msg::ContainersRead(id, result) => {
1792 if let Ok(containers) = &result {
1793 screen.freezing.listed(containers);
1794 }
1795 if let Some(workspace) = screen.workspace_mut(&id) {
1796 workspace.busy = false;
1797 match result {
1798 Ok(containers) => {
1799 workspace.container_row = workspace.container_row.min(containers.len().saturating_sub(1));
1800 workspace.containers = containers;
1801 workspace.container_error = None;
1802 }
1803 Err(failure) => workspace.container_error = Some(failure),
1804 }
1805 }
1806 Command::none()
1807 }
1808 Msg::StopContainer(name) => container_action(screen, &name, false),
1809 Msg::RestartContainer(name) => container_action(screen, &name, true),
1810 Msg::Noted(noting, message) => {
1811 let told = if screen.registry_told {
1812 Command::none()
1813 } else {
1814 screen.registry_told = true;
1815 let toast = match noting {
1816 Noting::Unwritten(reason) => Toast::warning(t!("workspace.registry-unwritten")).body(reason),
1817 Noting::Repaired(place) => Toast::warning(t!("workspace.registry-repaired")).body(place),
1818 };
1819 Command::toast(toast)
1820 };
1821 Command::batch([told, update(screen, *message)])
1822 }
1823 Msg::BackupDue(id, run) => backups::due(screen, &id, run),
1824 Msg::BackedUp { workspace, name, made, missed } => {
1825 backups::backed_up(screen, &workspace, &name, &made, &missed)
1826 }
1827 Msg::NewestBackup(id, at) => {
1828 backups::newest(screen, &id, at);
1829 Command::none()
1830 }
1831 Msg::SkipWritten(id, written) => backups::skip_written(screen, &id, written),
1832 Msg::BackupAssets(on) => match screen.workspaces.get(screen.active) {
1833 Some(workspace) => backups::set_assets(workspace, on),
1834 None => Command::none(),
1835 },
1836 Msg::AssetsWritten(id, written) => backups::assets_written(screen, &id, written),
1837 Msg::BackupSize(id, bytes) => {
1838 backups::sized(screen, &id, bytes);
1839 Command::none()
1840 }
1841 Msg::ShowBackups(file) => backups::show(screen, file),
1842 Msg::BackupsRead(reading, found) => backups::read(screen, reading, found),
1843 Msg::BackupsSlow(reading) => backups::slow(screen, reading),
1844 Msg::BackupsSettled(reading) => {
1845 backups::settled(screen, reading);
1846 Command::none()
1847 }
1848 Msg::HighlightBackup(row) => {
1849 backups::highlight(screen, row);
1850 Command::none()
1851 }
1852 Msg::ChooseBackup(row) => backups::choose(screen, row),
1853 Msg::BackupsOf(index) => backups::switch(screen, index),
1854 Msg::RestoreBackup(id) => backups::restore(screen, id, false),
1855 Msg::StopAndRestore(id) => backups::restore(screen, id, true),
1856 Msg::KeepBackup => {
1857 backups::declined(screen);
1858 Command::none()
1859 }
1860 Msg::CloseBackups => {
1861 backups::close(screen);
1862 Command::none()
1863 }
1864 Msg::Restored { workspace, name, of, file, done } => {
1865 let told = backups::restored(screen, &workspace, &name, &of, file.as_deref(), &done);
1866 Command::batch([told, containers_of(screen, &workspace)])
1868 }
1869 Msg::BackupEvery(every) => {
1870 backups::set_every(screen, every);
1871 Command::none()
1872 }
1873 Msg::Bridge(id, run, call) => bridge::called(screen, &id, run, call),
1874 Msg::Allow { from, to, allow } => bridge::allowed(screen, from, to, allow),
1875 Msg::StillAsking { from, to } => {
1876 bridge::still_asking(screen, from, to);
1877 Command::none()
1878 }
1879 Msg::Letters(key, action) => {
1880 bridge::letters(screen, key, action);
1881 Command::none()
1882 }
1883 Msg::Deliver => {
1884 screen.delivering = false;
1885 bridge::deliver(screen, std::time::Instant::now())
1886 }
1887 Msg::Return(key) => bridge::returned(screen, key, std::time::Instant::now()),
1888 Msg::Turn(now) => {
1889 busy::turn(screen, now);
1890 Command::none()
1891 }
1892 Msg::Reported(event) => relay::fell(screen, event),
1893 Msg::Silence(token, run) => {
1894 relay::silence(screen, &token, run);
1895 Command::none()
1896 }
1897 Msg::Asked(harness, trouble, message) => Command::batch([asked(harness, &trouble), update(screen, *message)]),
1898 Msg::Unapproved(words, message) => Command::batch([unapproved(&words), update(screen, *message)]),
1899 Msg::Unbridged(harness, trouble, message) => {
1900 Command::batch([bridge::unregistered(harness, &trouble), update(screen, *message)])
1901 }
1902 Msg::Unguided(harness, trouble, message) => {
1903 Command::batch([unguided(harness, &trouble), update(screen, *message)])
1904 }
1905 Msg::Behind(profile, failed, message) => {
1906 let told = Toast::warning(t!("workspace.admin.behind-title", profile = profile.as_str()))
1907 .body(t!("workspace.admin.behind", command = failed.as_str()));
1908 Command::batch([Command::toast(told), update(screen, *message)])
1909 }
1910 Msg::OpenAdmin(profile) => open_tab(screen, TabKind::Admin(profile)),
1911 Msg::ContainerActed(result) => {
1912 let refresh = list_containers(screen);
1913 match result {
1914 Ok(()) => refresh,
1915 Err(failure) => Command::batch([
1916 Command::toast(Toast::danger(t!("workspace.containers.failed")).body(failure.output)),
1917 refresh,
1918 ]),
1919 }
1920 }
1921 }
1922}
1923
1924fn settle(screen: &mut WorkspaceScreen, walking: bool) -> Command<Msg> {
1927 if walking { Command::none() } else { land(screen) }
1928}
1929
1930fn land(screen: &mut WorkspaceScreen) -> Command<Msg> {
1941 let waiting_for_its_terminal = screen.owed_focus.is_some_and(|owed| {
1942 screen
1943 .workspace()
1944 .and_then(OpenWorkspace::active_tab)
1945 .is_some_and(|tab| tab.key() == owed && tab.session().is_none())
1946 });
1947 if !waiting_for_its_terminal {
1948 screen.owed_focus = None;
1949 }
1950 let Some(tab) = screen.workspace().and_then(OpenWorkspace::active_tab) else { return Command::none() };
1951 let target = match tab.kind() {
1952 TabKind::New => blank::CHOICES_ID,
1953 TabKind::Markdown(_) | TabKind::Pdf(_) | TabKind::Office(_) => DOCUMENT_ID,
1954 TabKind::Desktop(_) => desktop::WINDOW_ID,
1955 _ if tab.session().is_some() => TERMINAL_ID,
1956 _ => BODY_ID,
1957 };
1958 Command::focus(target)
1959}
1960
1961fn open_blank(screen: &mut WorkspaceScreen) -> Command<Msg> {
1964 let key = TabKey(screen.next_key);
1965 let Some(workspace) = screen.workspaces.get_mut(screen.active) else { return Command::none() };
1966 screen.next_key += 1;
1967 workspace.active_tab = workspace.join(Tab::blank(key));
1968 screen.blank_row = 0;
1969 screen.expanded.clear();
1970 Command::batch([Command::focus(blank::CHOICES_ID), freezing::shown(screen)])
1971}
1972
1973fn choose(screen: &mut WorkspaceScreen, key: TabKey, choice: Choice) -> Command<Msg> {
1976 let Some(engine) = screen.engine.clone() else { return Command::none() };
1977 let user = screen.user;
1978 let registry = screen.registry.clone();
1979 let providers_path = screen.providers_path.clone();
1980 let Some(workspace) = screen.owner_mut(key) else { return Command::none() };
1981 let (kind, conversation) = match choice {
1982 Choice::Shell => (TabKind::Shell, None),
1983 Choice::NewChat(name) => (TabKind::Profile(name), None),
1984 Choice::Resume(name, id) => (TabKind::Profile(name), Some(id)),
1985 Choice::Window(name) => (TabKind::Desktop(name), None),
1986 };
1987 let Some(plan) = workspace.plan(&kind) else { return Command::none() };
1988 if workspace.find(key).is_none_or(|(_, tab)| tab.kind() != &TabKind::New) {
1990 return Command::none();
1991 }
1992 let record = match kind.profile() {
1993 Some(name) if !workspace.carries(name) => record_profile(workspace, name),
1994 _ => Command::none(),
1995 };
1996 let window = plan.window.is_some();
1997 let provider = provider_choice(workspace, &kind);
1998 let closed = kind
2001 .profile()
2002 .and_then(|name| workspace.profiles.iter().find(|profile| profile.name.as_str() == name))
2003 .and_then(|profile| {
2004 let words = crate::ui::profiles::closed_sign_in(profile)?;
2005 let title = t!("profiles.gemini.closed-title", name = profile.name.as_str());
2006 Some(Command::toast(Toast::warning(title).body(words)))
2007 })
2008 .unwrap_or_else(Command::none);
2009 let Some((_, tab)) = workspace.find(key) else { return Command::none() };
2010 tab.choose(kind, conversation);
2011 let run = tab.run();
2012 let token = tab.token().to_owned();
2013 let serving = serving(screen, key);
2014 if let Some(provider) = &provider
2015 && let Some(workspace) = screen.owner(key)
2016 {
2017 relay::entered(workspace, asking(serving.as_ref(), &token), provider);
2018 }
2019 if window {
2022 screen.owed_focus = None;
2023 return Command::batch([
2024 desktop::open(screen, key, run),
2025 Command::focus(desktop::WINDOW_ID),
2026 record,
2027 closed,
2028 freezing::shown(screen),
2029 ]);
2030 }
2031 screen.owed_focus = Some(key);
2034 let launch = Launch::new(key, run, token, provider, providers_path);
2035 Command::batch([
2036 harness_start(registry, engine, plan, user, launch, serving),
2037 record,
2038 closed,
2039 freezing::shown(screen),
2040 ])
2041}
2042
2043fn serving(screen: &WorkspaceScreen, key: TabKey) -> Option<Serving> {
2046 let workspace = screen.owner(key)?;
2047 let tab = workspace.tabs.iter().find(|tab| tab.key() == key)?;
2048 let TabKind::Profile(name) = tab.kind() else { return None };
2049 let profile = workspace.profiles.iter().find(|profile| profile.name.as_str() == name)?;
2050 shared::shares(profile).then(|| Serving {
2051 environment: screen.server_environment(workspace, profile),
2052 token: workspace.servers.token(name),
2053 folder: workspace.paths.mcp(),
2054 conversation: tab.conversation().map(str::to_owned),
2055 })
2056}
2057
2058fn asking<'a>(serving: Option<&'a Serving>, token: &'a str) -> &'a str {
2061 serving.map_or(token, |serving| serving.token.as_str())
2062}
2063
2064fn harness_start(
2068 registry: Option<PathBuf>,
2069 engine: Engine,
2070 plan: ContainerPlan,
2071 user: HostUser,
2072 launch: Launch,
2073 serving: Option<Serving>,
2074) -> Command<Msg> {
2075 Command::perform(move || {
2076 let Launch { key, run, gate, token } = launch;
2077 start(registry.as_deref(), &engine, &plan, user, gate.as_ref(), &token, |result| match serving {
2078 None => Msg::Ready(key, run, result),
2079 Some(serving) => {
2080 let chosen = result.and_then(|()| {
2081 shared::ready(
2082 &engine,
2083 &plan.name,
2084 &serving.folder,
2085 &serving.environment,
2086 serving.conversation.as_deref(),
2087 )
2088 });
2089 match chosen {
2090 Ok(conversation) => Msg::Woken(key, run, Ok(()), None, Some(conversation)),
2091 Err(failure) => Msg::Woken(key, run, Err(failure), None, None),
2092 }
2093 }
2094 })
2095 })
2096}
2097
2098impl WorkspaceScreen {
2099 fn server_environment(&self, workspace: &OpenWorkspace, profile: &Profile) -> Vec<(String, String)> {
2103 let token = workspace.servers.token(profile.name.as_str());
2104 let provider = profile
2105 .provider
2106 .as_ref()
2107 .map(|provider| provider.environment(profile.harness, &token, self.measured_window(provider)))
2108 .unwrap_or_default();
2109 shared::server_environment(&token, &provider)
2110 }
2111
2112 fn measured_window(&self, provider: &ProviderChoice) -> Option<u64> {
2124 let path = self.providers_path.as_ref()?;
2125 let providers = crate::provider::Providers::open(path).value;
2126 let entry = providers.get(&provider.tag)?;
2127 match &provider.pick {
2128 Pick::Model(id) => entry.model(id)?.window(entry.kind),
2129 Pick::Lineup(name) => entry.lineup(name)?.window(entry),
2130 }
2131 }
2132}
2133
2134fn agent_running(workspace: &OpenWorkspace, index: usize) -> Option<(TabKey, String)> {
2140 let tab = workspace.tabs.get(index)?;
2141 if !tab.kind().runs_agent() || !matches!(tab.state(), TabState::Running) {
2142 return None;
2143 }
2144 let name = tab.kind().profile()?;
2145 let harness = workspace.profiles.iter().find(|profile| profile.name.as_str() == name);
2146 Some((
2147 tab.key(),
2148 harness.map_or_else(|| name.to_owned(), |profile| profile.harness.record().display_name.to_owned()),
2149 ))
2150}
2151
2152fn ask_end(key: TabKey, harness: &str) -> Command<Msg> {
2154 Command::confirm(
2155 qframe::runtime::Confirm::new(t!("workspace.close-agent.title", harness = harness), Msg::CloseTabAnyway(key))
2156 .message(t!("workspace.close-agent.message"))
2157 .confirm_label(t!("workspace.close-agent.confirm"))
2158 .cancel_label(t!("workspace.close-agent.cancel"))
2159 .danger(),
2160 )
2161}
2162
2163fn close_tab(screen: &mut WorkspaceScreen, index: usize) -> Command<Msg> {
2166 let ending = screen
2169 .workspace()
2170 .and_then(|workspace| {
2171 let tab = workspace.tabs.get(index)?;
2172 if !matches!(tab.kind(), TabKind::Profile(_)) {
2173 return None;
2174 }
2175 let plan = workspace.plan(tab.kind())?;
2176 Some((plan.end_tab(screen.engine.as_ref()?, tab.token()), tab.key()))
2177 })
2178 .map_or_else(Command::none, |(command, key)| {
2179 let asleep = freezing::closing(screen, key);
2180 match asleep {
2181 Some(container) => freezing::thawing_then(screen, container, command),
2186 None => Command::perform(move || {
2187 let _ = crate::engine::run::capture(&command);
2188 Msg::TabEnded
2189 }),
2190 }
2191 });
2192 let silenced = match screen.workspace() {
2193 Some(workspace) => {
2194 let tabs: Vec<&Tab> = workspace.tabs.get(index).into_iter().collect();
2195 Command::batch([sound::silence(screen, workspace, &tabs), desktop::close_all(screen, workspace, &tabs)])
2196 }
2197 None => Command::none(),
2198 };
2199 if let Some(key) = screen.workspace().and_then(|workspace| workspace.tabs.get(index)).map(Tab::key) {
2202 bridge::returning_on_close(screen, key);
2203 }
2204 let Some(workspace) = screen.workspaces.get_mut(screen.active) else { return Command::none() };
2205 let mut gone = Vec::new();
2206 let mut token = None;
2207 if let Some(tab) = workspace.tabs.get_mut(index) {
2208 tab.close_session();
2209 gone.push(tab.key());
2210 token = Some(tab.token().to_owned());
2211 }
2212 if let Some(token) = token {
2213 relay::closed(workspace, &[token]);
2214 }
2215 TabEdit::Close(index).apply(&mut workspace.tabs, &mut workspace.active_tab);
2216 bridge::closed(screen, &gone);
2217 rename::closed(screen, &gone);
2218 if screen.owed_focus.is_some_and(|owed| gone.contains(&owed)) {
2220 screen.owed_focus = None;
2221 }
2222 Command::batch([silenced, ending, freezing::shown(screen)])
2225}
2226
2227fn provider_choice(workspace: &OpenWorkspace, kind: &TabKind) -> Option<ProviderChoice> {
2230 let name = kind.profile()?;
2231 workspace.profiles.iter().find(|profile| profile.name.as_str() == name)?.provider.clone()
2232}
2233
2234fn record_profile(workspace: &OpenWorkspace, name: &str) -> Command<Msg> {
2241 let id = workspace.id.as_str().to_owned();
2242 let paths = workspace.paths.clone();
2243 let name = name.to_owned();
2244 Command::perform(move || {
2245 let result = add_profile(&paths, &name, Date::today_utc()).map_err(|problem| problem.to_string());
2246 Msg::ProfileAdded(id, result)
2247 })
2248}
2249
2250fn wake(screen: &mut WorkspaceScreen) -> Command<Msg> {
2259 let Some(workspace) = screen.workspaces.get_mut(screen.active) else { return Command::none() };
2260 let Some(tab) = workspace.tabs.get_mut(workspace.active_tab) else { return Command::none() };
2261 if tab.state() != &TabState::Waiting {
2262 return Command::none();
2263 }
2264 if let TabKind::Markdown(file) = tab.kind() {
2267 let file = file.clone();
2268 tab.wake();
2269 let (key, run) = (tab.key(), tab.run());
2270 return read_document(workspace, key, run, &file);
2271 }
2272 if screen.engine.is_none() {
2273 return Command::none();
2274 }
2275 if let TabKind::Sound(_) = tab.kind() {
2276 tab.wake();
2277 let (key, run) = (tab.key(), tab.run());
2278 return sound::hear(screen, key, run);
2279 }
2280 if let TabKind::Desktop(_) = tab.kind() {
2283 tab.wake();
2284 let (key, run) = (tab.key(), tab.run());
2285 return desktop::open(screen, key, run);
2286 }
2287 if viewer::text_command(tab.kind()).is_some() && !viewer::draws(tab) {
2289 tab.wake();
2290 let (key, run) = (tab.key(), tab.run());
2291 return viewer::take_text(screen, key, run);
2292 }
2293 let Some(engine) = screen.engine.clone() else { return Command::none() };
2294 let user = screen.user;
2295 let registry = screen.registry.clone();
2296 let providers_path = screen.providers_path.clone();
2297 let serving = screen
2298 .workspaces
2299 .get(screen.active)
2300 .and_then(|workspace| workspace.tabs.get(workspace.active_tab))
2301 .and_then(|tab| serving(screen, tab.key()));
2302 let Some(workspace) = screen.workspaces.get_mut(screen.active) else { return Command::none() };
2303 let Some(tab) = workspace.tabs.get(workspace.active_tab) else { return Command::none() };
2304 let Some(plan) = workspace.plan(tab.kind()) else { return Command::none() };
2305 let provider = provider_choice(workspace, tab.kind());
2306 let file = tab.kind().file().map(|file| workspace.files.path(file));
2307 let harness = match tab.kind() {
2308 TabKind::Profile(name) if tab.conversation().is_none() && serving.is_none() => {
2309 workspace.profiles.iter().find(|profile| profile.name.as_str() == name).map(|profile| profile.harness)
2310 }
2311 _ => None,
2312 };
2313 let Some(tab) = workspace.tabs.get_mut(workspace.active_tab) else { return Command::none() };
2314 tab.wake();
2315 let (key, run) = (tab.key(), tab.run());
2316 let token = tab.token().to_owned();
2317 if let Some(provider) = &provider {
2318 relay::entered(&*workspace, asking(serving.as_ref(), &token), provider);
2319 }
2320 if serving.is_some() {
2321 let launch = Launch::new(key, run, token, provider, providers_path);
2322 return harness_start(registry, engine, plan, user, launch, serving);
2323 }
2324 match harness {
2325 None => bring_up(engine, plan, Launch::new(key, run, token, provider, providers_path), user, file, registry),
2326 Some(harness) => {
2327 let launch = Launch::new(key, run, token, provider, providers_path);
2328 Command::perform(move || {
2329 let Launch { key, run, gate, token } = launch;
2330 start(registry.as_deref(), &engine, &plan, user, gate.as_ref(), &token, |result| {
2331 let found =
2335 result.is_ok().then(|| history::read(&engine, &plan.name, harness, &mut || {}).ok()).flatten();
2336 Msg::Woken(key, run, result, found, None)
2337 })
2338 })
2339 }
2340 }
2341}
2342
2343struct Serving {
2346 environment: Vec<(String, String)>,
2347 token: String,
2348 folder: PathBuf,
2349 conversation: Option<String>,
2350}
2351
2352fn show(screen: &mut WorkspaceScreen, key: TabKey, run: u64, conversation: String) {
2355 let Some(workspace) = screen.owner_mut(key) else { return };
2356 let Some((_, tab)) = workspace.find(key) else { return };
2357 if tab.run() == run && tab.state().is_starting() {
2358 tab.show_conversation(conversation);
2359 }
2360}
2361
2362fn resume_newest(screen: &mut WorkspaceScreen, key: TabKey, run: u64, found: &[Conversation]) {
2366 let Some(workspace) = screen.owner_mut(key) else { return };
2367 let Some((_, tab)) = workspace.find(key) else { return };
2368 if tab.run() != run || !tab.state().is_starting() || tab.conversation().is_some() {
2369 return;
2370 }
2371 let (kind, opened) = (tab.kind().clone(), tab.opened());
2372 let taken: Vec<String> = workspace
2373 .tabs
2374 .iter()
2375 .filter(|other| other.key() != key && other.kind() == &kind)
2376 .filter_map(|other| other.conversation().map(str::to_owned))
2377 .collect();
2378 let taken: Vec<&str> = taken.iter().map(String::as_str).collect();
2379 let Some(chosen) = history::claim(found, opened, &taken).map(|conversation| conversation.id.clone()) else {
2380 return;
2381 };
2382 if let Some((_, tab)) = workspace.find(key) {
2383 tab.show_conversation(chosen);
2384 }
2385}
2386
2387fn show_page(screen: &mut WorkspaceScreen, again: bool) -> Command<Msg> {
2393 let showing = screen.workspace().and_then(OpenWorkspace::active_tab).filter(|tab| tab.kind() == &TabKind::New);
2394 let showing = showing.map(Tab::key);
2395 let new = showing != screen.shown_blank;
2396 screen.shown_blank = showing;
2397 if showing.is_none() || !(new || again) {
2398 return Command::none();
2399 }
2400 let Some(engine) = screen.engine.clone() else { return Command::none() };
2401 let registry = screen.registry.clone();
2402 let user = screen.user;
2403 let Some(workspace) = screen.workspaces.get_mut(screen.active) else { return Command::none() };
2404 let mut commands = Vec::new();
2405 for profile in &workspace.profiles {
2406 let key = HistoryKey { workspace: workspace.id.as_str().to_owned(), profile: profile.name.as_str().to_owned() };
2407 let generation = workspace.history.entry(key.profile.clone()).or_default().start();
2408 let plan = ContainerPlan::profile(&workspace.id, &workspace.paths, profile);
2409 let (engine, harness, answer, registry) = (engine.clone(), profile.harness, key.clone(), registry.clone());
2410 commands.push(Command::perform(move || {
2411 let (renewed, behind) = plan::renew_stale(&engine, &plan, user);
2415 let mut started = renewed;
2418 let container = plan.name;
2419 let read = history::read(&engine, &container, harness, &mut || started = true);
2420 let mut message = Msg::HistoryRead(answer.clone(), generation, read);
2421 if let Some(failed) = behind {
2423 message = Msg::Behind(answer.profile, failed, Box::new(message));
2424 }
2425 if started { noted(registry.as_deref(), engine.kind(), &container, message) } else { message }
2426 }));
2427 commands.push(after(history::SHOW_AFTER, Msg::HistorySlow(key, generation)));
2428 }
2429 Command::batch(commands)
2430}
2431
2432fn after(delay: std::time::Duration, message: Msg) -> Command<Msg> {
2434 Command::task(Task::new(t!("workspace.history.reading"), move |cx| {
2435 if cx.sleep(delay) { Ok(message) } else { Err(String::new()) }
2436 }))
2437}
2438
2439fn restart_tab(screen: &mut WorkspaceScreen, key: TabKey) -> Command<Msg> {
2441 let engine = screen.engine.clone();
2442 let user = screen.user;
2443 let registry = screen.registry.clone();
2444 let providers_path = screen.providers_path.clone();
2445 let Some(workspace) = screen.owner_mut(key) else { return Command::none() };
2446 let Some((_, tab)) = workspace.find(key) else { return Command::none() };
2447 let kind = tab.kind().clone();
2448 if let TabKind::Markdown(file) = &kind {
2449 tab.restarting();
2450 let run = tab.run();
2451 return read_document(workspace, key, run, file);
2452 }
2453 let Some(engine) = engine else { return Command::none() };
2454 if let TabKind::Sound(_) = &kind {
2457 tab.restarting();
2458 tab.hold(false);
2459 let run = tab.run();
2460 return sound::hear(screen, key, run);
2461 }
2462 if let TabKind::Desktop(_) = &kind {
2464 tab.restarting();
2465 tab.hold(false);
2466 let run = tab.run();
2467 return desktop::open(screen, key, run);
2468 }
2469 let reads = viewer::text_command(&kind).is_some() && !viewer::draws(tab);
2471 tab.restarting();
2472 let run = tab.run();
2473 let token = tab.token().to_owned();
2474 if reads {
2475 return viewer::take_text(screen, key, run);
2476 }
2477 let Some(plan) = workspace.plan(&kind) else { return Command::none() };
2478 let provider = provider_choice(workspace, &kind);
2479 let file = kind.file().map(|file| workspace.files.path(file));
2480 let serving = serving(screen, key);
2481 if let Some(provider) = &provider
2482 && let Some(workspace) = screen.owner(key)
2483 {
2484 relay::entered(workspace, asking(serving.as_ref(), &token), provider);
2485 }
2486 if screen.workspace().and_then(OpenWorkspace::active_tab).is_some_and(|tab| tab.key() == key) {
2489 screen.owed_focus = Some(key);
2490 }
2491 let launch = Launch::new(key, run, token, provider, providers_path);
2492 if serving.is_some() {
2493 return harness_start(registry, engine, plan, user, launch, serving);
2494 }
2495 bring_up(engine, plan, launch, user, file, registry)
2496}
2497
2498struct ProviderGate {
2505 provider: ProviderChoice,
2507 path: Option<PathBuf>,
2509 missing: String,
2512 lineup_missing: String,
2515}
2516
2517impl ProviderGate {
2518 fn of(provider: Option<ProviderChoice>, path: Option<PathBuf>) -> Option<Self> {
2520 let provider = provider?;
2521 let missing = t!("workspace.provider-missing", tag = provider.tag.as_str());
2522 let lineup_missing = match &provider.pick {
2523 Pick::Model(_) => String::new(),
2524 Pick::Lineup(lineup) => {
2525 t!("workspace.provider-lineup-missing", lineup = lineup.as_str(), tag = provider.tag.as_str())
2526 }
2527 };
2528 Some(Self { provider, path, missing, lineup_missing })
2529 }
2530
2531 fn refusal(&self) -> Option<String> {
2535 let providers = match &self.path {
2536 Some(path) => crate::provider::Providers::open(path).value,
2537 None => crate::provider::Providers::in_memory(),
2538 };
2539 let Some(entry) = providers.get(&self.provider.tag) else { return Some(self.missing.clone()) };
2540 match &self.provider.pick {
2541 Pick::Model(_) => None,
2542 Pick::Lineup(name) if entry.lineup(name).is_none_or(|lineup| lineup.models.is_empty()) => {
2545 Some(self.lineup_missing.clone())
2546 }
2547 Pick::Lineup(_) => None,
2548 }
2549 }
2550}
2551
2552struct Launch {
2556 key: TabKey,
2558 run: u64,
2560 gate: Option<ProviderGate>,
2562 token: String,
2564}
2565
2566impl Launch {
2567 fn new(
2571 key: TabKey,
2572 run: u64,
2573 token: String,
2574 provider: Option<ProviderChoice>,
2575 providers_path: Option<PathBuf>,
2576 ) -> Self {
2577 Self { key, run, gate: ProviderGate::of(provider, providers_path), token }
2578 }
2579}
2580
2581fn bring_up(
2587 engine: Engine,
2588 plan: ContainerPlan,
2589 launch: Launch,
2590 user: HostUser,
2591 file: Option<PathBuf>,
2592 registry: Option<PathBuf>,
2593) -> Command<Msg> {
2594 Command::perform(move || {
2595 let Launch { key, run, gate, token } = launch;
2596 if file.is_some_and(|file| !file.exists()) {
2597 return Msg::Missing(key, run);
2598 }
2599 start(registry.as_deref(), &engine, &plan, user, gate.as_ref(), &token, |result| Msg::Ready(key, run, result))
2600 })
2601}
2602
2603fn start(
2611 registry: Option<&Path>,
2612 engine: &Engine,
2613 plan: &ContainerPlan,
2614 user: HostUser,
2615 gate: Option<&ProviderGate>,
2616 token: &str,
2617 answer: impl FnOnce(Result<(), LaunchFailure>) -> Msg,
2618) -> Msg {
2619 if let Some(refusal) = gate.and_then(ProviderGate::refusal) {
2627 return answer(Err(LaunchFailure { command: String::new(), output: refusal, image_missing: false }));
2628 }
2629 let noted_start = plan::ensure_running_noting(engine, plan, user);
2630 let behind = noted_start.as_ref().ok().cloned().flatten();
2631 let result = noted_start.map(|_| ());
2632 let up = result.is_ok();
2633 let asked = match &plan.bridge {
2637 Some(bridge) if up => {
2638 unattended::ensure(engine, &plan.name, bridge.harness).err().map(|trouble| (bridge.harness, trouble))
2639 }
2640 _ => None,
2641 };
2642 let unregistered = match &plan.bridge {
2644 Some(bridge) if up => bridge_config::register(engine, &plan.name, bridge.harness, token)
2645 .err()
2646 .map(|trouble| (bridge.harness, trouble)),
2647 _ => None,
2648 };
2649 let unguided = match plan.guidance {
2652 Some(harness) if up => {
2653 plan::guide(engine, &plan.name, &plan.code, harness, plan.graphify).err().map(|trouble| (harness, trouble))
2654 }
2655 _ => None,
2656 };
2657 if up && plan.graphify {
2661 plan::build_map(engine, &plan.name, &plan.code);
2662 }
2663 let mut message = answer(result);
2664 if let Some((harness, trouble)) = asked {
2665 message = Msg::Asked(harness, trouble, Box::new(message));
2666 }
2667 if let Some((harness, trouble)) = unregistered {
2668 message = Msg::Unbridged(harness, trouble, Box::new(message));
2669 }
2670 if let Some((harness, trouble)) = unguided {
2671 message = Msg::Unguided(harness, trouble, Box::new(message));
2672 }
2673 if let (Some(failed), Some(home)) = (behind, &plan.home) {
2674 message = Msg::Behind(home.profile().to_string(), failed, Box::new(message));
2675 }
2676 if up { noted(registry, engine.kind(), &plan.name, message) } else { message }
2677}
2678
2679fn unapproved(words: &str) -> Command<Msg> {
2682 Command::toast(Toast::warning(t!("workspace.unapproved.title")).body(words.to_owned()))
2683}
2684
2685pub(super) fn asked(harness: HarnessKind, trouble: &Asks) -> Command<Msg> {
2689 let body = match trouble {
2690 Asks::Theirs(file, says) => t!("workspace.asked.theirs", file = file.as_str(), says = says.as_str()),
2691 Asks::Unreadable(file, place) => {
2692 t!("workspace.asked.unreadable", file = file.as_str(), place = place.as_str())
2693 }
2694 Asks::Engine(words) => words.clone(),
2695 };
2696 let title = t!("workspace.asked.title", harness = harness.record().display_name);
2697 Command::toast(Toast::warning(title).body(body))
2698}
2699
2700pub(super) fn unguided(harness: HarnessKind, trouble: &Unguided) -> Command<Msg> {
2704 let body = match trouble {
2705 Unguided::Graphify(words) => t!("workspace.unguided.graphify", words = words.as_str()),
2706 Unguided::Unwritten(file, words) => {
2707 t!("workspace.unguided.unwritten", file = file.as_str(), words = words.as_str())
2708 }
2709 Unguided::Broken(file) => t!("workspace.unguided.broken", file = file.as_str()),
2710 };
2711 let title = t!("workspace.unguided.title", harness = harness.record().display_name);
2712 Command::toast(Toast::warning(title).body(body))
2713}
2714
2715pub(super) fn noted(registry: Option<&Path>, engine: EngineKind, name: &str, message: Msg) -> Msg {
2718 let Some(path) = registry else { return message };
2719 let noting = match Registry::record(path, name, engine) {
2720 Ok(problems) => match problems.first() {
2721 None => return message,
2722 Some(problem) => Noting::Repaired(problem.to_string()),
2723 },
2724 Err(error) => Noting::Unwritten(format!("{}: {error}", path.display())),
2725 };
2726 Msg::Noted(noting, Box::new(message))
2727}
2728
2729fn read_document(workspace: &OpenWorkspace, key: TabKey, run: u64, file: &str) -> Command<Msg> {
2731 let path = workspace.files.path(file);
2732 let root = workspace.files.root().to_path_buf();
2733 Command::perform(move || Msg::DocumentRead(key, run, files::read_document(&path, &root)))
2734}
2735
2736fn document_read(screen: &mut WorkspaceScreen, key: TabKey, run: u64, answer: Result<String, DocumentTrouble>) {
2739 let Some((_, tab)) = screen.owner_mut(key).and_then(|workspace| workspace.find(key)) else { return };
2740 if tab.run() != run {
2741 return;
2742 }
2743 match answer {
2744 Ok(text) => tab.read(text, false),
2745 Err(DocumentTrouble::Missing) => tab.settled(TabState::Missing),
2746 Err(DocumentTrouble::Outside) => {
2747 let file = tab.kind().file().unwrap_or_default().to_owned();
2748 tab.settled(TabState::Unreadable(t!("workspace.file.outside", file = file)));
2749 }
2750 Err(DocumentTrouble::Unreadable(reason)) => tab.settled(TabState::Unreadable(reason)),
2751 }
2752}
2753
2754fn reread(screen: &mut WorkspaceScreen) -> Command<Msg> {
2758 let Some(workspace) = screen.workspaces.get(screen.active) else {
2759 screen.shown_document = None;
2760 return Command::none();
2761 };
2762 let showing = workspace.active_tab().filter(|tab| matches!(tab.kind(), TabKind::Markdown(_)));
2763 let key = showing.map(Tab::key);
2764 let again = key.is_some() && key != screen.shown_document;
2765 screen.shown_document = key;
2766 match showing {
2767 Some(tab) if again && tab.state() == &TabState::Running => {
2768 let file = tab.kind().file().unwrap_or_default().to_owned();
2769 read_document(workspace, tab.key(), tab.run(), &file)
2770 }
2771 _ => Command::none(),
2772 }
2773}
2774
2775fn open_file(screen: &mut WorkspaceScreen, key: &str) -> Command<Msg> {
2778 if !files::is_inside(key) {
2779 return Command::none();
2780 }
2781 let kind = match apps::classify(files::name(key)) {
2782 FileKind::Image => TabKind::Image(key.to_owned()),
2783 FileKind::Markdown => TabKind::Markdown(key.to_owned()),
2784 FileKind::Text => TabKind::Editor(key.to_owned()),
2785 FileKind::Pdf => TabKind::Pdf(key.to_owned()),
2786 FileKind::Office => TabKind::Office(key.to_owned()),
2787 FileKind::Sound => TabKind::Sound(key.to_owned()),
2788 FileKind::Unknown => {
2789 return Command::toast(Toast::info(t!("workspace.file.no-app")).body(files::name(key).to_owned()));
2790 }
2791 };
2792 open_tab(screen, kind)
2793}
2794
2795fn open_tab(screen: &mut WorkspaceScreen, kind: TabKind) -> Command<Msg> {
2799 let key = TabKey(screen.next_key);
2800 let Some(workspace) = screen.workspaces.get_mut(screen.active) else { return Command::none() };
2801 let focus = if matches!(kind, TabKind::Markdown(_) | TabKind::Pdf(_) | TabKind::Office(_)) {
2802 DOCUMENT_ID
2803 } else {
2804 TERMINAL_ID
2805 };
2806 if let Some(index) = workspace.tabs.iter().position(|tab| tab.kind() == &kind) {
2807 workspace.active_tab = index;
2808 let owed = {
2811 let tab = &workspace.tabs[index];
2812 (tab.session().is_none() && focus == TERMINAL_ID).then_some(tab.key())
2813 };
2814 screen.owed_focus = owed;
2815 return Command::batch([Command::focus(focus), freezing::shown(screen)]);
2816 }
2817 screen.next_key += 1;
2818 workspace.active_tab = workspace.join(Tab::waiting(key, kind));
2819 screen.owed_focus = (focus == TERMINAL_ID).then_some(key);
2820 Command::batch([Command::focus(focus), freezing::shown(screen)])
2821}
2822
2823fn ready(screen: &mut WorkspaceScreen, key: TabKey, run: u64, result: &Result<(), LaunchFailure>) -> Command<Msg> {
2825 let owed = screen.owed_focus == Some(key)
2827 && screen.workspace().and_then(OpenWorkspace::active_tab).is_some_and(|tab| tab.key() == key);
2828 let Some(command) = screen.launch_command(key) else { return Command::none() };
2829 let Some(workspace) = screen.owner_mut(key) else { return Command::none() };
2830 let folder = workspace.paths.root.clone();
2831 let id = workspace.id.as_str().to_owned();
2834 let Some((_, tab)) = workspace.find(key) else { return Command::none() };
2835 if tab.run() != run || !tab.state().is_starting() {
2836 return Command::none();
2837 }
2838 if let Err(failure) = result {
2839 let state = if failure.image_missing && matches!(tab.kind(), TabKind::Profile(_)) {
2841 TabState::NoImage
2842 } else {
2843 TabState::Failed(failure.clone())
2844 };
2845 tab.settled(state);
2846 if screen.owed_focus == Some(key) {
2848 screen.owed_focus = None;
2849 }
2850 return containers_of(screen, &id);
2851 }
2852 let args: Vec<OsString> = command.args.clone();
2853 let attached = match TerminalSession::spawn(command.program.as_os_str(), &args, &folder) {
2854 Ok(session) => {
2855 let watch = session.watch();
2856 tab.attached(session);
2857 let next = Command::perform(move || Msg::Output(key, run, watch.next()));
2858 let mut took = bridge::deliver(screen, std::time::Instant::now());
2860 if owed {
2861 screen.owed_focus = None;
2863 took = Command::batch([took, Command::focus(TERMINAL_ID)]);
2864 }
2865 Command::batch([next, took])
2866 }
2867 Err(error) => {
2868 tab.settled(TabState::Failed(LaunchFailure::spawn(&command, &error)));
2869 if screen.owed_focus == Some(key) {
2870 screen.owed_focus = None;
2871 }
2872 Command::none()
2873 }
2874 };
2875 Command::batch([attached, containers_of(screen, &id)])
2876}
2877
2878fn build_image(screen: &mut WorkspaceScreen, key: TabKey) -> Command<Msg> {
2881 let Some(engine) = screen.engine.clone() else { return Command::none() };
2882 let Some(workspace) = screen.owner_mut(key) else { return Command::none() };
2883 let Some((_, tab)) = workspace.find(key) else { return Command::none() };
2884 let (TabKind::Profile(name), TabState::NoImage) = (tab.kind(), tab.state()) else { return Command::none() };
2885 let name = name.clone();
2886 let Some(profile) = workspace.profiles.iter().find(|profile| profile.name.as_str() == name).cloned() else {
2887 return Command::none();
2888 };
2889 let profiles = workspace.paths.root.parent().and_then(Path::parent).map(|store| Store::new(store).profiles_dir());
2892 let task = Task::new(t!("workspace.image.building", profile = name), move |cx| {
2893 let cancel = || cx.is_cancelled();
2894 let mut line = |text: &str| cx.send(Msg::ImageLine(key, text.to_owned()));
2895 let built = match &profiles {
2896 Some(profiles) => {
2897 crate::ui::profiles::work::build_whole_in(&engine, &profile, profiles, &cancel, &mut || {}, &mut line)
2898 }
2899 None => crate::ui::profiles::work::build_whole(&engine, &profile, &cancel, &mut || {}, &mut line),
2900 };
2901 Ok(Msg::ImageBuilt(
2902 key,
2903 built.map_err(|problem| match problem {
2904 Problem::Cancelled => None,
2905 other => Some(other.output().unwrap_or_default().to_owned()),
2906 }),
2907 ))
2908 });
2909 if let Some((_, tab)) = workspace.find(key) {
2910 tab.building(task.id());
2911 }
2912 Command::task(task)
2913}
2914
2915fn image_built(screen: &mut WorkspaceScreen, key: TabKey, result: Result<(), Option<String>>) -> Command<Msg> {
2918 let Some((_, tab)) = screen.owner_mut(key).and_then(|workspace| workspace.find(key)) else {
2919 return Command::none();
2920 };
2921 if !matches!(tab.state(), TabState::Building(_)) {
2922 return Command::none();
2923 }
2924 match result {
2925 Ok(()) => restart_tab(screen, key),
2926 Err(None) => {
2927 tab.settled(TabState::NoImage);
2928 Command::none()
2929 }
2930 Err(Some(output)) => {
2931 tab.settled(TabState::Failed(LaunchFailure { command: String::new(), output, image_missing: false }));
2932 Command::none()
2933 }
2934 }
2935}
2936
2937fn output(screen: &mut WorkspaceScreen, key: TabKey, run: u64, event: TerminalEvent) -> Command<Msg> {
2939 let engine = screen.engine.clone();
2940 let Some(workspace) = screen.owner_mut(key) else { return Command::none() };
2941 let kind = workspace.find(key).map(|(_, tab)| tab.kind().clone());
2942 let plan = kind.and_then(|kind| workspace.plan(&kind));
2943 let Some((_, tab)) = workspace.find(key) else { return Command::none() };
2944 if tab.run() != run {
2945 return Command::none();
2946 }
2947 match event {
2948 TerminalEvent::Output => {
2949 let Some(session) = tab.session() else { return Command::none() };
2950 let watch = session.watch();
2951 let next = Command::perform(move || Msg::Output(key, run, watch.next()));
2952 let now = std::time::Instant::now();
2955 screen.looked = now;
2956 Command::batch([next, bridge::deliver(screen, now)])
2957 }
2958 TerminalEvent::Exited(code) => {
2959 tab.settled(TabState::Ended { code });
2960 match (engine, plan) {
2964 (Some(engine), Some(plan)) => {
2965 Command::perform(move || Msg::Checked(key, run, plan::is_running(&engine, &plan)))
2966 }
2967 _ => Command::none(),
2968 }
2969 }
2970 }
2971}
2972
2973fn load_root(screen: &mut WorkspaceScreen) -> Command<Msg> {
2977 let (live, patience) = (screen.live, screen.patience);
2978 let Some(workspace) = screen.workspaces.get_mut(screen.active) else { return Command::none() };
2979 match patience {
2980 Some(bound) if !workspace.files.follows_changes() => {
2981 let state = std::mem::replace(&mut workspace.files, FileManagerState::new(PathBuf::new()));
2982 workspace.files = state.following_within(bound);
2983 }
2984 _ => workspace.files.set_following(live),
2985 }
2986 files::load(workspace)
2987}
2988
2989fn list_containers(screen: &mut WorkspaceScreen) -> Command<Msg> {
2991 let Some(id) = screen.workspaces.get(screen.active).map(|workspace| workspace.id.as_str().to_owned()) else {
2992 return Command::none();
2993 };
2994 containers_of(screen, &id)
2995}
2996
2997fn containers_of(screen: &mut WorkspaceScreen, id: &str) -> Command<Msg> {
2999 let Some(engine) = screen.engine.clone() else { return Command::none() };
3000 let Some(workspace) = screen.workspace_mut(id) else { return Command::none() };
3001 workspace.busy = true;
3002 let id = workspace.id.as_str().to_owned();
3003 Command::perform(move || {
3004 let containers = plan::workspace_containers(&engine, &id);
3005 Msg::ContainersRead(id, containers)
3006 })
3007}
3008
3009fn container_action(screen: &WorkspaceScreen, name: &str, again: bool) -> Command<Msg> {
3011 let Some(engine) = screen.engine.clone() else { return Command::none() };
3012 let registry = screen.registry.clone();
3013 let name = name.to_owned();
3014 Command::perform(move || {
3015 if !again {
3016 return Msg::ContainerActed(plan::stop(&engine, &name));
3017 }
3018 let result = plan::restart(&engine, &name);
3019 let up = result.is_ok();
3020 let message = Msg::ContainerActed(result);
3021 if up { noted(registry.as_deref(), engine.kind(), &name, message) } else { message }
3022 })
3023}
3024
3025const TERMINAL_ID: &str = "workspace-terminal";
3027
3028const DOCUMENT_ID: &str = "workspace-document";
3030
3031const TABS_ID: &str = "workspace-tabs";
3033
3034const BODY_ID: &str = "workspace-tab-body";
3036
3037pub fn view<P: 'static>(
3048 screen: &WorkspaceScreen,
3049 ui: &mut View<'_, P>,
3050 wrap: fn(Msg) -> P,
3051 above: impl FnOnce(&mut View<'_, P>),
3052 below: impl FnOnce(&mut View<'_, P>),
3053 under: impl FnOnce(&mut View<'_, P>),
3054) {
3055 AppShell::new()
3056 .sidebar_width(RAIL_WIDTH)
3057 .collapse_below(0)
3060 .sidebar(|ui| {
3061 ui.column(|ui| {
3062 ui.map(wrap, |ui| rail(screen, ui)).fill();
3063 under(ui);
3064 })
3065 .fill();
3066 })
3067 .body(|ui| {
3068 SidePanel::new(screen.panel.width())
3069 .side(Side::Right)
3070 .open(screen.panel.is_open())
3071 .limits(PANEL_MIN, PANEL_MAX)
3072 .on_toggle(move |open| wrap(Msg::TogglePanel(open)))
3073 .on_resize(move |width| wrap(Msg::ResizePanel(width)))
3074 .panel(|ui| {
3075 ui.map(wrap, |ui| panel::view(screen, ui)).fill();
3076 })
3077 .body(|ui| {
3078 AppShell::new()
3079 .header(|ui| {
3080 above(ui);
3081 ui.map(wrap, |ui| header(screen, ui)).fill_width();
3082 })
3083 .body(|ui| {
3084 ui.map(wrap, |ui| center(screen, ui)).fill();
3085 })
3086 .footer(below)
3087 .show(ui);
3088 })
3089 .show(ui)
3090 .id("workspace-body");
3091 })
3092 .show(ui);
3093}
3094
3095fn rail(screen: &WorkspaceScreen, ui: &mut View<'_, Msg>) {
3097 let tabs = screen.workspaces.iter().map(|workspace| {
3098 let running = workspace.containers.iter().filter(|container| container.state.is_running()).count();
3099 let tab = RailTab::new(workspace.name.clone()).icon("folder");
3100 if running > 0 { tab.status("success").badge(running.to_string()) } else { tab }
3101 });
3102 let rail = TabRail::new(tabs)
3103 .active(screen.active)
3104 .collapsed(true)
3105 .collapsed_marker(CollapsedMarker::Initial)
3106 .row_height(RAIL_ROWS)
3107 .closable(Msg::CloseWorkspace)
3108 .on_add(|| Msg::AddWorkspace)
3109 .on_select(Msg::OpenWorkspace);
3110 ui.add(strip::Walked::new(rail, &[Key::Enter, Key::Right])).fill().id("workspace-rail");
3112}
3113
3114fn header(screen: &WorkspaceScreen, ui: &mut View<'_, Msg>) {
3120 let Some(workspace) = screen.workspace() else { return };
3121 let labels: Vec<String> = (0..workspace.tabs.len()).map(|index| workspace.tab_label(index)).collect();
3122 let mut tabs = Tabs::new(labels)
3123 .active(workspace.active_tab)
3124 .tab_width(TabWidth::Fit)
3125 .closable(Msg::CloseTab)
3126 .reorderable(move |from, to| Msg::MoveTab { from, to })
3127 .on_select(Msg::OpenTab)
3128 .on_add(|| Msg::NewTab)
3129 .context_menu(|index| vec![ContextItem::new(t!("workspace.rename.item"), Msg::RenameTab(index))]);
3130 for (index, tab) in workspace.tabs.iter().enumerate() {
3132 tabs = tabs.busy(index, busy::working(tab, screen.looked));
3133 }
3134 ui.add(strip::Walked::new(tabs, &[Key::Enter, Key::Down])).fill_width().id(TABS_ID);
3136 rename::view(screen, ui);
3137}
3138
3139fn center(screen: &WorkspaceScreen, ui: &mut View<'_, Msg>) {
3143 ui.column(|ui| {
3147 ui.column(|ui| tab_body(screen, ui)).fill().id(BODY_ID);
3148 if let Some(workspace) = screen.workspace()
3149 && let Some(tab) = workspace.active_tab()
3150 {
3151 if !tab.letters().is_empty() || tab.stopped().is_some() {
3152 bridge::view(tab, ui);
3153 }
3154 relay::view(workspace, tab, ui);
3155 }
3156 })
3157 .fill();
3158}
3159
3160fn tab_body(screen: &WorkspaceScreen, ui: &mut View<'_, Msg>) {
3162 let Some(workspace) = screen.workspace() else {
3163 let open =
3164 Button::new(t!("workspace.none.open")).icon("workspace").variant("primary").on_press(Msg::AddWorkspace);
3165 ui.add(
3166 EmptyState::new(t!("workspace.none.title"))
3167 .icon("inbox")
3168 .message(t!("workspace.none.message"))
3169 .action(open),
3170 )
3171 .fill()
3172 .id("workspace-none");
3173 return;
3174 };
3175 let Some(tab) = workspace.active_tab() else {
3176 let message = if screen.engine.is_some() { t!("workspace.empty.message") } else { t!("workspace.no-engine") };
3179 let open = Button::new(t!("workspace.new-tab")).variant("primary").on_press(Msg::NewTab);
3180 ui.add(EmptyState::new(t!("workspace.empty.title")).icon("inbox").message(message).action(open))
3181 .fill()
3182 .id("workspace-empty");
3183 return;
3184 };
3185 if tab.kind() == &TabKind::New {
3186 blank::view(screen, workspace, tab.key(), ui);
3187 return;
3188 }
3189 if let TabKind::Markdown(file) = tab.kind() {
3190 document(tab, file, ui);
3191 return;
3192 }
3193 if viewer::shows(tab) {
3194 viewer::view(tab, ui);
3195 return;
3196 }
3197 if let TabKind::Sound(file) = tab.kind()
3198 && sound::shows(tab)
3199 {
3200 sound::view(tab, file, ui);
3201 return;
3202 }
3203 if let TabKind::Desktop(profile) = tab.kind() {
3206 desktop::view(screen, tab, profile, ui);
3207 return;
3208 }
3209 let image = matches!(tab.kind(), TabKind::Image(_));
3210 let restart = t!("workspace.restart");
3211 match tab.state() {
3212 TabState::Waiting if screen.engine.is_none() => {
3215 ui.add(EmptyState::new(t!("workspace.waiting")).icon("inbox").message(t!("workspace.no-engine")))
3216 .fill()
3217 .id("workspace-waiting");
3218 }
3219 TabState::Waiting | TabState::Starting => {
3220 ui.column(|ui| {
3221 ui.add(ShimmerText::new(t!("workspace.starting")));
3222 })
3223 .fill()
3224 .align(Align::Center)
3225 .justify(Align::Center)
3226 .id("workspace-starting");
3227 }
3228 TabState::Running | TabState::Ended { .. } if image => picture(tab, ui),
3229 TabState::Running => match tab.session() {
3230 Some(session) => {
3231 terminal(ui, session);
3232 }
3233 None => {
3234 ui.add(ShimmerText::new(t!("workspace.starting"))).fill();
3235 }
3236 },
3237 TabState::Ended { .. } if matches!(tab.kind(), TabKind::Editor(_)) => {
3240 stopped(ui, tab, &t!("workspace.file.closed"), None, t!("workspace.file.reopen"));
3241 }
3242 TabState::Ended { code } => stopped(ui, tab, &ended_text(*code), None, restart),
3243 TabState::Stopped => stopped(ui, tab, &t!("workspace.stopped"), None, restart),
3244 TabState::Failed(failure) => {
3245 let detail = if failure.command.is_empty() {
3247 failure.output.clone()
3248 } else {
3249 format!("{}\n{}", failure.command, failure.output)
3250 };
3251 match screen.engine.as_ref().and_then(|engine| help(engine.kind(), &failure.output)) {
3254 Some(help) => refused(ui, tab, &help, &detail, restart),
3255 None => stopped(ui, tab, &t!("workspace.failed"), Some(&detail), restart),
3256 }
3257 }
3258 TabState::NoImage => no_image(screen, tab, ui),
3259 TabState::Building(_) => building(screen, tab, ui),
3260 TabState::Missing | TabState::Unreadable(_) => file_trouble(tab, ui),
3261 }
3262}
3263
3264fn no_image(screen: &WorkspaceScreen, tab: &Tab, ui: &mut View<'_, Msg>) {
3267 let key = tab.key();
3268 let profile = match tab.kind() {
3269 TabKind::Profile(name) => name.clone(),
3270 _ => String::new(),
3271 };
3272 let engine = screen.engine.as_ref().map(|engine| engine_name(engine.kind())).unwrap_or_default();
3273 page::column(ui, page::WIDTH, |ui| {
3274 ui.column(|ui| {
3275 ui.add(Text::new(t!("workspace.image.missing", profile = profile, engine = engine)).bold()).fill_width();
3276 ui.add(Text::new(t!("workspace.image.missing-why")).role("secondary")).fill_width();
3277 ui.row(|ui| {
3278 ui.add(Button::new(t!("workspace.image.build")).variant("primary").on_press(Msg::BuildImage(key)))
3279 .id("workspace-build-image");
3280 ui.add(Button::new(t!("workspace.image.cancel")).on_press(Msg::CancelOpen(key)))
3281 .id("workspace-cancel-open");
3282 })
3283 .gap(2);
3284 })
3285 .gap(1)
3286 .padding(Padding::symmetric(1, 2))
3287 .fill()
3288 .id("workspace-no-image");
3289 });
3290}
3291
3292fn building(screen: &WorkspaceScreen, tab: &Tab, ui: &mut View<'_, Msg>) {
3295 let key = tab.key();
3296 let profile = match tab.kind() {
3297 TabKind::Profile(name) => name.clone(),
3298 _ => String::new(),
3299 };
3300 page::column(ui, page::WIDTH, |ui| {
3301 ui.column(|ui| {
3302 ui.row(|ui| {
3303 ui.add(ShimmerText::new(t!("profiles.working.build")));
3304 ui.spacer();
3305 ui.add(Button::new(t!("workspace.image.stop")).on_press(Msg::StopBuild(key)))
3306 .id("workspace-stop-build");
3307 })
3308 .fill_width();
3309 ui.add(Text::new(t!("workspace.image.building", profile = profile)).role("secondary")).fill_width();
3312 if tab.is_stuck() {
3316 ui.add(Text::new(t!("build.stuck", minutes = screen.stall.minutes())).color("warning")).fill_width();
3317 }
3318 ui.add(LogView::new(tab.build_log())).fill().id("workspace-build-log");
3319 })
3320 .gap(1)
3321 .padding(Padding::symmetric(1, 2))
3322 .fill()
3323 .id("workspace-building");
3324 });
3325}
3326
3327fn picture(tab: &Tab, ui: &mut View<'_, Msg>) {
3332 let key = tab.key();
3333 let failed = matches!(tab.state(), TabState::Ended { code: Some(code) } if *code != 0);
3334 ui.column(|ui| {
3335 match tab.session() {
3336 Some(session) => {
3337 terminal(ui, session);
3338 }
3339 None if tab.state() == &TabState::Running => {
3340 ui.add(ShimmerText::new(t!("workspace.starting"))).fill();
3341 }
3342 None => {
3343 ui.spacer();
3344 }
3345 }
3346 ui.row(|ui| {
3347 if failed {
3348 ui.add(Text::new(t!("workspace.file.not-drawn")).role("secondary"));
3349 }
3350 ui.spacer();
3351 ui.add(Button::new(t!("workspace.file.redraw")).on_press(Msg::Restart(key))).id("workspace-redraw");
3352 })
3353 .gap(2)
3354 .padding(Padding { left: 1, right: 1, ..Padding::default() })
3355 .fill_width();
3356 })
3357 .fill()
3358 .id("workspace-picture");
3359}
3360
3361fn document(tab: &Tab, file: &str, ui: &mut View<'_, Msg>) {
3363 match (tab.state(), tab.document()) {
3364 (TabState::Missing | TabState::Unreadable(_), _) => file_trouble(tab, ui),
3365 (_, Some(text)) => {
3367 let edit = Msg::EditFile(file.to_owned());
3368 ui.column(|ui| {
3369 ui.row(|ui| {
3370 ui.add(Text::new(file.to_owned()).role("faint").no_wrap());
3371 ui.spacer();
3372 ui.add(Button::new(t!("workspace.file.edit")).on_press(edit)).id("workspace-edit");
3373 })
3374 .gap(2)
3375 .padding(Padding { left: 2, right: 1, ..Padding::default() })
3376 .fill_width();
3377 ui.add_with(ScrollView::new(), |ui| {
3378 ui.column(|ui| {
3379 ui.add(Markdown::new(text)).fill_width();
3380 })
3381 .padding(Padding::symmetric(0, 2))
3382 .fill_width();
3383 })
3384 .fill()
3385 .id(DOCUMENT_ID);
3386 })
3387 .gap(1)
3388 .fill()
3389 .id("workspace-document-page");
3390 }
3391 (_, None) => {
3392 ui.column(|ui| {
3393 ui.add(Spinner::new().label(t!("workspace.file.reading")));
3394 })
3395 .fill()
3396 .align(Align::Center)
3397 .justify(Align::Center)
3398 .id("workspace-reading");
3399 }
3400 }
3401}
3402
3403fn file_trouble(tab: &Tab, ui: &mut View<'_, Msg>) {
3405 let file = tab.kind().file().unwrap_or_default().to_owned();
3406 match tab.state() {
3407 TabState::Unreadable(reason) => {
3408 stopped(ui, tab, &t!("workspace.file.unreadable"), Some(reason), t!("workspace.file.try-again"));
3409 }
3410 _ => stopped(ui, tab, &t!("workspace.file.missing", file = file), None, t!("workspace.file.try-again")),
3411 }
3412}
3413
3414fn tab_terminal(session: &TerminalSession) -> Terminal {
3421 SWITCHES.iter().fold(
3422 Terminal::new(session)
3423 .pass_through(Scope::Global, "help")
3424 .pass_through(Scope::Global, "toggle-panel")
3425 .pass_through(Scope::App, "leave-terminal")
3426 .pass_through(Scope::App, "rename-tab"),
3427 |terminal, action| terminal.pass_through(Scope::App, *action),
3428 )
3429}
3430
3431pub const SWITCHES: [&str; 11] =
3434 ["next-tab", "previous-tab", "tab-1", "tab-2", "tab-3", "tab-4", "tab-5", "tab-6", "tab-7", "tab-8", "tab-9"];
3435
3436#[must_use]
3438pub fn switch(name: &str) -> Option<Msg> {
3439 match name {
3440 "next-tab" => Some(Msg::NextTab(true)),
3441 "previous-tab" => Some(Msg::NextTab(false)),
3442 _ => {
3443 let place: usize = name.strip_prefix("tab-")?.parse().ok()?;
3444 (1..=9).contains(&place).then(|| Msg::GoToTab(place - 1))
3445 }
3446 }
3447}
3448
3449fn terminal(ui: &mut View<'_, Msg>, session: &TerminalSession) {
3456 ui.add(tab_terminal(session)).fill().id(TERMINAL_ID).on_action(Scope::App, "leave-terminal", Msg::LeaveTerminal);
3457}
3458
3459fn ended_text(code: Option<u32>) -> String {
3461 match code {
3462 Some(0) | None => t!("workspace.ended"),
3463 Some(code) => t!("workspace.ended-code", code = code),
3464 }
3465}
3466
3467#[must_use]
3471pub fn agents_at_work(screen: &WorkspaceScreen) -> usize {
3472 screen
3473 .workspaces
3474 .iter()
3475 .flat_map(|workspace| workspace.tabs.iter())
3476 .filter(|tab| tab.kind().runs_agent())
3477 .filter(|tab| matches!(tab.state(), TabState::Running | TabState::Starting))
3478 .count()
3479}
3480
3481fn stopped(ui: &mut View<'_, Msg>, tab: &Tab, message: &str, detail: Option<&str>, again: String) {
3484 let key = tab.key();
3485 let words = |ui: &mut View<'_, Msg>| {
3486 ui.column(|ui| {
3487 ui.add(Text::new(message.to_owned()).role("secondary"));
3488 if let Some(detail) = detail {
3489 ui.add(Text::new(detail.to_owned()).role("faint")).selectable(true);
3490 }
3491 ui.add(Button::new(again).variant("primary").on_press(Msg::Restart(key))).id("workspace-restart");
3492 })
3493 .gap(1)
3494 .padding(Padding::symmetric(1, 2))
3495 .fill_width();
3496 };
3497 match tab.session() {
3498 Some(session) => {
3501 ui.column(|ui| {
3502 terminal(ui, session);
3503 words(ui);
3504 })
3505 .fill()
3506 .id("workspace-stopped");
3507 }
3508 None => page::column(ui, page::WIDTH, |ui| {
3509 ui.column(words).fill().id("workspace-stopped");
3510 }),
3511 }
3512}
3513
3514fn refused(ui: &mut View<'_, Msg>, tab: &Tab, help: &Help, detail: &str, again: String) {
3517 let key = tab.key();
3518 page::column(ui, page::WIDTH, |ui| {
3519 ui.column(|ui| {
3520 ui.column(|ui| {
3521 help.show(ui);
3522 ui.add(Button::new(again).variant("primary").on_press(Msg::Restart(key))).id("workspace-restart");
3523 ui.add(Text::new(t!("known.said")).role("faint"));
3524 ui.add(Text::new(detail.to_owned()).role("faint")).selectable(true);
3525 })
3526 .gap(1)
3527 .padding(Padding::symmetric(1, 2))
3528 .fill_width();
3529 })
3530 .fill()
3531 .id("workspace-stopped");
3532 });
3533}
3534
3535#[must_use]
3538pub fn entry(screen: &WorkspaceScreen) -> &'static str {
3539 match screen.workspace().and_then(OpenWorkspace::active_tab).map(Tab::kind) {
3540 Some(TabKind::New) => blank::CHOICES_ID,
3541 Some(TabKind::Markdown(_)) => DOCUMENT_ID,
3542 Some(TabKind::Pdf(_) | TabKind::Office(_))
3543 if !screen.workspace().and_then(OpenWorkspace::active_tab).is_some_and(viewer::draws) =>
3544 {
3545 DOCUMENT_ID
3546 }
3547 Some(TabKind::Sound(_))
3548 if screen.workspace().and_then(OpenWorkspace::active_tab).is_some_and(|tab| tab.quiet().is_some()) =>
3549 {
3550 DOCUMENT_ID
3551 }
3552 Some(TabKind::Desktop(_)) => desktop::WINDOW_ID,
3553 Some(_) => TERMINAL_ID,
3554 None => TABS_ID,
3555 }
3556}
3557
3558#[must_use]
3563pub fn action(name: &str) -> Option<Msg> {
3564 match name {
3565 "new-tab" => Some(Msg::NewTab),
3566 "leave-terminal" => Some(Msg::EnterTerminal),
3569 "rename-tab" => Some(Msg::RenameOpenTab),
3570 _ => switch(name),
3571 }
3572}
3573
3574#[must_use]
3578pub fn escape(screen: &WorkspaceScreen) -> Option<Msg> {
3579 screen
3580 .workspace()
3581 .filter(|workspace| !workspace.files.pending().is_empty())
3582 .map(|workspace| Msg::Files(workspace.id().to_owned(), FileManagerMsg::DropCut))
3583}
3584
3585#[must_use]
3591pub fn hints(icons: &Icons) -> Vec<(String, String)> {
3592 let tab_keys = format!("{}{}", icons.glyph("arrow-left"), icons.glyph("arrow-right"));
3593 let label = |chord: &str| chord.parse::<KeyChord>().map_or_else(|_| chord.to_owned(), |chord| chord.label());
3595 vec![
3596 (tab_keys, t!("workspace.hints.tabs")),
3597 (format!("{} {}", label("enter"), icons.glyph("arrow-down")), t!("workspace.hints.enter")),
3598 (label("ctrl+w"), t!("workspace.hints.close")),
3599 (label("shift+tab"), t!("workspace.hints.leave")),
3600 ]
3601}