diff --git a/package.json b/package.json index 7a2d8dd..b08f1e5 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@hono/zod-validator": "^0.7.2", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-hover-card": "^1.1.15", "@radix-ui/react-slot": "^1.2.3", "@tanstack/react-query": "^5.85.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d67908e..8898c7e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: '@radix-ui/react-collapsible': specifier: ^1.1.12 version: 1.1.12(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-dialog': + specifier: ^1.1.15 + version: 1.1.15(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@radix-ui/react-hover-card': specifier: ^1.1.15 version: 1.1.15(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -639,6 +642,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-dialog@1.1.15': + resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-dismissable-layer@1.1.11': resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} peerDependencies: @@ -652,6 +668,28 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-focus-guards@1.1.3': + resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.7': + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-hover-card@1.1.15': resolution: {integrity: sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==} peerDependencies: @@ -1100,6 +1138,10 @@ packages: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -1221,6 +1263,9 @@ packages: resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -1274,6 +1319,10 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -1714,6 +1763,36 @@ packages: '@types/react': '>=18' react: '>=18' + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.1: + resolution: {integrity: sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + react-syntax-highlighter@15.6.6: resolution: {integrity: sha512-DgXrc+AZF47+HvAPEmn7Ua/1p10jNoVZVI/LoPiYdtY+OM+/nG5yefLHKJwdKqY1adMuHFbeyBaG9j64ML7vTw==} peerDependencies: @@ -1912,6 +1991,26 @@ packages: unist-util-visit@5.0.0: resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + use-sync-external-store@1.5.0: resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} peerDependencies: @@ -2385,6 +2484,28 @@ snapshots: optionalDependencies: '@types/react': 19.1.12 + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.12)(react@19.1.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.1.12)(react@19.1.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.12)(react@19.1.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.12)(react@19.1.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.12)(react@19.1.1) + aria-hidden: 1.2.6 + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + react-remove-scroll: 2.7.1(@types/react@19.1.12)(react@19.1.1) + optionalDependencies: + '@types/react': 19.1.12 + '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -2398,6 +2519,23 @@ snapshots: '@types/react': 19.1.12 '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.1.12)(react@19.1.1)': + dependencies: + react: 19.1.1 + optionalDependencies: + '@types/react': 19.1.12 + + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.12)(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + optionalDependencies: + '@types/react': 19.1.12 + '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@radix-ui/react-hover-card@1.1.15(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -2777,6 +2915,10 @@ snapshots: ansi-styles@6.2.1: {} + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + assertion-error@2.0.1: {} bail@2.0.2: {} @@ -2877,6 +3019,8 @@ snapshots: detect-libc@2.0.4: {} + detect-node-es@1.1.0: {} + devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -2942,6 +3086,8 @@ snapshots: fsevents@2.3.3: optional: true + get-nonce@1.0.1: {} + graceful-fs@4.2.11: {} hast-util-parse-selector@2.2.5: {} @@ -3584,6 +3730,33 @@ snapshots: transitivePeerDependencies: - supports-color + react-remove-scroll-bar@2.3.8(@types/react@19.1.12)(react@19.1.1): + dependencies: + react: 19.1.1 + react-style-singleton: 2.2.3(@types/react@19.1.12)(react@19.1.1) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.12 + + react-remove-scroll@2.7.1(@types/react@19.1.12)(react@19.1.1): + dependencies: + react: 19.1.1 + react-remove-scroll-bar: 2.3.8(@types/react@19.1.12)(react@19.1.1) + react-style-singleton: 2.2.3(@types/react@19.1.12)(react@19.1.1) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.1.12)(react@19.1.1) + use-sidecar: 1.1.3(@types/react@19.1.12)(react@19.1.1) + optionalDependencies: + '@types/react': 19.1.12 + + react-style-singleton@2.2.3(@types/react@19.1.12)(react@19.1.1): + dependencies: + get-nonce: 1.0.1 + react: 19.1.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.12 + react-syntax-highlighter@15.6.6(react@19.1.1): dependencies: '@babel/runtime': 7.28.3 @@ -3845,6 +4018,21 @@ snapshots: unist-util-is: 6.0.0 unist-util-visit-parents: 6.0.1 + use-callback-ref@1.3.3(@types/react@19.1.12)(react@19.1.1): + dependencies: + react: 19.1.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.12 + + use-sidecar@1.1.3(@types/react@19.1.12)(react@19.1.1): + dependencies: + detect-node-es: 1.1.0 + react: 19.1.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.12 + use-sync-external-store@1.5.0(react@19.1.1): dependencies: react: 19.1.1 diff --git a/src/app/projects/[projectId]/components/ProjectPage.tsx b/src/app/projects/[projectId]/components/ProjectPage.tsx index 94785bf..081e677 100644 --- a/src/app/projects/[projectId]/components/ProjectPage.tsx +++ b/src/app/projects/[projectId]/components/ProjectPage.tsx @@ -35,14 +35,11 @@ export const ProjectPageContent = ({ projectId }: { projectId: string }) => {

