Skip to main content

qcode/ui/workspace/
mod.rs

1//! The workspace screen: the rail of open workspaces on the left, the tabs of one workspace on top,
2//! the container's terminal in the middle and the widget panel on the right.
3//!
4//! The rail holds the workspaces the person opened and nothing else, the way a browser holds its
5//! windows: a workspace joins it when it is opened from the list, leaves it when it is closed, and
6//! the rail and every tab in it are what "Continue" on the home screen brings back.
7//!
8//! The screen's whole reason for existing is one line of [`ContainerPlan::enter`]: a tab spawns
9//! the engine binary with `exec`, so the program a tab shows always runs inside a container and
10//! never on the machine QCode runs on. Everything else here is around that: which container a tab
11//! enters, what a tab says when its container is not there, and what the panel shows.
12//!
13//! The screen speaks its own [`Msg`]: [`update`] answers with commands of it and [`view`] draws
14//! for it, and the application maps both to its own message, so the screen is drawn and tested
15//! without naming anything of the application.
16
17mod 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
102/// Width of the workspace rail: the framework's collapsed strip, which is the same four cells on
103/// every screen. The rail never gives way to a narrow terminal, because losing it would mean
104/// losing the only way between workspaces.
105const RAIL_WIDTH: u16 = 4;
106
107/// Height of one workspace block in the rail, in lines.
108const RAIL_ROWS: u16 = 3;
109
110/// Width of the widget panel when it is opened, and the range it can be dragged through. The
111/// framework keeps the terminal a quarter of the screen whatever is asked for here.
112const PANEL_WIDTH: u16 = 32;
113/// Narrowest the widget panel can be dragged.
114const PANEL_MIN: u16 = 18;
115/// Widest the widget panel can be dragged.
116const PANEL_MAX: u16 = 52;
117
118/// Everything that can happen on the workspace screen.
119#[derive(Debug, Clone)]
120pub enum Msg {
121    /// A workspace of the rail was opened.
122    OpenWorkspace(usize),
123    /// A workspace was closed from the rail, with every tab in it.
124    CloseWorkspace(usize),
125    /// The list of workspaces was asked for, to open another one beside these. The screen itself
126    /// cannot go there; the application that holds both answers this message.
127    AddWorkspace,
128    /// Whether this machine can freeze a container at all was asked, and this is the answer: only
129    /// rootless podman with swap can, and nothing else about freezing happens without it.
130    Freezable(bool),
131    /// It is time to look at the open workspaces and see whether a profile's container has gone
132    /// quiet. Nothing is timed at all while freezing is off or the machine cannot do it.
133    Look,
134    /// One profile's container was read for a look: the workspace of the rail it belongs to, by its
135    /// place, and the profile by name, so a reading is matched to the profile it was about even if
136    /// the store was read again while the reading was in flight. With the CPU the container had
137    /// used when the reading was taken and the command line of every process in it; either may be
138    /// missing, and a missing one is not a yes.
139    Looked(usize, String, String, freezing::Read, std::time::Instant),
140    /// A profile's container was frozen, and this is whether it is: a freeze the engine or the
141    /// kernel refused leaves the container exactly as it was and says nothing to the person.
142    Frozen(String, bool),
143    /// A container QCode had frozen was woken, and this is whether it is awake. One that is still
144    /// frozen stays frozen, and the next thing that needs it asks the engine again.
145    Thawed(String, bool),
146    /// A tab was opened.
147    OpenTab(usize),
148    /// A tab was closed.
149    CloseTab(usize),
150    /// The person confirmed closing the tab `TabKey`, which still held messages from other tabs
151    /// or an agent that was still running.
152    CloseTabAnyway(TabKey),
153    /// What a closed tab had started inside its container was ended.
154    TabEnded,
155    /// A tab was dragged to another place.
156    MoveTab {
157        /// Where it was.
158        from: usize,
159        /// Where it lands.
160        to: usize,
161    },
162    /// A blank tab was asked for, to choose in it what it opens.
163    NewTab,
164    /// The person asked to name the tab at this place, from its menu on the strip.
165    RenameTab(usize),
166    /// The person asked to name the open tab, with its key.
167    RenameOpenTab,
168    /// The name being given to a tab was edited.
169    TabName(String),
170    /// The name being given to a tab was kept.
171    NameTab,
172    /// Naming a tab was given up; its name stays as it was.
173    CancelRename,
174    /// The keyboard was asked out of the harness, onto the tab strip.
175    LeaveTerminal,
176    /// The keyboard was asked back into the open tab's harness, from wherever it was.
177    EnterTerminal,
178    /// A key on the tab strip or the rail is being handled, or with `false` has been: a switch
179    /// it makes in between leaves the keyboard walking there.
180    Walking(bool),
181    /// The keyboard was asked into the open tab, from the strip or the rail.
182    EnterTab,
183    /// The next tab was asked for, or with `false` the one before, going round at the ends.
184    NextTab(bool),
185    /// The tab at this place on the strip was asked for, counted from 0.
186    GoToTab(usize),
187    /// A row of a blank tab's page was chosen: the blank tab of this key turns into it.
188    Choose(TabKey, Choice),
189    /// The keyboard moved to another row of a blank tab's page.
190    HighlightChoice(usize),
191    /// Every conversation of a profile was asked for on a blank tab's page, not only the newest.
192    ShowAll(HistoryKey),
193    /// A reading of a profile's conversations answered: the reading's number, and the
194    /// conversations newest first or why they could not be read.
195    HistoryRead(HistoryKey, u64, Result<Vec<Conversation>, String>),
196    /// A reading of a profile's conversations, by its number, has run long enough to be noticed.
197    HistorySlow(HistoryKey, u64),
198    /// The indicator of a reading has been on screen long enough to be read.
199    HistorySettled(HistoryKey),
200    /// A new profile was asked for, from the blank tab's "New profile" row: the application opens
201    /// the profiles screen with its wizard already open. The screen itself cannot go there; the
202    /// application that holds both answers this message.
203    ManageProfiles,
204    /// The profiles of the store were read again, after the person may have changed them.
205    Profiles(Vec<Profile>),
206    /// A profile was recorded in a workspace's `workspace.qcode`, or could not be.
207    ProfileAdded(String, Result<WorkspaceFile, String>),
208    /// The container of a tab is up, or the engine refused.
209    Ready(TabKey, u64, Result<(), LaunchFailure>),
210    /// Build the image of the profile of this tab, which the engine does not have.
211    BuildImage(TabKey),
212    /// A line the build of a tab's image said.
213    ImageLine(TabKey, String),
214    /// The build of a tab's image ended: done, or what went wrong in the engine's words, or
215    /// `None` when the person stopped it.
216    ImageBuilt(TabKey, Result<(), Option<String>>),
217    /// Stop building a tab's image.
218    StopBuild(TabKey),
219    /// The moment a tab whose image is being built was looked at, which is what says whether its
220    /// log has gone quiet for long enough to be stuck.
221    BuildLooked(std::time::Instant),
222    /// Do not open this tab after all: its image is missing and the person would rather not
223    /// build it now.
224    CancelOpen(TabKey),
225    /// The container of a new-chat tab brought back from the last session is up, or the engine
226    /// refused; with the profile's conversations when they could be read, to find the one the
227    /// tab was showing.
228    Woken(TabKey, u64, Result<(), LaunchFailure>, Option<Vec<Conversation>>, Option<String>),
229    /// A tab's session printed something or ended.
230    Output(TabKey, u64, TerminalEvent),
231    /// Whether the container of a tab whose session ended is still running.
232    Checked(TabKey, u64, bool),
233    /// A tab was asked to start again.
234    Restart(TabKey),
235    /// The widget panel was opened or closed.
236    TogglePanel(bool),
237    /// The widget panel was dragged to another width.
238    ResizePanel(u16),
239    /// A widget of the panel was opened or closed.
240    ToggleWidget(usize, bool),
241    /// A widget of the panel was dragged to another place.
242    MoveWidget {
243        /// Where it was.
244        from: usize,
245        /// Where it lands.
246        to: usize,
247    },
248    /// The chooser of widgets the panel does not carry was shown or dismissed.
249    ShowWidgets(bool),
250    /// The keyboard moved to another row of the chooser of widgets.
251    HighlightWidget(usize),
252    /// A widget was added to the panel.
253    AddWidget(PanelWidget),
254    /// A widget was taken off the panel.
255    RemoveWidget(PanelWidget),
256    /// Something happened in the file manager of the workspace of this id: a click, a key, a
257    /// folder read, an operation done or a change another program made on disk.
258    Files(String, FileManagerMsg),
259    /// The entry of this key, with the rest of the selection when it is part of it, was asked to
260    /// be left out of the workspace's backup, or taken into it again when `false`.
261    LeaveOut(String, bool),
262    /// A file of the file tree was opened: Enter or a click on it.
263    OpenFile(String),
264    /// A file was asked for in the editor, from the Markdown tab that shows it.
265    EditFile(String),
266    /// The file the tab of this key opens is not in the workspace any more.
267    Missing(TabKey, u64),
268    /// The document a Markdown tab shows was read, or could not be.
269    DocumentRead(TabKey, u64, Result<String, DocumentTrouble>),
270    /// The text of the file a tab shows was taken out in the container, or could not be.
271    TextRead(TabKey, u64, Result<Taken, LaunchFailure>),
272    /// A page of the PDF the tab of this key shows was asked for as a picture, counted from 1,
273    /// or with `None` its text again.
274    Page(TabKey, Option<usize>),
275    /// The sound of a tab can be played through the sound server's socket at this path, or the
276    /// base image it plays in could not be made.
277    Playable(TabKey, u64, Result<PathBuf, LaunchFailure>),
278    /// The details of the sound of a tab were read, or could not be, and why they are shown
279    /// rather than the sound played.
280    Described(TabKey, u64, Quiet, Result<Taken, LaunchFailure>),
281    /// The containers of sound tabs that were closed while playing were taken away.
282    Silenced,
283    /// The window of a tab is open, or it waits to be asked, or the engine refused.
284    WindowOpened(TabKey, u64, Result<Opening, LaunchFailure>),
285    /// The window of a tab closed, whoever closed it, with the exit code the engine reported.
286    WindowEnded(TabKey, u64, Option<u32>),
287    /// The window of this tab, in this run, asked for these web addresses to be opened.
288    SignInWanted(TabKey, u64, Vec<String>),
289    /// The opening of that address answered, saying where it was shown.
290    SignInOpened(TabKey, u64, String, Shown),
291    /// The listening for the way back of a sign-in, the one of that id, ended.
292    SignInBack(TabKey, u64, u64, crate::desktop::callback::Ending),
293    /// The person asked for the page of the sign-in in the window inside the container instead.
294    SignInHere(TabKey),
295    /// The open window of a tab was asked to show itself.
296    RaiseWindow(TabKey),
297    /// That asking finished; a refusal is worth saying, because nothing else would show it.
298    WindowRaised(Result<(), LaunchFailure>),
299    /// The window of a tab was asked to close.
300    CloseWindow(TabKey),
301    /// The containers of windows that were closed have been taken away, or one of them would not
302    /// go and this is the engine's word on it.
303    WindowsClosed(Option<LaunchFailure>),
304    /// A row of the container widget was selected.
305    SelectContainer(usize),
306    /// The container widget was asked for the engine's current answer.
307    RefreshContainers,
308    /// The containers of a workspace came back from the engine.
309    ContainersRead(String, Result<Vec<Container>, LaunchFailure>),
310    /// A container was asked to stop.
311    StopContainer(String),
312    /// A container was asked to stop and start again.
313    RestartContainer(String),
314    /// An engine command on a container finished.
315    ContainerActed(Result<(), LaunchFailure>),
316    /// A container QCode started could not be noted cleanly in the list of the containers it
317    /// started; the message that came with the start follows.
318    Noted(Noting, Box<Msg>),
319    /// It is time to back a workspace up: the workspace's id and the number of the timer that went
320    /// off.
321    BackupDue(String, u64),
322    /// A backup of a workspace finished.
323    BackedUp {
324        /// The workspace's id.
325        workspace: String,
326        /// Its name, to say a failure by after the workspace has left the rail.
327        name: String,
328        /// What came of the workspace's own snapshot.
329        made: Result<Snapshot, BackupTrouble>,
330        /// The other parts of the round that could not be backed up, and why.
331        missed: Vec<(Part, BackupTrouble)>,
332    },
333    /// The newest backup of a workspace was asked after: the workspace's id and when it was made, or
334    /// `None` when there is none or the backup could not be asked.
335    NewestBackup(String, Option<i64>),
336    /// How often the open workspaces are backed up was changed.
337    BackupEvery(BackupEvery),
338    /// What a workspace's backup leaves out was written into its `workspace.qcode`: the workspace's
339    /// id, and the list as the file now holds it or why it could not be written.
340    SkipWritten(String, Result<Vec<String>, String>),
341    /// How much a workspace's `Backup/` holds was read: the workspace's id and the bytes.
342    BackupSize(String, u64),
343    /// The backup of the open workspace was asked to take `Assets/` too, or no longer to.
344    BackupAssets(bool),
345    /// Whether a workspace's backup takes its assets was written into its `workspace.qcode`: the
346    /// workspace's id, and the choice as the file now holds it or why it could not be written.
347    AssetsWritten(String, Result<bool, String>),
348    /// The list of the open workspace's backups was asked for, or of the earlier versions of the
349    /// file of this key.
350    ShowBackups(Option<String>),
351    /// The list of backups was read: the number of the reading, and the backups newest first
352    /// or why they could not be read.
353    BackupsRead(u64, Result<Vec<Entry>, BackupTrouble>),
354    /// A reading of the list of backups, by its number, has run long enough to be noticed.
355    BackupsSlow(u64),
356    /// The indicator of a reading of the list has been on screen long enough to be read.
357    BackupsSettled(u64),
358    /// The keyboard moved to another row of the list of backups.
359    HighlightBackup(usize),
360    /// The backup at this row of the list was chosen; the person is asked first.
361    ChooseBackup(usize),
362    /// The list of backups was made one of what it offers, at this position: the workspace's own
363    /// files or a profile's conversations.
364    BackupsOf(usize),
365    /// The person said yes to bringing this backup back.
366    RestoreBackup(SnapshotId),
367    /// The person said yes to stopping the profile's container and then bringing this backup of
368    /// its conversations back.
369    StopAndRestore(SnapshotId),
370    /// The person said no to bringing a backup back.
371    KeepBackup,
372    /// The list of backups was closed.
373    CloseBackups,
374    /// A call came to the bridge of the workspace of this id, on the listener of this number, or
375    /// the listener was closed.
376    Bridge(String, u64, Option<Call>),
377    /// The person answered whether the agent of one tab may send messages to another's.
378    Allow {
379        /// The sending tab.
380        from: TabKey,
381        /// The receiving tab.
382        to: TabKey,
383        /// Whether it may.
384        allow: bool,
385    },
386    /// The question about one tab sending to another has waited long enough that the senders
387    /// are told their messages wait.
388    StillAsking {
389        /// The sending tab.
390        from: TabKey,
391        /// The receiving tab.
392        to: TabKey,
393    },
394    /// Something was done with the messages waiting in a tab.
395    Letters(TabKey, Letters),
396    /// Time to look again at the tabs that still hold messages for their agents.
397    Deliver,
398    /// Time to look at the Return this tab owes a message it has already taken.
399    Return(TabKey),
400    /// Time to look at the tabs again and mark the ones still working, as of this moment.
401    Turn(std::time::Instant),
402    /// The relay of an open workspace reported something. A report is a closure on the relay's own
403    /// thread, so what it found arrives as a message like any other and is said in this update.
404    Reported(crate::provider::RelayEvent),
405    /// The line under a tab about a fall back has been said for its ten seconds: the token of the
406    /// tab it was under, and which fall back it was, so a line that came after it stays.
407    Silence(String, u64),
408    /// The settings that keep a harness from asking could not be merged into the home of a tab
409    /// whose container came up; the message that came with the start follows.
410    Asked(HarnessKind, Asks, Box<Msg>),
411    /// The agent of a window whose home came up could not be told what it may do without asking;
412    /// the message that came with the open follows.
413    Unapproved(String, Box<Msg>),
414    /// The bridge could not be registered in the settings of a harness whose container came up;
415    /// the message that came with the start follows.
416    Unbridged(HarnessKind, Unregistered, Box<Msg>),
417    /// The instruction files of a QCode high profile whose container came up could not be brought
418    /// up to date in the workspace; the message that came with the start follows.
419    Unguided(HarnessKind, Unguided, Box<Msg>),
420    /// The profile's image was rebuilt under a workspace that installed things of its own as the
421    /// administrator, and running this command again on the new image failed: the workspace stays
422    /// on the image it had. The profile's name, the command, and the message that came with the
423    /// start.
424    Behind(String, String, Box<Msg>),
425    /// "As administrator" was asked for in the container of the profile of this name.
426    OpenAdmin(String),
427    /// Bringing a backup back finished.
428    Restored {
429        /// The workspace's id.
430        workspace: String,
431        /// Its name, to say what came back by.
432        name: String,
433        /// What was brought back.
434        of: BackupOf,
435        /// The file that was brought back, or `None` for all of it.
436        file: Option<String>,
437        /// What came of it.
438        done: Result<Brought, BackupTrouble>,
439    },
440}
441
442/// What went wrong noting a container QCode started, so that it can be stopped once no QCode is
443/// open.
444#[derive(Debug, Clone, PartialEq, Eq)]
445pub enum Noting {
446    /// The list could not be written: the container is not in it, and may keep running after
447    /// QCode closes. The machine's words.
448    Unwritten(String),
449    /// The list was damaged; what could be read of it was kept along with the new name. Where
450    /// it was damaged.
451    Repaired(String),
452}
453
454/// One workspace the rail can switch to, with its own tabs and its own panel content.
455#[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    /// The conversations of each profile, by the profile's name, as the blank tab's page last
465    /// read them.
466    history: HashMap<String, history::Shelf>,
467    /// The file manager of the workspace's own folder.
468    files: FileManagerState,
469    containers: Vec<Container>,
470    container_row: usize,
471    container_error: Option<LaunchFailure>,
472    busy: bool,
473    /// When the workspace was backed up last, and when it is backed up next.
474    backup: backups::State,
475    /// The folders and files inside the workspace its backup leaves out, as its file lists them.
476    skip: Vec<String>,
477    /// Whether its backup takes `Assets/` too, as its file says.
478    assets: bool,
479    /// Whether the workspace listens for its tabs' agents.
480    link: bridge::Link,
481    /// Whether the workspace's provider relay is listening, for a tab of a profile that runs on
482    /// a provider of the person's own.
483    relay: relay::Link,
484    /// The tokens of the opencode servers the workspace's profile containers share between their
485    /// tabs ([`shared`]).
486    servers: shared::ServerTokens,
487    /// The id the next tab that joins the workspace is given. It only grows while the workspace
488    /// is open, so no two tabs are ever given the same one, even after one of them was closed.
489    next_number: u32,
490}
491
492impl OpenWorkspace {
493    /// A workspace as its own file describes it, living at `paths`, offering `profiles`.
494    ///
495    /// The profiles are every profile of the store, already read: this screen shows
496    /// profiles and enters their containers, it does not go looking for their definitions. The
497    /// ones the workspace's `workspace.qcode` names come first; any other one is added to the workspace
498    /// the first time a tab of it is opened, so a profile made after the workspace needs no hand
499    /// edit to be used in it.
500    #[must_use]
501    pub fn new(file: &WorkspaceFile, paths: WorkspacePaths, profiles: Vec<Profile>) -> Self {
502        // Kept inside the folder, links included, since the folder is all a workspace hands its
503        // containers; and every entry shown, since a workspace's dotfiles are its work too.
504        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    /// Takes `profiles` as the store's profiles, the ones the workspace carries first in the
533    /// order its file lists them, then the others in the order they came.
534    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    /// The workspace's identifier, which is also what its containers are named after.
542    #[must_use]
543    pub fn id(&self) -> &str {
544        self.id.as_str()
545    }
546
547    /// The name the person gave the workspace.
548    #[must_use]
549    pub fn name(&self) -> &str {
550        &self.name
551    }
552
553    /// Where the workspace's folders are.
554    #[must_use]
555    pub fn paths(&self) -> &WorkspacePaths {
556        &self.paths
557    }
558
559    /// The profiles a new tab can open: every profile of the store, the ones the workspace
560    /// carries first.
561    #[must_use]
562    pub fn profiles(&self) -> &[Profile] {
563        &self.profiles
564    }
565
566    /// Whether the workspace's `workspace.qcode` names the profile `name`.
567    #[must_use]
568    pub fn carries(&self, name: &str) -> bool {
569        self.carried.iter().any(|carried| carried == name)
570    }
571
572    /// The tabs open in this workspace, in the order they are shown.
573    #[must_use]
574    pub fn tabs(&self) -> &[Tab] {
575        &self.tabs
576    }
577
578    /// The open tab, when there is one.
579    #[must_use]
580    pub fn active_tab(&self) -> Option<&Tab> {
581        self.tabs.get(self.active_tab)
582    }
583
584    /// What the file manager knows about the workspace's own folder.
585    #[must_use]
586    pub fn files(&self) -> &FileManagerState {
587        &self.files
588    }
589
590    /// The folders and files inside the workspace its backup leaves out.
591    #[must_use]
592    pub fn backup_skip(&self) -> &[String] {
593        &self.skip
594    }
595
596    /// Whether the workspace's backup takes `Assets/` too.
597    #[must_use]
598    pub fn backs_up_assets(&self) -> bool {
599        self.assets
600    }
601
602    /// The containers of this workspace, as the engine last listed them.
603    #[must_use]
604    pub fn containers(&self) -> &[Container] {
605        &self.containers
606    }
607
608    /// The container a tab of `kind` enters. A picture, a file in the editor, a PDF and an
609    /// office document are opened in the workspace's own container, the one its shell runs in; a
610    /// Markdown document needs none, and a sound plays in a container made for it each time it
611    /// plays, which no tab enters.
612    #[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            // The administrator works in the same container the profile's harness tabs do.
620            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            // A window has a container of its own, beside the one the same profile's harness tabs
626            // enter, so that the container's end and the window's are one thing.
627            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    /// What `tab` runs inside its container: a login shell, the harness of the profile with the
637    /// arguments that let it work without asking, opening the tab's conversation when it shows
638    /// one and a new conversation otherwise, chafa drawing a picture or a page of a PDF, or
639    /// `editor` on a file. A PDF whose text is shown runs nothing in a terminal.
640    #[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            // Root's `bash` is started by the engine as root, which the tab's command says, not
645            // its program.
646            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                // An interface attached to the profile's shared server; the relay, when there is
650                // one, starts the server rather than the tab.
651                if shared::shares(profile) {
652                    return Some(shared::tab_program(tab.conversation()));
653                }
654                let mut command = profile.harness.command_line(tab.conversation());
655                // A profile that runs on a provider of the person's own is pointed at the relay's
656                // address, so the relay has to be listening inside the container before the
657                // harness says anything: it is what starts the harness. A harness told of the
658                // provider on its command line gets that right after its program, where it is
659                // read whatever else the line says.
660                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            // A window is not run in a terminal, so it has no program a tab spawns: it is started
672            // by its own container, which `desktop` does.
673            TabKind::New
674            | TabKind::Markdown(_)
675            | TabKind::Pdf(_)
676            | TabKind::Office(_)
677            | TabKind::Sound(_)
678            | TabKind::Desktop(_) => None,
679        }
680    }
681
682    /// Adds `tab` after the last tab, with the next id of the workspace, and returns where it is.
683    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    /// The label of the tab at `index`: the name the person gave it, or else its automatic one;
691    /// an agent tab whose automatic label another agent tab also reads carries its id after it,
692    /// so five opencode tabs never all read `opencode`.
693    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    /// The label the tab at `index` reads when the person gave it no name: for a harness tab, the
709    /// title its harness gave the conversation it shows, when the blank tab's page read one, and the
710    /// profile's name otherwise; `Shell` numbered among the shell tabs so several of them can be
711    /// told apart, or `New tab` while it is blank.
712    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            // The window's tab is labelled by its profile like a harness tab, with the mark that
723            // says it is a window rather than a terminal.
724            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    /// Whether the workspace listens for its tabs' agents.
742    #[must_use]
743    pub fn is_bridged(&self) -> bool {
744        matches!(self.link, bridge::Link::On { .. })
745    }
746
747    /// The tab of `key` and where it sits.
748    fn find(&mut self, key: TabKey) -> Option<(usize, &mut Tab)> {
749        self.tabs.iter_mut().enumerate().find(|(_, tab)| tab.key() == key)
750    }
751}
752
753/// Where the workspace's file `key` is inside a container: under [`CODE_DIR`], which is the
754/// workspace's own folder mounted.
755fn inside_container(key: &str) -> String {
756    format!("{CODE_DIR}/{key}")
757}
758
759/// The workspace screen.
760#[derive(Debug)]
761pub struct WorkspaceScreen {
762    engine: Option<Engine>,
763    user: HostUser,
764    workspaces: Vec<OpenWorkspace>,
765    active: usize,
766    panel: Panel,
767    /// The row the keyboard rests on in a blank tab's page.
768    blank_row: usize,
769    /// The profiles whose every conversation a blank tab's page shows, not only the newest.
770    expanded: HashSet<HistoryKey>,
771    /// The blank tab whose page was shown last, so showing a page again is told apart from
772    /// drawing the same one again.
773    shown_blank: Option<TabKey>,
774    /// The Markdown tab that was shown last, so a document is read again when it comes back into
775    /// view and not on every frame it stays there.
776    shown_document: Option<TabKey>,
777    /// The editor a file opens in.
778    editor: Editor,
779    /// What opening a sound does.
780    sound: Sound,
781    /// This machine's runtime folder, where the sound server's socket is looked for.
782    runtime: Option<PathBuf>,
783    /// What this machine offers a window, or why it offers none. Read once when the screen is
784    /// made: a session does not change its compositor underneath QCode.
785    display: Result<crate::desktop::Display, crate::desktop::NoDisplay>,
786    next_key: u64,
787    /// Whether the open workspace's folders are watched, so the tree follows the disk.
788    live: bool,
789    /// How long one wait for the disk may last, when it may not last until something changes.
790    patience: Option<std::time::Duration>,
791    /// Where the containers this screen starts are noted, so they can be stopped once no QCode
792    /// is open; `None` notes nothing.
793    registry: Option<PathBuf>,
794    /// Where `providers.toml` is read from before a tab of a provider profile is started, so its
795    /// tag can be checked against the providers that exist right now rather than a stale copy
796    /// the profile once carried; `None` means this machine has no data folder to hold one, so no
797    /// provider profile can ever start.
798    providers_path: Option<PathBuf>,
799    /// How a provider is asked, which the network does for a person and a test answers for itself:
800    /// the relay of every open workspace is opened with this, so what a request of a test is
801    /// answered with is the test's own business and never the network's.
802    upstream: crate::provider::Upstream,
803    /// Whether a problem with that list was said already: it is said once, not at every tab.
804    registry_told: bool,
805    /// How often the open workspaces are backed up.
806    backup_every: BackupEvery,
807    /// The number the last backup timer was given.
808    last_timer: u64,
809    /// The list of backups, while it is open.
810    listing: Option<backups::Listing>,
811    /// The number the last reading of a list of backups was given.
812    last_listing: u64,
813    /// Whether each open workspace listens for its tabs' agents.
814    bridging: bool,
815    /// The number the last listener of a workspace was given.
816    last_link: u64,
817    /// What the bridge remembers to apply its rules.
818    rules: Rules,
819    /// The questions to the person about one tab sending to another that are not answered yet.
820    asking: Vec<bridge::Asking>,
821    /// Whether a look at the tabs that hold messages is already timed, so that one is timed at a
822    /// time and none at all while no tab holds a message.
823    delivering: bool,
824    /// Whether the person is asked before one tab first sends to another.
825    ask_first: bool,
826    /// Whether a profile whose container is doing nothing is frozen in the background, and its
827    /// memory written out to swap. On until the person turns it off in the settings.
828    freeze_idle: bool,
829    /// What the screen knows about freezing: whether this machine can do it, which containers it
830    /// has frozen, and where each one's CPU was last read.
831    freezing: freezing::Freezing,
832    /// The lengths a look is judged by and where the machine's control groups are read from. The
833    /// product's are the measured ones; a test shortens the first and points the last at a folder
834    /// of its own.
835    where_: freezing::Where,
836    /// The lengths a build's silence is judged by. The product's are the measured ones; a test
837    /// shortens them, since a test cannot wait five minutes for a warning.
838    stall: stalling::Lengths,
839    /// Whether the next look at a build is timed, so that one is timed at a time.
840    watching: bool,
841    /// The tab being named, while the dialog that names it is open.
842    renaming: Option<rename::Renaming>,
843    /// The tab the keyboard is owed to because it was opened while it was still starting: there was
844    /// no terminal to put it in then, and it takes it as soon as its container comes up. Cleared
845    /// wherever the person puts the keyboard somewhere else on purpose, and by a tab whose start
846    /// failed, which is never given a terminal at all.
847    owed_focus: Option<TabKey>,
848    /// Whether a key on the tab strip or the rail is being handled, so that a switch it makes
849    /// leaves the keyboard walking there instead of putting it in the tab.
850    walking: bool,
851    /// The moment the tabs were last looked at to mark the working ones: the last look, or the last
852    /// output of a tab since.
853    looked: std::time::Instant,
854    /// Whether the next look at the tabs is timed, so one is timed at a time.
855    turning: bool,
856}
857
858impl WorkspaceScreen {
859    /// A screen showing `workspaces`, entering their containers through `engine`.
860    ///
861    /// Without an engine the screen still opens: the design asks that a missing engine leave the
862    /// application usable, so every action that would touch a container is drawn faint and says
863    /// why instead of failing when it is pressed.
864    #[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    /// The same screen following the disk when `live` is true: the folders the open workspace's
911    /// tree shows are watched, and what other programs change in them shows at once.
912    ///
913    /// It is off unless asked for because a watch waits for the disk on a background thread, and
914    /// a test harness runs background work in line, where that wait would never end. Tests of
915    /// the watch bound each wait instead.
916    #[must_use]
917    pub fn watching(mut self, live: bool) -> Self {
918        self.live = live;
919        self
920    }
921
922    /// The same screen following the disk, with each wait for a change lasting at most `bound`,
923    /// so a test harness that runs the wait in line always gets its turn back.
924    #[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    /// The same screen, listening for the agents of each open workspace's tabs when `bridging` is
933    /// true, so they can send each other messages.
934    ///
935    /// It is off unless asked for for the same reason as [`WorkspaceScreen::watching`]: a listener
936    /// waits on a background thread, which a test harness would run in line. Tests hand the
937    /// screen its calls by hand instead.
938    #[must_use]
939    pub fn bridging(mut self, bridging: bool) -> Self {
940        self.bridging = bridging;
941        self
942    }
943
944    /// The same screen, noting every container it starts in the list at `path`, which is what
945    /// lets them be stopped once no QCode is open. `None` notes nothing.
946    #[must_use]
947    pub fn with_registry(mut self, path: Option<PathBuf>) -> Self {
948        self.registry = path;
949        self
950    }
951
952    /// The same screen, reading `providers.toml` at `path` rather than this machine's own data
953    /// folder before a provider tab starts, so a test can say what it holds without ever
954    /// touching a real one.
955    #[must_use]
956    pub fn with_providers_path(mut self, path: Option<PathBuf>) -> Self {
957        self.providers_path = path;
958        self
959    }
960
961    /// The same screen, asking every provider of every open workspace with `upstream` rather than
962    /// over the network, which is what lets a test say what a provider answers without one.
963    ///
964    /// It is set before a workspace opens its relay, since that is the moment the relay is opened
965    /// with it; afterwards the listeners already running keep the upstream they were given.
966    #[must_use]
967    pub fn with_upstream(mut self, upstream: crate::provider::Upstream) -> Self {
968        self.upstream = upstream;
969        self
970    }
971
972    /// The same screen, backing its workspaces up as often as `every` says.
973    #[must_use]
974    pub fn backing_up(mut self, every: BackupEvery) -> Self {
975        self.backup_every = every;
976        self
977    }
978
979    /// How often the open workspaces are backed up.
980    #[must_use]
981    pub fn backup_every(&self) -> BackupEvery {
982        self.backup_every
983    }
984
985    /// The workspace the rail has open.
986    #[must_use]
987    pub fn workspace(&self) -> Option<&OpenWorkspace> {
988        self.workspaces.get(self.active)
989    }
990
991    /// Every workspace of the rail, in its order.
992    #[must_use]
993    pub fn workspaces(&self) -> &[OpenWorkspace] {
994        &self.workspaces
995    }
996
997    /// Makes `editor` the one a file opens in from now on. Tabs already open keep the editor
998    /// they started with until they are opened again.
999    pub fn set_editor(&mut self, editor: Editor) {
1000        self.editor = editor;
1001    }
1002
1003    /// The editor a file opens in.
1004    #[must_use]
1005    pub fn editor(&self) -> Editor {
1006        self.editor
1007    }
1008
1009    /// Makes `sound` what opening a sound does from now on. A sound tab already open keeps what
1010    /// it does until it is started again.
1011    pub fn set_sound(&mut self, sound: Sound) {
1012        self.sound = sound;
1013    }
1014
1015    /// What opening a sound does.
1016    #[must_use]
1017    pub fn sound(&self) -> Sound {
1018        self.sound
1019    }
1020
1021    /// Makes the first message from one tab to another ask the person when `ask` is true, and
1022    /// go without asking otherwise. What the person already answered about a pair still holds.
1023    pub fn set_ask_first(&mut self, ask: bool) {
1024        self.ask_first = ask;
1025    }
1026
1027    /// Whether the first message from one tab to another asks the person.
1028    #[must_use]
1029    pub fn ask_first(&self) -> bool {
1030        self.ask_first
1031    }
1032
1033    /// Makes a profile whose container is doing nothing be frozen in the background, and its
1034    /// memory written out to swap, when `freeze` is true; nothing of the kind when it is false.
1035    pub fn set_freeze_idle(&mut self, freeze: bool) {
1036        self.freeze_idle = freeze;
1037    }
1038
1039    /// Whether a quiet profile's container is frozen in the background.
1040    #[must_use]
1041    pub fn freezes_idle(&self) -> bool {
1042        self.freeze_idle
1043    }
1044
1045    /// Asks on the screen's own account whether this machine can freeze a container at all, which
1046    /// is asked once, on a background thread, the first time the screen is opened, and then keeps
1047    /// the answer. Without an engine there is nothing to ask and the answer stays unknown, which
1048    /// runs no timer at all.
1049    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    /// The same screen, looking every `every` and judging a quiet time and a CPU window as short as
1058    /// `lengths` says, and reading the machine's control groups under `cgroups` rather than the
1059    /// real ones.
1060    ///
1061    /// Only for a test: the product uses [`freezing::EVERY`], [`freeze::QUIET`],
1062    /// [`freeze::CPU_WINDOW`] and the machine's own cgroup root, since a test cannot wait ten
1063    /// minutes for a rule to be judged and may not write to the real control group.
1064    #[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    /// The same screen, saying a tab's build is stuck after `quiet` of silence and looking every
1077    /// `look`, which the product uses as [`stalling::QUIET`] and [`stalling::LOOK`].
1078    ///
1079    /// Only for a test: the product uses those two measured lengths, since a test cannot wait five
1080    /// minutes to hear a warning and cannot make a build that really takes that long.
1081    #[cfg(test)]
1082    #[must_use]
1083    pub fn stalling(mut self, lengths: stalling::Lengths) -> Self {
1084        self.stall = lengths;
1085        self
1086    }
1087
1088    /// The same screen, looking for the sound server's socket in `runtime` rather than in the
1089    /// runtime folder this process was given.
1090    #[must_use]
1091    pub fn hearing_in(mut self, runtime: Option<PathBuf>) -> Self {
1092        self.runtime = runtime;
1093        self
1094    }
1095
1096    /// The same screen, opening windows on `display` rather than on the session this process was
1097    /// started in.
1098    ///
1099    /// The application never calls this: the screen reads the session itself. It exists so that
1100    /// what a window's tab does can be checked on a machine with no compositor, and what it says
1101    /// when there is none on a machine that has one.
1102    #[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    /// Opens the workspace at `index` of the rail, when there is one there.
1109    pub fn set_active(&mut self, index: usize) {
1110        if index < self.workspaces.len() {
1111            self.active = index;
1112        }
1113    }
1114
1115    /// Gives the workspace at `index` the tabs `record` lists, each waiting to be shown before it
1116    /// starts, with the tab the record had open open again.
1117    ///
1118    /// A profile tab whose profile is gone from the store is left out: there is no container
1119    /// it could enter, and a tab that can only ever fail is not worth bringing back. A blank tab
1120    /// comes back blank; it never had a container. A tab of one of the workspace's files comes back
1121    /// whether or not the file is still there, and says so when it is shown; one whose path
1122    /// climbs out of the workspace is left out, because nothing QCode wrote would name one.
1123    ///
1124    /// A tab keeps the id and the name it had. One whose id the record does not give, or gives to
1125    /// an earlier tab as well, is given a new one after every id the record gives.
1126    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            // Opening QCode again is not asking to hear a sound again, and no more is it asking for
1166            // a window on the screen: both come back saying so, with the way to ask for them.
1167            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    /// What is open, in the shape the session file keeps it: the workspaces in rail order, the one
1183    /// that is open, and the tabs of each.
1184    #[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                                // Root is given for the moment it was asked for, never by opening
1199                                // QCode again.
1200                                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    /// The engine the screen enters containers through.
1225    #[must_use]
1226    pub fn engine(&self) -> Option<&Engine> {
1227        self.engine.as_ref()
1228    }
1229
1230    /// The widget panel's state.
1231    #[must_use]
1232    pub fn panel(&self) -> &Panel {
1233        &self.panel
1234    }
1235
1236    /// The command the tab `key` spawns, which is always the engine entering a container.
1237    ///
1238    /// This is the one place a tab's program is decided, so a test can read it back and see for
1239    /// itself that no tab runs anything on the machine QCode runs on.
1240    #[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        // A harness tab carries the token its agent's bridge server hands back, which is how
1252        // QCode knows which tab a message comes from; a tab of a provider profile carries the
1253        // same token again, because the relay reuses the bridge's rather than minting a second
1254        // one, and the provider's own address and model besides.
1255        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                    // The tab starts the server when no other tab has, so it carries what the
1264                    // server starts with.
1265                    env.extend(self.server_environment(workspace, profile));
1266                } else if let Some(profile) = profile
1267                    && let Some(provider) = &profile.provider
1268                {
1269                    // Each harness is pointed at the relay its own way, so the environment
1270                    // follows the profile's harness as well as its provider.
1271                    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    /// The command that takes the text out of the file the tab `key` shows, which is always the
1282    /// engine running a program in the workspace's own container, without a terminal; `None` for a
1283    /// tab that shows no such text.
1284    #[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        // Taking the text out of a long PDF takes as long as the PDF is long.
1293        Some(engine.exec_without_terminal(&Exec { container: &plan.name, command: &parts }).unbounded())
1294    }
1295
1296    /// The workspace of `id`, when the screen has it open.
1297    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    /// What a blank tab's page knows of the conversations of `key`, when its workspace is open.
1302    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    /// The workspace holding the tab `key`, when one does.
1307    fn owner(&self, key: TabKey) -> Option<&OpenWorkspace> {
1308        self.workspaces.iter().find(|workspace| workspace.tabs.iter().any(|tab| tab.key() == key))
1309    }
1310
1311    /// The workspace holding the tab `key`, when one does.
1312    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    /// Attaches `session` to the tab `key` and marks the tab running, which is what the screen
1319    /// does itself once a tab's container is up. A program started some other way, such as the
1320    /// shell of a demonstration, is shown in the tab the same way.
1321    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    /// The number of the reading of `profile`'s conversations last started in the open workspace.
1328    /// An answer, [`Msg::HistoryRead`], is only taken when it carries this number.
1329    #[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    /// The number of the reading of the list of backups last started. An answer,
1335    /// [`Msg::BackupsRead`], is only taken when it carries this number.
1336    #[must_use]
1337    pub fn backups_reading(&self) -> u64 {
1338        self.listing.as_ref().map_or(0, |_| self.last_listing)
1339    }
1340}
1341
1342/// The work the screen needs the moment it is shown: reading the open workspace's folder and
1343/// asking the engine which of its containers are up.
1344///
1345/// The screen never touches the disk or the engine while drawing, so this is the caller's part
1346/// of the bargain: run it when the screen is entered, and again when it is returned to.
1347///
1348/// It also starts the open tab when it was brought back from the last session and has been
1349/// waiting to be shown, and reads the conversations a blank tab's page offers when the open tab
1350/// is one.
1351pub 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
1359/// Adds `workspace` to the rail and opens it, or only opens it when the rail has it already; the
1360/// tabs of every workspace stay as they are either way.
1361pub 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
1372/// Applies a message to the screen.
1373///
1374/// Whatever the message did, a tab that is now in view and was brought back from the last session
1375/// starts here: switching to it, closing the tab before it and opening its workspace all show it.
1376/// The same goes for a blank tab's page that comes into view, which reads its conversations.
1377pub fn update(screen: &mut WorkspaceScreen, message: Msg) -> Command<Msg> {
1378    // Profiles read again may carry a provider a workspace's did not before, so this runs before
1379    // anything else has the chance to start a tab that needs the relay already open.
1380    let relay = relay::follow(screen);
1381    // Profiles read again may be new ones, whose conversations the page shown has not read.
1382    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    // After the message, since a look is asked for on the state the screen is left in: a switch
1387    // turned off in the settings stops the timer before it can come round again.
1388    let settled = freezing::again(screen);
1389    Command::batch([command, relay, wake(screen), show_page(screen, profiles), kept, settled])
1390}
1391
1392/// Lets the watches of the workspaces that are not open go: their trees are not on screen, and
1393/// they are read again when they are opened. The open one's watch is the file manager's own, which
1394/// keeps it on the folders the tree shows whatever a message changed about them.
1395fn 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
1403/// Applies a message to the screen, leaving the start of a waiting tab to [`update`].
1404fn 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            // A workspace that leaves the rail takes its containers' readings with it: there is
1419            // nothing here that will look at them again.
1420            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            // A message in any of these prompts is sent as the workspace goes: nothing is left to
1431            // wait for, and the agents that sent them were told their tools had them.
1432            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        // The application opens the list; nothing on this screen changes until a workspace comes
1455        // back from it.
1456        Msg::AddWorkspace => Command::none(),
1457        // What this machine can do about freezing is asked once and kept, and the timer starts as
1458        // soon as the answer is a yes, since nothing else about freezing runs without it.
1459        Msg::Freezable(can) => {
1460            screen.freezing.supported = Some(can);
1461            freezing::again(screen)
1462        }
1463        // The minute came round. The timer is armed again first, so that a look which is a while
1464        // finding out is not a look that never comes again.
1465        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        // The person may have come back to a tab of the profile while the freeze was on its way, and
1472        // the screen did not wake a container it did not yet call frozen: it is woken now.
1473        // The panel is asked again as well, so that it says "frozen" of the container from now on
1474        // rather than what the engine said of it before.
1475        Msg::Frozen(container, frozen) => {
1476            screen.freezing.note(&container, frozen);
1477            Command::batch([freezing::shown(screen), list_containers(screen)])
1478        }
1479        // A container that woke is not frozen any more, and the reading it had before it slept says
1480        // nothing about what it has done since, so the next look starts it from this moment.
1481        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            // The panel would otherwise go on saying what the engine said while it was asleep.
1488            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            // Messages other agents left in the tab would go with it, and those agents were told
1500            // their work was taken: the person decides that knowingly. So is a message already in
1501            // the tab's prompt whose Return has not gone: closing now throws it away unsent, and
1502            // the agent that sent it was told it had it.
1503            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            // Closing ends the agent inside the container, so one in the middle of a task is cut
1510            // off: a stray `ctrl+w` or a click on the `×` costs a question, never that work.
1511            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        // The strip, because from there the arrows switch tabs and Tab reaches the rest of the
1547        // screen, the terminal included.
1548        Msg::LeaveTerminal => {
1549            screen.owed_focus = None;
1550            Command::focus(TABS_ID)
1551        }
1552        // Only a tab with a terminal on it has somewhere to go back into; on a blank or a
1553        // Markdown tab the key does nothing rather than send the keyboard nowhere.
1554        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        // The application opens the profiles screen; this one stays as it is.
1613        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            // The tab is open whatever the file says; only the record of it is missing, and the
1628            // person is told so rather than finding the profile gone from the file later.
1629            Err(reason) => Command::toast(Toast::warning(t!("workspace.add-failed")).body(reason)),
1630        },
1631        // The engine's own bringing a tab up wakes a container QCode froze, so the screen stops
1632        // calling it frozen here rather than asking the engine to wake what it is waking already.
1633        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        // The look came round: every tab building an image is marked by how long its log has been
1651        // quiet, and the next one is timed while a build is still running.
1652        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            // The build takes its half-made image away itself once it notices; the tab goes back
1659            // to offering it.
1660            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                // The container went away under the tab, so the panel's list no longer holds.
1692                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            // The list takes the keyboard as it opens, so a widget is chosen with the arrows and
1717            // Enter as readily as with the pointer.
1718            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        // Nothing is said when it worked: the window came forward, or the compositor marked it,
1774        // and either way the person is looking at their screen rather than at this tab.
1775        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            // Bringing conversations back may have stopped their container first.
1867            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
1924/// After a switch: the keyboard lands in the tab, unless the switch was the keyboard walking the
1925/// strip or the rail, where it stays.
1926fn settle(screen: &mut WorkspaceScreen, walking: bool) -> Command<Msg> {
1927    if walking { Command::none() } else { land(screen) }
1928}
1929
1930/// Puts the keyboard in the open tab: its terminal, the page of a blank tab, a document, a
1931/// window's button, or else the first thing on it that takes the keyboard. A tab still starting
1932/// has none yet and is given it when its terminal comes up, which is what it was owed.
1933///
1934/// The keyboard is where the person last wanted it, so no tab is owed it any more — except the tab
1935/// landed in, when that is the tab still starting that was owed it: there is nothing in it to take
1936/// the keyboard, so the debt stands until its terminal is there.
1937///
1938/// The widget is named rather than found inside the tab's part of the screen, because the runtime
1939/// looks for it in the frame drawn before the switch, where that part still holds the tab left.
1940fn 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
1961/// Opens a blank tab after the last one and hands its page the keyboard. Nothing starts: the
1962/// tab only asks what it should open.
1963fn 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
1973/// Turns the blank tab `key` into what was chosen on its page, in its own place in the strip,
1974/// and starts the work that brings its container up.
1975fn 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    // A tab that was chosen already is not chosen again: two quick presses open one container.
1989    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    // A profile whose sign-in its harness's makers closed still opens, since it goes on working
1999    // for some; the person is told why it may not, at the moment they open it.
2000    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    // A window is not entered with a terminal, so it takes the window's own path and the keyboard
2020    // goes to the tab's one action rather than to a terminal that is not there.
2021    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    // The tab is only just starting, so there is no terminal to put the keyboard in yet: it is owed
2032    // the keyboard instead, and takes it from [`ready`] once its container is up.
2033    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
2043/// What the tab `key` needs before it starts when it is an opencode tab of a profile whose tabs
2044/// share a server ([`shared`]); `None` for every other tab.
2045fn 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
2058/// The token the provider relay answers a tab's requests by: the tab's own, or its shared
2059/// server's, which outlives every tab.
2060fn asking<'a>(serving: Option<&'a Serving>, token: &'a str) -> &'a str {
2061    serving.map_or(token, |serving| serving.token.as_str())
2062}
2063
2064/// Brings the container of `plan` up for the harness tab of `launch` and answers with the message
2065/// that starts it: at once, or, for a tab of a shared opencode server, once the server has made or
2066/// found the conversation the tab shows.
2067fn 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    /// What the shared opencode server of `profile` in `workspace` starts with
2100    /// ([`shared::server_environment`]): its token, and the provider's configuration made for that
2101    /// token rather than a tab's, since the server outlives any one tab.
2102    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    /// The window a tab of `provider` is told the model has: what this machine measured, or for a
2113    /// ready-made service what it says it gives ([`crate::provider::Model::window`]); `None` when
2114    /// neither is known, or the provider is gone.
2115    ///
2116    /// A lineup is promised the smallest window among its steps
2117    /// ([`crate::provider::Lineup::window`]): a conversation runs on whichever step answers, so
2118    /// what is promised has to hold for the one that ends up answering it.
2119    ///
2120    /// Read from `providers.toml` as a tab starts rather than carried on the profile: a window
2121    /// measured again on the Providers page must reach the next tab, not the tab after QCode is
2122    /// started again.
2123    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
2134/// The tab at `index` and the name of its harness, when an agent is at work in it: a harness tab
2135/// whose program still runs, or a window that is open. The same tabs Ctrl+Q asks about, less the
2136/// ones still starting: nothing runs in those yet, so closing one cuts no work off.
2137///
2138/// A profile no longer on the list still ran its harness, so the tab is named by the profile then.
2139fn 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
2152/// Asks the person before the tab `key` is closed while `harness` is still running in it.
2153fn 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
2163/// Closes the tab at `index` of the open workspace: its program, its sound and its window stop,
2164/// its relay and its place in the bridge's rules are let go.
2165fn close_tab(screen: &mut WorkspaceScreen, index: usize) -> Command<Msg> {
2166    // A harness tab's processes inside its container outlive the engine's command that started
2167    // them, so they are ended by the tab's token.
2168    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                // A container QCode froze refuses the command that ends a tab's processes, so the
2182                // two are one: the container is woken, and only then is the work ended. A harness
2183                // nobody is left to read would go on working otherwise, in a container that stays
2184                // up until the reaper stops it.
2185                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    // A message in this tab's prompt is sent as the tab goes, since nothing is left to wait for:
2200    // the person is closing it, and the agent that sent the message was told its tool had it.
2201    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    // A tab that is gone is never owed the keyboard: no terminal of it is coming up.
2219    if screen.owed_focus.is_some_and(|owed| gone.contains(&owed)) {
2220        screen.owed_focus = None;
2221    }
2222    // The tab that takes this one's place is on the screen now, and may be one a frozen container
2223    // is holding.
2224    Command::batch([silenced, ending, freezing::shown(screen)])
2225}
2226
2227/// The provider and model a profile of `kind` runs on, when `kind` is a harness tab of a profile
2228/// that names one of its own.
2229fn 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
2234/// Writes into the workspace's `workspace.qcode` that it carries the profile `name`, on a background
2235/// thread.
2236///
2237/// The file is the one record of which workspace carries which profile: refreshing a login from its
2238/// profile reaches exactly the workspaces it names. The container itself does not wait for it, so
2239/// the tab opens either way.
2240fn 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
2250/// Starts the open tab of the open workspace when it was brought back from the last session and
2251/// is being shown for the first time. Without an engine it keeps waiting, and the middle says why.
2252/// A blank tab has no container to start, so it keeps asking what it should open.
2253///
2254/// A harness tab that was a new chat when the session was recorded has no conversation id: a new
2255/// conversation's id is the harness's to make, and it is not known when the tab starts. So once
2256/// its container is up the profile's conversations are read, and [`resume_newest`] gives the
2257/// tab the one it was most likely showing before its session is spawned.
2258fn 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    // A Markdown document is read from the disk by QCode itself, so it opens with or without an
2265    // engine.
2266    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    // A window is opened by a container of its own rather than entered, so it never goes through
2281    // the terminal path below.
2282    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    // A PDF shows its text first, which is read rather than run in a terminal.
2288    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                    // Not being able to read only means the tab cannot be matched to its
2332                    // conversation; it then starts a new one, the way it started last time. The
2333                    // container is up by now, so reading it starts nothing.
2334                    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
2343/// What the tab of a profile whose opencode tabs share a server needs before it starts, taken
2344/// from the screen while it is at hand ([`shared`]).
2345struct Serving {
2346    environment: Vec<(String, String)>,
2347    token: String,
2348    folder: PathBuf,
2349    conversation: Option<String>,
2350}
2351
2352/// Gives the tab `key`, woken as `run`, the conversation its shared server made or found for it,
2353/// so the tab opens it and the session file keeps it from now on.
2354fn 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
2362/// Gives the new-chat tab `key`, woken as `run`, the conversation of `found` it was most likely
2363/// showing ([`history::claim`]), so it opens that one and the session file keeps its id from now
2364/// on. A tab that was closed, restarted or given a conversation meanwhile is left as it is.
2365fn 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
2387/// Reads the conversations the page of the open tab offers, when the open tab is blank and its
2388/// page has just come into view, or always when `again` asks for it.
2389///
2390/// Every profile of the workspace is read at once, each on its own thread, and each keeps the rows
2391/// it had on screen until its answer is in.
2392fn 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            // A container made from an image that has been built again since, or from an earlier
2412            // plan, is made anew before it is read, since reading starts it and the tab would
2413            // then take it as it is.
2414            let (renewed, behind) = plan::renew_stale(&engine, &plan, user);
2415            // Reading a stopped container starts it and leaves it running for the tab that opens
2416            // a conversation from it, so it is noted like any container QCode starts.
2417            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            // The tab that opens next finds the container running and would never say it.
2422            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
2432/// Delivers `message` once `delay` has passed, without holding up anything meanwhile.
2433fn 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
2439/// Starts the tab `key` again, from its container upwards; a Markdown tab reads its file again.
2440fn 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    // A sound is met again from the start: the settings or the machine may have changed, and a
2455    // tab brought back from the last session has now been asked to play.
2456    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    // Asking for the window is what lifts the hold a tab brought back from the last session has.
2463    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    // A PDF whose text could not be read reads it again; one showing a page draws it again.
2470    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    // The button that asked for the start goes with the page it was on, so the tab in front of the
2487    // person is owed the keyboard until its terminal is there; one started behind them is not.
2488    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
2498/// What has to be true of a provider profile's provider before its tab is trusted to speak for
2499/// it, and the words to say when it is not: the provider it names, where its providers are read
2500/// from, and, made here rather than on the background thread [`start`] runs on, what to say if
2501/// the provider or the lineup it runs on is gone. Translating needs the thread the person's own
2502/// language is loaded on; `start` only reads the file and decides whether the words are needed.
2503/// `None` for a tab of no provider.
2504struct ProviderGate {
2505    /// The provider and what the tab's profile runs on: a model of it, or one of its lineups.
2506    provider: ProviderChoice,
2507    /// Where `providers.toml` is read from to check that provider and its lineup still exist.
2508    path: Option<PathBuf>,
2509    /// What to say if the provider is gone, made in the person's language before this ever reaches
2510    /// a background thread.
2511    missing: String,
2512    /// What to say if the lineup is gone while its provider is not, which is a different thing the
2513    /// person has to be told: the provider is there, and the order of its models is not.
2514    lineup_missing: String,
2515}
2516
2517impl ProviderGate {
2518    /// The gate a tab of `provider` is checked against, when it has one.
2519    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    /// The reason this tab must not start, read against the file as it is right now: the provider
2532    /// is gone, or the lineup its profile chose is not one the provider has any more. `None` when
2533    /// the tab may start.
2534    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            // A lineup that names no step is as good as gone: the relay would have nowhere to send
2543            // the request, which reads to the person as the harness itself being broken.
2544            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
2552/// What ties a container's launch to one tab: its key, the run it is starting as, and, for a
2553/// provider profile, what its tab is checked against before it starts. Bundled so the functions
2554/// that carry it stay under the argument count a reader can hold in their head at once.
2555struct Launch {
2556    /// The tab this launch is for.
2557    key: TabKey,
2558    /// The run it is starting as; an answer that names another run has moved on.
2559    run: u64,
2560    /// What this tab's provider is checked against, when it has one.
2561    gate: Option<ProviderGate>,
2562    /// The token by which the bridge knows this tab's agent.
2563    token: String,
2564}
2565
2566impl Launch {
2567    /// A launch for the tab `key` in its run `run`, known to the bridge as `token`, naming the
2568    /// provider `provider` runs on when it has one and where `providers.toml` is read from to
2569    /// check it still exists.
2570    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
2581/// Brings the container of `plan` up for `launch`'s tab, on a background thread.
2582///
2583/// A tab that opens one of the workspace's files first looks for the file at `file` on this
2584/// machine: an editor started on a file that is gone would open an empty one of that name and
2585/// save it back, which is not what the tab promised, so the tab says the file is gone instead.
2586fn 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
2603/// Brings the container of `plan` up and notes it in `registry` when it is up, then hands the
2604/// outcome to `answer` for the message it becomes.
2605///
2606/// A container that was running already is noted too: it is one of QCode's, and a list that
2607/// lost its name — to another QCode writing at the same moment, or to a damaged file — gets it
2608/// back the next time a tab opens in it. Runs engine commands and writes the disk, so it belongs
2609/// on a background thread.
2610fn 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    // A profile whose provider tag was removed from the Providers page since it was chosen here
2620    // cannot be pointed anywhere: the relay would refuse every request with an unknown tab, which
2621    // reads to the person as the harness itself being broken. A profile that chose a lineup of that
2622    // provider is in the same position once the lineup is gone, and is told so in its own words.
2623    // Checked before anything is started, against the file as it is right now rather than a stale
2624    // copy this profile once carried; the words for it were made before this thread was, because a
2625    // background thread has no language of its own to translate them into.
2626    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    // The keys that keep a harness from asking, merged into the home before anything else reads
2634    // it: the image filled the home once, so a workspace opened before the key existed has the
2635    // question in every start, and a profile on a custom set of additions has it in the first.
2636    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    // Registered before the harness starts, so it finds the bridge's server at its first start.
2643    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    // After the bridge and before the harness starts, so the agent reads the files as they are
2650    // meant to be from its first turn.
2651    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    // After graphify's installer under QCode high, which is what points the agent at the map;
2658    // under QCode basic the image points it there already. Started and not waited for, so the
2659    // harness starts at once.
2660    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
2679/// The words for a window whose agent could not be told what it may do without asking. The window
2680/// opens all the same: the agent works, and only some of its questions are left to the person.
2681fn unapproved(words: &str) -> Command<Msg> {
2682    Command::toast(Toast::warning(t!("workspace.unapproved.title")).body(words.to_owned()))
2683}
2684
2685/// The words for a harness whose home could not be brought to what keeps it from asking. Its tab
2686/// starts all the same: the agent works, and only some of its questions were left to be answered
2687/// by the person.
2688pub(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
2700/// The words for a QCode high profile whose instruction files in the workspace could not be
2701/// brought up to date. Its tab starts all the same: the agent works, it only has not been told
2702/// everything it would have been.
2703pub(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
2715/// Notes in the list at `registry` that QCode started the container `name` in `engine`, and
2716/// hands `message` on — wrapped with what went wrong, when something did.
2717pub(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
2729/// Reads the document of the Markdown tab `key`, in its run `run`, on a background thread.
2730fn 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
2736/// Takes what reading a Markdown tab's document answered. A tab that was restarted or closed
2737/// meanwhile is left alone.
2738fn 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
2754/// Reads the open Markdown tab's document again when the tab comes back into view, so a change
2755/// made in the editor meanwhile is what it shows. The text it had stays on screen until the new
2756/// one is in.
2757fn 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
2775/// Opens the file `key` of the open workspace's tree in the built-in app its name calls for, or
2776/// says that none opens it yet.
2777fn 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
2795/// Shows the tab of `kind` in the open workspace: the one already open when there is one, since a
2796/// click on a file in the tree opens it and a second click should not open it twice, or a new
2797/// tab after the last one. The new tab waits to be started, which [`update`] does at once.
2798fn 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        // A tab whose terminal is already there takes the keyboard at once; one still waiting for
2809        // its container is owed it until it comes up, as a tab chosen on a blank page is.
2810        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
2823/// Attaches a session to a tab whose container came up, or tells it why it did not.
2824fn ready(screen: &mut WorkspaceScreen, key: TabKey, run: u64, result: &Result<(), LaunchFailure>) -> Command<Msg> {
2825    // A tab that is still starting is owed the keyboard only while the person is looking at it.
2826    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    // Bringing a tab up makes, remakes or starts a container, and even one that failed halfway may
2832    // have left one behind; the panel's list is what the engine said before, so it is asked again.
2833    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        // Only a profile's tab is ever told its image is missing, and that one can build it.
2840        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        // The tab will never have a terminal to take the keyboard, so it is owed nothing.
2847        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            // A harness that has just come back takes whatever waited for it while it was gone.
2859            let mut took = bridge::deliver(screen, std::time::Instant::now());
2860            if owed {
2861                // There is a terminal in the tab at last, so the keyboard it was owed goes in.
2862                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
2878/// Builds the image of the profile of the tab `key`, the same whole build the profile wizard
2879/// runs, with every line of it in the tab's log.
2880fn 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    // The store's `Profiles/` holds what the person added in the profile's shell, which the
2890    // image this engine lacks is built with too; a workspace is always `Workspaces/<id>/` in it.
2891    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
2915/// Takes the end of a tab's image build: a built image opens the tab, which is what the person
2916/// asked for when they chose it; a failed build is the tab's failure, in the engine's words.
2917fn 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
2937/// Keeps a tab's session watched, and asks after its container when the session ends.
2938fn 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            // The tab has just spoken, so the wait for its quiet starts again from here, a
2953            // message held for it is looked at once that wait is over, and it is marked as working.
2954            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            // A session can end because the person left the shell, or because the container
2961            // under it went away. Only the engine knows which, and the answer changes what the
2962            // tab says, so it is asked rather than guessed.
2963            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
2973/// Reads the root of the open workspace's file tree the first time, and every folder it shows
2974/// again after that: the files may have changed while the screen was away. The open workspace
2975/// follows the disk from here on when the screen does.
2976fn 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
2989/// Asks the engine which containers the open workspace has.
2990fn 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
2997/// Asks the engine which containers the workspace `id` has, open or waiting behind another.
2998fn 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
3009/// Stops a container, or stops and starts it again.
3010fn 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
3025/// The name the terminal in the middle is focused by.
3026const TERMINAL_ID: &str = "workspace-terminal";
3027
3028/// The name the document of a Markdown tab, or the text of a PDF, is focused by.
3029const DOCUMENT_ID: &str = "workspace-document";
3030
3031/// The name the tab strip is focused by; with no tab its `+` is the whole strip, and the focus.
3032const TABS_ID: &str = "workspace-tabs";
3033
3034/// The open tab's own part of the screen, which the keyboard is put in when a tab is switched to.
3035const BODY_ID: &str = "workspace-tab-body";
3036
3037/// Draws the workspace screen for an application whose messages are `P`: `wrap` turns the screen's
3038/// own messages into the application's, `above` draws what the application puts over the tabs
3039/// (its header) and `below` what it puts under the terminal (its footer).
3040///
3041/// The rail runs down the whole left edge and the widget panel down the whole right edge, from
3042/// the very first row to the very last; the application's header and footer belong to the middle
3043/// column only, with the tabs and the terminal. That is why the application hands its parts in
3044/// here instead of drawing them around this screen. `under` is what the application puts at the
3045/// foot of the rail, under the workspaces: the ways out of this screen, which stand there rather
3046/// than over and under the terminal so that every row of the middle belongs to the work.
3047pub 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        // The rail is the only way between workspaces, so it never collapses away; four cells fit
3058        // on any terminal QCode can draw on.
3059        .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
3095/// The rail of open workspaces down the left, ending in the `+` that opens another one.
3096fn 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    // The rail runs down, so the arrow that points at the tabs steps into them.
3111    ui.add(strip::Walked::new(rail, &[Key::Enter, Key::Right])).fill().id("workspace-rail");
3112}
3113
3114/// The tab strip, ending in the `+` that opens a blank tab right after its last tab, the way a
3115/// browser's strip ends; with no tab at all the `+` stands at the top left.
3116///
3117/// The strip places the `+` itself: it keeps the button's room while laying out the tabs, so the
3118/// `+` follows the last tab while they fit and keeps its place at the right end once they scroll.
3119fn 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    // The mark of a working tab is the framework's own, so a tab is only asked whether it works.
3131    for (index, tab) in workspace.tabs.iter().enumerate() {
3132        tabs = tabs.busy(index, busy::working(tab, screen.looked));
3133    }
3134    // The strip runs across, so the arrow that points at the tab steps into it.
3135    ui.add(strip::Walked::new(tabs, &[Key::Enter, Key::Down])).fill_width().id(TABS_ID);
3136    rename::view(screen, ui);
3137}
3138
3139/// What the middle shows: the open tab, or why there is nothing to show, and under a tab the
3140/// messages other tabs' agents left waiting in it, whether the loop limit ended its exchange, and
3141/// which model of its lineup a request of it fell back to.
3142fn center(screen: &WorkspaceScreen, ui: &mut View<'_, Msg>) {
3143    // The same shape whether or not anything waits: a line appearing under the tab must not
3144    // rebuild the tab's terminal as another widget, or the keyboard the person is typing with is
3145    // taken from it the moment a message arrives.
3146    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
3160/// The open tab, or why there is nothing to show.
3161fn 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        // Without an engine a blank tab can still be opened: its page shows what there would be
3177        // to choose and says why nothing can be.
3178        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    // A window's tab draws its own status in every state it can be in, failure included: there is
3204    // no terminal on it and no last screen to keep, so the general shape below does not fit.
3205    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        // With an engine a waiting tab is started before the frame is drawn, so only a screen
3213        // without one ever shows it, and then the reason is what there is to say.
3214        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        // Leaving the editor is how a file is closed, so that is what the tab says, and the way
3238        // back opens the same file again.
3239        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            // A failure of the machine itself has no engine command to show above its words.
3246            let detail = if failure.command.is_empty() {
3247                failure.output.clone()
3248            } else {
3249                format!("{}\n{}", failure.command, failure.output)
3250            };
3251            // A refusal QCode recognises is said plainly first, with the line that puts it right;
3252            // the engine's words stay underneath for whoever wants to read them.
3253            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
3264/// A profile's tab whose image the engine does not have: what is missing, and the offer to build
3265/// it now, the tab opening once it is built, or not to open the tab after all.
3266fn 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
3292/// A profile's tab whose image is being built: the build as it speaks, the way to stop it, and a
3293/// build that has said nothing long enough to wonder about.
3294fn 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            // A shimmer is one line that is never wrapped, so the row it shares with the Stop
3310            // carries the short word and the sentence wraps under it, where all of it is read.
3311            ui.add(Text::new(t!("workspace.image.building", profile = profile)).role("secondary")).fill_width();
3312            // A build with no time limit is not one to stop on its own, so all that is offered is
3313            // the truth about the log: it has said nothing for a while, and the Stop above is
3314            // there for anyone who has waited long enough.
3315            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
3327/// A picture drawn in the tab: what chafa drew, which stays when chafa is done, and under it a
3328/// quiet way to draw it again once the tab has changed size. A picture that could not be drawn
3329/// says so beside it; one that was drawn says nothing, because a picture that is there needs no
3330/// note that the program drawing it has finished.
3331fn 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
3361/// A Markdown tab: the document, with the way to change it in the editor above it.
3362fn 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        // Read again while it is shown, the text it had stays until the new one is in.
3366        (_, 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
3403/// A tab whose file is gone or cannot be read, with the way to look for it again.
3404fn 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
3414/// The terminal of a harness or shell tab.
3415///
3416/// It hands the program every key but the few that must stay QCode's while a harness has the
3417/// keyboard: the key list, the side panel's key and the way out to the tabs. Only keys with
3418/// `ctrl` or `alt` and keys that type nothing, such as `f1`, can pass; `?` still types into the
3419/// program, because the terminal is where the person writes.
3420fn 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
3431/// The keymap's actions that switch tabs, which reach QCode from inside a harness so that one
3432/// chord both switches and leaves the keyboard in the tab switched to.
3433pub 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/// The message of the keymap action `name` when it switches tabs.
3437#[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
3449/// Puts a tab's terminal on screen.
3450///
3451/// The key that leaves it is answered here, by the terminal's own node: while the keyboard is in
3452/// the terminal the key goes to the tab strip, and anywhere else it reaches the application,
3453/// which sends it back in. The runtime looks at where the keyboard is when the key arrives, so a
3454/// click or Tab that moved it is never a press lost.
3455fn 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
3459/// The words for a session that ended by itself.
3460fn 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/// How many tabs of the open workspaces have an agent in them that is running or starting: a
3468/// harness in a terminal, or a harness's window. A shell, a file or a tab that has ended is left
3469/// out, since quitting takes nothing from it that opening the workspace again does not give back.
3470#[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
3481/// A tab that is not running: what happened, the engine's own words when there are any, and the
3482/// way back. The last screen of the session stays above it while there is one.
3483fn 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        // The last screen keeps every cell of the middle, as the terminal did; the words are its
3499        // foot.
3500        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
3514/// A tab the engine refused for a reason QCode recognises: the plain sentence, the line to run
3515/// when there is one, the engine's own words below them, and the way to try again.
3516fn 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/// The control that takes the keyboard when the screen opens: the terminal of the open tab, the
3536/// page of a blank one, or the strip's `+` that opens the first tab while there is none.
3537#[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/// The message a keymap action of the workspace screen stands for, when it is one: `new-tab`
3559/// opens a blank tab, `leave-terminal` takes the keyboard back into the open tab's harness,
3560/// `rename-tab` names the open tab and the switching keys ([`SWITCHES`]) go to another tab with the
3561/// keyboard in it. The application sends them only while a workspace is open.
3562#[must_use]
3563pub fn action(name: &str) -> Option<Msg> {
3564    match name {
3565        "new-tab" => Some(Msg::NewTab),
3566        // Inside a terminal the terminal's own node answers this key and leaves; here it arrives
3567        // only from outside one, so it goes back in.
3568        "leave-terminal" => Some(Msg::EnterTerminal),
3569        "rename-tab" => Some(Msg::RenameOpenTab),
3570        _ => switch(name),
3571    }
3572}
3573
3574/// What Esc means on the workspace screen before it means leaving it: while an entry of the file
3575/// tree is cut or copied, Esc lets it go, and it stays where it is. The application asks this when Esc reaches it,
3576/// which is only after every nearer widget and dialog passed it on.
3577#[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/// The keys of the workspace screen that are not in the keymap, for the key list.
3586///
3587/// A focused terminal hands every key to the program in it but a few: `shift+tab`, which leaves
3588/// it, `ctrl+q`, and the key list and the side panel's key. So the list says how to get out of
3589/// the terminal, after which the rest of these keys work again.
3590#[must_use]
3591pub fn hints(icons: &Icons) -> Vec<(String, String)> {
3592    let tab_keys = format!("{}{}", icons.glyph("arrow-left"), icons.glyph("arrow-right"));
3593    // Written the way the keymap's own keys are, so the list reads as one.
3594    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}