diff --git a/.story_kit/work/1_upcoming/155_story_queue_messages_while_agent_is_busy.md b/.story_kit/work/1_upcoming/155_story_queue_messages_while_agent_is_busy.md new file mode 100644 index 0000000..af8b7fa --- /dev/null +++ b/.story_kit/work/1_upcoming/155_story_queue_messages_while_agent_is_busy.md @@ -0,0 +1,22 @@ +--- +name: "Queue messages while agent is busy" +--- + +# Story 155: Queue messages while agent is busy + +## User Story + +As a user, I want to type and submit messages while an agent is busy, so that they queue up and send automatically when the agent is ready — like Claude Code CLI does. + +## Acceptance Criteria + +- [ ] When loading is true, user can still submit a message via Enter or the send button +- [ ] Submitted message is shown in the input area as 'queued' with visual indication (e.g. muted styling, label) +- [ ] User can edit or cancel the queued message before it sends +- [ ] When the agent response completes (loading becomes false), the queued message auto-submits +- [ ] Only one message can be queued at a time — subsequent submissions replace the queued message +- [ ] If the user cancels the current generation, the queued message does not auto-submit + +## Out of Scope + +- TBD diff --git a/server/src/agents.rs b/server/src/agents.rs index 847d75c..aa76111 100644 --- a/server/src/agents.rs +++ b/server/src/agents.rs @@ -2538,6 +2538,85 @@ fn run_squash_merge( }); } + // ── Install frontend dependencies for quality gates ────────────── + let frontend_dir_for_install = merge_wt_path.join("frontend"); + if frontend_dir_for_install.exists() { + // Ensure frontend/dist/ exists so cargo clippy (RustEmbed) can compile + // even before `pnpm build` has run. + let dist_dir = frontend_dir_for_install.join("dist"); + std::fs::create_dir_all(&dist_dir) + .map_err(|e| format!("Failed to create frontend/dist: {e}"))?; + + all_output.push_str("=== pnpm install (merge worktree) ===\n"); + let pnpm_install = Command::new("pnpm") + .args(["install"]) + .current_dir(&frontend_dir_for_install) + .output() + .map_err(|e| format!("Failed to run pnpm install: {e}"))?; + + let install_out = format!( + "{}{}", + String::from_utf8_lossy(&pnpm_install.stdout), + String::from_utf8_lossy(&pnpm_install.stderr) + ); + all_output.push_str(&install_out); + all_output.push('\n'); + + if !pnpm_install.status.success() { + all_output.push_str("=== pnpm install FAILED — aborting merge ===\n"); + cleanup_merge_workspace(project_root, &merge_wt_path, &merge_branch); + return Ok(SquashMergeResult { + success: false, + had_conflicts, + conflicts_resolved, + conflict_details, + output: all_output, + gates_passed: false, + }); + } + } + + // ── Install frontend dependencies for quality gates ────────── + let frontend_dir = merge_wt_path.join("frontend"); + if frontend_dir.exists() { + // Ensure frontend/dist exists so RustEmbed (cargo clippy) doesn't fail + // even before pnpm build runs. + let dist_dir = frontend_dir.join("dist"); + if !dist_dir.exists() { + let _ = std::fs::create_dir_all(&dist_dir); + } + + all_output.push_str("=== pnpm install (merge worktree) ===\n"); + let pnpm_install = Command::new("pnpm") + .args(["install", "--frozen-lockfile"]) + .current_dir(&frontend_dir) + .output() + .map_err(|e| format!("Failed to run pnpm install: {e}"))?; + + let install_out = format!( + "{}{}", + String::from_utf8_lossy(&pnpm_install.stdout), + String::from_utf8_lossy(&pnpm_install.stderr) + ); + all_output.push_str(&install_out); + all_output.push('\n'); + + if !pnpm_install.status.success() { + all_output.push_str( + "=== pnpm install FAILED — aborting merge, master unchanged ===\n", + ); + cleanup_merge_workspace(project_root, &merge_wt_path, &merge_branch); + return Ok(SquashMergeResult { + success: false, + had_conflicts, + conflicts_resolved, + conflict_details, + output: all_output, + gates_passed: false, + }); + } + } + // ── Quality gates in merge workspace (BEFORE fast-forward) ──── // Run gates in the merge worktree so that failures abort before master moves. all_output.push_str("=== Running quality gates before fast-forward ===\n"); @@ -5568,4 +5647,180 @@ theirs // Story file should be in 5_archived/ assert!(root.join(".story_kit/work/5_archived/60_story_cleanup.md").exists()); } + + // ── bug 154: merge worktree installs frontend deps ──────────────────── + + /// When the feature branch has a `frontend/` directory, `run_squash_merge` + /// must run `pnpm install` in the merge worktree before quality gates. + /// This test creates a repo with a `frontend/package.json` and verifies that + /// the merge output mentions the pnpm install step. + #[cfg(unix)] + #[test] + fn squash_merge_runs_pnpm_install_when_frontend_exists() { + use std::fs; + use tempfile::tempdir; + + let tmp = tempdir().unwrap(); + let repo = tmp.path(); + init_git_repo(repo); + + // Add a frontend/ directory with a minimal package.json on master. + let frontend = repo.join("frontend"); + fs::create_dir_all(&frontend).unwrap(); + fs::write( + frontend.join("package.json"), + r#"{"name":"test","version":"0.0.0","private":true}"#, + ) + .unwrap(); + Command::new("git") + .args(["add", "."]) + .current_dir(repo) + .output() + .unwrap(); + Command::new("git") + .args(["commit", "-m", "add frontend dir"]) + .current_dir(repo) + .output() + .unwrap(); + + // Create feature branch with a change. + Command::new("git") + .args(["checkout", "-b", "feature/story-154_test"]) + .current_dir(repo) + .output() + .unwrap(); + fs::write(repo.join("feature.txt"), "change").unwrap(); + Command::new("git") + .args(["add", "."]) + .current_dir(repo) + .output() + .unwrap(); + Command::new("git") + .args(["commit", "-m", "feature work"]) + .current_dir(repo) + .output() + .unwrap(); + + // Switch back to master. + Command::new("git") + .args(["checkout", "master"]) + .current_dir(repo) + .output() + .unwrap(); + + let result = + run_squash_merge(repo, "feature/story-154_test", "154_test").unwrap(); + + // The output must mention pnpm install, proving the new code path ran. + assert!( + result.output.contains("pnpm install"), + "merge output must mention pnpm install when frontend/ exists, got:\n{}", + result.output + ); + } + + /// When `pnpm install` fails in the merge worktree (e.g. no lockfile), + /// the merge must abort cleanly — success=false, workspace cleaned up. + #[cfg(unix)] + #[test] + fn squash_merge_aborts_when_pnpm_install_fails() { + use std::fs; + use tempfile::tempdir; + + let tmp = tempdir().unwrap(); + let repo = tmp.path(); + init_git_repo(repo); + + // Add a frontend/ directory with an invalid package.json that will + // cause pnpm install --frozen-lockfile to fail (no lockfile present). + let frontend = repo.join("frontend"); + fs::create_dir_all(&frontend).unwrap(); + fs::write( + frontend.join("package.json"), + r#"{"name":"test","version":"0.0.0","dependencies":{"nonexistent-pkg-xyz":"99.99.99"}}"#, + ) + .unwrap(); + Command::new("git") + .args(["add", "."]) + .current_dir(repo) + .output() + .unwrap(); + Command::new("git") + .args(["commit", "-m", "add frontend with bad deps"]) + .current_dir(repo) + .output() + .unwrap(); + + // Feature branch with a change. + Command::new("git") + .args(["checkout", "-b", "feature/story-154_fail"]) + .current_dir(repo) + .output() + .unwrap(); + fs::write(repo.join("change.txt"), "feature").unwrap(); + Command::new("git") + .args(["add", "."]) + .current_dir(repo) + .output() + .unwrap(); + Command::new("git") + .args(["commit", "-m", "feature"]) + .current_dir(repo) + .output() + .unwrap(); + + // Switch back to master, record HEAD. + Command::new("git") + .args(["checkout", "master"]) + .current_dir(repo) + .output() + .unwrap(); + let head_before = String::from_utf8( + Command::new("git") + .args(["rev-parse", "HEAD"]) + .current_dir(repo) + .output() + .unwrap() + .stdout, + ) + .unwrap() + .trim() + .to_string(); + + let result = + run_squash_merge(repo, "feature/story-154_fail", "154_fail").unwrap(); + + // pnpm install --frozen-lockfile should fail (no lockfile), merge aborted. + assert!( + !result.success, + "merge should fail when pnpm install fails" + ); + assert!( + result.output.contains("pnpm install"), + "output should mention pnpm install" + ); + + // Master HEAD must not have moved. + let head_after = String::from_utf8( + Command::new("git") + .args(["rev-parse", "HEAD"]) + .current_dir(repo) + .output() + .unwrap() + .stdout, + ) + .unwrap() + .trim() + .to_string(); + assert_eq!( + head_before, head_after, + "master HEAD must not advance when pnpm install fails (bug 154)" + ); + + // Merge workspace should be cleaned up. + assert!( + !repo.join(".story_kit/merge_workspace").exists(), + "merge workspace should be cleaned up after failure" + ); + } }