From cb395ee2c620ce476227e856e1e2dfa957fb6f88 Mon Sep 17 00:00:00 2001 From: BFlat Date: Wed, 4 Nov 2026 16:01:37 +0700 Subject: [PATCH] Let people fold the builder's plan card away, and keep the fold (#790) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The builder's plan card can be folded away, and the fold is kept in the project's feed until the builder makes a new plan (refs #504). Supersedes #779. Co-authored-by: Fábio Soares --- crates/ai/src/rebuild/feed.rs | 33 +++++++++++-- crates/engine/src/feed.rs | 61 ++++++++++++++++++++++-- crates/engine/src/tools/project_tools.rs | 42 ++++++++++++++++ crates/ui-egui/src/fr.tsv | 2 + crates/ui-egui/src/i18n/cs.tsv | 2 + crates/ui-egui/src/i18n/de.tsv | 2 + crates/ui-egui/src/i18n/ko.tsv | 2 + crates/ui-egui/src/i18n/ru.tsv | 2 + crates/ui-egui/src/i18n/zh-hans.tsv | 2 + crates/ui-egui/src/i18n/zh-hant.tsv | 2 + crates/ui-egui/src/ja.tsv | 2 + crates/ui-egui/src/project.rs | 34 ++++++++++--- crates/ui-egui/tests/headless_parity.rs | 1 + docs/ROADMAP.md | 6 +++ docs/STATUS.md | 2 +- docs/file-formats.md | 2 +- docs/tools.md | 5 +- 17 files changed, 186 insertions(+), 16 deletions(-) diff --git a/crates/ai/src/rebuild/feed.rs b/crates/ai/src/rebuild/feed.rs index 925d66bd..d7057d4e 100644 --- a/crates/ai/src/rebuild/feed.rs +++ b/crates/ai/src/rebuild/feed.rs @@ -26,9 +26,16 @@ pub fn apply(feed: &mut Vec, e: &RunEvent) { // Claude Code's TodoWrite, or our build_checklist (same shape) for API builders. RunEvent::Agent(AgentEvent::ToolStarted { tool, input, .. }) if tool == "TodoWrite" || tool == "build_checklist" => { if let Some(steps) = plan_from(input) { - match feed.iter_mut().find_map(|i| if let FeedItem::Plan(p) = i { Some(p) } else { None }) { - Some(p) => *p = steps, - None => feed.push(FeedItem::Plan(steps)), + if feed.iter().any(|i| matches!(i, FeedItem::Plan(_))) && craft_anything_engine::feed::visible_plan(feed).is_none() { + // The person folded the last list (#504): a new list is a new card at the end, + // and the folded one goes. + feed.retain(|i| !matches!(i, FeedItem::Plan(_) | FeedItem::PlanDismissed)); + feed.push(FeedItem::Plan(steps)); + } else { + match feed.iter_mut().find_map(|i| if let FeedItem::Plan(p) = i { Some(p) } else { None }) { + Some(p) => *p = steps, + None => feed.push(FeedItem::Plan(steps)), + } } } } @@ -202,6 +209,26 @@ mod tests { assert!(matches!(feed2.as_slice(), [FeedItem::Plan(p)] if p.len() == 2)); } + /// #504: a folded checklist doesn't come back by being updated; the next one a run writes is + /// a new card at the end, and there's only ever one checklist in the feed. + #[test] + fn a_folded_checklist_gives_way_to_the_next_one() { + let todo = |status: &str| json!({"todos": [{"content": "Lay out the arena", "activeForm": "Laying out the arena", "status": status}]}); + let call = |status: &str| RunEvent::Agent(AgentEvent::ToolStarted { call_id: "t".into(), tool: "TodoWrite".into(), input: todo(status) }); + let plans = |f: &[FeedItem]| f.iter().filter(|i| matches!(i, FeedItem::Plan(_))).count(); + let mut feed = vec![FeedItem::User(UserItem::Text("A game".into()))]; + apply(&mut feed, &call("in_progress")); + apply(&mut feed, &call("completed")); + assert_eq!(plans(&feed), 1, "updates in place while the list isn't folded"); + assert!(craft_anything_engine::feed::dismiss_plan(&mut feed), "the list folds"); + assert!(craft_anything_engine::feed::visible_plan(&feed).is_none()); + apply(&mut feed, &call("in_progress")); + assert_eq!(plans(&feed), 1, "one checklist, not one folded and one new"); + assert!(!feed.iter().any(|i| matches!(i, FeedItem::PlanDismissed)), "the fold went with the old list"); + assert!(craft_anything_engine::feed::visible_plan(&feed).is_some(), "the new list shows"); + assert!(matches!(feed.last(), Some(FeedItem::Plan(p)) if p[0].status == PlanStatus::Doing)); + } + #[test] fn a_run_keeps_the_feed_in_the_project() { let d = tempfile::tempdir().unwrap(); diff --git a/crates/engine/src/feed.rs b/crates/engine/src/feed.rs index e3c71906..14841ca4 100644 --- a/crates/engine/src/feed.rs +++ b/crates/engine/src/feed.rs @@ -13,9 +13,10 @@ use std::path::{Path, PathBuf}; /// Where the conversation is kept in the project folder. pub const FEED_FILE: &str = "feed.json"; -/// The format written today (3 saved a prompt's pictures with it); files without a version are -/// the earlier bare list. -pub const FEED_VERSION: u32 = 3; +/// The format written today (4 saves a dismissed plan card with it; 3 saved a prompt's pictures); +/// files without a version are the earlier bare list. An older Craft Anything won't rewrite a file +/// it can't read (`save` refuses a newer version). +pub const FEED_VERSION: u32 = 4; /// What the person sent: as the feed shows it, and the pictures attached to it (their copies in /// the project's `attachments/`). @@ -107,6 +108,40 @@ pub enum FeedItem { }, /// The person paused the run (not a failure); a message or Continue carries on. Paused, + /// The person folded the plan card above the prompt box (#504). It marks the plan before it as + /// hidden; the steps are kept, and a later run's checklist shows again. + PlanDismissed, +} + +/// The plan card to show: the checklist, unless the person folded it after it was last updated. +pub fn visible_plan(feed: &[FeedItem]) -> Option<&[PlanStep]> { + let at = feed.iter().rposition(|i| matches!(i, FeedItem::Plan(_)))?; + if feed[at + 1..].iter().any(|i| matches!(i, FeedItem::PlanDismissed)) { + return None; + } + match &feed[at] { + FeedItem::Plan(steps) => Some(steps.as_slice()), + _ => None, + } +} + +/// Fold the plan card in a feed. Returns false when there's no plan to fold or it's folded already. +pub fn dismiss_plan(feed: &mut Vec) -> bool { + if visible_plan(feed).is_none() { + return false; + } + feed.push(FeedItem::PlanDismissed); + true +} + +/// Fold the plan card in a project's saved conversation (the UI's write, while no run is going). +pub fn dismiss_plan_saved(ws: &Workspace) -> Result { + let mut feed = load(ws); + let folded = dismiss_plan(&mut feed); + if folded { + save(&ws.root, &feed)?; + } + Ok(folded) } /// Mark a question or approval answered, with the words shown for the answer. @@ -240,6 +275,26 @@ mod tests { assert!(raw["items"][0]["User"].is_string()); } + /// #504: a plan card can be folded, the steps stay in the conversation, and a folded plan stays + /// folded after a restart. A file from before folding (version 3) still shows its plan. + #[test] + fn a_folded_plan_stays_folded_and_its_steps_are_kept() { + let d = tempfile::tempdir().unwrap(); + let ws = Workspace::create(d.path(), "x", "A game", TargetRef::None).unwrap(); + let steps = vec![PlanStep { text: "Lay out the arena".into(), doing: "Laying out the arena".into(), status: PlanStatus::Doing }]; + save(&ws.root, &[FeedItem::User(UserItem::Text("A game".into())), FeedItem::Plan(steps.clone())]).unwrap(); + assert!(visible_plan(&load(&ws)).is_some(), "the plan shows before it's folded"); + assert!(dismiss_plan_saved(&ws).unwrap(), "the plan folds"); + let folded = load(&ws); + assert!(visible_plan(&folded).is_none(), "folded after a restart too"); + assert!(folded.iter().any(|i| matches!(i, FeedItem::Plan(p) if p == &steps)), "the steps are kept"); + assert!(!dismiss_plan_saved(&ws).unwrap(), "folding again changes nothing"); + save(&ws.root, &[FeedItem::User(UserItem::Text("Hi".into()))]).unwrap(); + assert!(!dismiss_plan_saved(&ws).unwrap(), "a conversation with no plan has nothing to fold"); + std::fs::write(ws.root.join(FEED_FILE), br#"{"version":3,"items":[{"Plan":[{"text":"Lay out","doing":"Laying out","status":"Doing"}]}]}"#).unwrap(); + assert!(visible_plan(&load(&ws)).is_some(), "a version 3 plan still shows"); + } + #[test] fn older_files_load_answers_are_kept_and_newer_files_are_left_alone() { let d = tempfile::tempdir().unwrap(); diff --git a/crates/engine/src/tools/project_tools.rs b/crates/engine/src/tools/project_tools.rs index 53c48463..50ec14d0 100644 --- a/crates/engine/src/tools/project_tools.rs +++ b/crates/engine/src/tools/project_tools.rs @@ -97,6 +97,17 @@ pub fn specs() -> Vec { input_schema: schema_of::, run: project_rename, }, + ToolSpec { + id: "project_plan_fold", + title: "Fold a project's plan", + summary: "Fold the builder's plan card away (#504). The steps stay in the project's conversation, and a later run's plan shows again. Call it between builds: a build saves its conversation as it goes, so a fold made during one can be overwritten by that build's next save.", + beginner: "Hides the builder's checklist until it makes a new one.", + toolset: Toolset::Projects, + effects: Effects::WRITES_FILESYSTEM, + requires: Requires::Nothing, + input_schema: schema_of::, + run: project_plan_fold, + }, ToolSpec { id: "project_duplicate", title: "Duplicate a project", @@ -182,6 +193,21 @@ fn project_rename(_ctx: &mut Ctx<'_>, params: Value) -> Result, params: Value) -> Result { + let p: FoldPlanParams = parse_params(params)?; + let ws = open(&p.project)?; + // `folded` is false when there's no plan to fold, or it's folded already. + let folded = crate::feed::dismiss_plan_saved(&ws)?; + Ok(ToolOutput::Value(json!({"project": ws.root, "folded": folded}))) +} + #[derive(Debug, Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct DuplicateParams { @@ -624,6 +650,22 @@ mod tests { assert!(call(json!({"project": ws.root, "budget_usd": -1})).is_err()); } + /// #504: the plan card is folded by a tool too (the app's button calls it), and says whether + /// it folded anything. + #[test] + fn a_plan_is_folded_by_its_tool_and_says_whether_it_did() { + use crate::feed::{FeedItem, PlanStep, UserItem, save, visible_plan}; + let d = tempfile::tempdir().unwrap(); + let ws = Workspace::create(d.path(), "Game", "A game", TargetRef::None).unwrap(); + let steps = vec![PlanStep { text: "Lay out".into(), doing: "Laying out".into(), status: crate::feed::PlanStatus::Todo }]; + save(&ws.root, &[FeedItem::User(UserItem::Text("A game".into())), FeedItem::Plan(steps)]).unwrap(); + let e = Engine::new(crate::testing::session()); + let fold = || e.call("project_plan_fold", json!({"project": ws.root}), CallOptions::new(Caller::Cli)).map(|o| o.to_json()["result"]["folded"].clone()); + assert_eq!(fold().unwrap(), true); + assert!(visible_plan(&crate::feed::load(&ws)).is_none()); + assert_eq!(fold().unwrap(), false, "already folded"); + } + #[test] fn a_project_generates_assets_only_once_allowed() { let d = tempfile::tempdir().unwrap(); diff --git a/crates/ui-egui/src/fr.tsv b/crates/ui-egui/src/fr.tsv index 8b9ded78..8aeba517 100644 --- a/crates/ui-egui/src/fr.tsv +++ b/crates/ui-egui/src/fr.tsv @@ -852,6 +852,8 @@ Providers Fournisseurs Harnesses Agents Nothing of this kind was found. Rien de ce type n'a été trouvé. Another model Autre modèle +Fold a project's plan Replier le plan d'un projet +Fold the plan Replier le plan The Claude desktop app is installed, but building needs Claude Code, its separate command-line tool. Install Claude Code, sign in to it once in a terminal, then look again. L'application de bureau Claude est installée, mais la création nécessite Claude Code, son outil en ligne de commande distinct. Installez Claude Code, connectez-vous-y une fois dans un terminal, puis relancez la recherche. Unreal is here, but its bundled Clang C++ toolchain wasn't found. Unreal est installé, mais sa chaîne d'outils Clang C++ intégrée est introuvable. Add Unreal's Linux C++ toolchain Ajouter la chaîne d'outils C++ Linux d'Unreal diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 0f9cda06..55265277 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -1417,6 +1417,8 @@ Providers Poskytovatelé Harnesses Agenti Nothing of this kind was found. Nic takového nebylo nalezeno. Another model Jiný model +Fold a project's plan Sbalit plán projektu +Fold the plan Sbalit plán The Claude desktop app is installed, but building needs Claude Code, its separate command-line tool. Install Claude Code, sign in to it once in a terminal, then look again. Desktopová aplikace Claude je nainstalovaná, ale k sestavení je potřeba Claude Code, její samostatný nástroj pro příkazový řádek. Nainstalujte Claude Code, jednou se do něj přihlaste v terminálu a pak hledejte znovu. Unreal is here, but its bundled Clang C++ toolchain wasn't found. Unreal je nainstalovaný, ale jeho přibalený nástrojový řetězec Clang C++ nebyl nalezen. Add Unreal's Linux C++ toolchain Přidat linuxový nástrojový řetězec C++ pro Unreal diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index da571497..84023812 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -1417,6 +1417,8 @@ Providers Anbieter Harnesses Agenten Nothing of this kind was found. Nichts dieser Art wurde gefunden. Another model Anderes Modell +Fold a project's plan Den Plan eines Projekts einklappen +Fold the plan Den Plan einklappen The Claude desktop app is installed, but building needs Claude Code, its separate command-line tool. Install Claude Code, sign in to it once in a terminal, then look again. Die Claude-Desktop-App ist installiert, aber zum Bauen wird Claude Code gebraucht, ihr eigenes Kommandozeilen-Tool. Installiere Claude Code, melde dich einmal in einem Terminal dort an und suche dann erneut. Unreal is here, but its bundled Clang C++ toolchain wasn't found. Unreal ist da, aber die mitgelieferte Clang-C++-Toolchain wurde nicht gefunden. Add Unreal's Linux C++ toolchain Unreals Linux-C++-Toolchain hinzufügen diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index edfbf6a6..302c728b 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -1417,6 +1417,8 @@ Providers 제공자 Harnesses 에이전트 Nothing of this kind was found. 이런 종류는 찾지 못했습니다. Another model 다른 모델 +Fold a project's plan 프로젝트 계획 접기 +Fold the plan 계획 접기 The Claude desktop app is installed, but building needs Claude Code, its separate command-line tool. Install Claude Code, sign in to it once in a terminal, then look again. Claude 데스크톱 앱은 설치되어 있지만, 빌드에는 별도의 명령줄 도구인 Claude Code가 필요합니다. Claude Code를 설치하고 터미널에서 한 번 로그인한 다음 다시 찾아보세요. Unreal is here, but its bundled Clang C++ toolchain wasn't found. Unreal은 설치되어 있지만 함께 제공되는 Clang C++ 도구 체인을 찾지 못했습니다. Add Unreal's Linux C++ toolchain Unreal의 Linux C++ 도구 체인 추가 diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 54edd41c..da44d3de 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -1417,6 +1417,8 @@ Providers Провайдеры Harnesses Агенты Nothing of this kind was found. Ничего такого не найдено. Another model Другая модель +Fold a project's plan Свернуть план проекта +Fold the plan Свернуть план The Claude desktop app is installed, but building needs Claude Code, its separate command-line tool. Install Claude Code, sign in to it once in a terminal, then look again. Приложение Claude для компьютера установлено, но для сборки нужен Claude Code — отдельный инструмент командной строки. Установите Claude Code, один раз войдите в него в терминале и выполните поиск снова. Unreal is here, but its bundled Clang C++ toolchain wasn't found. Unreal установлен, но встроенная цепочка инструментов Clang C++ не найдена. Add Unreal's Linux C++ toolchain Добавить цепочку инструментов C++ для Linux в Unreal diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 660f372a..852bf823 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -1417,6 +1417,8 @@ Providers 提供商 Harnesses 代理 Nothing of this kind was found. 没有找到这类项目。 Another model 其他模型 +Fold a project's plan 折叠项目的计划 +Fold the plan 折叠计划 The Claude desktop app is installed, but building needs Claude Code, its separate command-line tool. Install Claude Code, sign in to it once in a terminal, then look again. 已安装 Claude 桌面应用,但构建需要 Claude Code,即它单独的命令行工具。请安装 Claude Code,在终端中登录一次,然后重新查找。 Unreal is here, but its bundled Clang C++ toolchain wasn't found. Unreal 已安装,但未找到其自带的 Clang C++ 工具链。 Add Unreal's Linux C++ toolchain 添加 Unreal 的 Linux C++ 工具链 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 5f2d3773..8259e7c4 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -1417,6 +1417,8 @@ Providers 供應商 Harnesses 代理 Nothing of this kind was found. 沒有找到這類項目。 Another model 其他模型 +Fold a project's plan 摺疊專案的計畫 +Fold the plan 摺疊計畫 The Claude desktop app is installed, but building needs Claude Code, its separate command-line tool. Install Claude Code, sign in to it once in a terminal, then look again. 已安裝 Claude 桌面應用程式,但建置需要 Claude Code,也就是它獨立的命令列工具。請安裝 Claude Code,在終端機中登入一次,然後重新尋找。 Unreal is here, but its bundled Clang C++ toolchain wasn't found. Unreal 已安裝,但找不到其內建的 Clang C++ 工具鏈。 Add Unreal's Linux C++ toolchain 新增 Unreal 的 Linux C++ 工具鏈 diff --git a/crates/ui-egui/src/ja.tsv b/crates/ui-egui/src/ja.tsv index 9ea29ee4..11da34e4 100644 --- a/crates/ui-egui/src/ja.tsv +++ b/crates/ui-egui/src/ja.tsv @@ -854,6 +854,8 @@ Providers プロバイダ Harnesses ハーネス Nothing of this kind was found. この種類のものは見つかりませんでした。 Another model 別のモデル +Fold a project's plan プロジェクトのプランを畳む +Fold the plan プランを畳む The Claude desktop app is installed, but building needs Claude Code, its separate command-line tool. Install Claude Code, sign in to it once in a terminal, then look again. Claude デスクトップアプリはインストールされていますが、ビルドには別のコマンドラインツールである Claude Code が必要です。Claude Code をインストールし、ターミナルで一度サインインしてから、もう一度探してください。 Unreal is here, but its bundled Clang C++ toolchain wasn't found. Unreal はインストールされていますが、同梱の Clang C++ ツールチェーンが見つかりません。 Add Unreal's Linux C++ toolchain Unreal の Linux C++ ツールチェーンを追加 diff --git a/crates/ui-egui/src/project.rs b/crates/ui-egui/src/project.rs index fee3b226..c0d95982 100644 --- a/crates/ui-egui/src/project.rs +++ b/crates/ui-egui/src/project.rs @@ -35,6 +35,18 @@ impl CraftApp { self.workspace.as_ref().and_then(|ws| self.runs.get(&ws.root)) } + /// Fold the open project's plan card (#504). Only between runs: the saved conversation keeps + /// the steps, and a later run's checklist shows again. + fn fold_plan(&mut self) { + let Some(ws) = &self.workspace else { return }; + match craft_anything_engine::feed::dismiss_plan_saved(ws) { + Ok(_) => { + craft_anything_engine::feed::dismiss_plan(&mut self.state.feed); + } + Err(e) => self.toast(e.message, true), + } + } + /// The open project's place in the queue (1 starts next). pub fn queue_position(&self) -> Option { self.workspace.as_ref().and_then(|ws| self.runs.position(&ws.root)) @@ -601,21 +613,26 @@ pub enum Thumb { Missing, } -/// The open project's checklist, if the builder made one. +/// The open project's checklist, if the builder made one and the person hasn't folded it (#504). fn plan(feed: &[FeedItem]) -> Option<&[PlanStep]> { - feed.iter().find_map(|i| if let FeedItem::Plan(p) = i { Some(p.as_slice()) } else { None }) + craft_anything_engine::feed::visible_plan(feed) } /// The builder's checklist, pinned above the prompt box: what's done, what it's on now, what's /// next, and while it works how long it has been at it. Shown while it works, and afterwards only -/// if something was left undone. -fn checklist(ui: &mut Ui, steps: &[PlanStep], t: &Tokens, began: Option) { +/// if something was left undone. `fold` offers a control to fold it away (between runs); the +/// returned flag says the person pressed it. +fn checklist(ui: &mut Ui, steps: &[PlanStep], t: &Tokens, began: Option, fold: bool) -> bool { let done = steps.iter().filter(|s| s.status == PlanStatus::Done).count(); + let mut folded = false; widgets::card(t).inner_margin(Margin::symmetric(16, 12)).show(ui, |ui| { ui.set_width(ui.available_width()); ui.horizontal(|ui| { widgets::caption(ui, "Plan"); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if fold && icons::button(ui, "x", 18.0, false, &tr("Fold the plan")).clicked() { + folded = true; + } if let Some(b) = began { ui.label(RichText::new(crate::usage::duration_words(b.elapsed())).font(theme::mono(12.0)).color(t.text_faint)); ui.add(crate::widgets::Spinner::new().size(12.0).color(t.accent)); @@ -655,6 +672,7 @@ fn checklist(ui: &mut Ui, steps: &[PlanStep], t: &Tokens, began: Option work(ui, tools, notes, advanced, &t), FeedItem::Work { .. } => {} // Pinned above the prompt box instead (see `checklist`). - FeedItem::Plan(_) => {} + FeedItem::Plan(_) | FeedItem::PlanDismissed => {} FeedItem::Ask { request, answer } => { if let Some(a) = ask_card(app, ui, request, answer.as_deref()) { answers.push(a); @@ -1665,7 +1683,11 @@ fn composer(app: &mut CraftApp, ui: &mut Ui, width: f32, t: &Tokens) { let running = app.running(); if let Some(steps) = plan(&app.state.feed).filter(|p| running || p.iter().any(|s| s.status != PlanStatus::Done)) { let began = app.open_run().filter(|_| running).map(RebuildRun::started); - ui.allocate_ui(Vec2::new(width, 0.0), |ui| checklist(ui, steps, t, began)); + let mut fold = false; + ui.allocate_ui(Vec2::new(width, 0.0), |ui| fold = checklist(ui, steps, t, began, !running)); + if fold { + app.fold_plan(); + } ui.add_space(8.0); } if let Some(n) = app.queue_position() { diff --git a/crates/ui-egui/tests/headless_parity.rs b/crates/ui-egui/tests/headless_parity.rs index e831827a..3d0073b0 100644 --- a/crates/ui-egui/tests/headless_parity.rs +++ b/crates/ui-egui/tests/headless_parity.rs @@ -55,6 +55,7 @@ const ACTIONS: &[Action] = &[ a("add_project_folder", "list projects from another folder too", Tool(&["settings_set"])), a("remove_project_folder", "stop listing a folder's projects", Tool(&["settings_set"])), a("open_workspace", "show one project, its feed and checks", Tool(&["project_show"])), + a("fold_plan", "fold the builder's plan card away (between builds)", Tool(&["project_plan_fold"])), a("rename_project", "give a project a new name", Tool(&["project_rename"])), a("duplicate_project", "copy a project to try something else", Tool(&["project_duplicate"])), a("delete_project", "delete a project, keeping the person's files", Tool(&["project_delete"])), diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 77d5e7e4..f15fa1a9 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -667,6 +667,12 @@ Gaps found while using the app for real projects. - [ ] A question's own cost: a forked Claude Code session reports the whole session's running total, so a forked answer shows none; count questions in the project's usage - [ ] "Make this change" on an answer that suggests one (sends it as a normal message) +- [x] #504: the builder's plan card can be folded away with its X, or with `project_plan_fold` + (between builds: a fold during a build can be overwritten by its next save). The steps stay + in the conversation (`FEED_VERSION` 4, `FeedItem::PlanDismissed`), and a later run's + checklist shows again — claimed: claude, branch bugfix/r2.9-plan-card-dismiss +- [ ] A run that ends with steps still open marks them as stopped on its card, instead of + showing "Doing" until someone folds it (#504: Claude's runs left the plan unticked) - [x] #666: a long line in a feed's tool-work row (a Deep-mode decompile, a status note) ran off the window and pushed the feed left; the header title can't wrap in egui, so it's the note's first line now, cut at 90 characters, and the full note wraps in the body — claimed: claude, diff --git a/docs/STATUS.md b/docs/STATUS.md index 262bcea6..5b2094fc 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -47,7 +47,7 @@ workspace clippy, layers, fmt and the secret scan of all history were clean. Chi | WebAssembly tag interfaces | `formats_wasm_compare` resolves retained imported and defined tag exports to their declared parameter types. Identical unresolved or invalid diagnostics may still count as unchanged; those counts do not establish interface compatibility, and invalid/partial limitations remain present | Synthetic core modules with tag changes and re-exports, mixed index spaces, import/tag limits, and missing or non-function type references; formats and engine tests. No real module corpus or WebAssembly execution was used for this correction | | Questions and approvals | A file inbox per project; `core_person_ask`, `core_action_approve` and MCP consent requests become cards in the app | Cross-handle round-trip tests, a full approval round trip through the engine | | Runtime assets in pinned builds | Frozen and light pins, size estimates and project copies keep runtime files in `obj` folders. An `obj` directory beside a regular C#, F# or Visual Basic project file is still recognised as compiler output; ambiguous folders are retained. New pins record the direct-launch program relative to the copy so a newer helper cannot replace it; older pins retain discovery behavior | Synthetic filesystem tests cover both pin modes, model bytes, compiler-output controls and project duplication. A Linux tool-level regression starts temporary scripts through `project_pin_open`, with a newer `Build/obj/helper`, and verifies the originally selected program and its output; a missing recorded program is refused. No real pinned application was launched for this correction | -| Desktop app | Simple mode: sidebar with New project and the project history (kept across restarts); New project makes something, works on a project the person already has (Work on my project: its folder's files are the app, edited in place without a question, and the project takes the folder's name), or just looks inside an app or file, a website or a game's files; a Game files screen (a game's archives; pictures from Unreal paks, Unity .assets files and Unity bundles saved as PNG after the person confirms they may unpack the game; uncompressed textures and the block formats DXT1, DXT5 and BC4–BC7), make prompt with picture attachments (picked, dropped, or pasted with Ctrl+V, shown as picture tiles) and, for anything recreated, a Clean room / Deep choice (also in the title bar; in Clean room, Just look inside, Game files, opening a file and the workbench ask for Deep mode instead), project screen (steps with the time spent and the AI cost and tokens of every build, saved in the project's usage.json by the run (a coding agent's figure is shown as an estimate at API prices, not the bill), what the build is on now and its activity as it arrives (kept in `build-activity.log`), pinned builder checklist, feed, question and approval cards, a Continue card when a build was paused or stopped part-way (the app or computer closed; the project shows Interrupted) that carries on the builder's own session instead of starting over, and "It's done" beside Continue or Try again to keep the build as it is (`project_build_finish`), checks with screenshots and the requirements not met yet, the builder's open questions, preview, play, folder, follow-ups while it builds, which Claude Code takes at its next step; Up in the prompt box takes back the last note still waiting). Projects can start in a folder the person picks, whose files are either the app, edited in place (its stack read from those files unless the request names another, and, with nothing to recreate, judged by its own build and tests, whatever it's written in), or material to start from (asked, or the answer remembered in Settings), and are listed from any number of folders (`project_folders`, `project_list`, `rebuild --project-folder`). Make says which AI builds ("Built by": automatic, or any AI that can build, by name); a pick stays with the project and builds its later changes. With nothing to build with, Start offers the coding agents found on the computer. Settings → AI turns an AI off or removes it, and sets how hard the Claude API or Claude Code thinks (every Claude level, low to max; also `ai effort` in the CLI). Each Claude Code AI can keep its sign-in in a folder of its own (its `CLAUDE_CONFIG_DIR`, so its own account; `ai folder` in the CLI, a folder on that computer over SSH), used for its runs, its sign-in check and its kept session, and Add another adds a second Claude Code for a second account. In the prompt box, Up at the very start and Down at the very end go through earlier prompts when no note is waiting. Questions about a build ("Just ask" under the prompt box, or a message starting with `/btw`, the first slash command; `rebuild_question_ask`) are answered in the conversation, where each folds away and is kept in the project, without a build or a round and changing nothing: Claude Code answers from a read-only copy of the builder's own session. Under each project's prompt box, "Builder's questions: Ask me / Decide yourself" chooses whether the builder's questions come to the person as cards or are decided by the builder, which says what it chose (`project_questions_set`; the default is in Settings and follows Auto mode until picked). Projects can be renamed, duplicated and deleted (right-click in the sidebar, or the project's page; a delete keeps the person's own files). A build can be pinned (Pin this build on its page, or `project_pin_*` from the CLI and MCP): a frozen copy in the project's pinned/ folder (the default) or a light one that shares dependencies and media with the project through hard links (each one's size shown first), opened from the sidebar's collapsible Pinned section (icons with a running dot when folded); a pin or Play runs once per folder unless another copy is asked for, and a start that quits at once says so with its last output. Typed drafts survive a restart. In a narrow window the sidebar folds to icons and the project's details move above its feed; messages float over the screen and errors stay until dismissed. Advanced mode: workbench with every panel wired to real tools, as a screen beside the projects (builds and their questions stay a click away) | UI unit and smoke tests (egui_kittest), offscreen snapshots of every screen | +| Desktop app | Simple mode: sidebar with New project and the project history (kept across restarts); New project makes something, works on a project the person already has (Work on my project: its folder's files are the app, edited in place without a question, and the project takes the folder's name), or just looks inside an app or file, a website or a game's files; a Game files screen (a game's archives; pictures from Unreal paks, Unity .assets files and Unity bundles saved as PNG after the person confirms they may unpack the game; uncompressed textures and the block formats DXT1, DXT5 and BC4–BC7), make prompt with picture attachments (picked, dropped, or pasted with Ctrl+V, shown as picture tiles) and, for anything recreated, a Clean room / Deep choice (also in the title bar; in Clean room, Just look inside, Game files, opening a file and the workbench ask for Deep mode instead), project screen (steps with the time spent and the AI cost and tokens of every build, saved in the project's usage.json by the run (a coding agent's figure is shown as an estimate at API prices, not the bill), what the build is on now and its activity as it arrives (kept in `build-activity.log`), pinned builder checklist (its X folds it away between builds, kept in the project, `project_plan_fold`; a later run's checklist shows again), feed, question and approval cards, a Continue card when a build was paused or stopped part-way (the app or computer closed; the project shows Interrupted) that carries on the builder's own session instead of starting over, and "It's done" beside Continue or Try again to keep the build as it is (`project_build_finish`), checks with screenshots and the requirements not met yet, the builder's open questions, preview, play, folder, follow-ups while it builds, which Claude Code takes at its next step; Up in the prompt box takes back the last note still waiting). Projects can start in a folder the person picks, whose files are either the app, edited in place (its stack read from those files unless the request names another, and, with nothing to recreate, judged by its own build and tests, whatever it's written in), or material to start from (asked, or the answer remembered in Settings), and are listed from any number of folders (`project_folders`, `project_list`, `rebuild --project-folder`). Make says which AI builds ("Built by": automatic, or any AI that can build, by name); a pick stays with the project and builds its later changes. With nothing to build with, Start offers the coding agents found on the computer. Settings → AI turns an AI off or removes it, and sets how hard the Claude API or Claude Code thinks (every Claude level, low to max; also `ai effort` in the CLI). Each Claude Code AI can keep its sign-in in a folder of its own (its `CLAUDE_CONFIG_DIR`, so its own account; `ai folder` in the CLI, a folder on that computer over SSH), used for its runs, its sign-in check and its kept session, and Add another adds a second Claude Code for a second account. In the prompt box, Up at the very start and Down at the very end go through earlier prompts when no note is waiting. Questions about a build ("Just ask" under the prompt box, or a message starting with `/btw`, the first slash command; `rebuild_question_ask`) are answered in the conversation, where each folds away and is kept in the project, without a build or a round and changing nothing: Claude Code answers from a read-only copy of the builder's own session. Under each project's prompt box, "Builder's questions: Ask me / Decide yourself" chooses whether the builder's questions come to the person as cards or are decided by the builder, which says what it chose (`project_questions_set`; the default is in Settings and follows Auto mode until picked). Projects can be renamed, duplicated and deleted (right-click in the sidebar, or the project's page; a delete keeps the person's own files). A build can be pinned (Pin this build on its page, or `project_pin_*` from the CLI and MCP): a frozen copy in the project's pinned/ folder (the default) or a light one that shares dependencies and media with the project through hard links (each one's size shown first), opened from the sidebar's collapsible Pinned section (icons with a running dot when folded); a pin or Play runs once per folder unless another copy is asked for, and a start that quits at once says so with its last output. Typed drafts survive a restart. In a narrow window the sidebar folds to icons and the project's details move above its feed; messages float over the screen and errors stay until dismissed. Advanced mode: workbench with every panel wired to real tools, as a screen beside the projects (builds and their questions stay a click away) | UI unit and smoke tests (egui_kittest), offscreen snapshots of every screen | | CLI | Every tool, `--deep` or `--clean-room` for one run (never saved), `rebuild` (including `--continue-in`, and `--resume` to carry on a stopped build), `mcp --inbox`, `ai`, `setup`, `doctor`, `report` | CLI integration tests | | SQLite database files (`db_sqlite_show`) | A rowid alias column (`INTEGER PRIMARY KEY`, including the table-constraint form) shows the rowid; `DESC`, non-`INTEGER` and composite keys keep their stored values | Hand-built fixtures; generated files compared with the official SQLite library | | SQLite WITHOUT ROWID tables | Read from the index b-tree, interior records and overflow included; rows in primary-key order, values in declaration order, `rowid` null | Hand-built fixtures; generated files compared with the official SQLite library | diff --git a/docs/file-formats.md b/docs/file-formats.md index fa602944..f1db3815 100644 --- a/docs/file-formats.md +++ b/docs/file-formats.md @@ -18,7 +18,7 @@ version up to the current one, then turn the document into Rust types. The step | `/questions.json` | `engine::questions` (`AskedQuestion`, written by `rebuild_question_ask`) | `QUESTIONS_VERSION` = 1 (`{version, items}`) | read as before | read as far as its fields are known | | `/pinned//pin.json` | `engine::pins` (`Pin`) | `PIN_VERSION` = 2 (`version`; missing = 1; 2 records the selected program for a copied direct launch) | brought forward | read as far as its fields are known | | `/standin.json` | `engine::tools::standin_tools` (`analysis::standin::StandIn`; written only by `web_standin_build`) | `STANDIN_VERSION` = 1 (`version`, required) | brought forward | refused, with "update Craft Anything", and never written over | -| `/feed.json` | `ai::rebuild::feed` (written by the run) | `FEED_VERSION` = 3 (`{version, items}`; a bare list = 0) | read as before, saved as current | never written over; the conversation is rebuilt from the project for display | +| `/feed.json` | `ai::rebuild::feed` (written by the run) | `FEED_VERSION` = 4 (`{version, items}`; a bare list = 0; 4 adds `PlanDismissed`, a folded plan card) | read as before, saved as current | never written over; the conversation is rebuilt from the project for display | A saved `User` message with pictures is `{text, pictures: [paths]}`, where the pictures are the copies in the project's `attachments/`; one without pictures stays the plain string earlier diff --git a/docs/tools.md b/docs/tools.md index 7468f56c..32d8e02e 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -2,7 +2,7 @@ Generated by `cargo xtask tools`. Do not edit by hand. -**300 engine tools** in 17 toolsets. Each is an MCP tool, a CLI command (`craft-anything-cli `) and a tool the agent can call. The AI and setup layers add their own on top (`rebuild_*`, `assets_key_*`, `ai_models_list`, `ai_model_set`, `setup_*`); `craft-anything-cli commands` lists them all. [OpenCode model and thinking selection](opencode.md) describes the AI model tools. +**301 engine tools** in 17 toolsets. Each is an MCP tool, a CLI command (`craft-anything-cli `) and a tool the agent can call. The AI and setup layers add their own on top (`rebuild_*`, `assets_key_*`, `ai_models_list`, `ai_model_set`, `setup_*`); `craft-anything-cli commands` lists them all. [OpenCode model and thinking selection](opencode.md) describes the AI model tools. ## Essentials (`core`, 35) @@ -312,7 +312,7 @@ Generated by `cargo xtask tools`. Do not edit by hand. - `db_sqlite_show`: SQLite database contents -## Projects (`projects`, 19) +## Projects (`projects`, 20) - `project_list`: List projects - `project_show`: Show a project @@ -321,6 +321,7 @@ Generated by `cargo xtask tools`. Do not edit by hand. - `project_preview`: Preview a project's website - `project_open_folder`: Open a project's folder - `project_rename`: Rename a project +- `project_plan_fold`: Fold a project's plan - `project_duplicate`: Duplicate a project - `project_delete`: Delete a project - `project_questions_set`: Choose what the builder's questions get