feat(ui): the board — the main area's second view

A Board row under the sidebar search (with a count of agents waiting on
you) swaps the terminal area for five columns of cards: this workspace's
tasks, plus every tab running an agent no task claims. Columns come from
`task::column` / `task::live_column` on every frame.

- New task (N): title, prompt, agent; Add to queue or Start. Start opens a
  tab named for the task and launches the agent with the prompt as its
  first message, staying on the board.
- Cards: agent avatar, title, repo · branch, what a running agent is doing,
  the question a waiting one asks with an Open button, diff counts, age.
  Click opens the tab; right-click has start/resume/done/reopen/edit/delete,
  and "keep as task" for an agent started by hand.
- Runs record their agent's session id, so a closed run can be resumed.
- Toggle Board: secondary-alt-b, and in the command palette.
This commit is contained in:
l0ng-ai
2026-09-28 23:07:41 +08:00
parent e5907fdec1
commit 6237c03ed7
16 changed files with 1730 additions and 3 deletions
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="5" height="16" rx="1.6"/><rect x="9.5" y="4" width="5" height="11" rx="1.6"/><rect x="16" y="4" width="5" height="13.5" rx="1.6"/></svg>

After

Width:  |  Height:  |  Size: 345 B

+1
View File
@@ -119,6 +119,7 @@ actions!(
ShowSshForwards,
ToggleCodePanel,
ToggleDocumentFill,
ToggleBoard,
DocumentWidthThird,
DocumentWidthHalf,
DocumentWidthTwoThirds,
+1 -1
View File
@@ -149,7 +149,7 @@ pub(crate) fn fork_line(
/// Type `command` into the shell `slot` holds — now if it is up, or the
/// moment it lands if it is still connecting.
fn run_when_ready(slot: &PaneSlot, command: String, cx: &mut App) {
pub(crate) fn run_when_ready(slot: &PaneSlot, command: String, cx: &mut App) {
match slot {
PaneSlot::Ready(view) => view.read(cx).run_command_line(&command),
PaneSlot::Connecting(pending) => {
+23
View File
@@ -878,6 +878,9 @@ pub struct Tty7App {
pub(crate) closed: Vec<SessionTab>,
pub(crate) renaming: Option<Renaming>,
pub(crate) worktree_prompt: Option<crate::ui::worktree_prompt::WorktreePrompt>,
/// What the main area shows: the terminal, or the board over it.
pub(crate) main_view: crate::ui::board::MainView,
pub(crate) board: crate::ui::board::Board,
pub(crate) maximized: Option<Entity<TerminalView>>,
pub(crate) mod_hint_badges: bool,
pub(crate) mod_hint_gen: u64,
@@ -1516,6 +1519,8 @@ impl Tty7App {
closed: Vec::new(),
renaming: None,
worktree_prompt: None,
main_view: crate::ui::board::MainView::Terminal,
board: crate::ui::board::Board::new(cx),
maximized: None,
mod_hint_badges: false,
mod_hint_gen: 0,
@@ -3765,6 +3770,10 @@ impl Tty7App {
window.focus(&settings.focus_handle, cx);
return;
}
if self.board_open() {
window.focus(&self.board.focus, cx);
return;
}
let Some(tab) = self.tabs.get(self.active) else {
window.focus(&self.home_focus, cx);
return;
@@ -4056,6 +4065,9 @@ impl Tty7App {
};
// Something opened, so whatever the last failure was is stale.
self.startup_error = None;
// A new tab is one to look at. The board's own Start puts the board
// back once the tab is open.
self.main_view = crate::ui::board::MainView::Terminal;
self.remember_active_pane(window, cx);
self.maximized = None;
let insert_at = self.new_tab_insert_at(cx);
@@ -4908,6 +4920,12 @@ impl Tty7App {
}
pub(crate) fn activate(&mut self, index: usize, window: &mut Window, cx: &mut Context<Self>) {
// Picking a tab is asking to see it, board or not.
if self.board_open() && index < self.tabs.len() {
self.main_view = crate::ui::board::MainView::Terminal;
self.focus_active(window, cx);
cx.notify();
}
// A tab is woken by being looked at, the way a browser reloads a
// discarded tab when it is selected: a tab on screen is always a
// running one, so there is no third state to draw. A wake that fails
@@ -6365,6 +6383,7 @@ impl Tty7App {
ShowSshForwards => self.show_ssh_forwards(window, cx),
ToggleCodePanel => self.toggle_code_panel(window, cx),
ToggleDocumentFill => self.toggle_document_fill(cx),
ToggleBoard => self.toggle_board(window, cx),
DocumentWidthThird => {
self.set_document_ratio(crate::core::config::DOCUMENT_RATIO_THIRD, cx)
}
@@ -8947,6 +8966,7 @@ impl Render for Tty7App {
.when_some(self.render_remote_workspace_strip(cx), |this, el| {
this.child(el)
})
.when_some(self.render_board(window, cx), |this, el| this.child(el))
// Both of these used to anchor themselves at `bottom_4` and centre
// themselves, as siblings here — so a remote workspace whose ssh
// link had also dropped drew them one on top of the other. One
@@ -9379,6 +9399,9 @@ impl Render for Tty7App {
.on_action(cx.listener(|this, _: &ToggleDiffViewMode, _window, cx| {
this.toggle_diff_view_mode(cx)
}))
.on_action(
cx.listener(|this, _: &ToggleBoard, window, cx| this.toggle_board(window, cx)),
)
.on_action(cx.listener(|this, _: &ToggleDocumentFill, _window, cx| {
this.toggle_document_fill(cx)
}))
+1
View File
@@ -33,6 +33,7 @@ fn agent_icon(path: &str) -> Option<&'static [u8]> {
"icons/git-sync.svg" => include_bytes!("../../assets/icons/git-sync.svg"),
"icons/git-commit.svg" => include_bytes!("../../assets/icons/git-commit.svg"),
"icons/panel-left.svg" => include_bytes!("../../assets/icons/panel-left.svg"),
"icons/board.svg" => include_bytes!("../../assets/icons/board.svg"),
"icons/panel-right.svg" => include_bytes!("../../assets/icons/panel-right.svg"),
"icons/plus.svg" => include_bytes!("../../assets/icons/plus.svg"),
"icons/ellipsis.svg" => include_bytes!("../../assets/icons/ellipsis.svg"),
+1396
View File
File diff suppressed because it is too large Load Diff
+43
View File
@@ -1766,6 +1766,49 @@ pub fn translate_en(key: L10nKey) -> &'static str {
L10nKey::CmdDocumentDock => "Document: Dock Beside Terminal",
L10nKey::CmdDocumentFill => "Document: Fill Window",
L10nKey::CmdToggleDocumentFill => "Toggle Document Fill / Dock",
L10nKey::CmdToggleBoard => "Toggle Board",
L10nKey::BoardTitle => "Board",
L10nKey::BoardNewTask => "New task",
L10nKey::BoardEditTask => "Edit task",
L10nKey::BoardColQueued => "Queued",
L10nKey::BoardColRunning => "Running",
L10nKey::BoardColNeedsInput => "Needs input",
L10nKey::BoardColReview => "Review",
L10nKey::BoardColDone => "Done",
L10nKey::BoardAllAgents => "All agents",
L10nKey::BoardSummary => "{running} running · {input} need you · {review} to review",
L10nKey::BoardEmpty => "Nothing on the board yet",
L10nKey::BoardEmptyHint => {
"Add a task with New task (N), or start an agent in any tab — it shows up here on its own."
}
L10nKey::BoardOpen => "Open",
L10nKey::BoardStart => "Start",
L10nKey::BoardResume => "Resume",
L10nKey::BoardResumable => "Resumable",
L10nKey::BoardMarkDone => "Mark done",
L10nKey::BoardReopen => "Reopen",
L10nKey::BoardEdit => "Edit",
L10nKey::BoardDelete => "Delete",
L10nKey::BoardKeep => "Keep as task",
L10nKey::BoardFieldTitle => "Title",
L10nKey::BoardFieldPrompt => "Prompt",
L10nKey::BoardFieldAgent => "Agent",
L10nKey::BoardTitlePlaceholder => "What should the agent do?",
L10nKey::BoardPromptPlaceholder => {
"Details, context, how to check it’s done… (optional — the title is sent if this is empty)"
}
L10nKey::BoardSave => "Save",
L10nKey::BoardAddToQueue => "Add to queue",
L10nKey::BoardStartNow => "Start",
L10nKey::BoardNeedsTitle => "A task needs a title",
L10nKey::BoardNoAgent => "No agent found",
L10nKey::BoardPromptCopied => {
"{agent} can’t be given a starting prompt, so it was copied — paste it in once {agent} is up"
}
L10nKey::BoardUnavailable => {
"This machine’s server can’t keep tasks yet — update it to use the board"
}
L10nKey::BoardNow => "now",
L10nKey::CmdDocumentWidthThird => "Document: Third Width",
L10nKey::CmdDocumentWidthHalf => "Document: Half Width",
L10nKey::CmdDocumentWidthTwoThirds => "Document: Two-Thirds Width",
+43
View File
@@ -1811,6 +1811,49 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> {
L10nKey::CmdDocumentDock => "ドキュメント: ターミナルの隣にドック",
L10nKey::CmdDocumentFill => "ドキュメント: ウィンドウ全体",
L10nKey::CmdToggleDocumentFill => "ドキュメントのフィル / ドックを切り替え",
L10nKey::CmdToggleBoard => "ボードの表示を切り替え",
L10nKey::BoardTitle => "ボード",
L10nKey::BoardNewTask => "新しいタスク",
L10nKey::BoardEditTask => "タスクを編集",
L10nKey::BoardColQueued => "待機",
L10nKey::BoardColRunning => "実行中",
L10nKey::BoardColNeedsInput => "入力待ち",
L10nKey::BoardColReview => "レビュー",
L10nKey::BoardColDone => "完了",
L10nKey::BoardAllAgents => "すべてのエージェント",
L10nKey::BoardSummary => "実行中 {running} · 入力待ち {input} · レビュー待ち {review}",
L10nKey::BoardEmpty => "ボードはまだ空です",
L10nKey::BoardEmptyHint => {
"「新しいタスク」(N) でタスクを追加するか、どれかのタブでエージェントを起動してください。自動的にここに表示されます。"
}
L10nKey::BoardOpen => "開く",
L10nKey::BoardStart => "開始",
L10nKey::BoardResume => "再開",
L10nKey::BoardResumable => "再開可能",
L10nKey::BoardMarkDone => "完了にする",
L10nKey::BoardReopen => "再度開く",
L10nKey::BoardEdit => "編集",
L10nKey::BoardDelete => "削除",
L10nKey::BoardKeep => "タスクとして残す",
L10nKey::BoardFieldTitle => "タイトル",
L10nKey::BoardFieldPrompt => "プロンプト",
L10nKey::BoardFieldAgent => "エージェント",
L10nKey::BoardTitlePlaceholder => "エージェントに何をさせますか?",
L10nKey::BoardPromptPlaceholder => {
"詳細、背景、完了の確認方法など(任意 — 空ならタイトルを送ります)"
}
L10nKey::BoardSave => "保存",
L10nKey::BoardAddToQueue => "キューに追加",
L10nKey::BoardStartNow => "開始",
L10nKey::BoardNeedsTitle => "タスクにはタイトルが必要です",
L10nKey::BoardNoAgent => "エージェントが見つかりません",
L10nKey::BoardPromptCopied => {
"{agent} は起動時にプロンプトを受け取れないため、コピーしました。起動したら貼り付けてください"
}
L10nKey::BoardUnavailable => {
"このマシンのサーバーはまだタスクを保存できません。ボードを使うには更新してください"
}
L10nKey::BoardNow => "たった今",
L10nKey::CmdDocumentWidthThird => "ドキュメント: 幅3分の1",
L10nKey::CmdDocumentWidthHalf => "ドキュメント: 幅半分",
L10nKey::CmdDocumentWidthTwoThirds => "ドキュメント: 幅3分の2",
+35
View File
@@ -1500,6 +1500,41 @@ l10n_keys! {
CmdDocumentDock,
CmdDocumentFill,
CmdToggleDocumentFill,
CmdToggleBoard,
BoardTitle,
BoardNewTask,
BoardEditTask,
BoardColQueued,
BoardColRunning,
BoardColNeedsInput,
BoardColReview,
BoardColDone,
BoardAllAgents,
BoardSummary,
BoardEmpty,
BoardEmptyHint,
BoardOpen,
BoardStart,
BoardResume,
BoardResumable,
BoardMarkDone,
BoardReopen,
BoardEdit,
BoardDelete,
BoardKeep,
BoardFieldTitle,
BoardFieldPrompt,
BoardFieldAgent,
BoardTitlePlaceholder,
BoardPromptPlaceholder,
BoardSave,
BoardAddToQueue,
BoardStartNow,
BoardNeedsTitle,
BoardNoAgent,
BoardPromptCopied,
BoardUnavailable,
BoardNow,
CmdDocumentWidthThird,
CmdDocumentWidthHalf,
CmdDocumentWidthTwoThirds,
+41
View File
@@ -1638,6 +1638,47 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> {
L10nKey::CmdDocumentDock => "文档:停靠在终端旁",
L10nKey::CmdDocumentFill => "文档:铺满窗口",
L10nKey::CmdToggleDocumentFill => "切换文档铺满 / 停靠",
L10nKey::CmdToggleBoard => "切换看板",
L10nKey::BoardTitle => "看板",
L10nKey::BoardNewTask => "新任务",
L10nKey::BoardEditTask => "编辑任务",
L10nKey::BoardColQueued => "排队",
L10nKey::BoardColRunning => "运行中",
L10nKey::BoardColNeedsInput => "等待输入",
L10nKey::BoardColReview => "待审阅",
L10nKey::BoardColDone => "完成",
L10nKey::BoardAllAgents => "全部 Agent",
L10nKey::BoardSummary => "{running} 运行中 · {input} 等你 · {review} 待审阅",
L10nKey::BoardEmpty => "看板上还没有东西",
L10nKey::BoardEmptyHint => {
"用「新任务」(N) 添加一个任务,或者在任意标签页里启动 Agent——它会自己出现在这里。"
}
L10nKey::BoardOpen => "打开",
L10nKey::BoardStart => "开始",
L10nKey::BoardResume => "恢复会话",
L10nKey::BoardResumable => "可恢复",
L10nKey::BoardMarkDone => "标记完成",
L10nKey::BoardReopen => "重新打开",
L10nKey::BoardEdit => "编辑",
L10nKey::BoardDelete => "删除",
L10nKey::BoardKeep => "保留为任务",
L10nKey::BoardFieldTitle => "标题",
L10nKey::BoardFieldPrompt => "提示词",
L10nKey::BoardFieldAgent => "使用的 Agent",
L10nKey::BoardTitlePlaceholder => "要 Agent 做什么?",
L10nKey::BoardPromptPlaceholder => "细节、上下文、怎么算完成……(可选,留空则发送标题)",
L10nKey::BoardSave => "保存",
L10nKey::BoardAddToQueue => "加入排队",
L10nKey::BoardStartNow => "开始",
L10nKey::BoardNeedsTitle => "任务需要一个标题",
L10nKey::BoardNoAgent => "没有找到 Agent",
L10nKey::BoardPromptCopied => {
"{agent} 不支持启动时带提示词,已复制到剪贴板——{agent} 启动后粘贴进去"
}
L10nKey::BoardUnavailable => {
"这台机器的 server 版本太旧,还不能保存任务——更新后即可使用看板"
}
L10nKey::BoardNow => "刚刚",
L10nKey::CmdDocumentWidthThird => "文档:三分之一宽",
L10nKey::CmdDocumentWidthHalf => "文档:一半宽",
L10nKey::CmdDocumentWidthTwoThirds => "文档:三分之二宽",
+3
View File
@@ -659,6 +659,7 @@ fn shipped_bindings() -> Vec<(&'static str, &'static str)> {
// terminal back, so a default chord here would only be one more thing
// competing for a two-key combination nobody asked for.
("ToggleDocumentFill", ""),
("ToggleBoard", "secondary-alt-b"),
("DocumentWidthThird", ""),
("DocumentWidthHalf", ""),
("DocumentWidthTwoThirds", ""),
@@ -946,6 +947,7 @@ fn authored_entry(action: &str) -> Option<(CommandGroup, String)> {
CommandGroup::View,
t(L10nKey::CmdToggleDocumentFill).to_string(),
),
"ToggleBoard" => (CommandGroup::View, t(L10nKey::CmdToggleBoard).to_string()),
"DocumentWidthThird" => (
CommandGroup::View,
t(L10nKey::CmdDocumentWidthThird).to_string(),
@@ -1799,6 +1801,7 @@ fn make_binding(action: &str, keystroke: &str) -> Option<KeyBinding> {
"ShowSshForwards" => KeyBinding::new(keystroke, ShowSshForwards, None),
"ToggleCodePanel" => KeyBinding::new(keystroke, ToggleCodePanel, None),
"ToggleDocumentFill" => KeyBinding::new(keystroke, ToggleDocumentFill, None),
"ToggleBoard" => KeyBinding::new(keystroke, ToggleBoard, None),
"DocumentWidthThird" => KeyBinding::new(keystroke, DocumentWidthThird, None),
"DocumentWidthHalf" => KeyBinding::new(keystroke, DocumentWidthHalf, None),
"DocumentWidthTwoThirds" => KeyBinding::new(keystroke, DocumentWidthTwoThirds, None),
+11
View File
@@ -316,6 +316,17 @@ fn apply(machine: &mut Machine, workspace: WorkspaceId, delta: &LayoutDelta) ->
ws.groups = groups.clone();
true
}
LayoutDelta::TaskPut { task } => {
match ws.tasks.iter_mut().find(|t| t.id == task.id) {
Some(slot) => *slot = task.clone(),
None => ws.tasks.push(task.clone()),
}
true
}
LayoutDelta::TaskRemoved { task } => {
ws.tasks.retain(|t| t.id != *task);
true
}
LayoutDelta::TabMoved { tab, to } => {
let Some(from) = ws.tabs.iter().position(|t| t.id == *tab) else {
return false;
+1
View File
@@ -1,6 +1,7 @@
pub mod agent_launch;
pub mod app;
pub mod assets;
pub mod board;
pub mod code_editor;
pub mod dialog;
pub mod diff_list;
+4
View File
@@ -85,6 +85,7 @@ pub enum CommandKind {
ShowSshForwards,
ToggleCodePanel,
ToggleDocumentFill,
ToggleBoard,
DocumentWidthThird,
DocumentWidthHalf,
DocumentWidthTwoThirds,
@@ -265,6 +266,7 @@ impl CommandKind {
ShowSshForwards => "ssh-port-forwarding",
ToggleCodePanel => "code-panel",
ToggleDocumentFill => "document-fill",
ToggleBoard => "board",
DocumentWidthThird => "document-width-third",
DocumentWidthHalf => "document-width-half",
DocumentWidthTwoThirds => "document-width-two-thirds",
@@ -407,6 +409,7 @@ impl CommandKind {
ShowSshForwards => "ShowSshForwards",
ToggleCodePanel => "ToggleCodePanel",
ToggleDocumentFill => "ToggleDocumentFill",
ToggleBoard => "ToggleBoard",
DocumentWidthThird => "DocumentWidthThird",
DocumentWidthHalf => "DocumentWidthHalf",
DocumentWidthTwoThirds => "DocumentWidthTwoThirds",
@@ -690,6 +693,7 @@ impl Item {
ToggleRightPanel,
),
Item::localized(L10nKey::CmdShowCodePanel, ToggleCodePanel),
Item::localized(L10nKey::CmdToggleBoard, ToggleBoard),
Item::localized(L10nKey::CmdGoToFile, QuickOpenFile),
Item::localized(
if document_filled {
+3 -2
View File
@@ -72,7 +72,7 @@ const META_SEP: &str = " · ";
const META_SEP_TRIMMED: &str = "·";
/// Tabular numerals, so a column of diff counts lines up digit for digit.
fn tabular() -> gpui::FontFeatures {
pub(crate) fn tabular() -> gpui::FontFeatures {
gpui::FontFeatures(std::sync::Arc::new(vec![("tnum".to_string(), 1)]))
}
@@ -1774,7 +1774,8 @@ impl Tty7App {
.pt(px(4.))
.pb(px(16.))
.child(self.workspace_head(cx))
.child(search);
.child(search)
.child(self.render_board_nav(cx));
let container: Rc<Cell<Option<Bounds<Pixels>>>> = Rc::new(Cell::new(None));
// Read while there is still a `cx` to read it from: the drag handler
+123
View File
@@ -16,6 +16,7 @@ use crate::core::session::{Session, SessionPane, SessionTab, WorkspaceId, Worksp
use crate::ui::app::Tty7App;
use crate::ui::i18n::{L10nKey, t};
use crate::ui::pane::{Pane, PaneSlot};
use tty7_core::core::task::{Task, TaskId};
pub(crate) fn control_for(cx: &mut App, host: HostId) -> Option<Arc<ControlClient>> {
if host.is_local() {
@@ -340,6 +341,7 @@ pub(crate) struct WsMirror {
pub tabs: Vec<TreeTab>,
pub active: Option<TabId>,
pub groups: WorkspaceGroups,
pub tasks: Vec<Task>,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
@@ -1571,6 +1573,7 @@ fn primed(ws: Workspace, arrival: Arrival) -> (WsMirror, Option<String>, Arrival
tabs: ws.tabs,
active: ws.active_tab,
groups: ws.groups,
tasks: ws.tasks,
},
ws.name,
arrival,
@@ -1611,6 +1614,7 @@ fn finish_prime(
}
};
adopt_groups(cx, client_ws);
adopt_tasks(cx, client_ws);
let host = WorkspaceStore::host_of(cx, client_ws);
let machine_ws = tree_workspace_id(cx, client_ws);
crate::ui::machine_mirror::MachineMirrors::note_synced_workspace(
@@ -1712,6 +1716,104 @@ fn adopt_groups(cx: &mut App, client_ws: WorkspaceId) {
});
}
/// Whether the machine holding `client_ws` keeps a board. One that predates
/// the board would refuse the request that saves a card — and, worse, one it
/// cannot decode takes the whole link down — so a window never sends it one.
pub(crate) fn can_board_on(cx: &App, client_ws: WorkspaceId) -> bool {
let feature = tty7_core::daemon::control::feature::TASKS;
let host = WorkspaceStore::host_of(cx, client_ws);
match host.is_local() {
true => crate::ui::local_link::LocalLink::supports(cx, feature),
false => crate::ui::remote_connect::HostLinks::peer_supports(cx, host, feature),
}
}
/// Sends a card a window just made or changed up to the machine — the whole
/// card, which replaces whatever the machine had under its id.
///
/// Like groups, tasks are never part of the diff [`sync_window`] runs: a
/// window has nothing to say about a card except the edit someone just made
/// to it. Returns false when the window's tree has not landed yet (or the
/// machine keeps no board), in which case nothing was sent and the caller
/// should not show the edit as made.
pub(crate) fn push_task(cx: &mut App, client_ws: WorkspaceId, task: Task) -> bool {
if !cx.has_global::<crate::core::session::WorkspaceStore>() || !can_board_on(cx, client_ws) {
return false;
}
let machine_ws = tree_workspace_id(cx, client_ws);
let state = cx
.default_global::<TreeSync>()
.windows
.entry(client_ws)
.or_default();
let SyncPhase::Primed(mirror) = &mut state.sync else {
return false;
};
match mirror.tasks.iter_mut().find(|t| t.id == task.id) {
Some(slot) if *slot == task => return true,
Some(slot) => *slot = task.clone(),
None => mirror.tasks.push(task.clone()),
}
state.queue.push_back(ControlRequest::TaskPut {
workspace: machine_ws,
task,
});
pump(cx, client_ws);
true
}
/// Takes a card off the machine's board. See [`push_task`].
pub(crate) fn remove_task(cx: &mut App, client_ws: WorkspaceId, task: TaskId) -> bool {
if !cx.has_global::<crate::core::session::WorkspaceStore>() || !can_board_on(cx, client_ws) {
return false;
}
let machine_ws = tree_workspace_id(cx, client_ws);
let state = cx
.default_global::<TreeSync>()
.windows
.entry(client_ws)
.or_default();
let SyncPhase::Primed(mirror) = &mut state.sync else {
return false;
};
let before = mirror.tasks.len();
mirror.tasks.retain(|t| t.id != task);
if mirror.tasks.len() == before {
return true;
}
state.queue.push_back(ControlRequest::TaskRemove {
workspace: machine_ws,
task,
});
pump(cx, client_ws);
true
}
/// Hands the window the board a pull just brought in. Deferred for the same
/// reason as [`adopt_groups`].
fn adopt_tasks(cx: &mut App, client_ws: WorkspaceId) {
cx.defer(move |cx| {
let tasks = match cx
.default_global::<TreeSync>()
.windows
.get(&client_ws)
.map(|s| &s.sync)
{
Some(SyncPhase::Primed(mirror)) => mirror.tasks.clone(),
_ => return,
};
if !cx.has_global::<crate::ui::windows::WindowRegistry>() {
return;
}
let Some(app) =
crate::ui::windows::WindowRegistry::app_for(cx, client_ws).and_then(|a| a.upgrade())
else {
return;
};
app.update(cx, |app, cx| app.adopt_board_tasks(tasks, cx));
});
}
fn pump(cx: &mut App, client_ws: WorkspaceId) {
let host = WorkspaceStore::host_of(cx, client_ws);
let client = tree_control_for(cx, host);
@@ -2379,6 +2481,7 @@ fn layout_of(
tabs: ws.tabs.clone(),
active: ws.active_tab,
groups: ws.groups.clone(),
tasks: ws.tasks.clone(),
};
let session = session_from_tree(ws, &machine.panes);
Ok((machine, mirror, session))
@@ -2460,6 +2563,7 @@ fn settle_hydration(
dirty
};
adopt_groups(cx, client_ws);
adopt_tasks(cx, client_ws);
let Some(app) =
crate::ui::windows::WindowRegistry::app_for(cx, client_ws).and_then(|app| app.upgrade())
else {
@@ -2741,6 +2845,17 @@ fn apply_to_mirror(mirror: &mut WsMirror, delta: &LayoutDelta) -> bool {
mirror.groups = groups.clone();
true
}
LayoutDelta::TaskPut { task } => {
match mirror.tasks.iter_mut().find(|t| t.id == task.id) {
Some(slot) => *slot = task.clone(),
None => mirror.tasks.push(task.clone()),
}
true
}
LayoutDelta::TaskRemoved { task } => {
mirror.tasks.retain(|t| t.id != *task);
true
}
LayoutDelta::TabMoved { tab, to } => {
let Some(from) = mirror.tabs.iter().position(|t| t.id == *tab) else {
return false;
@@ -2915,6 +3030,14 @@ impl Tty7App {
self.adopt_sidebar_groups(groups.clone(), cx);
true
}
LayoutDelta::TaskPut { task } => {
self.board_task_put(task.clone());
true
}
LayoutDelta::TaskRemoved { task } => {
self.board_task_removed(*task);
true
}
LayoutDelta::TabMoved { tab, to } => {
if let Some(from) = index_of(&self.tabs, *tab) {
let active_id = self.tabs.get(self.active).map(|t| t.tree_id.get());