diff --git a/src/app/api/[[...route]]/route.ts b/src/app/api/[[...route]]/route.ts index 12ae66e..2485c85 100644 --- a/src/app/api/[[...route]]/route.ts +++ b/src/app/api/[[...route]]/route.ts @@ -11,9 +11,9 @@ import { EventBus } from "../../../server/service/events/EventBus"; import { FileWatcherService } from "../../../server/service/events/fileWatcher"; import { ProjectMetaService } from "../../../server/service/project/ProjectMetaService"; import { ProjectRepository } from "../../../server/service/project/ProjectRepository"; -import { VirtualConversationDatabase } from "../../../server/service/session/PredictSessionsDatabase"; import { SessionMetaService } from "../../../server/service/session/SessionMetaService"; import { SessionRepository } from "../../../server/service/session/SessionRepository"; +import { VirtualConversationDatabase } from "../../../server/service/session/VirtualConversationDatabase"; const program = routes(honoApp); diff --git a/src/app/components/MarkdownContent.tsx b/src/app/components/MarkdownContent.tsx index e5acb61..9b7d2d9 100644 --- a/src/app/components/MarkdownContent.tsx +++ b/src/app/components/MarkdownContent.tsx @@ -1,9 +1,13 @@ "use client"; +import { useTheme } from "next-themes"; import type { FC } from "react"; import Markdown from "react-markdown"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; -import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism"; +import { + oneDark, + oneLight, +} from "react-syntax-highlighter/dist/esm/styles/prism"; import remarkGfm from "remark-gfm"; interface MarkdownContentProps { @@ -15,6 +19,9 @@ export const MarkdownContent: FC = ({ content, className = "", }) => { + const { resolvedTheme } = useTheme(); + const syntaxTheme = resolvedTheme === "dark" ? oneDark : oneLight; + return (
= ({
= ({ children }) => { return ( ( -
-

Error

-

{error.message}

+ FallbackComponent={({ error, resetErrorBoundary }) => ( +
+ + +
+ +
+ Something went wrong + + An unexpected error occurred in the application + +
+
+
+ + + + Error Details + + {error.message} + + + +
+ + +
+
+
)} > diff --git a/src/app/components/SSEEventListeners.tsx b/src/app/components/SSEEventListeners.tsx index 50bf595..0d0b0c3 100644 --- a/src/app/components/SSEEventListeners.tsx +++ b/src/app/components/SSEEventListeners.tsx @@ -12,14 +12,12 @@ export const SSEEventListeners: FC = ({ children }) => { const setSessionProcesses = useSetAtom(sessionProcessesAtom); useServerEventListener("sessionListChanged", async (event) => { - // invalidate session list await queryClient.invalidateQueries({ queryKey: projectDetailQuery(event.projectId).queryKey, }); }); useServerEventListener("sessionChanged", async (event) => { - // invalidate session detail await queryClient.invalidateQueries({ queryKey: sessionDetailQuery(event.projectId, event.sessionId).queryKey, }); diff --git a/src/app/error.tsx b/src/app/error.tsx new file mode 100644 index 0000000..b52733a --- /dev/null +++ b/src/app/error.tsx @@ -0,0 +1,69 @@ +"use client"; + +import { AlertCircle, Home, RefreshCw } from "lucide-react"; + +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; + +export default function ErrorPage({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + return ( +
+ + +
+ +
+ Something went wrong + + An unexpected error occurred in the application + +
+
+
+ + + + Error Details + + {error.message} + {error.digest && ( +
+ Error ID: {error.digest} +
+ )} +
+
+ +
+ + +
+
+
+
+ ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f882369..66e7d12 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,6 @@ import { QueryClient } from "@tanstack/react-query"; import { Geist, Geist_Mono } from "next/font/google"; +import { ThemeProvider } from "next-themes"; import { Toaster } from "../components/ui/sonner"; import { QueryClientProviderWrapper } from "../lib/api/QueryClientProviderWrapper"; @@ -47,24 +48,26 @@ export default async function RootLayout({ .then((response) => response.json()); return ( - + - - - - - - {children} - - - - - - + + + + + + + {children} + + + + + + + ); diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx new file mode 100644 index 0000000..79097e7 --- /dev/null +++ b/src/app/not-found.tsx @@ -0,0 +1,41 @@ +import { FileQuestion, Home } from "lucide-react"; +import Link from "next/link"; + +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; + +export default function NotFoundPage() { + return ( +
+ + +
+ +
+ Page Not Found + + The page you are looking for does not exist + +
+
+
+ +
+ +
+
+
+
+ ); +} diff --git a/src/app/projects/[projectId]/components/ProjectPage.tsx b/src/app/projects/[projectId]/components/ProjectPage.tsx deleted file mode 100644 index 0d4077d..0000000 --- a/src/app/projects/[projectId]/components/ProjectPage.tsx +++ /dev/null @@ -1,221 +0,0 @@ -"use client"; - -import { useQueryClient } from "@tanstack/react-query"; -import { - ArrowLeftIcon, - ChevronDownIcon, - FolderIcon, - MessageSquareIcon, - PlusIcon, - SettingsIcon, -} from "lucide-react"; -import Link from "next/link"; -import { useEffect, useState } from "react"; -import { SettingsControls } from "@/components/SettingsControls"; -import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@/components/ui/collapsible"; -import { useConfig } from "../../../hooks/useConfig"; -import { useProject } from "../hooks/useProject"; -import { firstCommandToTitle } from "../services/firstCommandToTitle"; -import { NewChatModal } from "./newChat/NewChatModal"; - -export const ProjectPageContent = ({ projectId }: { projectId: string }) => { - const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = - useProject(projectId); - const { config } = useConfig(); - const queryClient = useQueryClient(); - const [isSettingsOpen, setIsSettingsOpen] = useState(false); - - // Flatten all pages to get project and sessions - const project = data.pages.at(0)?.project; - const sessions = data.pages.flatMap((page) => page.sessions); - - if (!project) { - throw new Error("Unreachable: Project must be defined."); - } - - // biome-ignore lint/correctness/useExhaustiveDependencies: invalidate when config changed - useEffect(() => { - void queryClient.refetchQueries({ - queryKey: ["projects", projectId], - }); - }, [config.hideNoUserMessageSession, config.unifySameTitleSession]); - - return ( -
-
- - -
-
- -

- {project.meta.projectPath ?? project.claudeProjectPath} -

-
-
- - - Start New Chat - New Chat - - } - /> -
-
-

- History File: {project.claudeProjectPath ?? "unknown"} -

-
- -
-
-

- Conversation Sessions{" "} - {sessions.length > 0 ? `(${sessions.length})` : ""} -

- - {/* Filter Controls */} - -
- - - - -
- -
-
-
-
- - {sessions.length === 0 ? ( - - - -

No sessions found

-

- No conversation sessions found for this project. Start a - conversation with Claude Code in this project to create - sessions. -

- - - Start First Chat - - } - /> -
-
- ) : ( -
- {sessions.map((session) => ( - - - - - {session.meta.firstCommand !== null - ? firstCommandToTitle(session.meta.firstCommand) - : session.id} - - - - {session.id} - - - -

- {session.meta.messageCount} messages -

-

- Last modified:{" "} - {session.lastModifiedAt - ? new Date(session.lastModifiedAt).toLocaleDateString() - : ""} -

-

- {session.jsonlFilePath} -

-
- - - -
- ))} -
- )} - - {/* Load More Button */} - {sessions.length > 0 && hasNextPage && ( -
- -
- )} -
-
-
- ); -}; diff --git a/src/app/projects/[projectId]/components/chatForm/ChatInput.tsx b/src/app/projects/[projectId]/components/chatForm/ChatInput.tsx index e707fd4..9d1fbdf 100644 --- a/src/app/projects/[projectId]/components/chatForm/ChatInput.tsx +++ b/src/app/projects/[projectId]/components/chatForm/ChatInput.tsx @@ -1,4 +1,9 @@ -import { AlertCircleIcon, LoaderIcon, SendIcon } from "lucide-react"; +import { + AlertCircleIcon, + LoaderIcon, + SendIcon, + SparklesIcon, +} from "lucide-react"; import { type FC, useCallback, useId, useRef, useState } from "react"; import { Button } from "../../../../../components/ui/button"; import { Textarea } from "../../../../../components/ui/textarea"; @@ -62,16 +67,28 @@ export const ChatInput: FC = ({ // IMEで変換中の場合は送信しない if (e.key === "Enter" && !e.nativeEvent.isComposing) { - const isEnterSend = config?.enterKeyBehavior === "enter-send"; + const enterKeyBehavior = config?.enterKeyBehavior; - if (isEnterSend && !e.shiftKey) { + if (enterKeyBehavior === "enter-send" && !e.shiftKey && !e.metaKey) { // Enter: Send mode e.preventDefault(); handleSubmit(); - } else if (!isEnterSend && e.shiftKey) { + } else if ( + enterKeyBehavior === "shift-enter-send" && + e.shiftKey && + !e.metaKey + ) { // Shift+Enter: Send mode (default) e.preventDefault(); handleSubmit(); + } else if ( + enterKeyBehavior === "command-enter-send" && + e.metaKey && + !e.shiftKey + ) { + // Command+Enter: Send mode (Mac) + e.preventDefault(); + handleSubmit(); } } }; @@ -148,78 +165,98 @@ export const ChatInput: FC = ({ return (
{error && ( -
- - Failed to send message. Please try again. +
+ + + Failed to send message. Please try again. +
)} -
-
-