From b6f61d15ffb48bc610a45d58996b9edcbd7ce5b3 Mon Sep 17 00:00:00 2001 From: BFlat Date: Wed, 4 Nov 2026 12:58:46 +0700 Subject: [PATCH 1/2] Say when the Claude desktop app is installed but Claude Code isn't (#789) When the Claude desktop app is installed but Claude Code isn't, the setup card and ai detect say so and how to install Claude Code (refs #259). Supersedes #781. Co-authored-by: FuryNocturn <28540819+FuryNocturn@users.noreply.github.com> --- apps/craft-anything-cli/src/ai_cmd.rs | 4 ++ crates/ai/src/detect.rs | 81 +++++++++++++++++++++++++++ crates/ai/src/lib.rs | 5 ++ crates/ui-egui/src/builders.rs | 10 ++++ crates/ui-egui/src/fr.tsv | 1 + crates/ui-egui/src/i18n/cs.tsv | 1 + crates/ui-egui/src/i18n/de.tsv | 1 + crates/ui-egui/src/i18n/ko.tsv | 1 + crates/ui-egui/src/i18n/ru.tsv | 1 + crates/ui-egui/src/i18n/zh-hans.tsv | 1 + crates/ui-egui/src/i18n/zh-hant.tsv | 1 + crates/ui-egui/src/ja.tsv | 1 + crates/ui-egui/src/lib.rs | 11 +++- crates/ui-egui/tests/smoke.rs | 23 ++++++++ docs/ROADMAP.md | 3 +- docs/STATUS.md | 1 + 16 files changed, 142 insertions(+), 4 deletions(-) diff --git a/apps/craft-anything-cli/src/ai_cmd.rs b/apps/craft-anything-cli/src/ai_cmd.rs index c3adef50..89862711 100644 --- a/apps/craft-anything-cli/src/ai_cmd.rs +++ b/apps/craft-anything-cli/src/ai_cmd.rs @@ -225,6 +225,10 @@ pub fn ai(cli: &Cli, c: &AiCommand, f: Format) -> u8 { } AiCommand::Detect => { emit(&serde_json::to_value(hub.detect()).unwrap_or(Value::Null), f); + // On stderr, so the list on stdout stays what scripts read. + for hint in hub.detect_hints() { + eprintln!("note: {} {}", hint.message(), hint.remediation()); + } 0 } AiCommand::Add { name, kind, model, base_url, ssh_host, key_env, key_stdin } => { diff --git a/crates/ai/src/detect.rs b/crates/ai/src/detect.rs index 28ba6119..41b3cca2 100644 --- a/crates/ai/src/detect.rs +++ b/crates/ai/src/detect.rs @@ -157,6 +157,59 @@ pub fn agent_clis() -> Vec { .collect() } +/// What a scan found that isn't an AI to use but explains why one is missing. +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum DetectHint { + /// The Claude desktop app is installed, but not Claude Code, the `claude` command builds run: + /// "Claude" installed from its website wasn't found and nothing said why (#259). + ClaudeDesktopWithoutCli { app: PathBuf }, +} + +impl DetectHint { + pub fn message(&self) -> String { + match self { + Self::ClaudeDesktopWithoutCli { app } => { + format!("The Claude desktop app is installed ({}), but Claude Code, the `claude` command-line tool building uses, isn't.", app.display()) + } + } + } + + pub fn remediation(&self) -> &'static str { + match self { + Self::ClaudeDesktopWithoutCli { .. } => "Install Claude Code apart from the desktop app, sign in to it once in a terminal, then look again.", + } + } +} + +/// Hints for what [`agent_clis`] can't offer. +pub fn hints() -> Vec { + if craft_anything_platform::which("claude").is_some() { + return Vec::new(); + } + let home = std::env::var_os(if cfg!(windows) { "USERPROFILE" } else { "HOME" }).map(PathBuf::from); + let local_data = std::env::var_os("LOCALAPPDATA").map(PathBuf::from); + claude_desktop_app(home.as_deref(), local_data.as_deref()).map(|app| DetectHint::ClaudeDesktopWithoutCli { app }).into_iter().collect() +} + +/// Where the Claude desktop app is, if it's installed. On Windows the Store package leaves a +/// `claude-desktop` app alias (a link that only `symlink_metadata` can read) and the older +/// installer an `AnthropicClaude` folder; on macOS it's `Claude.app`. There's no Linux app. +fn claude_desktop_app(home: Option<&std::path::Path>, local_data: Option<&std::path::Path>) -> Option { + let places: Vec = if cfg!(windows) { + let local_data = local_data.map(PathBuf::from).or_else(|| home.map(|h| h.join("AppData").join("Local"))); + local_data + .into_iter() + .flat_map(|d| [d.join("Microsoft").join("WindowsApps").join("claude-desktop.exe"), d.join("AnthropicClaude").join("claude.exe")]) + .collect() + } else if cfg!(target_os = "macos") { + std::iter::once(PathBuf::from("/Applications/Claude.app")).chain(home.map(|h| h.join("Applications").join("Claude.app"))).collect() + } else { + Vec::new() + }; + places.into_iter().find(|p| std::fs::symlink_metadata(p).is_ok()) +} + /// The most servers taken from OpenCode's settings. const OPENCODE_MAX: usize = 6; @@ -323,6 +376,34 @@ mod model_id_tests; mod tests { use super::*; + /// #259: the desktop app is found where its installers put it, and nothing is made up where + /// there's none. + #[test] + fn the_claude_desktop_app_is_found_where_its_installers_put_it() { + let empty = tempfile::tempdir().unwrap(); + let dir = tempfile::tempdir().unwrap(); + let app = dir.path().join("AnthropicClaude").join("claude.exe"); + std::fs::create_dir_all(app.parent().unwrap()).unwrap(); + std::fs::write(&app, b"app").unwrap(); + let mac = dir.path().join("Applications").join("Claude.app"); + std::fs::create_dir_all(&mac).unwrap(); + let found = claude_desktop_app(Some(dir.path()), Some(dir.path())); + if cfg!(windows) { + assert_eq!(found, Some(app)); + assert_eq!(claude_desktop_app(Some(empty.path()), Some(empty.path())), None); + // The Store package's alias wins over an old install. + let alias = dir.path().join("Microsoft").join("WindowsApps").join("claude-desktop.exe"); + std::fs::create_dir_all(alias.parent().unwrap()).unwrap(); + std::fs::write(&alias, b"").unwrap(); + assert_eq!(claude_desktop_app(None, Some(dir.path())), Some(alias)); + } else if cfg!(target_os = "linux") { + assert_eq!(found, None, "there's no Claude desktop app for Linux"); + } + let hint = DetectHint::ClaudeDesktopWithoutCli { app: PathBuf::from("Claude") }; + assert!(hint.message().contains("desktop app") && hint.message().contains("`claude`"), "{}", hint.message()); + assert_eq!(serde_json::to_value(&hint).unwrap()["kind"], "claude_desktop_without_cli"); + } + /// Non-Git SSH hosts offer both supported agent suggestions without probing the remote computer. #[test] fn ssh_hosts_offer_opencode_as_well_as_claude_code() { diff --git a/crates/ai/src/lib.rs b/crates/ai/src/lib.rs index b2b9758a..e15dbea4 100644 --- a/crates/ai/src/lib.rs +++ b/crates/ai/src/lib.rs @@ -422,6 +422,11 @@ impl AiHub { leave_out_set_up(detect::detect_all(), &self.backends()) } + /// Why a coding agent people expect may not be found (only the Claude desktop app, say). + pub fn detect_hints(&self) -> Vec { + detect::hints() + } + /// Host aliases from `~/.ssh/config`, for the "run on another machine" picker. pub fn ssh_hosts(&self) -> Vec { ssh::ssh_hosts() diff --git a/crates/ui-egui/src/builders.rs b/crates/ui-egui/src/builders.rs index 1b51f73a..c904ac9c 100644 --- a/crates/ui-egui/src/builders.rs +++ b/crates/ui-egui/src/builders.rs @@ -404,6 +404,16 @@ pub fn need_builder_modal(app: &mut CraftApp, ui: &mut Ui, start: bool, close: & }); } else if found.is_empty() { ui.label(RichText::new(tr("No coding agent was found on this computer yet.")).color(t.text_dim)); + for hint in &app.detect_hints { + match hint { + craft_anything_ai::detect::DetectHint::ClaudeDesktopWithoutCli { .. } => ui.add( + egui::Label::new( + RichText::new(tr("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.")).color(t.text_dim), + ) + .wrap(), + ), + }; + } } for b in found { widgets::panel(&t).show(ui, |ui| { diff --git a/crates/ui-egui/src/fr.tsv b/crates/ui-egui/src/fr.tsv index 10567ddc..8b9ded78 100644 --- a/crates/ui-egui/src/fr.tsv +++ b/crates/ui-egui/src/fr.tsv @@ -852,6 +852,7 @@ Providers Fournisseurs Harnesses Agents Nothing of this kind was found. Rien de ce type n'a été trouvé. Another model Autre modèle +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 Run Engine/Build/BatchFiles/Linux/SetupToolchain.sh in the engine folder to install its bundled Clang toolchain. If you built Unreal from source, Setup.sh downloads the native toolchain instead. A system Clang may not match the engine's version. Dans le dossier du moteur, exécutez Engine/Build/BatchFiles/Linux/SetupToolchain.sh pour installer sa chaîne d'outils Clang intégrée. Si vous compilez depuis les sources, Setup.sh télécharge à la place la chaîne d'outils native. Un Clang système peut ne pas correspondre à la version du moteur. diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index b06d0cfe..0f9cda06 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -1417,6 +1417,7 @@ Providers Poskytovatelé Harnesses Agenti Nothing of this kind was found. Nic takového nebylo nalezeno. Another model Jiný model +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 Run Engine/Build/BatchFiles/Linux/SetupToolchain.sh in the engine folder to install its bundled Clang toolchain. If you built Unreal from source, Setup.sh downloads the native toolchain instead. A system Clang may not match the engine's version. Ve složce enginu spusťte Engine/Build/BatchFiles/Linux/SetupToolchain.sh a nainstaluje se přibalený nástrojový řetězec Clang. Při sestavení ze zdrojů Setup.sh místo toho stáhne nativní nástrojový řetězec. Systémový Clang nemusí odpovídat verzi enginu. diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 444c97eb..da571497 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -1417,6 +1417,7 @@ Providers Anbieter Harnesses Agenten Nothing of this kind was found. Nichts dieser Art wurde gefunden. Another model Anderes Modell +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 Run Engine/Build/BatchFiles/Linux/SetupToolchain.sh in the engine folder to install its bundled Clang toolchain. If you built Unreal from source, Setup.sh downloads the native toolchain instead. A system Clang may not match the engine's version. Führe im Engine-Ordner Engine/Build/BatchFiles/Linux/SetupToolchain.sh aus, um die mitgelieferte Clang-Toolchain zu installieren. Bei einem Quellcode-Build lädt Setup.sh stattdessen die native Toolchain herunter. Ein System-Clang passt möglicherweise nicht zur Engine-Version. diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index a23ae8f6..edfbf6a6 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -1417,6 +1417,7 @@ Providers 제공자 Harnesses 에이전트 Nothing of this kind was found. 이런 종류는 찾지 못했습니다. Another model 다른 모델 +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++ 도구 체인 추가 Run Engine/Build/BatchFiles/Linux/SetupToolchain.sh in the engine folder to install its bundled Clang toolchain. If you built Unreal from source, Setup.sh downloads the native toolchain instead. A system Clang may not match the engine's version. 엔진 폴더에서 Engine/Build/BatchFiles/Linux/SetupToolchain.sh를 실행해 함께 제공되는 Clang 도구 체인을 설치하세요. 소스 빌드라면 Setup.sh가 대신 네이티브 도구 체인을 내려받습니다. 시스템 Clang은 엔진 버전과 맞지 않을 수 있습니다. diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index e5eb45ee..54edd41c 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -1417,6 +1417,7 @@ Providers Провайдеры Harnesses Агенты Nothing of this kind was found. Ничего такого не найдено. Another model Другая модель +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 Run Engine/Build/BatchFiles/Linux/SetupToolchain.sh in the engine folder to install its bundled Clang toolchain. If you built Unreal from source, Setup.sh downloads the native toolchain instead. A system Clang may not match the engine's version. Запустите Engine/Build/BatchFiles/Linux/SetupToolchain.sh в папке движка, чтобы установить встроенную цепочку инструментов Clang. При сборке из исходников Setup.sh вместо этого скачивает нативную цепочку инструментов. Системный Clang может не подходить к версии движка. diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 11f1b60b..660f372a 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -1417,6 +1417,7 @@ Providers 提供商 Harnesses 代理 Nothing of this kind was found. 没有找到这类项目。 Another model 其他模型 +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++ 工具链 Run Engine/Build/BatchFiles/Linux/SetupToolchain.sh in the engine folder to install its bundled Clang toolchain. If you built Unreal from source, Setup.sh downloads the native toolchain instead. A system Clang may not match the engine's version. 在引擎文件夹中运行 Engine/Build/BatchFiles/Linux/SetupToolchain.sh,安装其自带的 Clang 工具链。如果是源码构建,Setup.sh 会改为下载本机工具链。系统 Clang 可能与引擎版本不匹配。 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 6184f60d..5f2d3773 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -1417,6 +1417,7 @@ Providers 供應商 Harnesses 代理 Nothing of this kind was found. 沒有找到這類項目。 Another model 其他模型 +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++ 工具鏈 Run Engine/Build/BatchFiles/Linux/SetupToolchain.sh in the engine folder to install its bundled Clang toolchain. If you built Unreal from source, Setup.sh downloads the native toolchain instead. A system Clang may not match the engine's version. 在引擎資料夾中執行 Engine/Build/BatchFiles/Linux/SetupToolchain.sh,安裝其內建的 Clang 工具鏈。如果是原始碼建置,Setup.sh 會改為下載原生工具鏈。系統 Clang 可能與引擎版本不相符。 diff --git a/crates/ui-egui/src/ja.tsv b/crates/ui-egui/src/ja.tsv index d1e63a37..9ea29ee4 100644 --- a/crates/ui-egui/src/ja.tsv +++ b/crates/ui-egui/src/ja.tsv @@ -854,6 +854,7 @@ Providers プロバイダ Harnesses ハーネス Nothing of this kind was found. この種類のものは見つかりませんでした。 Another model 別のモデル +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++ ツールチェーンを追加 Run Engine/Build/BatchFiles/Linux/SetupToolchain.sh in the engine folder to install its bundled Clang toolchain. If you built Unreal from source, Setup.sh downloads the native toolchain instead. A system Clang may not match the engine's version. エンジンのフォルダーで Engine/Build/BatchFiles/Linux/SetupToolchain.sh を実行し、同梱の Clang ツールチェーンをインストールします。ソースからビルドした場合は、代わりに Setup.sh がネイティブツールチェーンをダウンロードします。システムの Clang はエンジンのバージョンと一致しない場合があります。 diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 5de7de7f..9b5a9b88 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -152,7 +152,8 @@ pub enum Done { path: PathBuf, result: Result, }, - Detected(Vec), + /// What a scan found, and why an expected coding agent may be missing. + Detected(Vec, Vec), /// The configured backends, the ones chosen to build with and to extract with, and each one's /// options. Backends(Vec, Option, Option, std::collections::BTreeMap), @@ -443,6 +444,8 @@ pub struct CraftApp { /// The backend chosen to take a game's files out in Deep mode, if any. pub extractor: Option, pub detected: Option>, + /// Why an expected coding agent may be missing, from the last scan. + pub detect_hints: Vec, pub detecting: bool, /// Counts AI choices, so an add that finishes later doesn't undo one made meanwhile. pub backend_choices: u64, @@ -559,6 +562,7 @@ impl CraftApp { builder: None, extractor: None, detected: None, + detect_hints: Vec::new(), detecting: false, backend_choices: 0, log: Vec::new(), @@ -1182,7 +1186,7 @@ impl CraftApp { } self.detecting = true; let hub = self.hub.clone(); - self.jobs.spawn("Look for AI on this computer", true, move |_| Ok(Done::Detected(hub.detect()))); + self.jobs.spawn("Look for AI on this computer", true, move |_| Ok(Done::Detected(hub.detect(), hub.detect_hints()))); } pub fn add_backend(&mut self, config: BackendConfig, key: Option) { @@ -1433,7 +1437,8 @@ impl CraftApp { self.toast(e.for_people(), true); } }, - Done::Detected(d) => { + Done::Detected(d, hints) => { + self.detect_hints = hints; self.detecting = false; self.log(false, format!("Found {} AI option(s) on this computer", d.len())); self.detected = Some(d); diff --git a/crates/ui-egui/tests/smoke.rs b/crates/ui-egui/tests/smoke.rs index bdec9e50..b5e8998c 100644 --- a/crates/ui-egui/tests/smoke.rs +++ b/crates/ui-egui/tests/smoke.rs @@ -2295,6 +2295,29 @@ fn with_nothing_to_build_with_start_offers_what_was_found_instead_of_a_dead_end( h.get_by_label("Build with Claude Code"); } +/// #259: with only the Claude desktop app installed, the dialog says that building needs Claude +/// Code apart from it, instead of only "No coding agent was found". +#[test] +fn only_the_claude_desktop_app_is_named_when_nothing_can_build() { + use craft_anything_ai::detect::DetectHint; + let mut h = harness(|| { + let mut a = demo::app(demo::engine(Mode::Beginner, true)); + a.backends.clear(); + a.detected = Some(Vec::new()); + a + }); + h.run_steps(3); + h.state_mut().state.make.request = "A notes app".into(); + h.state_mut().start_make(); + // The dialog looks on this computer when it opens; let that finish before standing in for it. + assert!(wait(&mut h, |a| !a.detecting)); + h.state_mut().detected = Some(Vec::new()); + h.state_mut().detect_hints = vec![DetectHint::ClaudeDesktopWithoutCli { app: "Claude".into() }]; + h.run_steps(2); + h.get_by_label("No coding agent was found on this computer yet."); + h.get_by_label("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."); +} + #[test] fn an_ai_can_be_turned_off_and_removed_in_settings() { use craft_anything_ai::rebuild::StandIns; diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 68674b2a..77d5e7e4 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -574,8 +574,9 @@ Owner: open · Status: in progress Cause is not confirmed on the reporter's machine (needs their config.toml); the fix covers the switched-off case that the code leaves out (`is_on`, `leave_out_set_up`, `builder_backend`). — claimed: fabox-os, branch bugfix/423-disabled-agent-not-offered -- [ ] Say plainly when what's found is the Claude desktop app rather than the `claude` program +- [x] Say plainly when what's found is the Claude desktop app rather than the `claude` program (#259, a guess at its cause) +- [ ] macOS: check that `Claude.app` is where the desktop app is found (#259's hint is untried there) - [x] macOS: `/Applications/Antigravity.app` ended up as the agent's command (#467). Found: the "Where is the program?" picker (`pick_file`) returns the bundle, and `set_command` saved any path. Refuse a folder or app bundle when it's saved (`set_command`), and don't run one that was saved before diff --git a/docs/STATUS.md b/docs/STATUS.md index f318b6c6..262bcea6 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -13,6 +13,7 @@ workspace clippy, layers, fmt and the secret scan of all history were clean. Chi | SQLite payload limits | Capped records retain column positions, disclose partial TEXT and BLOB values, omit incomplete BLOB hashes, and report partial coverage with read notes | Hand-built overflow-page regressions for five oversized records (including WITHOUT ROWID in declaration order) and two controls; table and index engine coverage and limitation regressions | | SQLite added columns | Short records include missing trailing fields using literal column defaults, or NULL when absent; unsupported default expressions produce NULL with a row note. Stored fields and records longer than the schema retain their values. Defaults are filled before restoring rowid aliases | Nine hand-built parser regressions and an engine tool test cover added-column defaults, rewritten records and short records with a rowid alias, including a missing alias field | | OpenCode models and thinking | OpenCode V2 locally and over SSH; Settings → AI lists only that instance's enabled chat models and supported Low/Medium/High/Xhigh/Max variants. Selection is validated and saved through UI, CLI and `ai_models_list` / `ai_model_set`. Explicit-selection execution is refused until native MCP integration exists; local ACP runs require clearing the selection. See [OpenCode](opencode.md). | Stand-in catalogue/process, persistence, remote-command and UI tests; local live catalogue checks (34 models, all five depths represented). Regression checks reject native chat/builds before process I/O with ACP on/off, local/SSH and model-only/depth-only/combined selections. No live SSH host or paid generation used | +| Claude desktop app without Claude Code | When the `claude` command isn't found but the Claude desktop app is (Windows: the Store package's `claude-desktop` app alias or the older installer's `AnthropicClaude` folder; macOS: `Claude.app`), the builder dialog under "No coding agent was found" and `ai detect` (on stderr) say the desktop app is installed but building needs Claude Code, installed apart (#259). Typed as `DetectHint` from `AiHub::detect_hints` | A regression finds the app in temp folders laid out as each Windows installer leaves it, and nothing on Linux; a UI test shows the hint in the dialog. On Windows 10 the real Store package's alias was found. Not checked: macOS, the older installer, and a computer with no `claude` installed | | Missing program paths | A program started by its path (a saved agent program, for one) that has no file there is reported with that path, as possibly moved by an update, with reason `path_missing`, instead of "isn't installed or isn't on your PATH"; a bare name still speaks of PATH. A relative path is looked up where the program starts (on Unix, its working folder), so a script there whose interpreter is missing isn't called a path with no file. It stays a missing program (`EngineMissing`), so builder turns aren't retried (#721) | A regression starts a program at a path with no file on Windows 10 and checks the message, remediation and reason, with a bare-name control; on Linux (Docker), a relative script with a missing interpreter started in its folder, through `Process` and `Launch`. Not checked: a real Codex desktop update, or the app's Settings screen with a stale path | | Mach-O Objective-C pointers | Chained offsets mapped only into file-empty `__PAGEZERO` reach the image-base fallback; pointer candidates must be backed by file bytes | Synthetic thin and fat Mach-O regression, plain-pointer and authenticated-rebase controls, null and unmapped-pointer checks. Real binaries were not checked for this correction | | Apple app graphs | Selecting a macOS bundle recognises its root-relative `Contents/Info.plist` and retains its service/extension type; nested XPC bundles appear in `services` and as `xpc-service` graph nodes, including their executables when hosted in frameworks | Synthetic inventory tests, including parent-directory and flat bundle layouts, selected services/extensions, framework-hosted service executables, and bare-program controls. Ordinary selected directories with `Contents/Info.plist` need an Apple bundle suffix; archives retain layout-based recognition. Framework libraries and dylibs stay excluded from executables. Real app bundles were not checked for this correction | From cb395ee2c620ce476227e856e1e2dfa957fb6f88 Mon Sep 17 00:00:00 2001 From: BFlat Date: Wed, 4 Nov 2026 16:01:37 +0700 Subject: [PATCH 2/2] 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