feat(FN-3277): add review tab reviewState persistence and enriched UI state

This merge completes a review tab feature with persistent `reviewState` — adding the core types, TaskReviewTab UI enhancements (enriched states and interactions), GitHub integration routes for refresh/address flows, and full regression coverage for both PR and non-PR modes. The architecture docs wer

Fusion-Task-Id: FN-3277
This commit is contained in:
Fusion
2026-05-08 10:32:53 -07:00
committed by gsxdsm
parent 6fffa830bf
commit 7b4cd5ed25
19 changed files with 466 additions and 167 deletions

View File

@@ -753,6 +753,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
return deduped.length > 0 ? deduped : undefined;
})(),
review: fromJson<import("./types.js").TaskReview>(row.review) ?? undefined,
reviewState: fromJson<import("./types.js").TaskReviewState>(row.review) ?? undefined,
workflowStepResults: (() => { const w = fromJson<import("./types.js").WorkflowStepResult[]>(row.workflowStepResults); return w && w.length > 0 ? w : undefined; })(),
prInfo: fromJson<import("./types.js").PrInfo>(row.prInfo),
issueInfo: fromJson<import("./types.js").IssueInfo>(row.issueInfo),
@@ -940,6 +941,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
attachments: task.attachments,
comments: task.comments,
review: task.review,
reviewState: task.reviewState,
prompt,
...agentLogFields,
log: [{ timestamp: archivedAt, action: "Task archived" }],
@@ -1254,7 +1256,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
toJson(task.attachments || []),
toJson(task.steeringComments || []),
toJson(task.comments || []),
toJsonNullable(task.review),
toJsonNullable(task.reviewState ?? task.review),
toJson(task.workflowStepResults || []),
toJsonNullable(task.prInfo),
toJsonNullable(task.issueInfo),
@@ -3188,7 +3190,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
async updateTask(
id: string,
updates: { title?: string; description?: string; priority?: TaskPriority | null; prompt?: string; worktree?: string | null; status?: string | null; dependencies?: string[]; steps?: import("./types.js").TaskStep[]; currentStep?: number; blockedBy?: string | null; assignedAgentId?: string | null; pausedByAgentId?: string | null; assigneeUserId?: string | null; nodeId?: string | null; effectiveNodeId?: string | null; effectiveNodeSource?: string | null; checkedOutBy?: string | null; checkedOutAt?: string | null; paused?: boolean; baseBranch?: string | null; branch?: string | null; executionStartBranch?: string | null; baseCommitSha?: string | null; size?: "S" | "M" | "L"; reviewLevel?: number; executionMode?: import("./types.js").ExecutionMode | null; mergeRetries?: number; workflowStepRetries?: number; stuckKillCount?: number | null; postReviewFixCount?: number | null; recoveryRetryCount?: number | null; taskDoneRetryCount?: number | null; verificationFailureCount?: number | null; mergeConflictBounceCount?: number | null; nextRecoveryAt?: string | null; enabledWorkflowSteps?: string[]; modelProvider?: string | null; modelId?: string | null; validatorModelProvider?: string | null; validatorModelId?: string | null; planningModelProvider?: string | null; planningModelId?: string | null; thinkingLevel?: string | null; error?: string | null; summary?: string | null; sessionFile?: string | null; executionStartedAt?: string | null; executionCompletedAt?: string | null; review?: import("./types.js").TaskReview | null; workflowStepResults?: import("./types.js").WorkflowStepResult[] | null; mergeDetails?: import("./types.js").MergeDetails | null; sourceIssue?: import("./types.js").TaskSourceIssue | null; tokenUsage?: import("./types.js").TaskTokenUsage | null; modifiedFiles?: string[] | null; missionId?: string | null; sliceId?: string | null },
updates: { title?: string; description?: string; priority?: TaskPriority | null; prompt?: string; worktree?: string | null; status?: string | null; dependencies?: string[]; steps?: import("./types.js").TaskStep[]; currentStep?: number; blockedBy?: string | null; assignedAgentId?: string | null; pausedByAgentId?: string | null; assigneeUserId?: string | null; nodeId?: string | null; effectiveNodeId?: string | null; effectiveNodeSource?: string | null; checkedOutBy?: string | null; checkedOutAt?: string | null; paused?: boolean; baseBranch?: string | null; branch?: string | null; executionStartBranch?: string | null; baseCommitSha?: string | null; size?: "S" | "M" | "L"; reviewLevel?: number; executionMode?: import("./types.js").ExecutionMode | null; mergeRetries?: number; workflowStepRetries?: number; stuckKillCount?: number | null; postReviewFixCount?: number | null; recoveryRetryCount?: number | null; taskDoneRetryCount?: number | null; verificationFailureCount?: number | null; mergeConflictBounceCount?: number | null; nextRecoveryAt?: string | null; enabledWorkflowSteps?: string[]; modelProvider?: string | null; modelId?: string | null; validatorModelProvider?: string | null; validatorModelId?: string | null; planningModelProvider?: string | null; planningModelId?: string | null; thinkingLevel?: string | null; error?: string | null; summary?: string | null; sessionFile?: string | null; executionStartedAt?: string | null; executionCompletedAt?: string | null; review?: import("./types.js").TaskReview | null; reviewState?: import("./types.js").TaskReviewState | null; workflowStepResults?: import("./types.js").WorkflowStepResult[] | null; mergeDetails?: import("./types.js").MergeDetails | null; sourceIssue?: import("./types.js").TaskSourceIssue | null; tokenUsage?: import("./types.js").TaskTokenUsage | null; modifiedFiles?: string[] | null; missionId?: string | null; sliceId?: string | null },
runContext?: RunMutationContext,
): Promise<Task> {
return this.withTaskLock(id, async () => {
@@ -3463,6 +3465,11 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
} else if (updates.review !== undefined) {
task.review = updates.review;
}
if (updates.reviewState === null) {
task.reviewState = undefined;
} else if (updates.reviewState !== undefined) {
task.reviewState = updates.reviewState;
}
if (updates.workflowStepResults === null) {
task.workflowStepResults = undefined;
} else if (updates.workflowStepResults !== undefined) {

View File

@@ -730,6 +730,74 @@ export interface TaskReview {
items: TaskReviewItem[];
}
export type PrCheckState =
| "success"
| "pending"
| "failure"
| "cancelled"
| "timed_out"
| "action_required"
| "neutral"
| "skipped"
| "stale"
| "startup_failure";
export interface PrCheckStatus {
name: string;
required: boolean;
state: PrCheckState;
}
export interface TaskReviewAuthor {
login: string;
}
export interface PrTaskReviewSummaryReviewer {
login: string;
state: "APPROVED" | "CHANGES_REQUESTED" | "COMMENTED" | "PENDING";
submittedAt?: string;
}
export interface PrTaskReviewSummary {
reviewDecision: "APPROVED" | "CHANGES_REQUESTED" | "REVIEW_REQUIRED" | null;
reviewers: PrTaskReviewSummaryReviewer[];
blockingReasons: string[];
checks: PrCheckStatus[];
}
export interface TaskReviewStateItem {
id: string;
threadId?: string;
githubCommentId?: number;
path?: string;
diffSide?: string;
body: string;
author: TaskReviewAuthor;
createdAt: string;
updatedAt?: string;
state?: string;
htmlUrl?: string;
isResolved?: boolean;
}
export interface ReviewAddressingRecord {
itemId: string;
status: "queued" | "in-progress" | "addressed" | "failed";
selectedAt: string;
startedAt?: string;
completedAt?: string;
error?: string;
stale?: boolean;
}
export interface TaskReviewState {
source: "pull-request" | "reviewer-agent";
lastRefreshedAt?: string;
summary?: PrTaskReviewSummary;
items: TaskReviewStateItem[];
addressing: ReviewAddressingRecord[];
}
export interface TaskDocument {
/** UUID primary key */
id: string;
@@ -949,8 +1017,10 @@ export interface Task {
attachments?: TaskAttachment[];
steeringComments?: SteeringComment[];
comments?: TaskComment[];
/** Structured review metadata shown in the Review tab. */
/** Structured review metadata shown in the Review tab (legacy contract). */
review?: TaskReview;
/** Structured review metadata shown in the Review tab (canonical contract). */
reviewState?: TaskReviewState;
/** PR information for tasks linked to GitHub pull requests */
prInfo?: PrInfo;
mergeDetails?: MergeDetails;
@@ -2372,8 +2442,10 @@ export interface ArchivedTaskEntry {
attachments?: TaskAttachment[];
/** User and agent comments remain searchable in the archive DB. */
comments?: TaskComment[];
/** Structured review metadata shown in the Review tab. */
/** Structured review metadata shown in the Review tab (legacy contract). */
review?: TaskReview;
/** Structured review metadata shown in the Review tab (canonical contract). */
reviewState?: TaskReviewState;
/** Reconstructed prompt content at archive time, without attachment blobs. */
prompt?: string;
/** Agent log retention mode used when this archive entry was written. */