Skip to content

Commit 9e9f744

Browse files
Show blocker links in the task chat (#11456)
<!-- Write all pull request text in Simplified Technical English (ASD-STE100): short sentences, one instruction per sentence, simple approved vocabulary, and the active voice. --> ## Thinking Path > - Paperclip helps operators supervise agent work through tasks and task threads. > - The redesigned task thread shows the current work and its state. > - A blocked task did not show the dependency that prevented progress. > - Operators had to leave the thread to find the direct and final blockers. > - This pull request adds compact blocker links at the top and bottom of the task thread. > - The benefit is that operators can identify and open the relevant tasks without adding a large notice to the thread. ## Linked Issues or Issue Description **What existing behavior does this improve?** The redesigned task thread did not show which task directly blocked the current task or which task ultimately blocked its dependency chain. **Subsystem affected** `server/`, `packages/shared/`, and `ui/` task-blocker presentation. **Current behavior** A blocked task can open in the redesigned thread without a visible dependency link at the top or bottom of the conversation. **Proposed behavior** Show one compact amber row for the direct blocker. Show a second row for the selected final blocker when one exists. Render the rows at both ends of the thread. **Reason and benefit** Operators can see the reason for the blocked state and open the relevant task from the conversation. The compact rows preserve thread density. **Breaking changes** None. The new blocker-attention fields are optional. Existing clients remain compatible. ## What Changed - Added a compact task-chat component for direct and selected final blocker links. - Added the blocker rows to the top and bottom of populated and empty task threads. - Added link-ready blocker-attention details so an intermediate selected task stays on its correct direct chain. - Included blocker-link changes in the thread content key so pinned threads follow a newly added bottom row. - Added component, scrolling, server contract, and Storybook coverage for the new states. ## Verification - `pnpm exec vitest run ui/src/components/TaskChatThread.test.tsx server/src/__tests__/issue-blocker-attention.test.ts` (38 tests passed) - `pnpm --filter @paperclipai/shared typecheck` - `pnpm --filter @paperclipai/server typecheck` - `pnpm --filter @paperclipai/ui typecheck` - `pnpm check:token-gates` ## Risks - Low risk. The rows only render while the task status is `blocked` and an unresolved blocker is available. - Long titles are truncated to keep each blocker on one line. The full task label remains available in the link title. - Older server payloads keep the original leaf-selection behavior because the new sampled details are optional. > 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 with GPT-5. The run used tool-enabled reasoning and code execution. The context-window size was not exposed to the run. ## 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 searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
1 parent e384d0a commit 9e9f744

10 files changed

Lines changed: 529 additions & 26 deletions

File tree

packages/shared/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -951,6 +951,7 @@ export type {
951951
IssueSubtreeDiagnosticEdge,
952952
IssueSubtreeDiagnosticsResponse,
953953
IssueBlockerAttention,
954+
IssueBlockerAttentionIssueSummary,
954955
IssueBlockerAttentionReason,
955956
IssueBlockerAttentionState,
956957
IssueReviewAttention,

packages/shared/src/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,7 @@ export type {
604604
IssueSubtreeDiagnosticEdge,
605605
IssueSubtreeDiagnosticsResponse,
606606
IssueBlockerAttention,
607+
IssueBlockerAttentionIssueSummary,
607608
IssueBlockerAttentionReason,
608609
IssueBlockerAttentionState,
609610
IssueReviewAttention,

packages/shared/src/types/issue.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,12 @@ export type IssueBlockerAttentionReason =
396396
| "attention_required"
397397
| null;
398398

399+
export interface IssueBlockerAttentionIssueSummary {
400+
id: string;
401+
identifier: string | null;
402+
title: string;
403+
}
404+
399405
export interface IssueBlockerAttention {
400406
state: IssueBlockerAttentionState;
401407
reason: IssueBlockerAttentionReason;
@@ -408,8 +414,12 @@ export interface IssueBlockerAttention {
408414
sampleStalledBlockerIdentifier: string | null;
409415
/** True when a blocker or one of its open descendants is actively progressing. */
410416
blockingTreeLive?: boolean;
411-
/** The sampled leaf blocker that requires action, rather than the blocked root. */
417+
/** The direct blocker whose chain contains the sampled terminal blocker. */
418+
directBlockerIssueId?: string | null;
419+
/** The sampled blocker that requires action, rather than the blocked root. */
412420
terminalBlockerIssueId?: string | null;
421+
/** Link-ready details for the sampled blocker, including non-terminal intermediate nodes. */
422+
terminalBlocker?: IssueBlockerAttentionIssueSummary | null;
413423
}
414424

415425
export type IssueReviewAttentionState = "none" | "covered" | "stalled";

server/src/__tests__/issue-blocker-attention.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,47 @@ describeEmbeddedPostgres("issue blocker attention", () => {
447447
});
448448
});
449449

450+
it("returns the direct path and link details when an intermediate blocker is selected", async () => {
451+
const { companyId, agentId } = await createCompany("PBI");
452+
const rootId = await insertIssue({ companyId, identifier: "PBI-1", title: "Root", status: "blocked" });
453+
const directId = await insertIssue({
454+
companyId,
455+
identifier: "PBI-2",
456+
title: "Direct blocker",
457+
status: "blocked",
458+
});
459+
const intermediateId = await insertIssue({
460+
companyId,
461+
identifier: "PBI-3",
462+
title: "Stalled intermediate review",
463+
status: "in_review",
464+
assigneeAgentId: agentId,
465+
});
466+
const leafId = await insertIssue({
467+
companyId,
468+
identifier: "PBI-4",
469+
title: "Downstream leaf",
470+
status: "todo",
471+
assigneeAgentId: agentId,
472+
});
473+
await block({ companyId, blockerIssueId: directId, blockedIssueId: rootId });
474+
await block({ companyId, blockerIssueId: intermediateId, blockedIssueId: directId });
475+
await block({ companyId, blockerIssueId: leafId, blockedIssueId: intermediateId });
476+
477+
const root = (await svc.list(companyId, { status: "blocked" })).find((issue) => issue.id === rootId);
478+
479+
expect(root?.blockerAttention).toMatchObject({
480+
state: "stalled",
481+
directBlockerIssueId: directId,
482+
terminalBlockerIssueId: intermediateId,
483+
terminalBlocker: {
484+
id: intermediateId,
485+
identifier: "PBI-3",
486+
title: "Stalled intermediate review",
487+
},
488+
});
489+
});
490+
450491
it("prefers needs_attention over stalled when the chain also has a hard attention case", async () => {
451492
const { companyId, agentId } = await createCompany("PBQ");
452493
const parentId = await insertIssue({ companyId, identifier: "PBQ-1", title: "Parent", status: "blocked" });

server/src/services/issues.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2094,7 +2094,9 @@ function createIssueBlockerAttention(input: Partial<IssueBlockerAttention> = {})
20942094
sampleBlockerIdentifier: input.sampleBlockerIdentifier ?? null,
20952095
sampleStalledBlockerIdentifier: input.sampleStalledBlockerIdentifier ?? null,
20962096
blockingTreeLive: input.blockingTreeLive ?? false,
2097+
directBlockerIssueId: input.directBlockerIssueId ?? null,
20972098
terminalBlockerIssueId: input.terminalBlockerIssueId ?? null,
2099+
terminalBlocker: input.terminalBlocker ?? null,
20982100
};
20992101
}
21002102

@@ -2758,6 +2760,11 @@ async function listIssueBlockerAttentionMap(
27582760
const sampledTerminalIdentifier = sampleEntry?.result.stalled
27592761
? sampleEntry.result.sampleStalledBlockerIdentifier ?? sampleEntry.result.sampleBlockerIdentifier
27602762
: sampleEntry?.result.sampleBlockerIdentifier ?? blockerSampleIdentifier(sampleNode);
2763+
const terminalBlockerIssueId =
2764+
sampleEntry?.result.terminalBlockerIssueId ?? issueIdForSample(sampledTerminalIdentifier);
2765+
const terminalBlockerNode = terminalBlockerIssueId
2766+
? nodesById.get(terminalBlockerIssueId) ?? null
2767+
: null;
27612768

27622769
let state: IssueBlockerAttention["state"];
27632770
let reason: IssueBlockerAttention["reason"];
@@ -2788,8 +2795,15 @@ async function listIssueBlockerAttentionMap(
27882795
sampleStalledBlockerIdentifier:
27892796
stalledEntry?.result.sampleStalledBlockerIdentifier ?? sampleStalledFromChain ?? null,
27902797
blockingTreeLive: topLevelEdges.some((edge) => pathHasLiveWork(edge.blockerIssueId, new Set([root.id]))),
2791-
terminalBlockerIssueId:
2792-
sampleEntry?.result.terminalBlockerIssueId ?? issueIdForSample(sampledTerminalIdentifier),
2798+
directBlockerIssueId: sampleEntry?.edge.blockerIssueId ?? null,
2799+
terminalBlockerIssueId,
2800+
terminalBlocker: terminalBlockerNode
2801+
? {
2802+
id: terminalBlockerNode.id,
2803+
identifier: terminalBlockerNode.identifier,
2804+
title: terminalBlockerNode.title,
2805+
}
2806+
: null,
27932807
}));
27942808
}
27952809

0 commit comments

Comments
 (0)