mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-11-06 00:15:06 +00:00
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:
@@ -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 |
@@ -119,6 +119,7 @@ actions!(
|
||||
ShowSshForwards,
|
||||
ToggleCodePanel,
|
||||
ToggleDocumentFill,
|
||||
ToggleBoard,
|
||||
DocumentWidthThird,
|
||||
DocumentWidthHalf,
|
||||
DocumentWidthTwoThirds,
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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)
|
||||
}))
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 => "文档:三分之二宽",
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user