mirror of
https://github.com/aljazceru/claude-code-viewer.git
synced 2026-01-05 22:54:23 +01:00
fix: bug fix related to effect-ts refactor
This commit is contained in:
@@ -21,7 +21,5 @@ export default async function LatestSessionPage({
|
||||
redirect(`/projects`);
|
||||
}
|
||||
|
||||
redirect(
|
||||
`/projects/${encodeURIComponent(projectId)}/sessions/${encodeURIComponent(latestSession.id)}`,
|
||||
);
|
||||
redirect(`/projects/${projectId}/sessions/${latestSession.id}`);
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@ interface ProjectPageProps {
|
||||
|
||||
export default async function ProjectPage({ params }: ProjectPageProps) {
|
||||
const { projectId } = await params;
|
||||
redirect(`/projects/${encodeURIComponent(projectId)}/latest`);
|
||||
redirect(`/projects/${projectId}/latest`);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { MessageSquareIcon, PlugIcon, SettingsIcon, XIcon } from "lucide-react";
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
MessageSquareIcon,
|
||||
PlugIcon,
|
||||
SettingsIcon,
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { type FC, Suspense, useEffect, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { NotificationSettings } from "@/components/NotificationSettings";
|
||||
import { SettingsControls } from "@/components/SettingsControls";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useProject } from "../../../../hooks/useProject";
|
||||
import { McpTab } from "./McpTab";
|
||||
@@ -157,52 +170,89 @@ export const MobileSidebar: FC<MobileSidebarProps> = ({
|
||||
>
|
||||
{/* Tab Icons */}
|
||||
<div className="w-12 flex flex-col border-r border-sidebar-border bg-sidebar/50">
|
||||
<div className="flex flex-col p-2 space-y-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleTabClick("sessions")}
|
||||
className={cn(
|
||||
"w-8 h-8 flex items-center justify-center rounded-md transition-colors",
|
||||
"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
activeTab === "sessions"
|
||||
? "bg-sidebar-accent text-sidebar-accent-foreground shadow-sm"
|
||||
: "text-sidebar-foreground/70",
|
||||
)}
|
||||
data-testid="sessions-tab-button-mobile"
|
||||
>
|
||||
<MessageSquareIcon className="w-4 h-4" />
|
||||
</button>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href="/projects"
|
||||
className="w-12 h-12 flex items-center justify-center border-b border-sidebar-border hover:bg-sidebar-accent transition-colors"
|
||||
>
|
||||
<ArrowLeftIcon className="w-4 h-4 text-sidebar-foreground/70" />
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p>プロジェクト一覧に戻る</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleTabClick("mcp")}
|
||||
className={cn(
|
||||
"w-8 h-8 flex items-center justify-center rounded-md transition-colors",
|
||||
"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
activeTab === "mcp"
|
||||
? "bg-sidebar-accent text-sidebar-accent-foreground shadow-sm"
|
||||
: "text-sidebar-foreground/70",
|
||||
)}
|
||||
data-testid="mcp-tab-button-mobile"
|
||||
>
|
||||
<PlugIcon className="w-4 h-4" />
|
||||
</button>
|
||||
<div className="flex flex-col p-2 space-y-1">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleTabClick("sessions")}
|
||||
className={cn(
|
||||
"w-8 h-8 flex items-center justify-center rounded-md transition-colors",
|
||||
"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
activeTab === "sessions"
|
||||
? "bg-sidebar-accent text-sidebar-accent-foreground shadow-sm"
|
||||
: "text-sidebar-foreground/70",
|
||||
)}
|
||||
data-testid="sessions-tab-button-mobile"
|
||||
>
|
||||
<MessageSquareIcon className="w-4 h-4" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p>セッション一覧を表示</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleTabClick("settings")}
|
||||
className={cn(
|
||||
"w-8 h-8 flex items-center justify-center rounded-md transition-colors",
|
||||
"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
activeTab === "settings"
|
||||
? "bg-sidebar-accent text-sidebar-accent-foreground shadow-sm"
|
||||
: "text-sidebar-foreground/70",
|
||||
)}
|
||||
data-testid="settings-tab-button-mobile"
|
||||
>
|
||||
<SettingsIcon className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleTabClick("mcp")}
|
||||
className={cn(
|
||||
"w-8 h-8 flex items-center justify-center rounded-md transition-colors",
|
||||
"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
activeTab === "mcp"
|
||||
? "bg-sidebar-accent text-sidebar-accent-foreground shadow-sm"
|
||||
: "text-sidebar-foreground/70",
|
||||
)}
|
||||
data-testid="mcp-tab-button-mobile"
|
||||
>
|
||||
<PlugIcon className="w-4 h-4" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p>MCPサーバー設定を表示</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleTabClick("settings")}
|
||||
className={cn(
|
||||
"w-8 h-8 flex items-center justify-center rounded-md transition-colors",
|
||||
"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
activeTab === "settings"
|
||||
? "bg-sidebar-accent text-sidebar-accent-foreground shadow-sm"
|
||||
: "text-sidebar-foreground/70",
|
||||
)}
|
||||
data-testid="settings-tab-button-mobile"
|
||||
>
|
||||
<SettingsIcon className="w-4 h-4" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p>表示と通知の設定</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
{/* Content Area */}
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { MessageSquareIcon, PlugIcon } from "lucide-react";
|
||||
import { ArrowLeftIcon, MessageSquareIcon, PlugIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { type FC, useMemo } from "react";
|
||||
import type { SidebarTab } from "@/components/GlobalSidebar";
|
||||
import { GlobalSidebar } from "@/components/GlobalSidebar";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useProject } from "../../../../hooks/useProject";
|
||||
import { McpTab } from "./McpTab";
|
||||
@@ -36,7 +43,7 @@ export const SessionSidebar: FC<{
|
||||
{
|
||||
id: "sessions",
|
||||
icon: MessageSquareIcon,
|
||||
title: "Sessions",
|
||||
title: "セッション一覧を表示",
|
||||
content: (
|
||||
<SessionsTab
|
||||
sessions={sessions.map((session) => ({
|
||||
@@ -54,7 +61,7 @@ export const SessionSidebar: FC<{
|
||||
{
|
||||
id: "mcp",
|
||||
icon: PlugIcon,
|
||||
title: "MCP Servers",
|
||||
title: "MCPサーバー設定を表示",
|
||||
content: <McpTab projectId={projectId} />,
|
||||
},
|
||||
],
|
||||
@@ -76,6 +83,23 @@ export const SessionSidebar: FC<{
|
||||
projectId={projectId}
|
||||
additionalTabs={additionalTabs}
|
||||
defaultActiveTab="sessions"
|
||||
headerButton={
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href="/projects"
|
||||
className="w-12 h-12 flex items-center justify-center hover:bg-sidebar-accent transition-colors"
|
||||
>
|
||||
<ArrowLeftIcon className="w-4 h-4 text-sidebar-foreground/70" />
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p>プロジェクト一覧に戻る</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -99,9 +99,7 @@ export const SessionsTab: FC<{
|
||||
return (
|
||||
<Link
|
||||
key={session.id}
|
||||
href={`/projects/${projectId}/sessions/${encodeURIComponent(
|
||||
session.id,
|
||||
)}`}
|
||||
href={`/projects/${projectId}/sessions/${session.id}`}
|
||||
className={cn(
|
||||
"block rounded-lg p-2.5 transition-all duration-200 hover:bg-blue-50/60 dark:hover:bg-blue-950/40 hover:border-blue-300/60 dark:hover:border-blue-700/60 hover:shadow-sm border border-sidebar-border/40 bg-sidebar/30",
|
||||
isActive &&
|
||||
|
||||
105
src/app/projects/components/CreateProjectDialog.tsx
Normal file
105
src/app/projects/components/CreateProjectDialog.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { Loader2, Plus } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { type FC, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { honoClient } from "@/lib/api/client";
|
||||
import { DirectoryPicker } from "./DirectoryPicker";
|
||||
|
||||
export const CreateProjectDialog: FC = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedPath, setSelectedPath] = useState<string>("");
|
||||
const router = useRouter();
|
||||
|
||||
const createProjectMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
const response = await honoClient.api.projects.$post({
|
||||
json: { projectPath: selectedPath },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to create project");
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
},
|
||||
|
||||
onSuccess: (result) => {
|
||||
toast.success("Project created successfully");
|
||||
setOpen(false);
|
||||
router.push(`/projects/${result.projectId}/sessions/${result.sessionId}`);
|
||||
},
|
||||
|
||||
onError: (error) => {
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "Failed to create project",
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
New Project
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create New Project</DialogTitle>
|
||||
<DialogDescription>
|
||||
Select a directory to initialize as a Claude Code project. This will
|
||||
run{" "}
|
||||
<code className="text-sm bg-muted px-1 py-0.5 rounded">/init</code>{" "}
|
||||
in the selected directory.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<DirectoryPicker
|
||||
selectedPath={selectedPath}
|
||||
onPathChange={setSelectedPath}
|
||||
/>
|
||||
{selectedPath ? (
|
||||
<div className="mt-4 p-3 bg-muted rounded-md">
|
||||
<p className="text-sm font-medium mb-1">Selected directory:</p>
|
||||
<p className="text-sm text-muted-foreground font-mono">
|
||||
{selectedPath}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => await createProjectMutation.mutateAsync()}
|
||||
disabled={!selectedPath || createProjectMutation.isPending}
|
||||
>
|
||||
{createProjectMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Creating...
|
||||
</>
|
||||
) : (
|
||||
"Create Project"
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
76
src/app/projects/components/DirectoryPicker.tsx
Normal file
76
src/app/projects/components/DirectoryPicker.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ChevronRight, Folder } from "lucide-react";
|
||||
import { type FC, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { directoryListingQuery } from "@/lib/api/queries";
|
||||
|
||||
export type DirectoryPickerProps = {
|
||||
selectedPath: string;
|
||||
onPathChange: (path: string) => void;
|
||||
};
|
||||
|
||||
export const DirectoryPicker: FC<DirectoryPickerProps> = ({ onPathChange }) => {
|
||||
const [currentPath, setCurrentPath] = useState<string | undefined>(undefined);
|
||||
|
||||
const { data, isLoading } = useQuery(directoryListingQuery(currentPath));
|
||||
|
||||
const handleNavigate = (entryPath: string) => {
|
||||
if (entryPath === "") {
|
||||
setCurrentPath(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const newPath = `/${entryPath}`;
|
||||
setCurrentPath(newPath);
|
||||
};
|
||||
|
||||
const handleSelect = () => {
|
||||
onPathChange(data?.currentPath || "");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border rounded-md">
|
||||
<div className="p-3 border-b bg-muted/50 flex items-center justify-between">
|
||||
<p className="text-sm font-medium">
|
||||
Current: <span className="font-mono">{data?.currentPath || "~"}</span>
|
||||
</p>
|
||||
<Button size="sm" onClick={handleSelect}>
|
||||
Select This Directory
|
||||
</Button>
|
||||
</div>
|
||||
<div className="max-h-96 overflow-auto">
|
||||
{isLoading ? (
|
||||
<div className="p-8 text-center text-sm text-muted-foreground">
|
||||
Loading...
|
||||
</div>
|
||||
) : data?.entries && data.entries.length > 0 ? (
|
||||
<div className="divide-y">
|
||||
{data.entries
|
||||
.filter((entry) => entry.type === "directory")
|
||||
.map((entry) => (
|
||||
<button
|
||||
key={entry.path}
|
||||
type="button"
|
||||
onClick={() => handleNavigate(entry.path)}
|
||||
className="w-full px-3 py-2 flex items-center gap-2 hover:bg-muted/50 transition-colors text-left"
|
||||
>
|
||||
{entry.name === ".." ? (
|
||||
<ChevronRight className="w-4 h-4 rotate-180" />
|
||||
) : (
|
||||
<Folder className="w-4 h-4 text-blue-500" />
|
||||
)}
|
||||
<span className="text-sm">{entry.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-8 text-center text-sm text-muted-foreground">
|
||||
No directories found
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -59,7 +59,7 @@ export const ProjectList: FC = () => {
|
||||
</CardContent>
|
||||
<CardContent className="pt-0">
|
||||
<Button asChild className="w-full">
|
||||
<Link href={`/projects/${encodeURIComponent(project.id)}/latest`}>
|
||||
<Link href={`/projects/${project.id}/latest`}>
|
||||
View Conversations
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { HistoryIcon } from "lucide-react";
|
||||
import { Suspense } from "react";
|
||||
import { GlobalSidebar } from "@/components/GlobalSidebar";
|
||||
import { CreateProjectDialog } from "./components/CreateProjectDialog";
|
||||
import { ProjectList } from "./components/ProjectList";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -27,7 +28,10 @@ export default function ProjectsPage() {
|
||||
|
||||
<main>
|
||||
<section>
|
||||
<h2 className="text-xl font-semibold mb-4">Your Projects</h2>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xl font-semibold">Your Projects</h2>
|
||||
<CreateProjectDialog />
|
||||
</div>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex items-center justify-center py-12">
|
||||
|
||||
Reference in New Issue
Block a user