mirror of
https://github.com/aljazceru/claude-code-viewer.git
synced 2025-12-24 16:54:21 +01:00
chore: format files
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
17
src/app/projects/[projectId]/services/firstCommandToTitle.ts
Normal file
17
src/app/projects/[projectId]/services/firstCommandToTitle.ts
Normal 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");
|
||||
}
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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<{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 () => {
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(),
|
||||
]);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}> => {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user