chore: format files

This commit is contained in:
d-kimsuon
2025-08-30 03:18:42 +09:00
parent 16ec283e50
commit bfa19a6e85
54 changed files with 539 additions and 541 deletions

View File

@@ -13,7 +13,7 @@
"lint:biome-lint": "biome check .",
"fix": "run-s 'fix:*'",
"fix:biome-format": "biome format --write .",
"fix:biome-lint": "biome check --write .",
"fix:biome-lint": "biome check --write --unsafe .",
"typecheck": "tsc --noEmit",
"test": "vitest --run",
"test:watch": "vitest"

View File

@@ -1,10 +1,10 @@
"use client";
import type { FC } from "react";
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
import type { FC } from "react";
import remarkGfm from "remark-gfm";
interface MarkdownContentProps {
content: string;

View File

@@ -1,9 +1,8 @@
"use client";
import { ArrowLeftIcon } from "lucide-react";
import { ArrowLeftIcon, FolderIcon, MessageSquareIcon } from "lucide-react";
import Link from "next/link";
import { FolderIcon, MessageSquareIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
@@ -11,11 +10,9 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { useProject } from "../hooks/useProject";
import { pagesPath } from "../../../../lib/$path";
import { parseCommandXml } from "../../../../server/service/parseCommandXml";
import { useProject } from "../hooks/useProject";
import { firstCommandToTitle } from "../services/firstCommandToTitle";
export const ProjectPageContent = ({ projectId }: { projectId: string }) => {
const {
@@ -78,31 +75,9 @@ export const ProjectPageContent = ({ projectId }: { projectId: string }) => {
<CardHeader>
<CardTitle className="flex items-center gap-2">
<span className="break-all overflow-ellipsis line-clamp-2 text-xl">
{session.meta.firstContent
? (() => {
const parsed = parseCommandXml(
session.meta.firstContent
);
if (parsed.kind === "command") {
return (
<span>
{parsed.commandName} {parsed.commandArgs}
</span>
);
}
if (parsed.kind === "local-command-1") {
return (
<span>
{parsed.commandName} {parsed.commandMessage}
</span>
);
}
if (parsed.kind === "local-command-2") {
return <span>{parsed.stdout}</span>;
}
return <span>{session.meta.firstContent}</span>;
})()
: ""}
{session.meta.firstCommand !== null
? firstCommandToTitle(session.meta.firstCommand)
: session.id}
</span>
</CardTitle>
<CardDescription className="font-mono text-xs">
@@ -117,7 +92,7 @@ export const ProjectPageContent = ({ projectId }: { projectId: string }) => {
Last modified:{" "}
{session.meta.lastModifiedAt
? new Date(
session.meta.lastModifiedAt
session.meta.lastModifiedAt,
).toLocaleDateString()
: ""}
</p>
@@ -129,7 +104,7 @@ export const ProjectPageContent = ({ projectId }: { projectId: string }) => {
<Button asChild className="w-full">
<Link
href={`/projects/${projectId}/sessions/${encodeURIComponent(
session.id
session.id,
)}`}
>
View Session

View File

@@ -1,40 +0,0 @@
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
export default function ProjectLoading() {
return (
<div className="container mx-auto px-4 py-8">
<header className="mb-8">
<Skeleton className="h-9 w-32 mb-4" />
<div className="flex items-center gap-3 mb-2">
<Skeleton className="w-6 h-6" />
<Skeleton className="h-9 w-80" />
</div>
<Skeleton className="h-4 w-96" />
</header>
<main>
<section>
<Skeleton className="h-7 w-64 mb-4" />
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{Array.from({ length: 6 }).map((_, index) => (
<Card key={index}>
<CardHeader>
<Skeleton className="h-6 w-3/4" />
<Skeleton className="h-4 w-1/2" />
</CardHeader>
<CardContent className="space-y-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-4 w-32" />
<Skeleton className="h-3 w-full" />
<Skeleton className="h-10 w-full mt-4" />
</CardContent>
</Card>
))}
</div>
</section>
</main>
</div>
);
}

View File

@@ -0,0 +1,17 @@
import type { ParsedCommand } from "../../../../server/service/parseCommandXml";
export const firstCommandToTitle = (firstCommand: ParsedCommand) => {
switch (firstCommand.kind) {
case "command":
return `${firstCommand.commandName} ${firstCommand.commandArgs}`;
case "local-command-1":
return firstCommand.commandMessage;
case "local-command-2":
return firstCommand.stdout;
case "text":
return firstCommand.content;
default:
firstCommand satisfies never;
throw new Error("Invalid first command");
}
};

View File

@@ -1,7 +1,7 @@
const regExp = /<(?<tag>[^>]+)>(?<content>\s*[^<]*?\s*)<\/\k<tag>>/g;
export const parseCommandXml = (
content: string
content: string,
):
| {
kind: "command";
@@ -37,16 +37,16 @@ export const parseCommandXml = (
}
const commandName = matches.find(
(match) => match.tag === "command-name"
(match) => match.tag === "command-name",
)?.content;
const commandArgs = matches.find(
(match) => match.tag === "command-args"
(match) => match.tag === "command-args",
)?.content;
const commandMessage = matches.find(
(match) => match.tag === "command-message"
(match) => match.tag === "command-message",
)?.content;
const localCommandStdout = matches.find(
(match) => match.tag === "local-command-stdout"
(match) => match.tag === "local-command-stdout",
)?.content;
switch (true) {
@@ -74,4 +74,4 @@ export const parseCommandXml = (
content,
};
}
};
};

View File

@@ -0,0 +1,54 @@
"use client";
import { ArrowLeftIcon, MessageSquareIcon } from "lucide-react";
import Link from "next/link";
import type { FC } from "react";
import { Button } from "@/components/ui/button";
import { firstCommandToTitle } from "../../../services/firstCommandToTitle";
import { useSession } from "../hooks/useSession";
import { ConversationList } from "./conversationList/ConversationList";
export const SessionPageContent: FC<{
projectId: string;
sessionId: string;
}> = ({ projectId, sessionId }) => {
const { session, conversations, getToolResult } = useSession(
projectId,
sessionId,
);
return (
<div className="container mx-auto px-4 py-8">
<header className="mb-8">
<Button asChild variant="ghost" className="mb-4">
<Link
href={`/projects/${projectId}`}
className="flex items-center gap-2"
>
<ArrowLeftIcon className="w-4 h-4" />
Back to Session List
</Link>
</Button>
<div className="flex items-center gap-3 mb-2">
<MessageSquareIcon className="w-6 h-6" />
<h1 className="text-3xl font-bold">
{session.meta.firstCommand !== null
? firstCommandToTitle(session.meta.firstCommand)
: sessionId}
</h1>
</div>
<p className="text-muted-foreground font-mono text-sm">
Session ID: {sessionId}
</p>
</header>
<main>
<ConversationList
conversations={conversations}
getToolResult={getToolResult}
/>
</main>
</div>
);
};

View File

@@ -1,12 +1,6 @@
import { ToolResultContent } from "@/lib/conversation-schema/content/ToolResultContentSchema";
import { AssistantMessageContent } from "@/lib/conversation-schema/message/AssistantMessageSchema";
import { FC } from "react";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { ChevronDown, FileText, Lightbulb, Settings } from "lucide-react";
import type { FC } from "react";
import { Badge } from "@/components/ui/badge";
import {
Card,
CardContent,
@@ -14,8 +8,13 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { ChevronDown, Lightbulb, Settings, FileText } from "lucide-react";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import type { ToolResultContent } from "@/lib/conversation-schema/content/ToolResultContentSchema";
import type { AssistantMessageContent } from "@/lib/conversation-schema/message/AssistantMessageSchema";
import { MarkdownContent } from "../../../../../../components/MarkdownContent";
export const AssistantConversationContent: FC<{

View File

@@ -1,11 +1,11 @@
import type { FC } from "react";
import type { Conversation } from "@/lib/conversation-schema";
import type { ToolResultContent } from "@/lib/conversation-schema/content/ToolResultContentSchema";
import type { FC } from "react";
import { UserConversationContent } from "./UserConversationContent";
import { AssistantConversationContent } from "./AssistantConversationContent";
import { MetaConversationContent } from "./MetaConversationContent";
import { SystemConversationContent } from "./SystemConversationContent";
import { SummaryConversationContent } from "./SummaryConversationContent";
import { SystemConversationContent } from "./SystemConversationContent";
import { UserConversationContent } from "./UserConversationContent";
export const ConversationItem: FC<{
conversation: Conversation;

View File

@@ -1,8 +1,8 @@
"use client";
import type { Conversation } from "@/lib/conversation-schema";
import type { FC } from "react";
import { useConversations } from "../../hooks/useConversations";
import type { Conversation } from "@/lib/conversation-schema";
import type { ToolResultContent } from "@/lib/conversation-schema/content/ToolResultContentSchema";
import { ConversationItem } from "./ConversationItem";
const getConversationKey = (conversation: Conversation) => {
@@ -26,19 +26,14 @@ const getConversationKey = (conversation: Conversation) => {
};
type ConversationListProps = {
projectId: string;
sessionId: string;
conversations: Conversation[];
getToolResult: (toolUseId: string) => ToolResultContent | undefined;
};
export const ConversationList: FC<ConversationListProps> = ({
projectId,
sessionId,
conversations,
getToolResult,
}) => {
const { conversations, getToolResult } = useConversations(
projectId,
sessionId
);
return (
<ul>
{conversations.flatMap((conversation) => {
@@ -60,8 +55,8 @@ export const ConversationList: FC<ConversationListProps> = ({
conversation.type === "user"
? "justify-end"
: conversation.type === "assistant"
? "justify-start"
: "justify-center"
? "justify-start"
: "justify-center"
}`}
key={getConversationKey(conversation)}
>
@@ -70,8 +65,8 @@ export const ConversationList: FC<ConversationListProps> = ({
conversation.type === "user"
? "w-[90%]"
: conversation.type === "assistant"
? "w-[90%]"
: "w-[100%]"
? "w-[90%]"
: "w-[100%]"
}`}
>
{elm}

View File

@@ -1,10 +1,10 @@
import { ChevronDown } from "lucide-react";
import type { FC, PropsWithChildren } from "react";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { ChevronDown } from "lucide-react";
export const MetaConversationContent: FC<PropsWithChildren> = ({
children,

View File

@@ -1,10 +1,10 @@
import { ChevronDown } from "lucide-react";
import type { FC, PropsWithChildren } from "react";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { ChevronDown } from "lucide-react";
export const SummaryConversationContent: FC<PropsWithChildren> = ({
children,

View File

@@ -1,10 +1,10 @@
import { ChevronDown } from "lucide-react";
import type { FC, PropsWithChildren } from "react";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { ChevronDown } from "lucide-react";
export const SystemConversationContent: FC<PropsWithChildren> = ({
children,

View File

@@ -1,9 +1,8 @@
import type { FC } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Terminal } from "lucide-react";
import type { FC } from "react";
import { parseCommandXml } from "@/app/projects/[projectId]/services/parseCommandXml";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { MarkdownContent } from "../../../../../../components/MarkdownContent";
export const TextContent: FC<{ text: string }> = ({ text }) => {
@@ -11,7 +10,7 @@ export const TextContent: FC<{ text: string }> = ({ text }) => {
if (parsed.kind === "command") {
return (
<Card className="border-green-200 bg-green-50/50 dark:border-green-800 dark:bg-green-950/20 gap-2 py-3">
<Card className="border-green-200 bg-green-50/50 dark:border-green-800 dark:bg-green-950/20 gap-2 py-3 mb-3">
<CardHeader className="py-0 px-4">
<div className="flex items-center gap-2">
<Terminal className="h-4 w-4 text-green-600 dark:text-green-400" />
@@ -46,7 +45,7 @@ export const TextContent: FC<{ text: string }> = ({ text }) => {
if (parsed.kind === "local-command-1") {
return (
<Card className="border-green-200 bg-green-50/50 dark:border-green-800 dark:bg-green-950/20 gap-2 py-3">
<Card className="border-green-200 bg-green-50/50 dark:border-green-800 dark:bg-green-950/20 gap-2 py-3 mb-3">
<CardHeader className="py-0 px-4">
<div className="flex items-center gap-2">
<Terminal className="h-4 w-4 text-green-600 dark:text-green-400" />
@@ -62,7 +61,7 @@ export const TextContent: FC<{ text: string }> = ({ text }) => {
if (parsed.kind === "local-command-2") {
return (
<Card className="border-green-200 bg-green-50/50 dark:border-green-800 dark:bg-green-950/20 gap-2 py-3">
<Card className="border-green-200 bg-green-50/50 dark:border-green-800 dark:bg-green-950/20 gap-2 py-3 mb-3">
<CardHeader className="py-0 px-4">
<div className="flex items-center gap-2">
<Terminal className="h-4 w-4 text-green-600 dark:text-green-400" />

View File

@@ -1,5 +1,7 @@
import type { UserMessageContent } from "@/lib/conversation-schema/message/UserMessageSchema";
import { AlertCircle, Image as ImageIcon } from "lucide-react";
import Image from "next/image";
import type { FC } from "react";
import { Badge } from "@/components/ui/badge";
import {
Card,
CardContent,
@@ -7,8 +9,7 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Image as ImageIcon, AlertCircle } from "lucide-react";
import type { UserMessageContent } from "@/lib/conversation-schema/message/UserMessageSchema";
import { TextContent } from "./TextContent";
export const UserConversationContent: FC<{
@@ -43,7 +44,7 @@ export const UserConversationContent: FC<{
</CardHeader>
<CardContent>
<div className="rounded-lg border overflow-hidden bg-background">
<img
<Image
src={`data:${content.source.media_type};base64,${content.source.data}`}
alt="User uploaded content"
className="max-w-full h-auto max-h-96 object-contain"

View File

@@ -1,8 +1,8 @@
import { useConversationsQuery } from "./useConversationsQuery";
import { useCallback, useMemo } from "react";
import { useSessionQuery } from "./useSessionQuery";
export const useConversations = (projectId: string, sessionId: string) => {
const query = useConversationsQuery(projectId, sessionId);
export const useSession = (projectId: string, sessionId: string) => {
const query = useSessionQuery(projectId, sessionId);
const toolResultMap = useMemo(() => {
const entries = query.data.session.conversations.flatMap((conversation) => {
@@ -34,10 +34,11 @@ export const useConversations = (projectId: string, sessionId: string) => {
(toolUseId: string) => {
return toolResultMap.get(toolUseId);
},
[toolResultMap]
[toolResultMap],
);
return {
session: query.data.session,
conversations: query.data.session.conversations,
getToolResult,
};

View File

@@ -1,7 +1,7 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import { honoClient } from "../../../../../../lib/api/client";
export const useConversationsQuery = (projectId: string, sessionId: string) => {
export const useSessionQuery = (projectId: string, sessionId: string) => {
return useSuspenseQuery({
queryKey: ["conversations", sessionId],
queryFn: async () => {

View File

@@ -1,34 +0,0 @@
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
export default function SessionLoading() {
return (
<div className="container mx-auto px-4 py-8">
<header className="mb-8">
<Skeleton className="h-9 w-32 mb-4" />
<div className="flex items-center gap-3 mb-2">
<Skeleton className="w-6 h-6" />
<Skeleton className="h-9 w-64" />
</div>
<Skeleton className="h-4 w-80" />
</header>
<main>
<Card className="max-w-4xl mx-auto">
<CardHeader>
<Skeleton className="h-6 w-48" />
<Skeleton className="h-4 w-64" />
</CardHeader>
<CardContent className="space-y-4">
<Skeleton className="h-32 w-full" />
<Skeleton className="h-24 w-full" />
<Skeleton className="h-16 w-full" />
<div className="flex justify-center pt-4">
<Skeleton className="h-10 w-32" />
</div>
</CardContent>
</Card>
</main>
</div>
);
}

View File

@@ -1,8 +1,5 @@
import Link from "next/link";
import { ArrowLeftIcon, MessageSquareIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import type { Metadata } from "next";
import { ConversationList } from "./components/conversationList/ConversationList";
import { SessionPageContent } from "./components/SessionPageContent";
type PageParams = {
projectId: string;
@@ -28,31 +25,5 @@ interface SessionPageProps {
export default async function SessionPage({ params }: SessionPageProps) {
const { projectId, sessionId } = await params;
return (
<div className="container mx-auto px-4 py-8">
<header className="mb-8">
<Button asChild variant="ghost" className="mb-4">
<Link
href={`/projects/${projectId}`}
className="flex items-center gap-2"
>
<ArrowLeftIcon className="w-4 h-4" />
Back to Session List
</Link>
</Button>
<div className="flex items-center gap-3 mb-2">
<MessageSquareIcon className="w-6 h-6" />
<h1 className="text-3xl font-bold">Conversation Session</h1>
</div>
<p className="text-muted-foreground font-mono text-sm">
Session ID: {sessionId}
</p>
</header>
<main>
<ConversationList projectId={projectId} sessionId={sessionId} />
</main>
</div>
);
return <SessionPageContent projectId={projectId} sessionId={sessionId} />;
}

View File

@@ -1,16 +1,16 @@
"use client";
import type { FC } from "react";
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
} from "@/components/ui/card";
import { FolderIcon } from "lucide-react";
import Link from "next/link";
import type { FC } from "react";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { useProjects } from "../hooks/useProjects";
export const ProjectList: FC = () => {

View File

@@ -1,7 +1,7 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cva, type VariantProps } from "class-variance-authority";
import type * as React from "react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
@@ -16,8 +16,8 @@ const alertVariants = cva(
defaultVariants: {
variant: "default",
},
}
)
},
);
function Alert({
className,
@@ -31,7 +31,7 @@ function Alert({
className={cn(alertVariants({ variant }), className)}
{...props}
/>
)
);
}
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
@@ -40,11 +40,11 @@ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
data-slot="alert-title"
className={cn(
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
className
className,
)}
{...props}
/>
)
);
}
function AlertDescription({
@@ -56,11 +56,11 @@ function AlertDescription({
data-slot="alert-description"
className={cn(
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
className
className,
)}
{...props}
/>
)
);
}
export { Alert, AlertTitle, AlertDescription }
export { Alert, AlertTitle, AlertDescription };

View File

@@ -1,9 +1,9 @@
"use client"
"use client";
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import type * as React from "react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Avatar({
className,
@@ -14,11 +14,11 @@ function Avatar({
data-slot="avatar"
className={cn(
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
className
className,
)}
{...props}
/>
)
);
}
function AvatarImage({
@@ -31,7 +31,7 @@ function AvatarImage({
className={cn("aspect-square size-full", className)}
{...props}
/>
)
);
}
function AvatarFallback({
@@ -43,11 +43,11 @@ function AvatarFallback({
data-slot="avatar-fallback"
className={cn(
"bg-muted flex size-full items-center justify-center rounded-full",
className
className,
)}
{...props}
/>
)
);
}
export { Avatar, AvatarImage, AvatarFallback }
export { Avatar, AvatarImage, AvatarFallback };

View File

@@ -1,8 +1,8 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import type * as React from "react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
@@ -22,8 +22,8 @@ const badgeVariants = cva(
defaultVariants: {
variant: "default",
},
}
)
},
);
function Badge({
className,
@@ -32,7 +32,7 @@ function Badge({
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span"
const Comp = asChild ? Slot : "span";
return (
<Comp
@@ -40,7 +40,7 @@ function Badge({
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
);
}
export { Badge, badgeVariants }
export { Badge, badgeVariants };

View File

@@ -1,8 +1,8 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import type * as React from "react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
@@ -32,8 +32,8 @@ const buttonVariants = cva(
variant: "default",
size: "default",
},
}
)
},
);
function Button({
className,
@@ -43,9 +43,9 @@ function Button({
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "button"
const Comp = asChild ? Slot : "button";
return (
<Comp
@@ -53,7 +53,7 @@ function Button({
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
);
}
export { Button, buttonVariants }
export { Button, buttonVariants };

View File

@@ -1,6 +1,6 @@
import * as React from "react"
import type * as React from "react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
@@ -8,11 +8,11 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
className,
)}
{...props}
/>
)
);
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
@@ -21,11 +21,11 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
className,
)}
{...props}
/>
)
);
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
@@ -35,7 +35,7 @@ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
);
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
@@ -45,7 +45,7 @@ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
);
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
@@ -54,11 +54,11 @@ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
className,
)}
{...props}
/>
)
);
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
@@ -68,7 +68,7 @@ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
className={cn("px-6", className)}
{...props}
/>
)
);
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
@@ -78,7 +78,7 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
);
}
export {
@@ -89,4 +89,4 @@ export {
CardAction,
CardDescription,
CardContent,
}
};

View File

@@ -1,11 +1,11 @@
"use client"
"use client";
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
function Collapsible({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
}
function CollapsibleTrigger({
@@ -16,7 +16,7 @@ function CollapsibleTrigger({
data-slot="collapsible-trigger"
{...props}
/>
)
);
}
function CollapsibleContent({
@@ -27,7 +27,7 @@ function CollapsibleContent({
data-slot="collapsible-content"
{...props}
/>
)
);
}
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
export { Collapsible, CollapsibleTrigger, CollapsibleContent };

View File

@@ -1,14 +1,14 @@
"use client"
"use client";
import * as React from "react"
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
import type * as React from "react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function HoverCard({
...props
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />;
}
function HoverCardTrigger({
@@ -16,7 +16,7 @@ function HoverCardTrigger({
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
return (
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
)
);
}
function HoverCardContent({
@@ -33,12 +33,12 @@ function HoverCardContent({
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className
className,
)}
{...props}
/>
</HoverCardPrimitive.Portal>
)
);
}
export { HoverCard, HoverCardTrigger, HoverCardContent }
export { HoverCard, HoverCardTrigger, HoverCardContent };

View File

@@ -1,4 +1,4 @@
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
@@ -7,7 +7,7 @@ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
className={cn("bg-accent animate-pulse rounded-md", className)}
{...props}
/>
)
);
}
export { Skeleton }
export { Skeleton };

View File

@@ -1,10 +1,12 @@
import { z } from 'zod'
import { z } from "zod";
export const ImageContentSchema = z.object({
type: z.literal('image'),
source: z.object({
type: z.literal('base64'),
data: z.string(),
media_type: z.enum(['image/png']),
}),
}).strict()
export const ImageContentSchema = z
.object({
type: z.literal("image"),
source: z.object({
type: z.literal("base64"),
data: z.string(),
media_type: z.enum(["image/png"]),
}),
})
.strict();

View File

@@ -1,6 +1,8 @@
import { z } from 'zod'
import { z } from "zod";
export const TextContentSchema = z.object({
type: z.literal('text'),
text: z.string(),
}).strict()
export const TextContentSchema = z
.object({
type: z.literal("text"),
text: z.string(),
})
.strict();

View File

@@ -1,7 +1,9 @@
import { z } from 'zod'
import { z } from "zod";
export const ThinkingContentSchema = z.object({
type: z.literal('thinking'),
thinking: z.string(),
signature: z.string().optional(),
}).strict()
export const ThinkingContentSchema = z
.object({
type: z.literal("thinking"),
thinking: z.string(),
signature: z.string().optional(),
})
.strict();

View File

@@ -1,16 +1,17 @@
import { z } from 'zod'
import { TextContentSchema } from './TextContentSchema'
import { ImageContentSchema } from './ImageContentSchema'
import { z } from "zod";
import { ImageContentSchema } from "./ImageContentSchema";
import { TextContentSchema } from "./TextContentSchema";
export const ToolResultContentSchema = z.object({
type: z.literal('tool_result'),
tool_use_id: z.string(),
content: z.union([z.string(), z.array(z.union([
TextContentSchema,
ImageContentSchema
]))]),
is_error: z.boolean().optional(),
}).strict()
export const ToolResultContentSchema = z
.object({
type: z.literal("tool_result"),
tool_use_id: z.string(),
content: z.union([
z.string(),
z.array(z.union([TextContentSchema, ImageContentSchema])),
]),
is_error: z.boolean().optional(),
})
.strict();
export type ToolResultContent = z.infer<typeof ToolResultContentSchema>
export type ToolResultContent = z.infer<typeof ToolResultContentSchema>;

View File

@@ -1,8 +1,10 @@
import { z } from 'zod'
import { z } from "zod";
export const ToolUseContentSchema = z.object({
type: z.literal('tool_use'),
id: z.string(),
name: z.string(),
input: z.record(z.string(), z.unknown()),
}).strict()
export const ToolUseContentSchema = z
.object({
type: z.literal("tool_use"),
id: z.string(),
name: z.string(),
input: z.record(z.string(), z.unknown()),
})
.strict();

View File

@@ -1,10 +1,10 @@
import { z } from 'zod'
import { AssistantMessageSchema } from '../message/AssistantMessageSchema'
import { BaseEntrySchema } from './BaseEntrySchema'
import { z } from "zod";
import { AssistantMessageSchema } from "../message/AssistantMessageSchema";
import { BaseEntrySchema } from "./BaseEntrySchema";
export const AssistantEntrySchema = BaseEntrySchema.extend({
// discriminator
type: z.literal('assistant'),
type: z.literal("assistant"),
// required
message: AssistantMessageSchema,
@@ -12,4 +12,4 @@ export const AssistantEntrySchema = BaseEntrySchema.extend({
// optional
requestId: z.string().optional(),
isApiErrorMessage: z.boolean().optional(),
}).strict()
}).strict();

View File

@@ -1,21 +1,23 @@
import { z } from 'zod'
import { z } from "zod";
export const BaseEntrySchema = z.object({
// required
isSidechain: z.boolean(),
userType: z.enum(['external']),
cwd: z.string(),
sessionId: z.string(),
version: z.string(),
uuid: z.uuid(),
timestamp: z.string(),
export const BaseEntrySchema = z
.object({
// required
isSidechain: z.boolean(),
userType: z.enum(["external"]),
cwd: z.string(),
sessionId: z.string(),
version: z.string(),
uuid: z.uuid(),
timestamp: z.string(),
// nullable
parentUuid: z.uuid().nullable(),
// nullable
parentUuid: z.uuid().nullable(),
// optional
isMeta: z.boolean().optional(),
toolUseResult: z.unknown().optional(), // スキーマがツールごとに異なりすぎるし利用もしなそうなので unknown
gitBranch: z.string().optional(),
isCompactSummary: z.boolean().optional(),
}).strict()
// optional
isMeta: z.boolean().optional(),
toolUseResult: z.unknown().optional(), // スキーマがツールごとに異なりすぎるし利用もしなそうなので unknown
gitBranch: z.string().optional(),
isCompactSummary: z.boolean().optional(),
})
.strict();

View File

@@ -1,7 +1,9 @@
import { z } from 'zod'
import { z } from "zod";
export const SummaryEntrySchema = z.object({
type: z.literal('summary'),
summary: z.string(),
leafUuid: z.string().uuid(),
}).strict()
export const SummaryEntrySchema = z
.object({
type: z.literal("summary"),
summary: z.string(),
leafUuid: z.string().uuid(),
})
.strict();

View File

@@ -1,12 +1,12 @@
import { z } from 'zod'
import { BaseEntrySchema } from './BaseEntrySchema'
import { z } from "zod";
import { BaseEntrySchema } from "./BaseEntrySchema";
export const SystemEntrySchema = BaseEntrySchema.extend({
// discriminator
type: z.literal('system'),
type: z.literal("system"),
// required
content: z.string(),
toolUseID: z.string(),
level: z.enum(['info']),
}).strict()
level: z.enum(["info"]),
}).strict();

View File

@@ -1,11 +1,11 @@
import { z } from 'zod'
import { UserMessageSchema } from '../message/UserMessageSchema'
import { BaseEntrySchema } from './BaseEntrySchema'
import { z } from "zod";
import { UserMessageSchema } from "../message/UserMessageSchema";
import { BaseEntrySchema } from "./BaseEntrySchema";
export const UserEntrySchema = BaseEntrySchema.extend({
// discriminator
type: z.literal('user'),
type: z.literal("user"),
// required
message: UserMessageSchema,
}).strict()
}).strict();

View File

@@ -1,14 +1,14 @@
import { z } from 'zod'
import { UserEntrySchema } from './entry/UserEntrySchema'
import { AssistantEntrySchema } from './entry/AssistantEntrySchema'
import { SummaryEntrySchema } from './entry/SummaryEntrySchema'
import { SystemEntrySchema } from './entry/SystemEntrySchema'
import { z } from "zod";
import { AssistantEntrySchema } from "./entry/AssistantEntrySchema";
import { SummaryEntrySchema } from "./entry/SummaryEntrySchema";
import { SystemEntrySchema } from "./entry/SystemEntrySchema";
import { UserEntrySchema } from "./entry/UserEntrySchema";
export const ConversationSchema = z.union([
UserEntrySchema,
AssistantEntrySchema,
SummaryEntrySchema,
SystemEntrySchema,
])
]);
export type Conversation = z.infer<typeof ConversationSchema>
export type Conversation = z.infer<typeof ConversationSchema>;

View File

@@ -1,38 +1,46 @@
import { z } from 'zod'
import { ThinkingContentSchema } from '../content/ThinkingContentSchema'
import { TextContentSchema } from '../content/TextContentSchema'
import { ToolUseContentSchema } from '../content/ToolUseContentSchema'
import { ToolResultContentSchema } from '../content/ToolResultContentSchema'
import { z } from "zod";
import { TextContentSchema } from "../content/TextContentSchema";
import { ThinkingContentSchema } from "../content/ThinkingContentSchema";
import { ToolResultContentSchema } from "../content/ToolResultContentSchema";
import { ToolUseContentSchema } from "../content/ToolUseContentSchema";
const AssistantMessageContentSchema = z.union([
ThinkingContentSchema,
TextContentSchema,
ToolUseContentSchema,
ToolResultContentSchema,
])
]);
export type AssistantMessageContent = z.infer<typeof AssistantMessageContentSchema>
export type AssistantMessageContent = z.infer<
typeof AssistantMessageContentSchema
>;
export const AssistantMessageSchema = z.object({
id: z.string(),
type: z.literal('message'),
role: z.literal('assistant'),
model: z.string(),
content: z.array(AssistantMessageContentSchema),
stop_reason: z.string().nullable(),
stop_sequence: z.string().nullable(),
usage: z.object({
input_tokens: z.number(),
cache_creation_input_tokens: z.number().optional(),
cache_read_input_tokens: z.number().optional(),
cache_creation: z.object({
ephemeral_5m_input_tokens: z.number(),
ephemeral_1h_input_tokens: z.number(),
}).optional(),
output_tokens: z.number(),
service_tier: z.string().nullable().optional(),
server_tool_use: z.object({
web_search_requests: z.number(),
}).optional(),
}),
}).strict()
export const AssistantMessageSchema = z
.object({
id: z.string(),
type: z.literal("message"),
role: z.literal("assistant"),
model: z.string(),
content: z.array(AssistantMessageContentSchema),
stop_reason: z.string().nullable(),
stop_sequence: z.string().nullable(),
usage: z.object({
input_tokens: z.number(),
cache_creation_input_tokens: z.number().optional(),
cache_read_input_tokens: z.number().optional(),
cache_creation: z
.object({
ephemeral_5m_input_tokens: z.number(),
ephemeral_1h_input_tokens: z.number(),
})
.optional(),
output_tokens: z.number(),
service_tier: z.string().nullable().optional(),
server_tool_use: z
.object({
web_search_requests: z.number(),
})
.optional(),
}),
})
.strict();

View File

@@ -1,25 +1,23 @@
import { z } from 'zod'
import { ToolResultContentSchema } from '../content/ToolResultContentSchema'
import { TextContentSchema } from '../content/TextContentSchema'
import { ImageContentSchema } from '../content/ImageContentSchema'
import { z } from "zod";
import { ImageContentSchema } from "../content/ImageContentSchema";
import { TextContentSchema } from "../content/TextContentSchema";
import { ToolResultContentSchema } from "../content/ToolResultContentSchema";
const UserMessageContentSchema = z.union([
z.string(),
TextContentSchema,
ToolResultContentSchema,
ImageContentSchema
])
ImageContentSchema,
]);
export type UserMessageContent = z.infer<typeof UserMessageContentSchema>
export type UserMessageContent = z.infer<typeof UserMessageContentSchema>;
export const UserMessageSchema = z.object({
role: z.literal('user'),
content: z.union([
z.string(),
z.array(z.union([
export const UserMessageSchema = z
.object({
role: z.literal("user"),
content: z.union([
z.string(),
UserMessageContentSchema
]))
]),
}).strict()
z.array(z.union([z.string(), UserMessageContentSchema])),
]),
})
.strict();

View File

@@ -1,59 +1,71 @@
import { z } from 'zod'
import { StructuredPatchSchema } from './StructuredPatchSchema'
import { z } from "zod";
import { StructuredPatchSchema } from "./StructuredPatchSchema";
export const CommonToolResultSchema = z.union([
z.object({
stdout: z.string(),
stderr: z.string(),
interrupted: z.boolean(),
isImage: z.boolean(),
}).strict(),
z
.object({
stdout: z.string(),
stderr: z.string(),
interrupted: z.boolean(),
isImage: z.boolean(),
})
.strict(),
// create
z.object({
type: z.literal('create'),
filePath: z.string(),
content: z.string(),
structuredPatch: z.array(StructuredPatchSchema)
}).strict(),
// update
z.object({
filePath: z.string(),
oldString: z.string(),
newString: z.string(),
originalFile: z.string(),
userModified: z.boolean(),
replaceAll: z.boolean(),
structuredPatch: z.array(StructuredPatchSchema)
}).strict(),
// search?
z.object({
filenames: z.array(z.string()),
durationMs: z.number(),
numFiles: z.number(),
truncated: z.boolean(),
}).strict(),
// text
z.object({
type: z.literal('text'),
file: z.object({
z
.object({
type: z.literal("create"),
filePath: z.string(),
content: z.string(),
numLines: z.number(),
startLine: z.number(),
totalLines: z.number(),
structuredPatch: z.array(StructuredPatchSchema),
})
}).strict(),
.strict(),
// update
z
.object({
filePath: z.string(),
oldString: z.string(),
newString: z.string(),
originalFile: z.string(),
userModified: z.boolean(),
replaceAll: z.boolean(),
structuredPatch: z.array(StructuredPatchSchema),
})
.strict(),
// search?
z
.object({
filenames: z.array(z.string()),
durationMs: z.number(),
numFiles: z.number(),
truncated: z.boolean(),
})
.strict(),
// text
z
.object({
type: z.literal("text"),
file: z.object({
filePath: z.string(),
content: z.string(),
numLines: z.number(),
startLine: z.number(),
totalLines: z.number(),
}),
})
.strict(),
// content
z.object({
mode: z.literal('content'),
numFiles: z.number(),
filenames: z.array(z.string()),
content: z.string(),
numLines: z.number(),
}).strict(),
])
z
.object({
mode: z.literal("content"),
numFiles: z.number(),
filenames: z.array(z.string()),
content: z.string(),
numLines: z.number(),
})
.strict(),
]);

View File

@@ -1,9 +1,11 @@
import { z } from 'zod'
import { z } from "zod";
export const StructuredPatchSchema = z.object({
oldStart: z.number(),
oldLines: z.number(),
newStart: z.number(),
newLines: z.number(),
lines: z.array(z.string()),
}).strict()
export const StructuredPatchSchema = z
.object({
oldStart: z.number(),
oldLines: z.number(),
newStart: z.number(),
newLines: z.number(),
lines: z.array(z.string()),
})
.strict();

View File

@@ -1,13 +1,17 @@
import z from "zod";
const TodoSchema = z.object({
content: z.string(),
status: z.enum(['pending', 'in_progress', 'completed']),
priority: z.enum(['low', 'medium', 'high']),
id: z.string(),
}).strict()
const TodoSchema = z
.object({
content: z.string(),
status: z.enum(["pending", "in_progress", "completed"]),
priority: z.enum(["low", "medium", "high"]),
id: z.string(),
})
.strict();
export const TodoToolResultSchema = z.object({
oldTodos: z.array(TodoSchema).optional(),
newTodos: z.array(TodoSchema).optional(),
}).strict()
export const TodoToolResultSchema = z
.object({
oldTodos: z.array(TodoSchema).optional(),
newTodos: z.array(TodoSchema).optional(),
})
.strict();

View File

@@ -1,6 +1,6 @@
import { z } from "zod";
import { TodoToolResultSchema } from "./TodoSchema";
import { CommonToolResultSchema } from "./CommonToolSchema";
import { TodoToolResultSchema } from "./TodoSchema";
export const ToolUseResultSchema = z.union([
z.string(),

View File

@@ -1,8 +1,8 @@
import type { HonoAppType } from "./app";
import { getProjects } from "../service/project/getProjects";
import { getProject } from "../service/project/getProject";
import { getSessions } from "../service/session/getSessions";
import { getProjects } from "../service/project/getProjects";
import { getSession } from "../service/session/getSession";
import { getSessions } from "../service/session/getSessions";
import type { HonoAppType } from "./app";
export const routes = (app: HonoAppType) => {
return app

View File

@@ -7,9 +7,7 @@ const matchSchema = z.object({
content: z.string(),
});
export const parseCommandXml = (
content: string
):
export type ParsedCommand =
| {
kind: "command";
commandName: string;
@@ -28,7 +26,9 @@ export const parseCommandXml = (
| {
kind: "text";
content: string;
} => {
};
export const parseCommandXml = (content: string): ParsedCommand => {
const matches = Array.from(content.matchAll(regExp))
.map((match) => matchSchema.safeParse(match.groups))
.filter((result) => result.success)
@@ -42,16 +42,16 @@ export const parseCommandXml = (
}
const commandName = matches.find(
(match) => match.tag === "command-name"
(match) => match.tag === "command-name",
)?.content;
const commandArgs = matches.find(
(match) => match.tag === "command-args"
(match) => match.tag === "command-args",
)?.content;
const commandMessage = matches.find(
(match) => match.tag === "command-message"
(match) => match.tag === "command-message",
)?.content;
const localCommandStdout = matches.find(
(match) => match.tag === "local-command-stdout"
(match) => match.tag === "local-command-stdout",
)?.content;
switch (true) {

View File

@@ -1,11 +1,11 @@
import { existsSync } from "node:fs";
import type { Project } from "../types";
import { decodeProjectId } from "./id";
import { getProjectMeta } from "./getProjectMeta";
import { decodeProjectId } from "./id";
export const getProject = async (
projectId: string
projectId: string,
): Promise<{ project: Project }> => {
const fullPath = decodeProjectId(projectId);
if (!existsSync(fullPath)) {

View File

@@ -1,13 +1,18 @@
import { statSync } from "node:fs";
import { readdir, readFile } from "node:fs/promises";
import { basename, dirname, resolve } from "node:path";
import { basename, resolve } from "node:path";
import { parseJsonl } from "../parseJsonl";
import type { ProjectMeta } from "../types";
const projectMetaCache = new Map<string, ProjectMeta>();
const projectPathCache = new Map<string, string | null>();
const extractProjectPathFromJsonl = async (filePath: string) => {
const cached = projectPathCache.get(filePath);
if (cached !== undefined) {
return cached;
}
const extractMetaFromJsonl = async (filePath: string) => {
const content = await readFile(filePath, "utf-8");
const lines = content.split("\n");
@@ -25,19 +30,16 @@ const extractMetaFromJsonl = async (filePath: string) => {
break;
}
return {
cwd,
} as const;
if (cwd !== null) {
projectPathCache.set(filePath, cwd);
}
return cwd;
};
export const getProjectMeta = async (
claudeProjectPath: string
claudeProjectPath: string,
): Promise<ProjectMeta> => {
const cached = projectMetaCache.get(claudeProjectPath);
if (cached !== undefined) {
return cached;
}
const dirents = await readdir(claudeProjectPath, { withFileTypes: true });
const files = dirents
.filter((d) => d.isFile() && d.name.endsWith(".jsonl"))
@@ -46,7 +48,7 @@ export const getProjectMeta = async (
({
fullPath: resolve(d.parentPath, d.name),
stats: statSync(resolve(d.parentPath, d.name)),
} as const)
}) as const,
)
.toSorted((a, b) => {
return a.stats.ctime.getTime() - b.stats.ctime.getTime();
@@ -54,30 +56,26 @@ export const getProjectMeta = async (
const lastModifiedUnixTime = files.at(-1)?.stats.ctime.getTime();
let cwd: string | null = null;
let projectPath: string | null = null;
for (const file of files) {
const result = await extractMetaFromJsonl(file.fullPath);
projectPath = await extractProjectPathFromJsonl(file.fullPath);
if (result.cwd === null) {
if (projectPath === null) {
continue;
}
cwd = result.cwd;
break;
}
const projectMeta: ProjectMeta = {
projectName: cwd ? basename(cwd) : null,
projectPath: cwd,
projectName: projectPath ? basename(projectPath) : null,
projectPath,
lastModifiedAt: lastModifiedUnixTime
? new Date(lastModifiedUnixTime)
: null,
sessionCount: files.length,
};
projectMetaCache.set(claudeProjectPath, projectMeta);
return projectMeta;
};

View File

@@ -3,8 +3,8 @@ import { resolve } from "node:path";
import { claudeProjectPath } from "../paths";
import type { Project } from "../types";
import { encodeProjectId } from "./id";
import { getProjectMeta } from "./getProjectMeta";
import { encodeProjectId } from "./id";
export const getProjects = async (): Promise<{ projects: Project[] }> => {
const dirents = await readdir(claudeProjectPath, { withFileTypes: true });
@@ -24,6 +24,11 @@ export const getProjects = async (): Promise<{ projects: Project[] }> => {
);
return {
projects,
projects: projects.sort((a, b) => {
return (
(b.meta.lastModifiedAt?.getTime() ?? 0) -
(a.meta.lastModifiedAt?.getTime() ?? 0)
);
}),
};
};

View File

@@ -1,13 +1,13 @@
import { readFile } from "node:fs/promises";
import { decodeProjectId } from "../project/id";
import { resolve } from "node:path";
import { parseJsonl } from "../parseJsonl";
import { decodeProjectId } from "../project/id";
import type { SessionDetail } from "../types";
import { getSessionMeta } from "./getSessionMeta";
export const getSession = async (
projectId: string,
sessionId: string
sessionId: string,
): Promise<{
session: SessionDetail;
}> => {

View File

@@ -1,26 +1,21 @@
import { statSync } from "node:fs";
import { readFile } from "node:fs/promises";
import { parseJsonl } from "../parseJsonl";
import type { Conversation } from "../../../lib/conversation-schema";
import { type ParsedCommand, parseCommandXml } from "../parseCommandXml";
import { parseJsonl } from "../parseJsonl";
import type { SessionMeta } from "../types";
const sessionMetaCache = new Map<string, SessionMeta>();
const firstCommandCache = new Map<string, ParsedCommand | null>();
export const getSessionMeta = async (
jsonlFilePath: string
): Promise<SessionMeta> => {
const cached = sessionMetaCache.get(jsonlFilePath);
const getFirstCommand = (
jsonlFilePath: string,
lines: string[],
): ParsedCommand | null => {
const cached = firstCommandCache.get(jsonlFilePath);
if (cached !== undefined) {
return cached;
}
const stats = statSync(jsonlFilePath);
const lastModifiedUnixTime = stats.ctime.getTime();
const content = await readFile(jsonlFilePath, "utf-8");
const lines = content.split("\n");
let firstUserMessage: Conversation | null = null;
for (const line of lines) {
@@ -35,12 +30,10 @@ export const getSessionMeta = async (
break;
}
const sessionMeta: SessionMeta = {
messageCount: lines.length,
firstContent:
firstUserMessage === null
? null
: typeof firstUserMessage.message.content === "string"
const firstMessageText =
firstUserMessage === null
? null
: typeof firstUserMessage.message.content === "string"
? firstUserMessage.message.content
: (() => {
const firstContent = firstUserMessage.message.content.at(0);
@@ -48,13 +41,34 @@ export const getSessionMeta = async (
if (typeof firstContent === "string") return firstContent;
if (firstContent.type === "text") return firstContent.text;
return null;
})(),
})();
const firstCommand =
firstMessageText === null ? null : parseCommandXml(firstMessageText);
if (firstCommand !== null) {
firstCommandCache.set(jsonlFilePath, firstCommand);
}
return firstCommand;
};
export const getSessionMeta = async (
jsonlFilePath: string,
): Promise<SessionMeta> => {
const stats = statSync(jsonlFilePath);
const lastModifiedUnixTime = stats.ctime.getTime();
const content = await readFile(jsonlFilePath, "utf-8");
const lines = content.split("\n");
const sessionMeta: SessionMeta = {
messageCount: lines.length,
firstCommand: getFirstCommand(jsonlFilePath, lines),
lastModifiedAt: lastModifiedUnixTime
? new Date(lastModifiedUnixTime)
: null,
};
sessionMetaCache.set(jsonlFilePath, sessionMeta);
return sessionMeta;
};

View File

@@ -26,6 +26,11 @@ export const getSessions = async (
);
return {
sessions,
sessions: sessions.sort((a, b) => {
return (
(b.meta.lastModifiedAt?.getTime() ?? 0) -
(a.meta.lastModifiedAt?.getTime() ?? 0)
);
}),
};
};

View File

@@ -1,4 +1,5 @@
import type { Conversation } from "../../lib/conversation-schema";
import type { ParsedCommand } from "./parseCommandXml";
export type Project = {
id: string;
@@ -21,7 +22,7 @@ export type Session = {
export type SessionMeta = {
messageCount: number;
firstContent: string | null;
firstCommand: ParsedCommand | null;
lastModifiedAt: Date | null;
};