Skip to content

Commit fc85b91

Browse files
committed
feat(issue): show blocked indicator in mine and query output
Add a 'B' column (with ⊘) to 'linear issue mine' and 'linear issue query' that flags issues with at least one active blocker — a blocked-by relation whose blocker is not in a completed or canceled workflow state. The inverseRelations connection is also surfaced in --json output.
1 parent eb8a659 commit fc85b91

8 files changed

Lines changed: 330 additions & 8 deletions

File tree

src/commands/issue/issue-mine.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@ import {
1616
getProjectOptionsByName,
1717
getTeamIdByKey,
1818
getTeamKey,
19+
isIssueBlocked,
1920
selectOption,
2021
} from "../../utils/linear.ts"
2122
import { openTeamAssigneeView } from "../../utils/actions.ts"
2223
import { pipeToUserPager, shouldUsePager } from "../../utils/pager.ts"
23-
import { header, muted } from "../../utils/styling.ts"
24+
import { header, muted, warning } from "../../utils/styling.ts"
2425
import { shouldShowSpinner } from "../../utils/hyperlink.ts"
2526
import {
2627
handleError,
@@ -291,6 +292,7 @@ export const mineCommand = new Command()
291292
? Deno.consoleSize()
292293
: { columns: 120 }
293294
const PRIORITY_WIDTH = 3
295+
const BLOCKED_WIDTH = 1
294296
const ID_WIDTH = Math.max(
295297
2, // minimum width for "ID" header
296298
...issues.map((issue) => issue.identifier.length),
@@ -323,6 +325,7 @@ export const mineCommand = new Command()
323325

324326
type TableRow = {
325327
priorityStr: string
328+
blockedStr: string
326329
identifier: string
327330
title: string
328331
labels: string
@@ -394,8 +397,11 @@ export const mineCommand = new Command()
394397
)
395398
const statePadded = stateColored + " ".repeat(stateRemainingSpace)
396399

400+
const blockedStr = isIssueBlocked(issue) ? warning("⊘") : " "
401+
397402
return {
398403
priorityStr,
404+
blockedStr,
399405
identifier: issue.identifier,
400406
title: issue.title,
401407
labels,
@@ -405,7 +411,8 @@ export const mineCommand = new Command()
405411
}
406412
})
407413

