Fix bugs 2 and 3: agent panel expand and stale worktree references

Bug 2: Expand triangle now works when no agents are started - shows
"No agents started" message. AgentPanel moved to top of panels.

Bug 3: Run `git worktree prune` before `git worktree add` to clean
stale references from externally-deleted worktree directories.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dave
2026-02-20 13:17:20 +00:00
parent c6a04f5e53
commit 1eae2410f3
3 changed files with 93 additions and 10 deletions

View File

@@ -471,17 +471,23 @@ export function AgentPanel({ stories }: AgentPanelProps) {
> >
<button <button
type="button" type="button"
onClick={() => onClick={() => {
const isExpanded =
expandedKey?.startsWith(`${story.story_id}:`) ||
expandedKey === story.story_id;
setExpandedKey( setExpandedKey(
expandedKey?.startsWith(`${story.story_id}:`) isExpanded
? null ? null
: (storyAgentEntries[0]?.[0] ?? story.story_id), : (storyAgentEntries[0]?.[0] ?? story.story_id),
) );
} }}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") { if (e.key === "Enter" || e.key === " ") {
const isExpanded =
expandedKey?.startsWith(`${story.story_id}:`) ||
expandedKey === story.story_id;
setExpandedKey( setExpandedKey(
expandedKey?.startsWith(`${story.story_id}:`) isExpanded
? null ? null
: (storyAgentEntries[0]?.[0] ?? story.story_id), : (storyAgentEntries[0]?.[0] ?? story.story_id),
); );
@@ -494,7 +500,9 @@ export function AgentPanel({ stories }: AgentPanelProps) {
cursor: "pointer", cursor: "pointer",
fontSize: "0.8em", fontSize: "0.8em",
padding: "0 4px", padding: "0 4px",
transform: expandedKey?.startsWith(`${story.story_id}:`) transform:
expandedKey?.startsWith(`${story.story_id}:`) ||
expandedKey === story.story_id
? "rotate(90deg)" ? "rotate(90deg)"
: "rotate(0deg)", : "rotate(0deg)",
transition: "transform 0.15s", transition: "transform 0.15s",
@@ -643,6 +651,21 @@ export function AgentPanel({ stories }: AgentPanelProps) {
)} )}
</div> </div>
{/* Empty state when expanded with no agents */}
{expandedKey === story.story_id && storyAgentEntries.length === 0 && (
<div
style={{
borderTop: "1px solid #2a2a2a",
padding: "12px",
fontSize: "0.8em",
color: "#555",
textAlign: "center",
}}
>
No agents started. Use the Run button to start an agent.
</div>
)}
{/* Expanded detail per agent */} {/* Expanded detail per agent */}
{storyAgentEntries.map(([key, a]) => { {storyAgentEntries.map(([key, a]) => {
if (expandedKey !== key) return null; if (expandedKey !== key) return null;

View File

@@ -703,6 +703,9 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
margin: "0 auto", margin: "0 auto",
width: "100%", width: "100%",
padding: "12px 24px 0", padding: "12px 24px 0",
flexShrink: 1,
overflowY: "auto",
minHeight: 0,
}} }}
> >
<div <div
@@ -712,6 +715,8 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
gap: "12px", gap: "12px",
}} }}
> >
<AgentPanel stories={upcomingStories} />
<ReviewPanel <ReviewPanel
reviewQueue={reviewQueue} reviewQueue={reviewQueue}
isReviewLoading={isReviewLoading} isReviewLoading={isReviewLoading}
@@ -754,8 +759,6 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
lastRefresh={lastUpcomingRefresh} lastRefresh={lastUpcomingRefresh}
onRefresh={refreshUpcomingStories} onRefresh={refreshUpcomingStories}
/> />
<AgentPanel stories={upcomingStories} />
</div> </div>
</div> </div>

View File

@@ -95,6 +95,12 @@ fn create_worktree_sync(
.map_err(|e| format!("Create worktree dir: {e}"))?; .map_err(|e| format!("Create worktree dir: {e}"))?;
} }
// Prune stale worktree references (e.g. directories deleted externally)
let _ = Command::new("git")
.args(["worktree", "prune"])
.current_dir(project_root)
.output();
// Try to create branch. If it already exists that's fine. // Try to create branch. If it already exists that's fine.
let _ = Command::new("git") let _ = Command::new("git")
.args(["branch", branch]) .args(["branch", branch])
@@ -199,6 +205,57 @@ async fn run_teardown_commands(wt_path: &Path, config: &ProjectConfig) -> Result
Ok(()) Ok(())
} }
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
/// Initialise a bare-minimum git repo so worktree operations work.
fn init_git_repo(dir: &Path) {
Command::new("git")
.args(["init"])
.current_dir(dir)
.output()
.expect("git init");
Command::new("git")
.args(["commit", "--allow-empty", "-m", "init"])
.current_dir(dir)
.output()
.expect("git commit");
}
#[test]
fn create_worktree_after_stale_reference() {
let tmp = TempDir::new().unwrap();
let project_root = tmp.path().join("my-project");
fs::create_dir_all(&project_root).unwrap();
init_git_repo(&project_root);
let wt_path = tmp.path().join("my-worktree");
let branch = "feature/test-stale";
// First creation should succeed
create_worktree_sync(&project_root, &wt_path, branch).unwrap();
assert!(wt_path.exists());
// Simulate external deletion (e.g., rm -rf by another agent)
fs::remove_dir_all(&wt_path).unwrap();
assert!(!wt_path.exists());
// Second creation should succeed despite stale git reference.
// Without `git worktree prune`, this fails with "already checked out"
// or "already exists".
let result = create_worktree_sync(&project_root, &wt_path, branch);
assert!(
result.is_ok(),
"Expected worktree creation to succeed after stale reference, got: {:?}",
result.err()
);
assert!(wt_path.exists());
}
}
async fn run_shell_command(cmd: &str, cwd: &Path) -> Result<(), String> { async fn run_shell_command(cmd: &str, cwd: &Path) -> Result<(), String> {
let cmd = cmd.to_string(); let cmd = cmd.to_string();
let cwd = cwd.to_path_buf(); let cwd = cwd.to_path_buf();