Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions rust/crates/caos-cli/TUI.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<conversation>` directly at the validated conversation head,
pushes it, and uses the authenticated `gh` CLI to find or open its pull
Expand Down
52 changes: 45 additions & 7 deletions rust/crates/caos-cli/src/bin/tui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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");
Expand Down