408-
const fixed = PRIORITY_WIDTH + ID_WIDTH + UPDATED_WIDTH + SPACE_WIDTH +
414+
const fixed = PRIORITY_WIDTH + BLOCKED_WIDTH + ID_WIDTH +
415+
UPDATED_WIDTH + SPACE_WIDTH +
409416
LABEL_WIDTH + ESTIMATE_WIDTH + STATE_WIDTH + SPACE_WIDTH
410417
const PADDING = 1
411418
const maxTitleWidth = Math.max(
@@ -418,6 +425,7 @@ export const mineCommand = new Command()
418425
padDisplay("ID", ID_WIDTH),
419426
padDisplay("TITLE", titleWidth),
420427
padDisplay("LABELS", LABEL_WIDTH),
428+
padDisplay("B", BLOCKED_WIDTH),
421429
padDisplay("E", ESTIMATE_WIDTH),
422430
padDisplay("STATE", STATE_WIDTH),
423431
padDisplay(updatedHeader, UPDATED_WIDTH),
@@ -432,6 +440,7 @@ export const mineCommand = new Command()
432440
for (const row of tableData) {
433441
const {
434442
priorityStr,
443+
blockedStr,
435444
identifier,
436445
title,
437446
labels,
@@ -446,7 +455,7 @@ export const mineCommand = new Command()
446455

447456
const issueLine = `${padDisplay(priorityStr, PRIORITY_WIDTH)} ${
448457
padDisplay(identifier, ID_WIDTH)
449-
} ${truncTitle} ${labels} ${
458+
} ${truncTitle} ${labels} ${padDisplay(blockedStr, BLOCKED_WIDTH)} ${
450459
padDisplay(estimate?.toString() || "-", ESTIMATE_WIDTH)
451460
} ${state} ${muted(padDisplay(timeAgo, UPDATED_WIDTH))}`
452461
outputLines.push(issueLine)

src/commands/issue/issue-query.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@ import {
1616
getProjectOptionsByName,
1717
getTeamIdByKey,
1818
getTeamKey,
19+
isIssueBlocked,
1920
searchIssuesByTerm,
2021
selectOption,
2122
} from "../../utils/linear.ts"
2223
import { pipeToUserPager, shouldUsePager } from "../../utils/pager.ts"
2324
import { shouldShowSpinner } from "../../utils/hyperlink.ts"
24-
import { header, muted } from "../../utils/styling.ts"
25+
import { header, muted, warning } from "../../utils/styling.ts"
2526
import {
2627
handleError,
2728
NotFoundError,
@@ -423,6 +424,12 @@ interface DisplayableIssue {
423424
assignee?: { initials: string } | null
424425
team?: { key: string }
425426
labels: { nodes: Array<{ name: string; color: string }> }
427+
inverseRelations?: {
428+
nodes: Array<{
429+
type: string
430+
issue?: { state?: { type?: string | null } | null } | null
431+
}>
432+
} | null
426433
}
427434

428435
function formatIssueTable(
@@ -435,6 +442,7 @@ function formatIssueTable(
435442
: { columns: 120 }
436443

437444
const priorityWidth = 3
445+
const blockedWidth = 1
438446
const idWidth = Math.max(2, ...issues.map((i) => i.identifier.length))
439447
const teamWidth = showTeamColumn
440448
? Math.max(
@@ -468,6 +476,7 @@ function formatIssueTable(
468476
idWidth,
469477
...(showTeamColumn ? [teamWidth] : []),
470478
labelWidth,
479+
blockedWidth,
471480
estimateWidth,
472481
...(showAssigneeColumn ? [assigneeWidth] : []),
473482
stateWidth,
@@ -485,6 +494,7 @@ function formatIssueTable(
485494
...(showTeamColumn ? [padDisplay("TEAM", teamWidth)] : []),
486495
padDisplay("TITLE", titleWidth),
487496
padDisplay("LABELS", labelWidth),
497+
padDisplay("B", blockedWidth),
488498
padDisplay("E", estimateWidth),
489499
...(showAssigneeColumn ? [padDisplay("A", assigneeWidth)] : []),
490500
padDisplay("STATE", stateWidth),
@@ -508,12 +518,14 @@ function formatIssueTable(
508518
const timeAgo = muted(
509519
padDisplay(getTimeAgo(new Date(issue.updatedAt)), updatedWidth),
510520
)
521+
const blockedCell = isIssueBlocked(issue) ? warning("⊘") : " "
511522
const cells = [
512523
padDisplay(getPriorityDisplay(issue.priority), priorityWidth),
513524
padDisplay(issue.identifier, idWidth),
514525
...(showTeamColumn ? [padDisplay(issue.team?.key ?? "", teamWidth)] : []),
515526
title,
516527
formatLabels(issue.labels.nodes, labelWidth),
528+
padDisplay(blockedCell, blockedWidth),
517529
padDisplay(issue.estimate?.toString() || "-", estimateWidth),
518530
...(showAssigneeColumn
519531
? [

src/utils/linear.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,26 @@ export function parseDateFilter(value: string, flagName: string): string {
4747
return parsed.toISOString()
4848
}
4949

50+
type InverseRelationNode = {
51+
type: string
52+
issue?: { state?: { type?: string | null } | null } | null
53+
}
54+
55+
export function isIssueBlocked(issue: {
56+
inverseRelations?: { nodes: ReadonlyArray<InverseRelationNode> } | null
57+
}): boolean {
58+
const nodes = issue.inverseRelations?.nodes
59+
if (!nodes) return false
60+
for (const rel of nodes) {
61+
if (rel.type !== "blocks") continue
62+
const blockerStateType = rel.issue?.state?.type
63+
if (blockerStateType !== "completed" && blockerStateType !== "canceled") {
64+
return true
65+
}
66+
}
67+
return false
68+
}
69+
5070
export function formatIssueIdentifier(providedId: string): string {
5171
return normalizeIssueIdentifier(providedId) ?? providedId.toUpperCase()
5272
}
@@ -596,6 +616,7 @@ export async function fetchIssuesForState(
596616
id
597617
name
598618
color
619+
type
599620
}
600621
labels {
601622
nodes {
@@ -604,6 +625,19 @@ export async function fetchIssuesForState(
604625
color
605626
}
606627
}
628+
inverseRelations(first: 100) {
629+
nodes {
630+
id
631+
type
632+
issue {
633+
id
634+
identifier
635+
state {
636+
type
637+
}
638+
}
639+
}
640+
}
607641
updatedAt
608642
}
609643
pageInfo {
@@ -732,6 +766,19 @@ const queryIssuesQuery = gql(/* GraphQL */ `
732766
color
733767
}
734768
}
769+
inverseRelations(first: 100) {
770+
nodes {
771+
id
772+
type
773+
issue {
774+
id
775+
identifier
776+
state {
777+
type
778+
}
779+
}
780+
}
781+
}
735782
}
736783
pageInfo {
737784
hasNextPage
@@ -971,6 +1018,19 @@ const searchIssuesQuery = gql(/* GraphQL */ `
9711018
color
9721019
}
9731020
}
1021+
inverseRelations(first: 100) {
1022+
nodes {
1023+
id
1024+
type
1025+
issue {
1026+
id
1027+
identifier
1028+
state {
1029+
type
1030+
}
1031+
}
1032+
}
1033+
}
9741034
metadata
9751035
}
9761036
pageInfo {

test/commands/issue/__snapshots__/issue-query.test.ts.snap

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ stdout:
8585
"color": "#eb5757"
8686
}
8787
]
88+
},
89+
"inverseRelations": {
90+
"nodes": []
8891
}
8992
}
9093
],
@@ -144,6 +147,9 @@ stdout:
144147
}
145148
]
146149
},
150+
"inverseRelations": {
151+
"nodes": []
152+
},
147153
"metadata": {
148154
"context": {},
149155
"score": 0.42

test/commands/issue/issue-list.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ Deno.test("Issue List Command - Filter By Label", async () => {
6767
id: "state-1",
6868
name: "In Progress",
6969
color: "#f2c94c",
70+
type: "started",
7071
},
7172
labels: {
7273
nodes: [{
@@ -75,6 +76,7 @@ Deno.test("Issue List Command - Filter By Label", async () => {
7576
color: "#eb5757",
7677
}],
7778
},
79+
inverseRelations: { nodes: [] },
7880
updatedAt: "2026-03-13T10:00:00.000Z",
7981
},
8082
],
@@ -102,8 +104,8 @@ Deno.test("Issue List Command - Filter By Label", async () => {
102104

103105
assertEquals(
104106
logs.join("\n") + "\n",
105-
"◌ ID TITLE LABELS E STATE UPDATED \n" +
106-
"⚠⚠⚠ ENG-101 Fix login bug Bug 3 In Progress 17 days ago\n",
107+
"◌ ID TITLE LABELS B E STATE UPDATED \n" +
108+
"⚠⚠⚠ ENG-101 Fix login bug Bug 3 In Progress 17 days ago\n",
107109
)
108110
} finally {
109111
logStub.restore()

0 commit comments

Comments
 (0)