From 563e9ed4dff66060bbf6c4f181fd7d41b6affde7 Mon Sep 17 00:00:00 2001
From: 0xRoy <1997roylee@gmail.com>
Date: Thu, 4 Jun 2026 00:33:07 +0800
Subject: [PATCH] Add Issues nav and normalize board statuses
---
.../chat-room/chat-room-settings-sidebar.tsx | 6 +-
.../chat-room/chat-room-sidebar-nav.tsx | 66 ++++++++++++++++
.../chat-room/chat-room-sidebar.tsx | 33 ++------
.../types/chat-room-sidebar.types.ts | 6 ++
.../issue-detail-editor-utils.tsx | 4 +-
.../issues-board/issues-board-utils.ts | 25 +++---
.../issues-board/issues-board.constants.ts | 12 ++-
.../web-shell/web-shell.constants.ts | 1 +
.../tests/issue-detail-editor-utils.test.ts | 36 ++++++++-
packages/web/tests/issues-board-utils.test.ts | 77 +++++++++++++++++++
.../web/tests/web-shell-navigation.test.ts | 9 +++
11 files changed, 223 insertions(+), 52 deletions(-)
create mode 100644 packages/web/src/components/chat-room/chat-room-sidebar-nav.tsx
create mode 100644 packages/web/tests/issues-board-utils.test.ts
diff --git a/packages/web/src/components/chat-room/chat-room-settings-sidebar.tsx b/packages/web/src/components/chat-room/chat-room-settings-sidebar.tsx
index 7e569ebc..915f63d2 100644
--- a/packages/web/src/components/chat-room/chat-room-settings-sidebar.tsx
+++ b/packages/web/src/components/chat-room/chat-room-settings-sidebar.tsx
@@ -1,7 +1,10 @@
"use client";
+import { Button } from "@/components/ui/button";
+import { Typography } from "@/components/ui/typography";
import type { SidebarNavItem } from "@/components/web-shell/types/web-shell.types";
import { navItems } from "@/components/web-shell/web-shell.constants";
+import { cn } from "@/lib/utils";
import {
ArrowLeft,
BookOpen,
@@ -23,9 +26,6 @@ import {
import Link from "next/link";
import { usePathname } from "next/navigation";
import type { ComponentType, ReactElement } from "react";
-import { Button } from "@/components/ui/button";
-import { Typography } from "@/components/ui/typography";
-import { cn } from "@/lib/utils";
import type { ChatRoomSettingsSidebarProps } from "./types/chat-room.types";
const iconByKey: Record<
diff --git a/packages/web/src/components/chat-room/chat-room-sidebar-nav.tsx b/packages/web/src/components/chat-room/chat-room-sidebar-nav.tsx
new file mode 100644
index 00000000..4a7c9324
--- /dev/null
+++ b/packages/web/src/components/chat-room/chat-room-sidebar-nav.tsx
@@ -0,0 +1,66 @@
+"use client";
+
+import { ListChecks, Settings } from "lucide-react";
+import Link from "next/link";
+import type { ReactElement } from "react";
+
+import { Button } from "@/components/ui/button";
+import { Typography } from "@/components/ui/typography";
+import { cn } from "@/lib/utils";
+import type { ChatRoomSidebarNavProps } from "./types/chat-room-sidebar.types";
+
+export function ChatRoomSidebarNav({
+ isCollapsed,
+ onCloseSidebar,
+ onSettingsClick,
+}: ChatRoomSidebarNavProps): ReactElement {
+ return (
+
+ );
+}
diff --git a/packages/web/src/components/chat-room/chat-room-sidebar.tsx b/packages/web/src/components/chat-room/chat-room-sidebar.tsx
index 85c3bae0..edff56f8 100644
--- a/packages/web/src/components/chat-room/chat-room-sidebar.tsx
+++ b/packages/web/src/components/chat-room/chat-room-sidebar.tsx
@@ -1,6 +1,6 @@
"use client";
-import { MessageSquarePlus, Search, Settings, X } from "lucide-react";
+import { MessageSquarePlus, Search, X } from "lucide-react";
import { type ReactElement, useState } from "react";
import { Button } from "@/components/ui/button";
@@ -10,6 +10,7 @@ import { cn } from "@/lib/utils";
import { ChatRoomSessionList } from "./chat-room-session-list";
import { ChatRoomSettingsSidebar } from "./chat-room-settings-sidebar";
import { ChatRoomSidebarHeader } from "./chat-room-sidebar-header";
+import { ChatRoomSidebarNav } from "./chat-room-sidebar-nav";
import { buildChatSessionSidebarContent } from "./chat-room-sidebar-utils";
import type { ChatRoomSidebarProps } from "./types/chat-room-sidebar.types";
import type { ChatRoomSidebarView } from "./types/chat-room.types";
@@ -64,7 +65,6 @@ export function ChatRoomSidebar({
}
function handleSettingsClick(): void {
- console.log("sidebarView", sidebarView)
if (isCollapsed) {
onToggleCollapsed();
}
@@ -199,30 +199,11 @@ export function ChatRoomSidebar({
onUnpinSession={unpinSession}
/>
-
+
void;
}
+export interface ChatRoomSidebarNavProps {
+ isCollapsed: boolean;
+ onCloseSidebar: () => void;
+ onSettingsClick: () => void;
+}
+
export interface ChatRoomSessionListProps {
activeSessionId: string;
collapsedProjectIds: Set;
diff --git a/packages/web/src/components/issues-board/issue-detail-editor-utils.tsx b/packages/web/src/components/issues-board/issue-detail-editor-utils.tsx
index 6281a890..1823cac2 100644
--- a/packages/web/src/components/issues-board/issue-detail-editor-utils.tsx
+++ b/packages/web/src/components/issues-board/issue-detail-editor-utils.tsx
@@ -4,7 +4,7 @@ import type { ReactElement, ReactNode } from "react";
import { Typography } from "@/components/ui/typography";
import type { ProjectBoardTaskRecord, TaskMutationRequest } from "@/lib/api";
-import { normalizeDueDate } from "./issues-board-utils";
+import { normalizeBoardStatus, normalizeDueDate } from "./issues-board-utils";
import type { IssueDetailDraft } from "./types/issues-board.types";
export function DetailField({
@@ -53,7 +53,7 @@ export function createDetailDraft(
title: task.title,
content: task.content,
priority: String(task.priority),
- status: task.status,
+ status: normalizeBoardStatus(task.status),
creatorId: task.creatorId,
dueDate: toDateInputValue(task.dueDate),
linkedPr: task.linkedPr ?? "",
diff --git a/packages/web/src/components/issues-board/issues-board-utils.ts b/packages/web/src/components/issues-board/issues-board-utils.ts
index 84c65408..e93e6b94 100644
--- a/packages/web/src/components/issues-board/issues-board-utils.ts
+++ b/packages/web/src/components/issues-board/issues-board-utils.ts
@@ -11,9 +11,8 @@ import {
import type { IssueDraft, IssueTab } from "./types/issues-board.types";
const LEGACY_PR_CREATED_STATUS = "pr_created";
-const LEGACY_PLANNING_STATUS = "planning";
-const LEGACY_PLAN_STATUS = "todo";
-const LEGACY_IN_PROGRESS_STATUS = "implementing";
+const TODO_STATUSES = ["planning", "plan", "todo"] as const;
+const RUNNING_STATUSES = ["implementing", "in_progress", "running"] as const;
const LEGACY_REVIEW_STATUSES = ["reviewing", "testing"] as const;
export function getStatusLabel(status: string): string {
@@ -147,17 +146,17 @@ function normalizeIndex(index: number): number {
}
export function normalizeBoardStatus(status: string): string {
- if (status === LEGACY_PLANNING_STATUS) {
- return "plan";
+ if ((TODO_STATUSES as readonly string[]).includes(status)) {
+ return "todo";
}
- if (status === LEGACY_PLAN_STATUS) {
- return "plan";
+ if ((RUNNING_STATUSES as readonly string[]).includes(status)) {
+ return "running";
}
- if (status === LEGACY_IN_PROGRESS_STATUS) {
- return "in_progress";
- }
- return status === LEGACY_PR_CREATED_STATUS ||
+ if (
+ status === LEGACY_PR_CREATED_STATUS ||
(LEGACY_REVIEW_STATUSES as readonly string[]).includes(status)
- ? "in_review"
- : status;
+ ) {
+ return "in_review";
+ }
+ return status === "failed" ? "canceled" : status;
}
diff --git a/packages/web/src/components/issues-board/issues-board.constants.ts b/packages/web/src/components/issues-board/issues-board.constants.ts
index f2bf8bbb..c102040c 100644
--- a/packages/web/src/components/issues-board/issues-board.constants.ts
+++ b/packages/web/src/components/issues-board/issues-board.constants.ts
@@ -8,19 +8,18 @@ export const DEFAULT_CREATOR_ID = "member-1";
export const STATUS_ORDER = [
"backlog",
- "plan",
- "in_progress",
+ "todo",
+ "running",
"in_review",
"done",
"canceled",
- "failed",
] as const;
export const STATUS_PRESENTATION: Record = {
backlog: { label: "Backlog", tone: "border-slate-700/70 bg-surface-panel" },
- plan: { label: "Plan", tone: "border-slate-700/70 bg-surface-panel" },
- in_progress: {
- label: "In Progress",
+ todo: { label: "To Do", tone: "border-slate-700/70 bg-surface-panel" },
+ running: {
+ label: "Running",
tone: "border-yellow-900/50 bg-yellow-950/35",
},
in_review: {
@@ -29,7 +28,6 @@ export const STATUS_PRESENTATION: Record = {
},
done: { label: "Done", tone: "border-indigo-900/50 bg-indigo-950/35" },
canceled: { label: "Canceled", tone: "border-zinc-700/70 bg-zinc-950/35" },
- failed: { label: "Failed", tone: "border-red-900/50 bg-red-950/35" },
};
export const PRIORITY_OPTIONS: readonly PriorityOption[] = [
diff --git a/packages/web/src/components/web-shell/web-shell.constants.ts b/packages/web/src/components/web-shell/web-shell.constants.ts
index 7a3c774b..c96d0b93 100644
--- a/packages/web/src/components/web-shell/web-shell.constants.ts
+++ b/packages/web/src/components/web-shell/web-shell.constants.ts
@@ -7,6 +7,7 @@ import type {
export const navItems: SidebarNavItem[] = [
{ key: "agents", label: "Agents", href: "/agents" },
{ key: "projects", label: "Projects", href: "/projects" },
+ { key: "issues", label: "Issues", href: "/issues" },
{ key: "integrations", label: "Integrations", href: "/integrations" },
{ key: "git", label: "Git", href: "/git" },
{ key: "usage", label: "Usage", href: "/usage" },
diff --git a/packages/web/tests/issue-detail-editor-utils.test.ts b/packages/web/tests/issue-detail-editor-utils.test.ts
index 1e4b44f3..5d043cb7 100644
--- a/packages/web/tests/issue-detail-editor-utils.test.ts
+++ b/packages/web/tests/issue-detail-editor-utils.test.ts
@@ -1,8 +1,21 @@
import { describe, expect, it } from "bun:test";
-import { createSaveRequest } from "../src/components/issues-board/issue-detail-editor-utils";
+import {
+ createDetailDraft,
+ createSaveRequest,
+} from "../src/components/issues-board/issue-detail-editor-utils";
+import type { ProjectBoardTaskRecord } from "../src/lib/api";
describe("issue detail editor utilities", () => {
+ it("normalizes existing task status values for the issue status picker", () => {
+ expect(createDetailDraft(buildTask({ status: "plan" })).status).toBe(
+ "todo",
+ );
+ expect(createDetailDraft(buildTask({ status: "in_progress" })).status).toBe(
+ "running",
+ );
+ });
+
it("allows saving tasks with empty description content", () => {
const result = createSaveRequest(
{
@@ -32,3 +45,24 @@ describe("issue detail editor utilities", () => {
});
});
});
+
+function buildTask(
+ overrides: Partial = {},
+): ProjectBoardTaskRecord {
+ return {
+ id: "task-1",
+ taskKey: "TASK-1",
+ projectId: "project-1",
+ title: "Task",
+ content: "Do the work",
+ priority: 1,
+ status: "backlog",
+ dueDate: null,
+ creatorId: "member-1",
+ assigneeId: null,
+ linkedPr: null,
+ createdAt: "2026-06-01T00:00:00.000Z",
+ updatedAt: "2026-06-01T00:00:00.000Z",
+ ...overrides,
+ };
+}
diff --git a/packages/web/tests/issues-board-utils.test.ts b/packages/web/tests/issues-board-utils.test.ts
new file mode 100644
index 00000000..552efeca
--- /dev/null
+++ b/packages/web/tests/issues-board-utils.test.ts
@@ -0,0 +1,77 @@
+import { describe, expect, it } from "bun:test";
+
+import {
+ buildStatusColumns,
+ getStatusLabel,
+ normalizeBoardStatus,
+} from "../src/components/issues-board/issues-board-utils";
+import { STATUS_ORDER } from "../src/components/issues-board/issues-board.constants";
+import type { ProjectBoardTaskRecord } from "../src/lib/api";
+
+describe("issues board utilities", () => {
+ it("uses the requested issue status columns in order", () => {
+ expect([...STATUS_ORDER]).toEqual([
+ "backlog",
+ "todo",
+ "running",
+ "in_review",
+ "done",
+ "canceled",
+ ]);
+ });
+
+ it("normalizes existing task statuses into the six board columns", () => {
+ const columns = buildStatusColumns([
+ buildTask({ id: "task-plan", status: "plan" }),
+ buildTask({ id: "task-progress", status: "in_progress" }),
+ buildTask({ id: "task-failed", status: "failed" }),
+ buildTask({ id: "task-reviewing", status: "reviewing" }),
+ ]);
+
+ expect(columns.map((column) => column.status)).toEqual([
+ "backlog",
+ "todo",
+ "running",
+ "in_review",
+ "done",
+ "canceled",
+ ]);
+ expect(columns.find((column) => column.status === "todo")?.tasks).toEqual([
+ expect.objectContaining({ id: "task-plan", status: "todo" }),
+ ]);
+ expect(
+ columns.find((column) => column.status === "running")?.tasks,
+ ).toEqual([
+ expect.objectContaining({ id: "task-progress", status: "running" }),
+ ]);
+ expect(
+ columns.find((column) => column.status === "canceled")?.tasks,
+ ).toEqual([
+ expect.objectContaining({ id: "task-failed", status: "canceled" }),
+ ]);
+ expect(normalizeBoardStatus("todo")).toBe("todo");
+ expect(getStatusLabel("running")).toBe("Running");
+ expect(getStatusLabel("plan")).toBe("To Do");
+ });
+});
+
+function buildTask(
+ overrides: Partial = {},
+): ProjectBoardTaskRecord {
+ return {
+ id: "task-1",
+ taskKey: "TASK-1",
+ projectId: "project-1",
+ title: "Task",
+ content: "Do the work",
+ priority: 1,
+ status: "backlog",
+ dueDate: null,
+ creatorId: "member-1",
+ assigneeId: null,
+ linkedPr: null,
+ createdAt: "2026-06-01T00:00:00.000Z",
+ updatedAt: "2026-06-01T00:00:00.000Z",
+ ...overrides,
+ };
+}
diff --git a/packages/web/tests/web-shell-navigation.test.ts b/packages/web/tests/web-shell-navigation.test.ts
index 0f00b4ce..71e1be22 100644
--- a/packages/web/tests/web-shell-navigation.test.ts
+++ b/packages/web/tests/web-shell-navigation.test.ts
@@ -14,4 +14,13 @@ describe("web shell navigation", () => {
});
expect(hrefForNavKey("git")).toBe("/git");
});
+
+ it("shows Issues in workspace navigation and routes it to the task board", () => {
+ expect(navItems).toContainEqual({
+ key: "issues",
+ label: "Issues",
+ href: "/issues",
+ });
+ expect(hrefForNavKey("issues")).toBe("/issues");
+ });
});