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:
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user