Show task summary above conversation and distinguish sidebar PRs

Signed-off-by: John Tennant <jtennant@squareup.com>
This commit is contained in:
John Tennant
2026-09-09 13:13:40 -04:00
parent 3b5a5f4752
commit dafa6b3319
4 changed files with 61 additions and 46 deletions

View File

@@ -1,5 +1,5 @@
import { openUrl } from "@tauri-apps/plugin-opener";
import * as React from "react";
import type * as React from "react";
import { toast } from "sonner";
import { useCanvasQuery } from "@/features/channels/hooks";
import {
@@ -68,49 +68,26 @@ export function TaskChannelWorkspace({
}) {
const canvas = useCanvasQuery(channelId, channelId !== null);
const task = parseChannelBackedTask(canvas.data?.content);
const [view, setView] = React.useState<"overview" | "conversation">(
"overview",
);
if (!task) return children;
return (
<div className="flex min-h-0 flex-1 flex-col">
<nav
aria-label="Task views"
<div
className={cn(
channelChrome.contentPadding,
"flex gap-1 border-b px-5 pb-2",
"shrink-0 border-b px-5 pb-3",
)}
data-testid="task-view-tabs"
>
{(["overview", "conversation"] as const).map((item) => (
<button
key={item}
type="button"
aria-current={view === item ? "page" : undefined}
data-testid={`task-view-${item}`}
className={cn(
"rounded-full px-3 py-1.5 text-xs capitalize hover:bg-muted",
view === item && "bg-muted",
)}
onClick={() => setView(item)}
>
{item}
</button>
))}
</nav>
{view === "conversation" ? (
children
) : (
<div className="min-h-0 flex-1 overflow-y-auto">
<details open data-testid="task-overview">
<summary className="cursor-pointer text-sm font-medium">
{task.task.title}
</summary>
<div
className="mx-auto max-w-3xl space-y-6 px-6 py-8"
data-testid="task-overview"
className="mt-2 max-h-48 space-y-2 overflow-y-auto text-sm"
data-testid="task-overview-details"
>
<h2 className="text-xl font-semibold">{task.task.title}</h2>
<p className="text-sm text-muted-foreground">
{task.task.description}
</p>
<div className="space-y-2 text-sm">
<p className="text-muted-foreground">{task.task.description}</p>
{task.branch && <BranchStatus branch={task.branch} />}
<div className="flex flex-wrap gap-x-6 gap-y-2 text-xs">
<div>
Origin <Markdown content={task.originatingThread} />
</div>
@@ -118,10 +95,17 @@ export function TaskChannelWorkspace({
Parent <Markdown content={task.parentChannel} />
</div>
</div>
{task.branch && <BranchStatus branch={task.branch} />}
</div>
</div>
)}
</details>
</div>
<div
className="flex min-h-0 flex-1 flex-col"
style={
{ "--buzz-channel-content-top-padding": "0px" } as React.CSSProperties
}
>
{children}
</div>
</div>
);
}

View File

@@ -1,4 +1,5 @@
import { useContext } from "react";
import { CircleAlert } from "lucide-react";
import { TaskChannelBranches } from "@/features/channels/useChannelBackedTaskParents";
import { useTaskBranchStatus } from "@/features/channels/useTaskBranchStatus";
import type { ChannelBackedTask } from "@/features/channels/lib/channelBackedTask";
@@ -17,11 +18,36 @@ function BranchGlyph({
className="ml-auto flex shrink-0"
data-testid="task-channel-status"
>
<Icon
role="img"
aria-label={description}
className={cn("h-3.5 w-3.5", color)}
/>
{query.error && !pr ? (
<CircleAlert
role="img"
aria-label={description}
className="h-3.5 w-3.5 text-muted-foreground"
/>
) : pr ? (
<Icon
role="img"
aria-label={description}
className={cn("h-3.5 w-3.5", color)}
/>
) : (
<svg
role="img"
aria-label={description}
className={cn("h-3.5 w-3.5", color)}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
>
<path d="M6 7v10" />
<path d="M18 4v13" strokeDasharray="1 4" />
<circle cx="6" cy="4" r="2.5" />
<circle cx="6" cy="20" r="2.5" />
<circle cx="18" cy="20" r="2.5" />
</svg>
)}
</span>
);
}

View File

@@ -14,7 +14,7 @@ branch:
name: "jtennant/berd-voice-status-sounds"
---`;
test("a task canvas exposes overview and conversation only", async ({
test("a task summary sits above the conversation and can collapse", async ({
page,
}) => {
await installMockBridge(page, { canvasContent: TASK_CANVAS });
@@ -30,8 +30,13 @@ test("a task canvas exposes overview and conversation only", async ({
"jtennant/berd-voice-status-sounds",
);
await expect(page.getByTestId("task-work-tree")).toHaveCount(0);
await page.getByTestId("task-view-conversation").click();
await expect(page.getByTestId("task-view-tabs")).toHaveCount(0);
await expect(page.getByTestId("message-timeline")).toBeVisible();
await page.getByTestId("task-overview").locator("summary").click();
await expect(page.getByTestId("task-overview-details")).toBeHidden();
await expect(page.getByTestId("message-timeline")).toBeVisible();
await page.getByTestId("task-overview").locator("summary").press("Enter");
await expect(page.getByTestId("task-overview-details")).toBeVisible();
await expect(page.getByTestId("task-work-navigator")).toHaveCount(0);
});

View File

@@ -16,7 +16,7 @@ Conversation → local task + new channel → implementation branch → pull req
- A branch created outside this workflow can remain unbound temporarily and should be flagged for resolution. A person or agent can attach it to an appropriate channel or create a new one.
- Rebinding an established branch is for correcting mistakes, not routine restructuring.
- The branch, pull request, CI, and review use the task's channel.
- The client recognizes the canvas schema and adds Overview and Conversation views. Overview shows the selected task and its branch or pull request. The nested left sidebar shows branch and PR status glyphs, using the local `gh` login to discover pull requests.
- The client recognizes the canvas schema and shows a collapsible task summary above the conversation, including its branch or pull request. The nested left sidebar shows branch and PR status glyphs, using the local `gh` login to discover pull requests.
## Canvas data model