Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions DEVLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@

> Append-only session log. Read at session start. Update at session end.

## 2026-08-27 — Explicit native issue blockers

- Added a direct issue-detail blocker control backed by the existing
`BLOCKED_BY` relation model, with searchable blocker selection, a visible
active-blocker banner, navigation, removal, and automatic terminal-state
clearing without introducing a second blocked-state model.
- Kept `/blocked <reason>` as a separate operator-attention request and made
that distinction explicit in the blocker picker.
- Replaced the Dashboard Blocked tab's duplicate stalled-run rows with actual
dependency-blocked issues and removed those issues from ordinary work lanes.
- Added focused unit and production-build Playwright coverage for set, display,
dashboard reconciliation, deduplication, and removal behavior.

## 2026-08-26 — v0.34.0 release preparation

- Squash-merged AXI-107 / PR #100 after exact-head lint, typecheck, unit,
Expand Down
7 changes: 7 additions & 0 deletions src/app/(app)/w/[slug]/issues/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ import { IssueRail } from "@/components/issue-detail/issue-rail";
import { AgentRunStrip } from "@/components/issue-detail/agent-run-strip";
import { TerminalRunFailureBanner } from "@/components/issue-detail/run-failure-banner";
import { IssueFollowThroughBanner } from "@/components/issue-detail/issue-follow-through-banner";
import {
IssueBlockerBanner,
IssueBlockerControl,
} from "@/components/issue-detail/issue-blocker-control";
import { RuntimePreflightBanner } from "@/components/issue-detail/runtime-preflight-banner";
import { GitHubLinksPanel } from "@/components/issue-detail/github-links-panel";
import { AiTriageCard } from "@/components/ai-triage-card";
Expand Down Expand Up @@ -551,6 +555,7 @@ export default function IssueDetailPage({ params }: { params: Promise<{ id: stri
onChange={(userIds) => assign.mutate({ id: issue.id, userIds })}
/>
<AgentChip current={issue.assignedAgent} onOpen={() => setAgentPickerOpen(true)} />
<IssueBlockerControl issueId={issue.id} />
</div>
}
actions={siblingScope ? <IssueSiblingNav issueId={issue.id} scope={siblingScope} /> : null}
Expand All @@ -572,6 +577,8 @@ export default function IssueDetailPage({ params }: { params: Promise<{ id: stri
</span>
</div>

<IssueBlockerBanner issueId={issue.id} />

<AgentRunStrip
issueId={issue.id}
assignedAgent={issue.assignedAgent ?? null}
Expand Down
51 changes: 39 additions & 12 deletions src/components/dashboard/operator-home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,26 @@ export function OperatorHome() {
const { data: ws } = trpc.workspace.current.useQuery();
const { data: me } = trpc.workspace.me.useQuery();
const myWork = trpc.dashboard.myWork.useQuery();
const dependencyBlocked = trpc.issue.list.useQuery(
{ blocked: true, includeDone: false, limit: 200 },
{ refetchOnWindowFocus: true, staleTime: 30_000 },
);
const blockedIssueRows = useMemo(
() => dependencyBlocked.data?.items ?? [],
[dependencyBlocked.data?.items],
);
const blockedIssueIds = useMemo(
() => new Set(blockedIssueRows.map((issue) => issue.id)),
[blockedIssueRows],
);

const lanes = useMemo(
() => buildOperatorLanes(myWork.data?.focus ?? [], myWork.data?.resume ?? []),
[myWork.data?.focus, myWork.data?.resume],
() =>
buildOperatorLanes(
(myWork.data?.focus ?? []).filter((issue) => !blockedIssueIds.has(issue.id)),
(myWork.data?.resume ?? []).filter((issue) => !blockedIssueIds.has(issue.id)),
),
[blockedIssueIds, myWork.data?.focus, myWork.data?.resume],
);

const firstName = (me?.user.name ?? me?.user.email ?? "").split(/[\s@]/)[0] || "there";
Expand Down Expand Up @@ -138,7 +154,11 @@ export function OperatorHome() {
aria-label="Live operations"
data-testid="dashboard-live-operations"
>
<AttentionRail slug={workspace.slug} />
<AttentionRail
slug={workspace.slug}
blockedIssues={blockedIssueRows}
blockedLoading={dependencyBlocked.isLoading}
/>
<AgentActivityTile slug={workspace.slug} />
<TodayWidget slug={workspace.slug} workspaceKey={ws?.key ?? "—"} maxDueSoon={4} />
</aside>
Expand Down Expand Up @@ -369,7 +389,15 @@ function WorkRow({

type AttentionTab = "decisions" | "exceptions" | "blocked";

function AttentionRail({ slug }: { slug: string }) {
function AttentionRail({
slug,
blockedIssues,
blockedLoading,
}: {
slug: string;
blockedIssues: Array<{ id: string; title: string }>;
blockedLoading: boolean;
}) {
const { data, isLoading } = trpc.commandCenter.summary.useQuery(
{ limit: 8 },
{ refetchOnWindowFocus: true, staleTime: 30_000 },
Expand Down Expand Up @@ -422,13 +450,12 @@ function AttentionRail({ slug }: { slug: string }) {
];
}, [data, slug]);

const blocked =
data?.stalledRuns.map((run) => ({
id: `blocked-${run.id}`,
title: run.issue.title,
detail: run.recoveryDetail ?? "Work is blocked",
href: `/w/${slug}/issues/${run.issue.id}`,
})) ?? [];
const blocked = blockedIssues.map((issue) => ({
id: `blocked-${issue.id}`,
title: issue.title,
detail: "Blocked by an open issue dependency",
href: `/w/${slug}/issues/${issue.id}`,
}));
const activeItems = tab === "decisions" ? decisions : tab === "exceptions" ? exceptions : blocked;
const total = decisions.length + exceptions.length + blocked.length;

Expand Down Expand Up @@ -474,7 +501,7 @@ function AttentionRail({ slug }: { slug: string }) {
</AttentionTabButton>
</div>
<div role="tabpanel" className="min-h-32 p-2">
{isLoading ? (
{isLoading || blockedLoading ? (
<div className="h-24 animate-pulse rounded bg-subtle/40" />
) : activeItems.length === 0 ? (
<div className="text-meta flex min-h-28 flex-col items-center justify-center text-center text-muted-foreground">
Expand Down
Loading
Loading