mirror of
https://github.com/storytold/craft-anything.git
synced 2026-11-02 18:06:58 +00:00
"It's done" keeps a build as it is and stops offering to carry on (#221)
A paused, stopped or failed build only offered Continue or Try again; there was no way to say "enough now". project_build_finish marks the project Ready and adds a Done step to its conversation, keeping everything; the project screen shows It's done beside Continue and Try again when nothing is running. Fixes #210
This commit is contained in:
@@ -56,6 +56,7 @@ pub const TOOLS: &[&str] = &[
|
||||
"project_duplicate",
|
||||
"project_delete",
|
||||
"project_questions_set",
|
||||
"project_build_finish",
|
||||
"project_pin_create",
|
||||
"project_pins_list",
|
||||
"project_pin_open",
|
||||
|
||||
@@ -130,6 +130,17 @@ pub fn specs() -> Vec<ToolSpec> {
|
||||
input_schema: schema_of::<QuestionsParams>,
|
||||
run: project_questions_set,
|
||||
},
|
||||
ToolSpec {
|
||||
id: "project_build_finish",
|
||||
title: "Call the build done",
|
||||
summary: "The person is happy with the build as it is (#210): its status becomes Ready and the conversation says they called it done, so a paused, stopped or failed build no longer offers to carry on. Everything so far is kept, and a new message carries on from here. It doesn't stop a build that is still running: pause it first.",
|
||||
beginner: "Keeps the build as it is and stops offering to carry on.",
|
||||
toolset: Toolset::Projects,
|
||||
effects: Effects::WRITES_FILESYSTEM,
|
||||
requires: Requires::Nothing,
|
||||
input_schema: schema_of::<FinishParams>,
|
||||
run: project_build_finish,
|
||||
},
|
||||
ToolSpec {
|
||||
id: "project_assets_budget",
|
||||
title: "Set a project's asset budget",
|
||||
@@ -292,6 +303,27 @@ pub enum QuestionsChoice {
|
||||
Default,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct FinishParams {
|
||||
/// The project's folder.
|
||||
pub project: PathBuf,
|
||||
}
|
||||
|
||||
/// What the conversation and the project say when the person calls a build done.
|
||||
pub const CALLED_DONE: &str = "Ready: you called it done. Everything so far is kept; a new message carries on from here.";
|
||||
|
||||
fn project_build_finish(_ctx: &mut Ctx<'_>, params: Value) -> Result<ToolOutput, ToolError> {
|
||||
use crate::workspace::{Status, Step};
|
||||
let p: FinishParams = parse_params(params)?;
|
||||
let mut ws = open(&p.project)?;
|
||||
ws.set_status(Status::Ready, CALLED_DONE)?;
|
||||
let mut items = feed::load(&ws);
|
||||
items.push(feed::FeedItem::Step { step: Step::Done, text: CALLED_DONE.into() });
|
||||
feed::save(&ws.root, &items)?;
|
||||
Ok(ToolOutput::Value(json!({"project": ws.root, "status": ws.manifest.status})))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct QuestionsParams {
|
||||
@@ -518,6 +550,28 @@ mod tests {
|
||||
use crate::workspace::TargetRef;
|
||||
use crate::{CallOptions, Caller, Engine};
|
||||
|
||||
/// #210: "enough now". A paused build becomes Ready, says so in its conversation, and keeps
|
||||
/// everything it had.
|
||||
#[test]
|
||||
fn a_build_called_done_is_ready_and_says_so() {
|
||||
use crate::workspace::{Status, Step};
|
||||
let d = tempfile::tempdir().unwrap();
|
||||
let mut ws = Workspace::create(d.path(), "Site", "A website", TargetRef::None).unwrap();
|
||||
ws.set_status(Status::Stopped, "Paused.").unwrap();
|
||||
let e = Engine::new(crate::testing::session());
|
||||
let r = e.call("project_build_finish", json!({"project": ws.root}), CallOptions::new(Caller::Cli)).unwrap().to_json();
|
||||
assert_eq!(r["result"]["status"], "ready", "{r}");
|
||||
let ws = Workspace::open(&ws.root).unwrap();
|
||||
assert_eq!((ws.manifest.status, ws.manifest.note.as_str()), (Status::Ready, CALLED_DONE));
|
||||
let items = feed::load(&ws);
|
||||
assert!(
|
||||
matches!(items.as_slice(), [feed::FeedItem::User(a), feed::FeedItem::Step { step: Step::Done, text }] if a == "A website" && text == CALLED_DONE),
|
||||
"{items:?}"
|
||||
);
|
||||
let err = e.call("project_build_finish", json!({"project": d.path().join("nope")}), CallOptions::new(Caller::Cli)).unwrap_err();
|
||||
assert!(err.message.contains("isn't a project folder"), "{err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_show_and_answer_a_project_without_the_app() {
|
||||
let d = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -194,6 +194,8 @@ Built with 构建工具
|
||||
Built with {stack}, as you asked, so no game engine is needed. 已按你的要求使用 {stack} 构建,因此不需要游戏引擎。
|
||||
Call graph 调用图
|
||||
Call path 调用路径
|
||||
Call the build done 将构建标为完成
|
||||
Called done. A new message carries on from here. 已标为完成。发送新消息即可从这里继续。
|
||||
Cancel 取消
|
||||
Cancel: don't use this folder 取消:不使用此文件夹
|
||||
Capture a website for rebuilding 采集网站用于重建
|
||||
@@ -544,6 +546,7 @@ It isn't on this computer yet. Craft Anything can download Ghidra {version} and
|
||||
It isn't on this computer yet. Install Ghidra {version} from its release page, then choose its folder. 这台计算机尚未安装 Ghidra。请从发布页面安装 Ghidra {version},然后选择其文件夹。
|
||||
It's already open. 它已经打开了。
|
||||
It's already open. Right-click it to open another copy. 已经打开。右键点击可再打开一个副本。
|
||||
It's done 就这样吧
|
||||
It's mine, or I'm allowed to look 这是我的,或我已获准查看
|
||||
Its copy is deleted, with anything it saved there. The project itself stays as it is. 将删除此副本及其保存的所有内容。项目本身保持不变。
|
||||
Its folder is deleted with everything in it: what was built, the checks and the conversation. 将删除其文件夹及其中的全部内容:构建成果、检查结果和对话。
|
||||
@@ -563,6 +566,7 @@ Just look inside 只查看内部
|
||||
Keep going without asking 继续执行,无须询问
|
||||
Keep it 保留
|
||||
Keep it in the sidebar to open any time, whatever later builds change. 将其保留在侧栏中,随时打开,不受后续构建改动的影响。
|
||||
Keep the build as it is and stop offering to carry on. 保留构建的现状,不再提示继续。
|
||||
Keep this version in the sidebar to open any time. 将此版本保留在侧栏中,随时打开。
|
||||
Keeps this version exactly as it is now. Later builds can't change or break it, and anything it saves stays with the copy. 完整保留当前版本。后续构建无法修改或破坏它,它保存的所有内容也会留在此副本中。
|
||||
Key saved 密钥已保存
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 48 and column 52.
|
@@ -194,6 +194,8 @@ Built with 建置工具
|
||||
Built with {stack}, as you asked, so no game engine is needed. 已依你的要求使用 {stack} 建置,因此不需要遊戲引擎。
|
||||
Call graph 呼叫圖
|
||||
Call path 呼叫路徑
|
||||
Call the build done 將建置標為完成
|
||||
Called done. A new message carries on from here. 已標為完成。傳送新訊息即可從這裡繼續。
|
||||
Cancel 取消
|
||||
Cancel: don't use this folder 取消:不使用此資料夾
|
||||
Capture a website for rebuilding 擷取網站用於重建
|
||||
@@ -544,6 +546,7 @@ It isn't on this computer yet. Craft Anything can download Ghidra {version} and
|
||||
It isn't on this computer yet. Install Ghidra {version} from its release page, then choose its folder. 這台電腦尚未安裝 Ghidra。請從發佈頁面安裝 Ghidra {version},然後選擇其資料夾。
|
||||
It's already open. 它已經開啟了。
|
||||
It's already open. Right-click it to open another copy. 已經開啟。按右鍵可再開啟一個副本。
|
||||
It's done 就這樣吧
|
||||
It's mine, or I'm allowed to look 這是我的,或我已獲准查看
|
||||
Its copy is deleted, with anything it saved there. The project itself stays as it is. 將刪除此副本及其儲存的所有內容。專案本身保持不變。
|
||||
Its folder is deleted with everything in it: what was built, the checks and the conversation. 將刪除其資料夾及其中的全部內容:建置成果、檢查結果和對話。
|
||||
@@ -563,6 +566,7 @@ Just look inside 只檢視內部
|
||||
Keep going without asking 繼續執行,無須詢問
|
||||
Keep it 保留
|
||||
Keep it in the sidebar to open any time, whatever later builds change. 將其保留在側邊欄中,隨時開啟,不受後續建置變更的影響。
|
||||
Keep the build as it is and stop offering to carry on. 保留建置的現狀,不再提示繼續。
|
||||
Keep this version in the sidebar to open any time. 將此版本保留在側邊欄中,隨時開啟。
|
||||
Keeps this version exactly as it is now. Later builds can't change or break it, and anything it saves stays with the copy. 完整保留目前版本。後續建置無法修改或破壞它,它儲存的所有內容也會留在此副本中。
|
||||
Key saved 金鑰已儲存
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 48 and column 52.
|
@@ -815,6 +815,15 @@ impl CraftApp {
|
||||
("project_play", Err(e)) => self.toast(format!("{} {}", tr("Couldn't start it."), e.for_people()), true),
|
||||
("project_assets_budget" | "project_assets_allow", Ok(_)) => self.assets_changed(),
|
||||
("rebuild_question_ask", r) => self.question_done(r),
|
||||
("project_build_finish", Ok(v)) => {
|
||||
if let Some(ws) = self.workspace.as_mut().filter(|w| v["project"].as_str().is_some_and(|p| w.root == std::path::Path::new(p))) {
|
||||
let _ = ws.reload();
|
||||
// What the tool added to the saved conversation.
|
||||
let text = craft_anything_engine::tools::project_tools::CALLED_DONE.to_string();
|
||||
self.state.feed.push(craft_anything_engine::feed::FeedItem::Step { step: craft_anything_engine::workspace::Step::Done, text });
|
||||
}
|
||||
self.toast(tr("Called done. A new message carries on from here."), false);
|
||||
}
|
||||
("project_questions_set", Ok(v)) => {
|
||||
// What was saved (the person's other changes on disk come along).
|
||||
if let Some(ws) = self.workspace.as_mut().filter(|w| v["project"].as_str().is_some_and(|p| w.root == std::path::Path::new(p))) {
|
||||
|
||||
@@ -1159,6 +1159,7 @@ fn draw_items(
|
||||
if widgets::primary_icon(ui, Some("refresh-cw"), "Try again", true).clicked() {
|
||||
*retry = true;
|
||||
}
|
||||
done_button(app, ui, ws);
|
||||
// A fix that names a command to run (sign in, install…) offers to copy it.
|
||||
if let Some(cmd) = remediation.as_deref().and_then(command_in)
|
||||
&& widgets::secondary(ui, Some("copy"), "Copy the command").on_hover_text(cmd).clicked()
|
||||
@@ -1245,7 +1246,10 @@ fn continue_card(app: &mut CraftApp, ui: &mut Ui, ws: &Workspace) {
|
||||
});
|
||||
ui.add(egui::Label::new(RichText::new(tr(body)).color(t.text_dim)).wrap());
|
||||
ui.add_space(6.0);
|
||||
go = widgets::primary_icon(ui, Some("play"), "Continue", true).clicked();
|
||||
ui.horizontal(|ui| {
|
||||
go = widgets::primary_icon(ui, Some("play"), "Continue", true).clicked();
|
||||
done_button(app, ui, ws);
|
||||
});
|
||||
});
|
||||
ui.add_space(chat::MESSAGE_GAP);
|
||||
if go {
|
||||
@@ -1253,6 +1257,14 @@ fn continue_card(app: &mut CraftApp, ui: &mut Ui, ws: &Workspace) {
|
||||
}
|
||||
}
|
||||
|
||||
/// "It's done": the person is happy with the build as it is, so it stops offering to carry on
|
||||
/// (#210). Shown only when nothing is running.
|
||||
fn done_button(app: &mut CraftApp, ui: &mut Ui, ws: &Workspace) {
|
||||
if widgets::secondary(ui, Some("check"), "It's done").on_hover_text(tr("Keep the build as it is and stop offering to carry on.")).clicked() {
|
||||
app.act("project_build_finish", serde_json::json!({"project": ws.root}), tr("Call the build done"));
|
||||
}
|
||||
}
|
||||
|
||||
fn ask_card(app: &mut CraftApp, ui: &mut Ui, r: &Request, answered: Option<&str>) -> Option<(String, Answer, String)> {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let approval = r.kind == RequestKind::Approval;
|
||||
|
||||
@@ -1623,6 +1623,13 @@ fn a_build_that_stopped_part_way_offers_to_continue() {
|
||||
h.run_steps(3);
|
||||
// In the feed, and on the project's status pill.
|
||||
assert!(h.query_all_by_label("Paused").count() >= 1);
|
||||
// #210: "enough now". It's kept as it is and stops offering to carry on.
|
||||
h.get_by_label("It's done").click();
|
||||
assert!(wait(&mut h, |app| app.workspace.as_ref().is_some_and(|w| w.manifest.status == Status::Ready)), "called done");
|
||||
h.run_steps(3);
|
||||
assert_eq!(Workspace::open(&paused.root).unwrap().manifest.status, Status::Ready);
|
||||
assert!(h.query_by_label("Continue").is_none(), "nothing left to carry on");
|
||||
h.get_by_label(craft_anything_engine::tools::project_tools::CALLED_DONE);
|
||||
h.state_mut().open_workspace(cut.clone());
|
||||
h.run_steps(3);
|
||||
h.get_by_label("Beta, Interrupted");
|
||||
|
||||
@@ -812,6 +812,8 @@ Gaps found while using the app for real projects.
|
||||
- [ ] "Make this change" on an answer that suggests one (sends it as a normal message)
|
||||
- [ ] A program started from the CLI (`project_play`) keeps the shell's output pipe open, so the
|
||||
command doesn't return until it closes: start it without inheriting the CLI's handles (#207)
|
||||
- [x] "It's done" beside Continue and Try again: the person keeps a build as it is and it stops
|
||||
offering to carry on (`project_build_finish`, #210)
|
||||
- [x] The desktop app keeps `desktop.log` beside its settings: startup steps, warnings, a window
|
||||
that couldn't start, panics
|
||||
- [x] The cost says when it's an estimate: Claude Code's figure is its own guess at API prices,
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ workspace clippy, layers, fmt and the secret scan of all history were clean. Chi
|
||||
| Agents over ACP | Opt-in per backend (`acp = true`): Gemini CLI, Qwen Code, opencode and goose in their ACP mode, one agent process per chat or build with our MCP server. In chat, permission requests become consent questions; in builds they go through the same approval policy as Claude Code's (inbox, Auto mode) | Protocol unit tests against scripted agent messages and an approval-policy test; not yet run against a real agent |
|
||||
| WASM plugins | With `--features wasm-plugins`: `[[plugins.wasm]]` components exporting `craft-anything:plugin/tools` add tools, and ones exporting `craft-anything:plugin/checks` add checks run after each kind's own (they get the build's file list as JSON) (added, listed, turned on or off and removed in Settings → Plugins or with `plugins_add`/`plugins_remove`/`plugins_list`); no imports, fresh instance per call, fuel and memory limits | Tests with components written in WAT (describe, call, checks, a looping call stopped, imports refused, end to end through the engine); `examples/wasm-plugin-wordcount` built from Rust and loaded |
|
||||
| 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 |
|
||||
| Desktop app | Simple mode: sidebar with New project and the project history (kept across restarts); New project either makes something 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 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, 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, 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. 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 either makes something 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 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, 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. 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 |
|
||||
|
||||
### Linux Unreal setup
|
||||
|
||||
+3
-2
@@ -2,7 +2,7 @@
|
||||
|
||||
Generated by `cargo xtask tools`. Do not edit by hand.
|
||||
|
||||
**298 engine tools** in 17 toolsets. Each is an MCP tool, a CLI command (`craft-anything-cli <id with dashes>`) and a tool the agent can call. The AI and setup layers add their own on top (`rebuild_*`, `assets_key_*`, `setup_*`); `craft-anything-cli commands` lists them all.
|
||||
**299 engine tools** in 17 toolsets. Each is an MCP tool, a CLI command (`craft-anything-cli <id with dashes>`) and a tool the agent can call. The AI and setup layers add their own on top (`rebuild_*`, `assets_key_*`, `setup_*`); `craft-anything-cli commands` lists them all.
|
||||
|
||||
## Essentials (`core`, 34)
|
||||
|
||||
@@ -311,7 +311,7 @@ Generated by `cargo xtask tools`. Do not edit by hand.
|
||||
|
||||
- `db_sqlite_show`: SQLite database contents
|
||||
|
||||
## Projects (`projects`, 18)
|
||||
## Projects (`projects`, 19)
|
||||
|
||||
- `project_list`: List projects
|
||||
- `project_show`: Show a project
|
||||
@@ -323,6 +323,7 @@ Generated by `cargo xtask tools`. Do not edit by hand.
|
||||
- `project_duplicate`: Duplicate a project
|
||||
- `project_delete`: Delete a project
|
||||
- `project_questions_set`: Choose what the builder's questions get
|
||||
- `project_build_finish`: Call the build done
|
||||
- `project_assets_budget`: Set a project's asset budget
|
||||
- `project_assets_allow`: Let a project generate assets
|
||||
- `project_pin_create`: Pin a project's build
|
||||
|
||||
Reference in New Issue
Block a user