story-kit: create 155_story_queue_messages_while_agent_is_busy
This commit is contained in:
@@ -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
|
||||||
@@ -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) ────
|
// ── Quality gates in merge workspace (BEFORE fast-forward) ────
|
||||||
// Run gates in the merge worktree so that failures abort before master moves.
|
// Run gates in the merge worktree so that failures abort before master moves.
|
||||||
all_output.push_str("=== Running quality gates before fast-forward ===\n");
|
all_output.push_str("=== Running quality gates before fast-forward ===\n");
|
||||||
@@ -5568,4 +5647,180 @@ theirs
|
|||||||
// Story file should be in 5_archived/
|
// Story file should be in 5_archived/
|
||||||
assert!(root.join(".story_kit/work/5_archived/60_story_cleanup.md").exists());
|
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"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user