mirror of
https://github.com/paperclipai/paperclip
synced 2026-04-25 17:25:15 +02:00
[codex] Clear stale queued comment targets (#4234)
## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies. > - Operators interact with agent work through issue threads and queued comments. > - When the selected comment target becomes stale, the composer can keep pointing at an invalid target after thread state changes. > - That makes follow-up comments easier to misroute and harder to reason about. > - This pull request clears stale queued comment targets and covers the behavior with tests. > - The benefit is more predictable issue-thread commenting during live agent work. ## What Changed - Clears queued comment targets when they no longer match the current issue thread state. - Adjusts issue detail comment-target handling to avoid stale target reuse. - Adds regression tests for optimistic issue comment target behavior. ## Verification - `pnpm exec vitest run ui/src/lib/optimistic-issue-comments.test.ts` ## Risks - Low risk; scoped to comment-target state handling in the issue UI. - No migrations. > Checked `ROADMAP.md`; this is a focused UI reliability fix, not a new roadmap-level feature. ## Model Used - OpenAI Codex, GPT-5-based coding agent, tool-enabled repository editing and local test execution. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] If this change affects the UI, I have included before/after screenshots - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge
This commit is contained in:
@@ -720,7 +720,7 @@ describe("optimistic issue comments", () => {
|
||||
|
||||
const result = applyLocalQueuedIssueCommentState(comment, {
|
||||
queuedTargetRunId: "run-1",
|
||||
hasLiveRuns: true,
|
||||
targetRunIsLive: true,
|
||||
runningRunId: "run-1",
|
||||
});
|
||||
|
||||
@@ -746,10 +746,31 @@ describe("optimistic issue comments", () => {
|
||||
|
||||
const result = applyLocalQueuedIssueCommentState(comment, {
|
||||
queuedTargetRunId: "run-1",
|
||||
hasLiveRuns: false,
|
||||
targetRunIsLive: false,
|
||||
runningRunId: null,
|
||||
});
|
||||
|
||||
expect(result).toBe(comment);
|
||||
});
|
||||
|
||||
it("does not keep local queued state when a different run is live", () => {
|
||||
const comment = {
|
||||
id: "comment-1",
|
||||
companyId: "company-1",
|
||||
issueId: "issue-1",
|
||||
authorAgentId: null,
|
||||
authorUserId: "board-1",
|
||||
body: "Follow up after the active run",
|
||||
createdAt: new Date("2026-03-28T16:20:05.000Z"),
|
||||
updatedAt: new Date("2026-03-28T16:20:05.000Z"),
|
||||
};
|
||||
|
||||
const result = applyLocalQueuedIssueCommentState(comment, {
|
||||
queuedTargetRunId: "run-1",
|
||||
targetRunIsLive: true,
|
||||
runningRunId: "run-2",
|
||||
});
|
||||
|
||||
expect(result).toBe(comment);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -91,12 +91,12 @@ export function applyLocalQueuedIssueCommentState<T extends IssueComment>(
|
||||
comment: T,
|
||||
params: {
|
||||
queuedTargetRunId?: string | null;
|
||||
hasLiveRuns: boolean;
|
||||
targetRunIsLive: boolean;
|
||||
runningRunId?: string | null;
|
||||
},
|
||||
): T | LocallyQueuedIssueComment<T> {
|
||||
const queuedTargetRunId = params.queuedTargetRunId ?? null;
|
||||
if (!queuedTargetRunId || !params.hasLiveRuns) return comment;
|
||||
if (!queuedTargetRunId || !params.targetRunIsLive) return comment;
|
||||
if (params.runningRunId && params.runningRunId !== queuedTargetRunId) return comment;
|
||||
|
||||
return {
|
||||
|
||||
@@ -613,19 +613,22 @@ const IssueDetailChatTab = memo(function IssueDetailChatTab({
|
||||
() => resolveRunningIssueRun(resolvedActiveRun, resolvedLiveRuns),
|
||||
[resolvedActiveRun, resolvedLiveRuns],
|
||||
);
|
||||
const liveRunIds = useMemo(() => {
|
||||
const ids = new Set<string>();
|
||||
for (const run of resolvedLiveRuns) ids.add(run.id);
|
||||
if (resolvedActiveRun) ids.add(resolvedActiveRun.id);
|
||||
return ids;
|
||||
}, [resolvedActiveRun, resolvedLiveRuns]);
|
||||
const timelineRuns = useMemo(() => {
|
||||
const liveIds = new Set<string>();
|
||||
for (const run of resolvedLiveRuns) liveIds.add(run.id);
|
||||
if (activeRun) liveIds.add(activeRun.id);
|
||||
const historicalRuns = liveIds.size === 0
|
||||
const historicalRuns = liveRunIds.size === 0
|
||||
? resolvedLinkedRuns
|
||||
: resolvedLinkedRuns.filter((run) => !liveIds.has(run.runId));
|
||||
: resolvedLinkedRuns.filter((run) => !liveRunIds.has(run.runId));
|
||||
return historicalRuns.map((run) => ({
|
||||
...run,
|
||||
adapterType: run.adapterType,
|
||||
hasStoredOutput: (run.logBytes ?? 0) > 0,
|
||||
}));
|
||||
}, [activeRun, resolvedLinkedRuns, resolvedLiveRuns]);
|
||||
}, [liveRunIds, resolvedLinkedRuns]);
|
||||
const commentsWithRunMeta = useMemo<IssueDetailComment[]>(() => {
|
||||
const activeRunStartedAt = runningIssueRun?.startedAt ?? runningIssueRun?.createdAt ?? null;
|
||||
const runMetaByCommentId = new Map<string, { runId: string; runAgentId: string | null; interruptedRunId: string | null }>();
|
||||
@@ -651,9 +654,10 @@ const IssueDetailChatTab = memo(function IssueDetailChatTab({
|
||||
return comments.map((comment) => {
|
||||
const meta = runMetaByCommentId.get(comment.id);
|
||||
const nextComment: IssueDetailComment = meta ? { ...comment, ...meta } : { ...comment };
|
||||
const queuedTargetRunId = locallyQueuedCommentRunIds.get(comment.id) ?? null;
|
||||
const locallyQueuedComment = applyLocalQueuedIssueCommentState(nextComment, {
|
||||
queuedTargetRunId: locallyQueuedCommentRunIds.get(comment.id) ?? null,
|
||||
hasLiveRuns,
|
||||
queuedTargetRunId,
|
||||
targetRunIsLive: queuedTargetRunId ? liveRunIds.has(queuedTargetRunId) : false,
|
||||
runningRunId: runningIssueRun?.id ?? null,
|
||||
});
|
||||
if (locallyQueuedComment !== nextComment) {
|
||||
@@ -676,7 +680,7 @@ const IssueDetailChatTab = memo(function IssueDetailChatTab({
|
||||
}
|
||||
return nextComment;
|
||||
});
|
||||
}, [comments, hasLiveRuns, locallyQueuedCommentRunIds, resolvedActivity, resolvedLinkedRuns, runningIssueRun]);
|
||||
}, [comments, liveRunIds, locallyQueuedCommentRunIds, resolvedActivity, resolvedLinkedRuns, runningIssueRun]);
|
||||
const timelineEvents = useMemo(
|
||||
() => extractIssueTimelineEvents(resolvedActivity),
|
||||
[resolvedActivity],
|
||||
@@ -1625,6 +1629,7 @@ export function IssueDetail() {
|
||||
const previousLiveRuns = queryClient.getQueryData<LiveRunForIssue[]>(queryKeys.issues.liveRuns(issueId!));
|
||||
const previousActiveRun = queryClient.getQueryData<ActiveRunForIssue | null>(queryKeys.issues.activeRun(issueId!));
|
||||
const previousIssue = queryClient.getQueryData<Issue>(queryKeys.issues.detail(issueId!));
|
||||
const previousLocalQueuedCommentRunIds = locallyQueuedCommentRunIds;
|
||||
const liveRunList = previousLiveRuns ?? [];
|
||||
const cachedActiveRun = previousActiveRun ?? null;
|
||||
const runningIssueRun = resolveRunningIssueRun(cachedActiveRun, liveRunList);
|
||||
@@ -1653,12 +1658,17 @@ export function IssueDetail() {
|
||||
queryKeys.issues.detail(issueId!),
|
||||
(current: Issue | undefined) => clearIssueExecutionRun(current, runId),
|
||||
);
|
||||
setLocallyQueuedCommentRunIds((current) => {
|
||||
const next = new Map([...current].filter(([, targetRunId]) => targetRunId !== runId));
|
||||
return next.size === current.size ? current : next;
|
||||
});
|
||||
|
||||
return {
|
||||
previousRuns,
|
||||
previousLiveRuns,
|
||||
previousActiveRun,
|
||||
previousIssue,
|
||||
previousLocalQueuedCommentRunIds,
|
||||
};
|
||||
},
|
||||
onSuccess: () => {
|
||||
@@ -1675,6 +1685,9 @@ export function IssueDetail() {
|
||||
queryClient.setQueryData(queryKeys.issues.liveRuns(issueId!), context?.previousLiveRuns);
|
||||
queryClient.setQueryData(queryKeys.issues.activeRun(issueId!), context?.previousActiveRun);
|
||||
queryClient.setQueryData(queryKeys.issues.detail(issueId!), context?.previousIssue);
|
||||
if (context?.previousLocalQueuedCommentRunIds) {
|
||||
setLocallyQueuedCommentRunIds(context.previousLocalQueuedCommentRunIds);
|
||||
}
|
||||
pushToast({
|
||||
title: "Interrupt failed",
|
||||
body: err instanceof Error ? err.message : "Unable to interrupt the active run",
|
||||
|
||||
Reference in New Issue
Block a user