mirror of
https://github.com/aljazceru/claude-code-viewer.git
synced 2025-12-24 16:54:21 +01:00
feat: implement sub chain message view feature
This commit is contained in:
@@ -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",
|
||||
|
||||
188
pnpm-lock.yaml
generated
188
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -35,14 +35,11 @@ export const ProjectPageContent = ({ projectId }: { projectId: string }) => {
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<FolderIcon className="w-6 h-6" />
|
||||
<h1 className="text-3xl font-bold">
|
||||
{project.meta.projectName ?? "unknown"}
|
||||
{project.meta.projectPath ?? project.claudeProjectPath}
|
||||
</h1>
|
||||
</div>
|
||||
<p className="text-muted-foreground font-mono text-sm">
|
||||
Workspace: {project.meta.projectPath ?? "unknown"}
|
||||
</p>
|
||||
<p className="text-muted-foreground font-mono text-sm">
|
||||
Claude History: {project.claudeProjectPath ?? "unknown"}
|
||||
History File: {project.claudeProjectPath ?? "unknown"}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<SummaryConversationContent>
|
||||
@@ -30,7 +38,25 @@ export const ConversationItem: FC<{
|
||||
if (conversation.isSidechain) {
|
||||
// sidechain = サブタスクのこと
|
||||
// 別途ツール呼び出しの方で描画可能にするのでここでは表示しない
|
||||
return null;
|
||||
if (!isRootSidechain(conversation)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SidechainConversationModal
|
||||
conversation={conversation}
|
||||
sidechainConversations={getSidechainConversations(
|
||||
conversation.uuid,
|
||||
).map((original) => {
|
||||
if (original.type === "summary") return original;
|
||||
return {
|
||||
...original,
|
||||
isSidechain: false,
|
||||
};
|
||||
})}
|
||||
getToolResult={getToolResult}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (conversation.type === "user") {
|
||||
|
||||
@@ -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<ConversationListProps> = ({
|
||||
conversations,
|
||||
getToolResult,
|
||||
}) => {
|
||||
const { isRootSidechain, getSidechainConversations } =
|
||||
useSidechain(conversations);
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{conversations.flatMap((conversation) => {
|
||||
@@ -42,13 +46,11 @@ export const ConversationList: FC<ConversationListProps> = ({
|
||||
key={getConversationKey(conversation)}
|
||||
conversation={conversation}
|
||||
getToolResult={getToolResult}
|
||||
isRootSidechain={isRootSidechain}
|
||||
getSidechainConversations={getSidechainConversations}
|
||||
/>
|
||||
);
|
||||
|
||||
if (elm === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
<li
|
||||
className={`w-full flex ${
|
||||
|
||||
@@ -20,9 +20,7 @@ export const MetaConversationContent: FC<PropsWithChildren> = ({
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="bg-background rounded border p-3 mt-2">
|
||||
<pre className="text-xs overflow-x-auto">{children}</pre>
|
||||
</div>
|
||||
<div className="bg-background rounded border p-3 mt-2">{children}</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="w-full mb-3">
|
||||
<div className="flex items-center gap-2 overflow-hidden">
|
||||
<Eye className="h-4 w-4 flex-shrink-0" />
|
||||
<span className="overflow-hidden text-ellipsis">
|
||||
View Task: {title}
|
||||
</span>
|
||||
</div>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="!w-[1200px] !max-w-none max-h-[80vh] overflow-hidden flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{title.length > 100 ? `${title.slice(0, 100)}...` : title}
|
||||
</DialogTitle>
|
||||
<DialogDescription>Root UUID: {rootUuid}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex-1 overflow-auto">
|
||||
<ConversationList
|
||||
conversations={sidechainConversations}
|
||||
getToolResult={getToolResult}
|
||||
/>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -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<string, Conversation>(
|
||||
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<string, Conversation[]>();
|
||||
|
||||
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,
|
||||
};
|
||||
};
|
||||
@@ -40,10 +40,9 @@ export const ProjectList: FC = () => {
|
||||
{project.meta.projectName ?? project.claudeProjectPath}
|
||||
</span>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{project.meta.sessionCount} conversation
|
||||
{project.meta.sessionCount !== 1 ? "s" : ""}
|
||||
</CardDescription>
|
||||
{project.meta.projectPath ? (
|
||||
<CardDescription>{project.meta.projectPath}</CardDescription>
|
||||
) : null}
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
@@ -52,14 +51,14 @@ export const ProjectList: FC = () => {
|
||||
? new Date(project.meta.lastModifiedAt).toLocaleDateString()
|
||||
: ""}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground font-mono truncate">
|
||||
Workspace: {project.meta.projectPath ?? "unknown"}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Messages: {project.meta.sessionCount}
|
||||
</p>
|
||||
</CardContent>
|
||||
<CardContent className="pt-0">
|
||||
<Button asChild className="w-full">
|
||||
<Link href={`/projects/${encodeURIComponent(project.id)}`}>
|
||||
View Conversations
|
||||
View Sessions
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
|
||||
@@ -7,7 +7,7 @@ export default async function ProjectsPage() {
|
||||
<header className="mb-8">
|
||||
<h1 className="text-3xl font-bold mb-2 flex items-center gap-2">
|
||||
<HistoryIcon className="w-8 h-8" />
|
||||
Claude Code History Viewer
|
||||
Claude Code Viewer
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Browse your Claude Code conversation history and project interactions
|
||||
|
||||
143
src/components/ui/dialog.tsx
Normal file
143
src/components/ui/dialog.tsx
Normal file
@@ -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<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
data-slot="dialog-close"
|
||||
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
};
|
||||
@@ -20,7 +20,7 @@ export const getProjects = async (): Promise<{ projects: Project[] }> => {
|
||||
claudeProjectPath: fullPath,
|
||||
meta: await getProjectMeta(fullPath),
|
||||
};
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -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<string, ParsedCommand | null>();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user