feat(KB-092): polish planning mode modal UX

- Rework the planning modal into scrollable content regions with fixed footers for the initial, question, and summary states
- Refresh question and summary styling with clearer progress, polished selection controls, and improved dependency and deliverable presentation
- Improve responsive behavior with full-height mobile layout, stacked actions, and better overflow handling
- Expand PlanningModeModal coverage to verify the new structure and add a TaskDetailModal smoke check
This commit is contained in:
gsxdsm
2026-03-30 03:46:55 -07:00
parent 4e23716b7d
commit 0322f50907
3 changed files with 648 additions and 296 deletions

View File

@@ -1,19 +1,40 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { PlanningModeModal } from "./PlanningModeModal";
import type { Task, PlanningQuestion, PlanningSummary } from "@kb/core";
import { TaskDetailModal } from "./TaskDetailModal";
import type { Task, TaskDetail, PlanningQuestion, PlanningSummary, MergeResult } from "@kb/core";
// Mock the API functions
const mockStartPlanning = vi.fn();
const mockRespondToPlanning = vi.fn();
const mockCancelPlanning = vi.fn();
const mockCreateTaskFromPlanning = vi.fn();
const mockUploadAttachment = vi.fn();
const mockDeleteAttachment = vi.fn();
const mockUpdateTask = vi.fn();
const mockPauseTask = vi.fn();
const mockUnpauseTask = vi.fn();
const mockFetchTaskDetail = vi.fn();
const mockRequestSpecRevision = vi.fn();
const mockApprovePlan = vi.fn();
const mockRejectPlan = vi.fn();
const mockRefineTask = vi.fn();
vi.mock("../api", () => ({
startPlanning: (...args: any[]) => mockStartPlanning(...args),
respondToPlanning: (...args: any[]) => mockRespondToPlanning(...args),
cancelPlanning: (...args: any[]) => mockCancelPlanning(...args),
createTaskFromPlanning: (...args: any[]) => mockCreateTaskFromPlanning(...args),
uploadAttachment: (...args: any[]) => mockUploadAttachment(...args),
deleteAttachment: (...args: any[]) => mockDeleteAttachment(...args),
updateTask: (...args: any[]) => mockUpdateTask(...args),
pauseTask: (...args: any[]) => mockPauseTask(...args),
unpauseTask: (...args: any[]) => mockUnpauseTask(...args),
fetchTaskDetail: (...args: any[]) => mockFetchTaskDetail(...args),
requestSpecRevision: (...args: any[]) => mockRequestSpecRevision(...args),
approvePlan: (...args: any[]) => mockApprovePlan(...args),
rejectPlan: (...args: any[]) => mockRejectPlan(...args),
refineTask: (...args: any[]) => mockRefineTask(...args),
}));
const mockTasks: Task[] = [
@@ -50,6 +71,22 @@ const mockSummary: PlanningSummary = {
keyDeliverables: ["Login page", "Signup page", "Auth API"],
};
const mockTaskDetail = {
id: "KB-999",
title: "Example task",
description: "Example description",
column: "todo",
dependencies: [],
steps: [],
currentStep: 0,
log: [],
attachments: [],
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
prompt: "# Task\n\nExample prompt",
paused: false,
} as TaskDetail;
describe("PlanningModeModal", () => {
const mockOnClose = vi.fn();
const mockOnTaskCreated = vi.fn();
@@ -61,7 +98,7 @@ describe("PlanningModeModal", () => {
describe("Initial view", () => {
it("renders the initial input view when open", () => {
render(
const { container } = render(
<PlanningModeModal
isOpen={true}
onClose={mockOnClose}
@@ -72,6 +109,9 @@ describe("PlanningModeModal", () => {
expect(screen.getByText("Planning Mode")).toBeDefined();
expect(screen.getByPlaceholderText(/e.g., Build a user authentication/)).toBeDefined();
expect(container.querySelector(".planning-modal-body")).not.toBeNull();
expect(container.querySelector(".planning-modal-body")?.classList.contains("modal-body")).toBe(false);
expect(container.querySelector(".planning-examples-label")?.textContent).toBe("Try an example:");
});
it("does not render when closed", () => {
@@ -174,13 +214,7 @@ describe("PlanningModeModal", () => {
describe("Question view", () => {
it("renders single_select question with options", async () => {
mockStartPlanning.mockResolvedValue({
sessionId: "session-123",
currentQuestion: mockQuestion,
summary: null,
});
render(
const { container } = render(
<PlanningModeModal
isOpen={true}
onClose={mockOnClose}
@@ -189,6 +223,12 @@ describe("PlanningModeModal", () => {
/>
);
mockStartPlanning.mockResolvedValue({
sessionId: "session-123",
currentQuestion: mockQuestion,
summary: null,
});
const textarea = screen.getByPlaceholderText(/e.g., Build a user authentication/);
fireEvent.change(textarea, { target: { value: "Build auth system" } });
fireEvent.click(screen.getByText("Start Planning"));
@@ -198,6 +238,9 @@ describe("PlanningModeModal", () => {
expect(screen.getByText("Medium")).toBeDefined();
expect(screen.getByText("Large")).toBeDefined();
});
expect(container.querySelector(".planning-question-form > .planning-view-scroll")).not.toBeNull();
expect(container.querySelector(".planning-question-form > .planning-actions")).not.toBeNull();
});
});
@@ -209,7 +252,7 @@ describe("PlanningModeModal", () => {
summary: mockSummary,
});
render(
const { container } = render(
<PlanningModeModal
isOpen={true}
onClose={mockOnClose}
@@ -225,6 +268,10 @@ describe("PlanningModeModal", () => {
await waitFor(() => {
expect(screen.getByText("Planning Complete!")).toBeDefined();
});
expect(container.querySelector(".planning-summary > .planning-view-scroll")).not.toBeNull();
expect(container.querySelector(".planning-summary > .planning-actions")).not.toBeNull();
expect(container.querySelector(".planning-summary .planning-deps-list")).not.toBeNull();
});
it("creates task from summary", async () => {
@@ -274,4 +321,30 @@ describe("PlanningModeModal", () => {
});
});
});
describe("Modal smoke checks", () => {
it("renders TaskDetailModal with the standard detail body structure", () => {
const onMoveTask = vi.fn<(_: string, __: any) => Promise<Task>>().mockResolvedValue(mockTasks[0]);
const onDeleteTask = vi.fn<(_: string) => Promise<Task>>().mockResolvedValue(mockTasks[0]);
const onMergeTask = vi
.fn<(_: string) => Promise<MergeResult>>()
.mockResolvedValue({ merged: true, branch: "kb/kb-999" });
const { container } = render(
<TaskDetailModal
task={mockTaskDetail}
tasks={mockTasks}
onClose={mockOnClose}
onOpenDetail={vi.fn()}
onMoveTask={onMoveTask}
onDeleteTask={onDeleteTask}
onMergeTask={onMergeTask}
addToast={vi.fn()}
/>
);
expect(screen.getByText("Definition")).toBeDefined();
expect(container.querySelector(".detail-body")).not.toBeNull();
});
});
});