feat(FN-1433): wire prompt overrides into mission interview service
- Import resolvePrompt and PromptOverrideMap from @fusion/core
- Add promptOverrides parameter to all agent creation paths:
- createMissionInterviewSession
- submitMissionInterviewResponse
- retryMissionInterviewSession
- initializeAgent
- createMissionInterviewAgent
- ensureMissionInterviewAgent
- Use resolvePrompt('planning-system', promptOverrides) for effective prompt
- Fall back to MISSION_INTERVIEW_SYSTEM_PROMPT when override absent
- Update module docs to reflect prompt override behavior
This commit is contained in:
@@ -497,7 +497,7 @@ function TaskCardComponent({
|
||||
task.column,
|
||||
task.mergeDetails?.commitSha,
|
||||
projectId,
|
||||
{ enabled: isInViewport },
|
||||
{ enabled: isInViewport, worktree: task.worktree },
|
||||
);
|
||||
|
||||
// Get fresh batch data if available
|
||||
@@ -910,17 +910,24 @@ function TaskCardComponent({
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
{task.worktree && (task.column === "in-progress" || task.column === "in-review") && (
|
||||
<button
|
||||
type="button"
|
||||
className="card-session-files"
|
||||
onClick={handleOpenFiles}
|
||||
disabled={!onOpenDetailWithTab}
|
||||
>
|
||||
<Folder size={12} />
|
||||
<span>View files</span>
|
||||
</button>
|
||||
)}
|
||||
{task.worktree && (task.column === "in-progress" || task.column === "in-review") && (() => {
|
||||
const activeCount = diffStats?.filesChanged;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="card-session-files"
|
||||
onClick={handleOpenFiles}
|
||||
disabled={!onOpenDetailWithTab}
|
||||
>
|
||||
<Folder size={12} />
|
||||
<span>
|
||||
{activeCount != null && activeCount > 0
|
||||
? `${activeCount} ${activeCount === 1 ? "file" : "files"} changed`
|
||||
: "View files"}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})()}
|
||||
{task.column === "done" && (() => {
|
||||
// Prefer diff stats from the same endpoint the modal uses so the
|
||||
// count is always consistent with the Changes tab.
|
||||
|
||||
@@ -3227,13 +3227,54 @@ describe("TaskCard singular/plural file count", () => {
|
||||
mockUseTaskDiffStats.mockReturnValue({ stats: null, loading: false });
|
||||
});
|
||||
|
||||
it("shows a static files action for in-progress worktrees without fetching file counts", () => {
|
||||
it("shows changed-file counts for in-progress worktrees", () => {
|
||||
const task = makeTask({
|
||||
column: "in-progress",
|
||||
worktree: "/repo/.worktrees/fn-099",
|
||||
status: "executing",
|
||||
});
|
||||
mockUseSessionFiles.mockReturnValue({ files: ["src/a.ts"], loading: false });
|
||||
mockUseTaskDiffStats.mockReturnValue({ stats: { filesChanged: 3, additions: 10, deletions: 2 }, loading: false });
|
||||
|
||||
render(
|
||||
<TaskCard
|
||||
task={task}
|
||||
onOpenDetail={vi.fn()}
|
||||
addToast={noopToast}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("3 files changed")).toBeInTheDocument();
|
||||
expect(screen.queryByText("View files")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Checking files…")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows changed-file counts for in-review worktrees", () => {
|
||||
const task = makeTask({
|
||||
column: "in-review",
|
||||
worktree: "/repo/.worktrees/fn-099",
|
||||
status: "reviewing",
|
||||
});
|
||||
mockUseTaskDiffStats.mockReturnValue({ stats: { filesChanged: 2, additions: 7, deletions: 1 }, loading: false });
|
||||
|
||||
render(
|
||||
<TaskCard
|
||||
task={task}
|
||||
onOpenDetail={vi.fn()}
|
||||
addToast={noopToast}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("2 files changed")).toBeInTheDocument();
|
||||
expect(screen.queryByText("View files")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("falls back to View files for in-progress worktrees without a positive diff count", () => {
|
||||
const task = makeTask({
|
||||
column: "in-progress",
|
||||
worktree: "/repo/.worktrees/fn-099",
|
||||
status: "executing",
|
||||
});
|
||||
mockUseTaskDiffStats.mockReturnValue({ stats: { filesChanged: 0, additions: 0, deletions: 0 }, loading: false });
|
||||
|
||||
render(
|
||||
<TaskCard
|
||||
@@ -3245,27 +3286,6 @@ describe("TaskCard singular/plural file count", () => {
|
||||
|
||||
expect(screen.getByText("View files")).toBeInTheDocument();
|
||||
expect(screen.queryByText(/files? changed/)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Checking files…")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not use session file counts for in-progress worktree cards", () => {
|
||||
const task = makeTask({
|
||||
column: "in-progress",
|
||||
worktree: "/repo/.worktrees/fn-099",
|
||||
status: "executing",
|
||||
});
|
||||
mockUseSessionFiles.mockReturnValue({ files: ["src/a.ts", "src/b.ts"], loading: false });
|
||||
|
||||
render(
|
||||
<TaskCard
|
||||
task={task}
|
||||
onOpenDetail={vi.fn()}
|
||||
addToast={noopToast}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("View files")).toBeInTheDocument();
|
||||
expect(screen.queryByText("2 files changed")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("displays '1 file changed' (singular) for done column with displayCount=1 via diffStats", () => {
|
||||
|
||||
@@ -53,7 +53,7 @@ describe("useTaskDiffStats", () => {
|
||||
expect(mockFetchTaskDiff).toHaveBeenCalledWith("FN-123", undefined, "proj-1");
|
||||
});
|
||||
|
||||
it("does not fetch for non-done columns", async () => {
|
||||
it("does not fetch for active columns without a worktree", async () => {
|
||||
const { result: inProgress } = renderHook(() =>
|
||||
useTaskDiffStats("FN-123", "in-progress", "abc1234", undefined),
|
||||
);
|
||||
@@ -74,6 +74,30 @@ describe("useTaskDiffStats", () => {
|
||||
expect(mockFetchTaskDiff).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("fetches diff stats for active tasks with a worktree", async () => {
|
||||
mockFetchTaskDiff.mockResolvedValueOnce({
|
||||
files: [
|
||||
{ path: "src/a.ts", status: "modified", additions: 10, deletions: 2, patch: "" },
|
||||
],
|
||||
stats: { filesChanged: 1, additions: 10, deletions: 2 },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useTaskDiffStats(
|
||||
"FN-123",
|
||||
"in-progress",
|
||||
undefined,
|
||||
"proj-1",
|
||||
{ worktree: "/repo/.worktrees/fn-123" },
|
||||
),
|
||||
);
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toBe(false));
|
||||
|
||||
expect(result.current.stats).toEqual({ filesChanged: 1, additions: 10, deletions: 2 });
|
||||
expect(mockFetchTaskDiff).toHaveBeenCalledWith("FN-123", "/repo/.worktrees/fn-123", "proj-1");
|
||||
});
|
||||
|
||||
it("does not fetch for done tasks without a commit SHA", async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useTaskDiffStats("FN-123", "done", undefined, undefined),
|
||||
|
||||
@@ -15,6 +15,8 @@ interface UseTaskDiffStatsResult {
|
||||
interface UseTaskDiffStatsOptions {
|
||||
/** Enable fetching when true (default). Suppresses fetches for offscreen cards. */
|
||||
enabled?: boolean;
|
||||
/** Worktree path for active task columns. */
|
||||
worktree?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -25,12 +27,12 @@ interface UseTaskDiffStatsOptions {
|
||||
const diffStatsCache = new Map<string, { stats: DiffStats; expiresAt: number }>();
|
||||
const CACHE_TTL_MS = 30_000; // 30 seconds
|
||||
|
||||
function getCacheKey(taskId: string, projectId?: string): string {
|
||||
return `${taskId}:${projectId ?? ""}`;
|
||||
function getCacheKey(taskId: string, projectId?: string, worktree?: string): string {
|
||||
return `${taskId}:${projectId ?? ""}:${worktree ?? ""}`;
|
||||
}
|
||||
|
||||
function getCachedStats(taskId: string, projectId?: string): DiffStats | null {
|
||||
const key = getCacheKey(taskId, projectId);
|
||||
function getCachedStats(taskId: string, projectId?: string, worktree?: string): DiffStats | null {
|
||||
const key = getCacheKey(taskId, projectId, worktree);
|
||||
const entry = diffStatsCache.get(key);
|
||||
|
||||
if (!entry) return null;
|
||||
@@ -44,8 +46,8 @@ function getCachedStats(taskId: string, projectId?: string): DiffStats | null {
|
||||
return entry.stats;
|
||||
}
|
||||
|
||||
function setCachedStats(taskId: string, projectId: string | undefined, stats: DiffStats): void {
|
||||
const key = getCacheKey(taskId, projectId);
|
||||
function setCachedStats(taskId: string, projectId: string | undefined, worktree: string | undefined, stats: DiffStats): void {
|
||||
const key = getCacheKey(taskId, projectId, worktree);
|
||||
diffStatsCache.set(key, {
|
||||
stats,
|
||||
expiresAt: Date.now() + CACHE_TTL_MS,
|
||||
@@ -61,13 +63,12 @@ export function __test_clearDiffStatsCache(): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches diff stats for a done task that has a merge commit SHA.
|
||||
* Fetches diff stats for a task's Changes tab.
|
||||
*
|
||||
* This ensures the TaskCard shows the same file-changed count as the
|
||||
* TaskChangesTab (which fetches from `/api/tasks/:id/diff`). Without this
|
||||
* hook the card falls back to `mergeDetails.filesChanged`, which is
|
||||
* computed at merge time via `git show --shortstat` and can differ from the
|
||||
* diff endpoint's count when the merge includes changes from multiple branches.
|
||||
* For active worktree-backed tasks, this keeps the TaskCard count aligned with
|
||||
* the Changes tab. For done tasks, it uses the same endpoint so the card does
|
||||
* not fall back to `mergeDetails.filesChanged`, which is computed at merge time
|
||||
* and can differ from the endpoint's count.
|
||||
*
|
||||
* @param taskId - Task identifier
|
||||
* @param column - Current task column
|
||||
@@ -83,6 +84,7 @@ export function useTaskDiffStats(
|
||||
options: UseTaskDiffStatsOptions = {},
|
||||
): UseTaskDiffStatsResult {
|
||||
const enabled = options.enabled ?? true;
|
||||
const worktree = options.worktree;
|
||||
const [stats, setStats] = useState<DiffStats | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
@@ -94,15 +96,18 @@ export function useTaskDiffStats(
|
||||
return;
|
||||
}
|
||||
|
||||
// Only fetch for done tasks with a recorded merge commit
|
||||
if (!taskId || column !== "done" || !commitSha) {
|
||||
const shouldFetchDoneTask = column === "done" && Boolean(commitSha);
|
||||
const shouldFetchActiveTask = (column === "in-progress" || column === "in-review") && Boolean(worktree);
|
||||
|
||||
if (!taskId || (!shouldFetchDoneTask && !shouldFetchActiveTask)) {
|
||||
setStats(null);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check cache first - return immediately without loading flicker
|
||||
const cached = getCachedStats(taskId, projectId);
|
||||
const activeWorktree = shouldFetchActiveTask ? worktree : undefined;
|
||||
const cached = getCachedStats(taskId, projectId, activeWorktree);
|
||||
if (cached) {
|
||||
setStats(cached);
|
||||
setLoading(false);
|
||||
@@ -114,11 +119,11 @@ export function useTaskDiffStats(
|
||||
async function load() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await fetchTaskDiff(taskId, undefined, projectId);
|
||||
const data = await fetchTaskDiff(taskId, activeWorktree, projectId);
|
||||
if (!cancelled) {
|
||||
setStats(data.stats);
|
||||
// Store in cache
|
||||
setCachedStats(taskId, projectId, data.stats);
|
||||
setCachedStats(taskId, projectId, activeWorktree, data.stats);
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) {
|
||||
@@ -136,7 +141,7 @@ export function useTaskDiffStats(
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [taskId, column, commitSha, projectId, enabled]);
|
||||
}, [taskId, column, commitSha, projectId, enabled, worktree]);
|
||||
|
||||
return { stats, loading };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user