- {project.meta.projectName ?? "unknown"} + {project.meta.projectPath ?? project.claudeProjectPath}

- Workspace: {project.meta.projectPath ?? "unknown"} -

-

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

diff --git a/src/app/projects/[projectId]/sessions/[sessionId]/components/conversationList/ConversationItem.tsx b/src/app/projects/[projectId]/sessions/[sessionId]/components/conversationList/ConversationItem.tsx index 06db794..6cfd752 100644 --- a/src/app/projects/[projectId]/sessions/[sessionId]/components/conversationList/ConversationItem.tsx +++ b/src/app/projects/[projectId]/sessions/[sessionId]/components/conversationList/ConversationItem.tsx @@ -1,6 +1,7 @@ import type { FC } from "react"; import type { Conversation } from "@/lib/conversation-schema"; import type { ToolResultContent } from "@/lib/conversation-schema/content/ToolResultContentSchema"; +import { SidechainConversationModal } from "../conversationModal/SidechainConversationModal"; import { AssistantConversationContent } from "./AssistantConversationContent"; import { MetaConversationContent } from "./MetaConversationContent"; import { SummaryConversationContent } from "./SummaryConversationContent"; @@ -10,7 +11,14 @@ import { UserConversationContent } from "./UserConversationContent"; export const ConversationItem: FC<{ conversation: Conversation; getToolResult: (toolUseId: string) => ToolResultContent | undefined; -}> = ({ conversation, getToolResult }) => { + isRootSidechain: (conversation: Conversation) => boolean; + getSidechainConversations: (rootUuid: string) => Conversation[]; +}> = ({ + conversation, + getToolResult, + isRootSidechain, + getSidechainConversations, +}) => { if (conversation.type === "summary") { return ( @@ -30,7 +38,25 @@ export const ConversationItem: FC<{ if (conversation.isSidechain) { // sidechain = サブタスクのこと // 別途ツール呼び出しの方で描画可能にするのでここでは表示しない - return null; + if (!isRootSidechain(conversation)) { + return null; + } + + return ( + { + if (original.type === "summary") return original; + return { + ...original, + isSidechain: false, + }; + })} + getToolResult={getToolResult} + /> + ); } if (conversation.type === "user") { diff --git a/src/app/projects/[projectId]/sessions/[sessionId]/components/conversationList/ConversationList.tsx b/src/app/projects/[projectId]/sessions/[sessionId]/components/conversationList/ConversationList.tsx index f623adc..d224252 100644 --- a/src/app/projects/[projectId]/sessions/[sessionId]/components/conversationList/ConversationList.tsx +++ b/src/app/projects/[projectId]/sessions/[sessionId]/components/conversationList/ConversationList.tsx @@ -3,6 +3,7 @@ import type { FC } from "react"; import type { Conversation } from "@/lib/conversation-schema"; import type { ToolResultContent } from "@/lib/conversation-schema/content/ToolResultContentSchema"; +import { useSidechain } from "../../hooks/useSidechain"; import { ConversationItem } from "./ConversationItem"; const getConversationKey = (conversation: Conversation) => { @@ -34,6 +35,9 @@ export const ConversationList: FC = ({ conversations, getToolResult, }) => { + const { isRootSidechain, getSidechainConversations } = + useSidechain(conversations); + return (
    {conversations.flatMap((conversation) => { @@ -42,13 +46,11 @@ export const ConversationList: FC = ({ key={getConversationKey(conversation)} conversation={conversation} getToolResult={getToolResult} + isRootSidechain={isRootSidechain} + getSidechainConversations={getSidechainConversations} /> ); - if (elm === null) { - return []; - } - return [
  • = ({ -
    -
    {children}
    -
    +
    {children}
    ); diff --git a/src/app/projects/[projectId]/sessions/[sessionId]/components/conversationModal/SidechainConversationModal.tsx b/src/app/projects/[projectId]/sessions/[sessionId]/components/conversationModal/SidechainConversationModal.tsx new file mode 100644 index 0000000..55047f1 --- /dev/null +++ b/src/app/projects/[projectId]/sessions/[sessionId]/components/conversationModal/SidechainConversationModal.tsx @@ -0,0 +1,91 @@ +"use client"; + +import { Eye } from "lucide-react"; +import type { FC } from "react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import type { Conversation } from "@/lib/conversation-schema"; +import type { ToolResultContent } from "@/lib/conversation-schema/content/ToolResultContentSchema"; +import { ConversationList } from "../conversationList/ConversationList"; + +type SidechainConversationModalProps = { + conversation: Conversation; + sidechainConversations: Conversation[]; + getToolResult: (toolUseId: string) => ToolResultContent | undefined; +}; + +const sidechainTitle = (conversations: Conversation[]): string => { + const firstConversation = conversations.at(0); + + const defaultTitle = `${conversations.length} conversations (${ + firstConversation?.type !== "summary" ? firstConversation?.uuid : "" + })`; + + if (!firstConversation) { + return defaultTitle; + } + + if (firstConversation.type !== "user") { + return defaultTitle; + } + + const textContent = + typeof firstConversation.message.content === "string" + ? firstConversation.message.content + : (() => { + const firstContent = firstConversation.message.content.at(0); + if (firstContent === undefined) return null; + + if (typeof firstContent === "string") return firstContent; + if (firstContent.type === "text") return firstContent.text; + + return null; + })(); + + return textContent ?? defaultTitle; +}; + +export const SidechainConversationModal: FC< + SidechainConversationModalProps +> = ({ conversation, sidechainConversations, getToolResult }) => { + const title = sidechainTitle(sidechainConversations); + + const rootUuid = + conversation.type !== "summary" ? conversation.uuid : conversation.leafUuid; + + return ( + + + + + + + + {title.length > 100 ? `${title.slice(0, 100)}...` : title} + + Root UUID: {rootUuid} + +
    + +
    +
    +
    + ); +}; diff --git a/src/app/projects/[projectId]/sessions/[sessionId]/hooks/useSidechain.ts b/src/app/projects/[projectId]/sessions/[sessionId]/hooks/useSidechain.ts new file mode 100644 index 0000000..37e5ce7 --- /dev/null +++ b/src/app/projects/[projectId]/sessions/[sessionId]/hooks/useSidechain.ts @@ -0,0 +1,80 @@ +import { useCallback, useMemo } from "react"; +import type { Conversation } from "@/lib/conversation-schema"; + +export const useSidechain = (conversations: Conversation[]) => { + const conversationMap = useMemo(() => { + return new Map( + conversations + .filter((conv) => conv.type !== "summary") + .map((conv) => [conv.uuid, conv] as const), + ); + }, [conversations]); + + const getRootConversationRecursive = useCallback( + (conversation: Conversation): Conversation => { + if (conversation.type === "summary") { + return conversation; + } + + if (conversation.parentUuid === null) { + return conversation; + } + + const parent = conversationMap.get(conversation.parentUuid); + if (parent === undefined) { + return conversation; + } + + return getRootConversationRecursive(parent); + }, + [conversationMap], + ); + + const sidechainConversationGroups = useMemo(() => { + const filtered = conversations + .filter((conv) => conv.type !== "summary") + .filter((conv) => conv.isSidechain === true); + + const groups = new Map(); + + for (const conv of filtered) { + const rootConversation = getRootConversationRecursive(conv); + + if (rootConversation.type === "summary") { + // たぶんない + continue; + } + + if (groups.has(rootConversation.uuid)) { + groups.get(rootConversation.uuid)?.push(conv); + } else { + groups.set(rootConversation.uuid, [conv]); + } + } + + return groups; + }, [conversations, getRootConversationRecursive]); + + const isRootSidechain = useCallback( + (conversation: Conversation) => { + if (conversation.type === "summary") { + return false; + } + + return sidechainConversationGroups.has(conversation.uuid); + }, + [sidechainConversationGroups], + ); + + const getSidechainConversations = useCallback( + (rootUuid: string) => { + return sidechainConversationGroups.get(rootUuid) ?? []; + }, + [sidechainConversationGroups], + ); + + return { + isRootSidechain, + getSidechainConversations, + }; +}; diff --git a/src/app/projects/components/ProjectList.tsx b/src/app/projects/components/ProjectList.tsx index f6763d7..291849c 100644 --- a/src/app/projects/components/ProjectList.tsx +++ b/src/app/projects/components/ProjectList.tsx @@ -40,10 +40,9 @@ export const ProjectList: FC = () => { {project.meta.projectName ?? project.claudeProjectPath} - - {project.meta.sessionCount} conversation - {project.meta.sessionCount !== 1 ? "s" : ""} - + {project.meta.projectPath ? ( + {project.meta.projectPath} + ) : null}

    @@ -52,14 +51,14 @@ export const ProjectList: FC = () => { ? new Date(project.meta.lastModifiedAt).toLocaleDateString() : ""}

    -

    - Workspace: {project.meta.projectPath ?? "unknown"} +

    + Messages: {project.meta.sessionCount}

    diff --git a/src/app/projects/page.tsx b/src/app/projects/page.tsx index 3816b43..3717058 100644 --- a/src/app/projects/page.tsx +++ b/src/app/projects/page.tsx @@ -7,7 +7,7 @@ export default async function ProjectsPage() {

    - Claude Code History Viewer + Claude Code Viewer

    Browse your Claude Code conversation history and project interactions diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx new file mode 100644 index 0000000..2c325c4 --- /dev/null +++ b/src/components/ui/dialog.tsx @@ -0,0 +1,143 @@ +"use client"; + +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { XIcon } from "lucide-react"; +import type * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Dialog({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: React.ComponentProps & { + showCloseButton?: boolean; +}) { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ); +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +

    + ); +} + +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ); +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +}; diff --git a/src/server/service/project/getProjects.ts b/src/server/service/project/getProjects.ts index 7b75553..ee24322 100644 --- a/src/server/service/project/getProjects.ts +++ b/src/server/service/project/getProjects.ts @@ -20,7 +20,7 @@ export const getProjects = async (): Promise<{ projects: Project[] }> => { claudeProjectPath: fullPath, meta: await getProjectMeta(fullPath), }; - }) + }), ); return { diff --git a/src/server/service/session/getSessionMeta.ts b/src/server/service/session/getSessionMeta.ts index 3862b1d..2f1e8b2 100644 --- a/src/server/service/session/getSessionMeta.ts +++ b/src/server/service/session/getSessionMeta.ts @@ -1,12 +1,13 @@ import { statSync } from "node:fs"; import { readFile } from "node:fs/promises"; -import type { Conversation } from "../../../lib/conversation-schema"; import { type ParsedCommand, parseCommandXml } from "../parseCommandXml"; import { parseJsonl } from "../parseJsonl"; import type { SessionMeta } from "../types"; const firstCommandCache = new Map(); +const ignoreCommands = ["/clear", "/login", "/logout", "/exit"]; + const getFirstCommand = ( jsonlFilePath: string, lines: string[], @@ -16,7 +17,7 @@ const getFirstCommand = ( return cached; } - let firstUserMessage: Conversation | null = null; + let firstCommand: ParsedCommand | null = null; for (const line of lines) { const conversation = parseJsonl(line).at(0); @@ -25,27 +26,49 @@ const getFirstCommand = ( continue; } - firstUserMessage = conversation; + const firstUserText = + conversation === null + ? null + : typeof conversation.message.content === "string" + ? conversation.message.content + : (() => { + const firstContent = conversation.message.content.at(0); + if (firstContent === undefined) return null; + if (typeof firstContent === "string") return firstContent; + if (firstContent.type === "text") return firstContent.text; + return null; + })(); + if (firstUserText === null) { + continue; + } + + if ( + firstUserText === + "Caveat: The messages below were generated by the user while running local commands. DO NOT respond to these messages or otherwise consider them in your response unless the user explicitly asks you to." + ) { + continue; + } + + const command = parseCommandXml(firstUserText); + if ( + command.kind === "local-command-1" || + command.kind === "local-command-2" + ) { + continue; + } + + if ( + command.kind === "command" && + ignoreCommands.includes(command.commandName) + ) { + continue; + } + + firstCommand = command; break; } - const firstMessageText = - firstUserMessage === null - ? null - : typeof firstUserMessage.message.content === "string" - ? firstUserMessage.message.content - : (() => { - const firstContent = firstUserMessage.message.content.at(0); - if (firstContent === undefined) return null; - 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); } diff --git a/src/server/service/session/getSessions.ts b/src/server/service/session/getSessions.ts index 6b24043..07e8a4f 100644 --- a/src/server/service/session/getSessions.ts +++ b/src/server/service/session/getSessions.ts @@ -6,7 +6,7 @@ import type { Session } from "../types"; import { getSessionMeta } from "./getSessionMeta"; export const getSessions = async ( - projectId: string + projectId: string, ): Promise<{ sessions: Session[] }> => { const claudeProjectPath = decodeProjectId(projectId); @@ -22,7 +22,7 @@ export const getSessions = async ( jsonlFilePath: fullPath, meta: await getSessionMeta(fullPath), }; - }) + }), ); return {