From b5cc4db11b3dcdae453e77a57ea878d6580d5213 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 21:38:21 +0000 Subject: [PATCH] Publish PRs from conversations whose workspace diff is empty Ctrl+P refused any conversation whose base-to-head patch was empty with "there are no conversation changes to publish". That blocked real publications: a conversation whose changes already landed in the base, or one whose value is the transcript itself. /publish-branch already accepts these for the same reason. The gate now only requires a completed turn to point the branch at, matching /publish-branch's "no completed turn" rule; an empty diff proceeds to the base prompt as usual. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_019FKeyxNMPfqcUR2Ts9ghqq --- rust/crates/caos-cli/TUI.md | 3 ++ rust/crates/caos-cli/src/bin/tui/app.rs | 52 +++++++++++++++++++++---- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/rust/crates/caos-cli/TUI.md b/rust/crates/caos-cli/TUI.md index f2b788b8..268717ba 100644 --- a/rust/crates/caos-cli/TUI.md +++ b/rust/crates/caos-cli/TUI.md @@ -220,6 +220,9 @@ conversation head, the preparation turn runs without asking the agent to merge it again. For another base, only this conversation's delta is applied, so child conversations form clean PR stacks. Unresolved conflicts stop before the branch moves. The checkout and index remain untouched. +Any conversation with a completed turn can be published, including one whose +workspace diff is empty — the transcript history is the content, and its +changes may already be part of the base. CAOS points `caos/` directly at the validated conversation head, pushes it, and uses the authenticated `gh` CLI to find or open its pull diff --git a/rust/crates/caos-cli/src/bin/tui/app.rs b/rust/crates/caos-cli/src/bin/tui/app.rs index df1dce90..503c67c7 100644 --- a/rust/crates/caos-cli/src/bin/tui/app.rs +++ b/rust/crates/caos-cli/src/bin/tui/app.rs @@ -3909,14 +3909,13 @@ impl App { if self.selected().is_busy() { self.selected_mut() .show_command_error("finish this conversation's operation before publishing it"); - } else if self - .selected() - .diff - .as_ref() - .is_none_or(|diff| diff.patch.is_empty()) - { + } else if self.selected().diff.is_none() { + // An empty workspace diff is NOT a blocker: the conversation + // history itself is worth publishing (and its changes may already + // be in the base). Only a conversation with no completed turn has + // nothing to point a branch at. self.selected_mut() - .show_command_error("there are no conversation changes to publish"); + .show_command_error("this conversation has no completed turn to publish"); } else if self.confirm_action.is_none() { let default_base = match remote_default_branch(&self.repo_dir) { Ok(branch) => branch, @@ -6163,6 +6162,45 @@ mod tests { std::fs::remove_dir_all(publish_remote).unwrap(); } + #[test] + fn publish_opens_the_base_prompt_for_an_empty_workspace_diff() { + let mut conversation = state("talk-1"); + conversation.diff = Some(WorkspaceDiff { + base_commit: "a".repeat(40), + head: "b".repeat(40), + patch: String::new(), + }); + let (mut app, _) = app_with(vec![conversation]); + let (repo, remote, _) = repo_with_default_branch("publish-empty-diff", "main"); + app.repo_dir = repo.clone(); + + app.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL)); + + assert_eq!( + app.confirm_action, + Some(ConfirmAction::Publish { + default_base: "main".to_string(), + base_input: String::new(), + }) + ); + assert!(app.selected().command_error.is_none()); + std::fs::remove_dir_all(repo).unwrap(); + std::fs::remove_dir_all(remote).unwrap(); + } + + #[test] + fn publish_requires_a_completed_turn() { + let (mut app, _) = app_with(vec![state("talk-1")]); + + app.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL)); + + assert!(app.confirm_action.is_none()); + assert_eq!( + app.selected().command_error.as_deref(), + Some("this conversation has no completed turn to publish") + ); + } + #[test] fn idle_chat_header_keeps_only_the_title_and_head_metadata() { let mut conversation = state("A concise title");