[codex] Surface live run comment context (#4957)
## Thinking Path > - Paperclip orchestrates AI agents through issue comments and heartbeat runs > - The board UI needs to distinguish a comment that triggered a live run from comments queued after that run started > - The run payload already stores comment context, but active-run API responses did not expose the ids the UI needs > - Without those ids, the triggering comment can flash as queued while the agent is already responding to it > - This pull request exposes live-run comment context and teaches the optimistic comment helper to ignore the trigger comment > - The benefit is clearer issue-chat state during comment-triggered agent interruptions ## What Changed - Added `contextCommentId` and `contextWakeCommentId` to active/live run payloads. - Threaded those ids through server routes, heartbeat summaries, UI API types, and issue detail rendering. - Updated optimistic comment classification to avoid marking the triggering comment as queued. - Added server and UI regression coverage. ## Verification - `pnpm exec vitest run server/src/__tests__/agent-live-run-routes.test.ts ui/src/lib/optimistic-issue-comments.test.ts` ## Risks - Low-to-medium risk: adds optional fields to existing run payloads. Existing consumers should ignore unknown fields, and UI handling is null-safe. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex, GPT-5 coding agent, tool use and local command execution. Exact context window was not exposed in the runtime. ## 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 - [ ] 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 --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -187,6 +187,8 @@ describe("agent live run routes", () => {
|
||||
status: "running",
|
||||
invocationSource: "on_demand",
|
||||
triggerDetail: "manual",
|
||||
contextCommentId: "comment-1",
|
||||
contextWakeCommentId: "comment-1",
|
||||
startedAt: new Date("2026-04-10T09:30:00.000Z"),
|
||||
finishedAt: null,
|
||||
createdAt: new Date("2026-04-10T09:29:59.000Z"),
|
||||
@@ -224,6 +226,8 @@ describe("agent live run routes", () => {
|
||||
status: "running",
|
||||
invocationSource: "on_demand",
|
||||
triggerDetail: "manual",
|
||||
contextCommentId: "comment-1",
|
||||
contextWakeCommentId: "comment-1",
|
||||
startedAt: "2026-04-10T09:30:00.000Z",
|
||||
finishedAt: null,
|
||||
createdAt: "2026-04-10T09:29:59.000Z",
|
||||
|
||||
@@ -2858,6 +2858,8 @@ export function agentRoutes(
|
||||
status: heartbeatRuns.status,
|
||||
invocationSource: heartbeatRuns.invocationSource,
|
||||
triggerDetail: heartbeatRuns.triggerDetail,
|
||||
contextCommentId: sql<string | null>`${heartbeatRuns.contextSnapshot} ->> 'commentId'`.as("contextCommentId"),
|
||||
contextWakeCommentId: sql<string | null>`${heartbeatRuns.contextSnapshot} ->> 'wakeCommentId'`.as("contextWakeCommentId"),
|
||||
startedAt: heartbeatRuns.startedAt,
|
||||
finishedAt: heartbeatRuns.finishedAt,
|
||||
createdAt: heartbeatRuns.createdAt,
|
||||
@@ -3094,6 +3096,8 @@ export function agentRoutes(
|
||||
status: heartbeatRuns.status,
|
||||
invocationSource: heartbeatRuns.invocationSource,
|
||||
triggerDetail: heartbeatRuns.triggerDetail,
|
||||
contextCommentId: sql<string | null>`${heartbeatRuns.contextSnapshot} ->> 'commentId'`.as("contextCommentId"),
|
||||
contextWakeCommentId: sql<string | null>`${heartbeatRuns.contextSnapshot} ->> 'wakeCommentId'`.as("contextWakeCommentId"),
|
||||
startedAt: heartbeatRuns.startedAt,
|
||||
finishedAt: heartbeatRuns.finishedAt,
|
||||
createdAt: heartbeatRuns.createdAt,
|
||||
|
||||
@@ -744,6 +744,8 @@ const heartbeatRunIssueSummaryColumns = {
|
||||
status: heartbeatRuns.status,
|
||||
invocationSource: heartbeatRuns.invocationSource,
|
||||
triggerDetail: heartbeatRuns.triggerDetail,
|
||||
contextCommentId: sql<string | null>`${heartbeatRuns.contextSnapshot} ->> 'commentId'`.as("contextCommentId"),
|
||||
contextWakeCommentId: sql<string | null>`${heartbeatRuns.contextSnapshot} ->> 'wakeCommentId'`.as("contextWakeCommentId"),
|
||||
startedAt: heartbeatRuns.startedAt,
|
||||
finishedAt: heartbeatRuns.finishedAt,
|
||||
createdAt: heartbeatRuns.createdAt,
|
||||
|
||||
@@ -19,6 +19,8 @@ export interface ActiveRunForIssue {
|
||||
status: string;
|
||||
invocationSource: string;
|
||||
triggerDetail: string | null;
|
||||
contextCommentId?: string | null;
|
||||
contextWakeCommentId?: string | null;
|
||||
startedAt: string | Date | null;
|
||||
finishedAt: string | Date | null;
|
||||
createdAt: string | Date;
|
||||
@@ -41,6 +43,8 @@ export interface LiveRunForIssue {
|
||||
status: string;
|
||||
invocationSource: string;
|
||||
triggerDetail: string | null;
|
||||
contextCommentId?: string | null;
|
||||
contextWakeCommentId?: string | null;
|
||||
startedAt: string | null;
|
||||
finishedAt: string | null;
|
||||
createdAt: string;
|
||||
|
||||
@@ -726,14 +726,61 @@ describe("optimistic issue comments", () => {
|
||||
expect(
|
||||
isQueuedIssueComment({
|
||||
comment: {
|
||||
id: "comment-2",
|
||||
createdAt: new Date("2026-03-28T16:20:05.000Z"),
|
||||
},
|
||||
activeRunStartedAt: new Date("2026-03-28T16:20:00.000Z"),
|
||||
activeRunWakeCommentId: "comment-1",
|
||||
runId: null,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not mark the comment that triggered the active run as queued", () => {
|
||||
expect(
|
||||
isQueuedIssueComment({
|
||||
comment: {
|
||||
id: "comment-1",
|
||||
createdAt: new Date("2026-03-28T16:20:05.000Z"),
|
||||
},
|
||||
activeRunStartedAt: new Date("2026-03-28T16:20:00.000Z"),
|
||||
activeRunCommentId: "comment-1",
|
||||
activeRunWakeCommentId: "comment-1",
|
||||
runId: null,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("does not mark the active run context comment as queued", () => {
|
||||
expect(
|
||||
isQueuedIssueComment({
|
||||
comment: {
|
||||
id: "context-comment",
|
||||
createdAt: new Date("2026-03-28T16:20:05.000Z"),
|
||||
},
|
||||
activeRunStartedAt: new Date("2026-03-28T16:20:00.000Z"),
|
||||
activeRunCommentId: "context-comment",
|
||||
activeRunWakeCommentId: "wake-comment",
|
||||
runId: null,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("does not mark the active run wake comment as queued", () => {
|
||||
expect(
|
||||
isQueuedIssueComment({
|
||||
comment: {
|
||||
id: "wake-comment",
|
||||
createdAt: new Date("2026-03-28T16:20:05.000Z"),
|
||||
},
|
||||
activeRunStartedAt: new Date("2026-03-28T16:20:00.000Z"),
|
||||
activeRunCommentId: "context-comment",
|
||||
activeRunWakeCommentId: "wake-comment",
|
||||
runId: null,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("does not mark comments with an associated run as queued", () => {
|
||||
expect(
|
||||
isQueuedIssueComment({
|
||||
|
||||
@@ -70,15 +70,24 @@ export function createOptimisticIssueComment(params: {
|
||||
export function isQueuedIssueComment(params: {
|
||||
comment: Pick<IssueTimelineComment, "createdAt"> &
|
||||
Partial<Pick<OptimisticIssueComment, "clientStatus">> & {
|
||||
id?: string;
|
||||
authorAgentId?: string | null;
|
||||
};
|
||||
activeRunStartedAt?: Date | string | null;
|
||||
activeRunAgentId?: string | null;
|
||||
activeRunCommentId?: string | null;
|
||||
activeRunWakeCommentId?: string | null;
|
||||
runId?: string | null;
|
||||
interruptedRunId?: string | null;
|
||||
}) {
|
||||
if (params.runId) return false;
|
||||
if (params.interruptedRunId) return false;
|
||||
if (
|
||||
params.comment.id &&
|
||||
(params.comment.id === params.activeRunWakeCommentId || params.comment.id === params.activeRunCommentId)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (params.comment.authorAgentId && params.activeRunAgentId && params.comment.authorAgentId === params.activeRunAgentId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -778,6 +778,8 @@ const IssueDetailChatTab = memo(function IssueDetailChatTab({
|
||||
comment: nextComment,
|
||||
activeRunStartedAt,
|
||||
activeRunAgentId: runningIssueRun?.agentId ?? null,
|
||||
activeRunCommentId: runningIssueRun?.contextCommentId ?? null,
|
||||
activeRunWakeCommentId: runningIssueRun?.contextWakeCommentId ?? null,
|
||||
runId: meta?.runId ?? nextComment.runId ?? null,
|
||||
interruptedRunId: meta?.interruptedRunId ?? nextComment.interruptedRunId ?? null,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user