refactor: 右侧面板 Tasks/Files 合并为单视图 + 简化 TaskList

- RightPanel: 移除 Files/Tasks 双 Tab 切换,改为单视图布局
  - 顶部标题改为 TASKS,下方直接渲染 TaskList
  - 中间用 mx-4 非贯通分隔线 + border-border/40 降低视觉强度
  - 分隔线下方增加 FILES 小标题,再接 FileTree
- TaskList: 大幅简化
  - 移除筛选 Tab(全部/进行中/已完成)
  - 移除新增任务输入框
  - 移除 TaskCard 组件引用,改为 checkbox + 标题单行展示
  - 点击切换 completed/pending 状态
  - 勾选框使用 foreground 色(黑/白)替代蓝色
  - 空状态显示"暂无任务"
- bump version to 0.20.1

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
7418andClaude Opus 4.6 committed 2026-02-28 13:27:30 +08:00
1 parent b174527719
commit 7696cbf5c5
4 files changed
+76 -155

No files matched your search

+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "codepilot",
"version": "0.20.0",
"version": "0.20.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "codepilot",
"version": "0.20.0",
"version": "0.20.1",
"dependencies": {
"@ai-sdk/anthropic": "^3.0.47",
"@ai-sdk/google": "^3.0.31",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "codepilot",
"version": "0.20.0",
"version": "0.20.1",
"private": true,
"author": {
"name": "op7418",
+21 -29
View File
@@ -13,14 +13,13 @@ import { usePanel } from "@/hooks/usePanel";
import { useTranslation } from "@/hooks/useTranslation";
import { FileTree } from "@/components/project/FileTree";
import { TaskList } from "@/components/project/TaskList";
import { cn } from "@/lib/utils";
interface RightPanelProps {
width?: number;
}
export function RightPanel({ width }: RightPanelProps) {
const { panelOpen, setPanelOpen, panelContent, setPanelContent, workingDirectory, sessionId, previewFile, setPreviewFile } = usePanel();
const { panelOpen, setPanelOpen, workingDirectory, sessionId, previewFile, setPreviewFile } = usePanel();
const { t } = useTranslation();
const handleFileAdd = useCallback((path: string) => {
@@ -73,26 +72,9 @@ export function RightPanel({ width }: RightPanelProps) {
<aside className="hidden h-full shrink-0 flex-col overflow-hidden bg-background lg:flex" style={{ width: width ?? 288 }}>
{/* Header */}
<div className="flex h-12 mt-5 shrink-0 items-center justify-between px-4">
<div className="flex items-center gap-1">
<button
className={cn(
"text-[11px] font-semibold uppercase tracking-wider px-2 py-1 rounded",
panelContent === "files" ? "bg-accent text-accent-foreground" : "text-muted-foreground hover:text-foreground"
)}
onClick={() => setPanelContent("files")}
>
{t('panel.files')}
</button>
<button
className={cn(
"text-[11px] font-semibold uppercase tracking-wider px-2 py-1 rounded",
panelContent === "tasks" ? "bg-accent text-accent-foreground" : "text-muted-foreground hover:text-foreground"
)}
onClick={() => setPanelContent("tasks")}
>
{t('panel.tasks')}
</button>
</div>
<span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
{t('panel.tasks')}
</span>
<Tooltip>
<TooltipTrigger asChild>
<Button
@@ -108,19 +90,29 @@ export function RightPanel({ width }: RightPanelProps) {
</Tooltip>
</div>
{/* Body */}
{/* Body — TaskList + divider + FileTree */}
<div className="flex flex-1 flex-col min-h-0 overflow-hidden">
{panelContent === "files" ? (
{/* Tasks */}
<div className="shrink-0 px-3 pb-3">
<TaskList sessionId={sessionId} />
</div>
{/* Divider */}
<div className="mx-4 mt-1 mb-2 border-t border-border/40" />
{/* File tree */}
<div className="flex-1 min-h-0 overflow-hidden">
<div className="px-4 pt-1 pb-1">
<span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
{t('panel.files')}
</span>
</div>
<FileTree
workingDirectory={workingDirectory}
onFileSelect={handleFileSelect}
onFileAdd={handleFileAdd}
/>
) : (
<div className="px-3 pt-1 h-full">
<TaskList sessionId={sessionId} />
</div>
)}
</div>
</div>
</aside>
);
+52 -123
View File
@@ -1,13 +1,7 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import { HugeiconsIcon } from "@hugeicons/react";
import { PlusSignIcon } from "@hugeicons/core-free-icons";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
import { TaskCard } from "./TaskCard";
import { useTranslation } from "@/hooks/useTranslation";
import type { TaskItem, TaskStatus } from "@/types";
@@ -15,14 +9,10 @@ interface TaskListProps {
sessionId: string;
}
type FilterTab = "all" | "in_progress" | "completed";
export function TaskList({ sessionId }: TaskListProps) {
const { t } = useTranslation();
const [tasks, setTasks] = useState<TaskItem[]>([]);
const [loading, setLoading] = useState(false);
const [newTitle, setNewTitle] = useState("");
const [filter, setFilter] = useState<FilterTab>("all");
const fetchTasks = useCallback(async () => {
if (!sessionId) return;
@@ -51,135 +41,74 @@ export function TaskList({ sessionId }: TaskListProps) {
return () => window.removeEventListener('tasks-updated', handler);
}, [fetchTasks]);
const handleCreate = async () => {
const title = newTitle.trim();
if (!title || !sessionId) return;
const handleToggle = async (task: TaskItem) => {
const nextStatus: TaskStatus = task.status === "completed" ? "pending" : "completed";
try {
const res = await fetch("/api/tasks", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ session_id: sessionId, title }),
});
if (res.ok) {
const data = await res.json();
setTasks((prev) => [...prev, data.task]);
setNewTitle("");
}
} catch {
// silently fail
}
};
const handleUpdate = async (
id: string,
updates: { title?: string; status?: TaskStatus }
) => {
try {
const res = await fetch(`/api/tasks/${id}`, {
const res = await fetch(`/api/tasks/${task.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(updates),
body: JSON.stringify({ status: nextStatus }),
});
if (res.ok) {
const data = await res.json();
setTasks((prev) => prev.map((t) => (t.id === id ? data.task : t)));
setTasks((prev) => prev.map((t) => (t.id === task.id ? data.task : t)));
}
} catch {
// silently fail
}
};
const handleDelete = async (id: string) => {
try {
const res = await fetch(`/api/tasks/${id}`, { method: "DELETE" });
if (res.ok) {
setTasks((prev) => prev.filter((t) => t.id !== id));
}
} catch {
// silently fail
}
};
if (loading && tasks.length === 0) {
return (
<p className="py-2 text-center text-xs text-muted-foreground">
{t('tasks.loading')}
</p>
);
}
const filtered = tasks.filter((task) => {
if (filter === "all") return true;
if (filter === "in_progress")
return task.status === "pending" || task.status === "in_progress";
if (filter === "completed") return task.status === "completed";
return true;
});
const filterTabs: { key: FilterTab; label: string }[] = [
{ key: "all", label: t('tasks.all') },
{ key: "in_progress", label: t('tasks.active') },
{ key: "completed", label: t('tasks.done') },
];
if (tasks.length === 0) {
return (
<p className="py-2 text-center text-xs text-muted-foreground">
{t('tasks.noTasks')}
</p>
);
}
return (
<div className="flex h-full flex-col">
{/* Filter tabs */}
<div className="flex items-center gap-1 pb-2">
{filterTabs.map((tab) => (
<Button
key={tab.key}
variant="ghost"
size="sm"
className={cn(
"h-6 px-2 text-[10px]",
filter === tab.key && "bg-accent"
)}
onClick={() => setFilter(tab.key)}
<div className="flex flex-col gap-0.5">
{tasks.map((task) => {
const isDone = task.status === "completed";
return (
<button
key={task.id}
className="flex items-center gap-2 rounded-md px-1 py-1 text-left hover:bg-accent/50 transition-colors"
onClick={() => handleToggle(task)}
>
{tab.label}
</Button>
))}
</div>
{/* Add task input */}
<div className="flex items-center gap-1 pb-2">
<Input
placeholder={t('tasks.addPlaceholder')}
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") handleCreate();
}}
className="h-7 text-xs"
/>
<Button
variant="ghost"
size="icon-sm"
onClick={handleCreate}
disabled={!newTitle.trim()}
>
<HugeiconsIcon icon={PlusSignIcon} className="h-3.5 w-3.5" />
<span className="sr-only">{t('tasks.addTask')}</span>
</Button>
</div>
{/* Task list */}
<ScrollArea className="flex-1">
{loading && tasks.length === 0 ? (
<p className="py-4 text-center text-xs text-muted-foreground">
{t('tasks.loading')}
</p>
) : filtered.length === 0 ? (
<p className="py-4 text-center text-xs text-muted-foreground">
{tasks.length === 0 ? t('tasks.noTasks') : t('tasks.noMatching')}
</p>
) : (
<div className="flex flex-col gap-1.5 pb-4">
{filtered.map((task) => (
<TaskCard
key={task.id}
task={task}
onUpdate={handleUpdate}
onDelete={handleDelete}
/>
))}
</div>
)}
</ScrollArea>
<span
className={cn(
"flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded border transition-colors",
isDone
? "border-foreground bg-foreground text-background"
: "border-muted-foreground/40"
)}
>
{isDone && (
<svg className="h-2.5 w-2.5" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M2.5 6l2.5 2.5 4.5-5" />
</svg>
)}
</span>
<span
className={cn(
"flex-1 truncate text-xs",
isDone && "text-muted-foreground line-through"
)}
>
{task.title}
</span>
</button>
);
})}
</div>
);
}