mirror of
https://github.com/aljazceru/claude-code-viewer.git
synced 2026-01-04 22:24:22 +01:00
feat: add support for file history snapshots in conversation components
- Introduced a new `FileHistorySnapshotConversationContent` component to handle rendering of file history snapshots. - Updated `ConversationItem` and `ConversationList` to accommodate the new conversation type. - Modified the conversation schema to include `FileHistorySnapshotEntrySchema` and related types. - Enhanced the `useSidechain` hook to filter out file history snapshots from sidechain conversations. - Adjusted the `SidechainConversationModal` to correctly handle the new conversation type.
This commit is contained in:
@@ -3,6 +3,7 @@ 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 { FileHistorySnapshotConversationContent } from "./FileHistorySnapshotConversationContent";
|
||||
import { MetaConversationContent } from "./MetaConversationContent";
|
||||
import { SummaryConversationContent } from "./SummaryConversationContent";
|
||||
import { SystemConversationContent } from "./SystemConversationContent";
|
||||
@@ -35,6 +36,12 @@ export const ConversationItem: FC<{
|
||||
);
|
||||
}
|
||||
|
||||
if (conversation.type === "file-history-snapshot") {
|
||||
return (
|
||||
<FileHistorySnapshotConversationContent conversation={conversation} />
|
||||
);
|
||||
}
|
||||
|
||||
// sidechain = サブタスクのこと
|
||||
if (conversation.isSidechain) {
|
||||
// Root 以外はモーダルで中身を表示するのでここでは描画しない
|
||||
|
||||
@@ -31,6 +31,11 @@ const getConversationKey = (conversation: Conversation) => {
|
||||
return `summary_${conversation.leafUuid}`;
|
||||
}
|
||||
|
||||
if (conversation.type === "file-history-snapshot") {
|
||||
return `file-history-snapshot_${conversation.messageId}`;
|
||||
}
|
||||
|
||||
conversation satisfies never;
|
||||
throw new Error(`Unknown conversation type: ${conversation}`);
|
||||
};
|
||||
|
||||
@@ -132,7 +137,9 @@ export const ConversationList: FC<ConversationListProps> = ({
|
||||
);
|
||||
|
||||
const isSidechain =
|
||||
conversation.type !== "summary" && conversation.isSidechain;
|
||||
conversation.type !== "summary" &&
|
||||
conversation.type !== "file-history-snapshot" &&
|
||||
conversation.isSidechain;
|
||||
|
||||
return [
|
||||
<li
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import type { FileHistorySnapshotEntry } from "@/lib/conversation-schema/entry/FileHIstorySnapshotEntrySchema";
|
||||
|
||||
export const FileHistorySnapshotConversationContent: FC<{
|
||||
conversation: FileHistorySnapshotEntry;
|
||||
}> = ({ conversation }) => {
|
||||
const fileCount = Object.keys(
|
||||
conversation.snapshot.trackedFileBackups,
|
||||
).length;
|
||||
|
||||
return (
|
||||
<Collapsible>
|
||||
<CollapsibleTrigger asChild>
|
||||
<div className="flex items-center justify-between cursor-pointer hover:bg-muted/50 rounded p-2 -mx-2">
|
||||
<h4 className="text-xs font-medium text-muted-foreground">
|
||||
File History Snapshot {fileCount > 0 && `(${fileCount} files)`}
|
||||
</h4>
|
||||
<ChevronDown className="h-3 w-3 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="bg-background rounded border p-3 mt-2">
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs">
|
||||
<span className="text-muted-foreground">Timestamp: </span>
|
||||
<span>
|
||||
{new Date(conversation.snapshot.timestamp).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs">
|
||||
<span className="text-muted-foreground">Message ID: </span>
|
||||
<span className="font-mono">{conversation.messageId}</span>
|
||||
</div>
|
||||
<div className="text-xs">
|
||||
<span className="text-muted-foreground">
|
||||
Is Snapshot Update:{" "}
|
||||
</span>
|
||||
<span>{conversation.isSnapshotUpdate ? "Yes" : "No"}</span>
|
||||
</div>
|
||||
{fileCount > 0 && (
|
||||
<div className="text-xs">
|
||||
<div className="text-muted-foreground mb-1">Tracked Files:</div>
|
||||
<ul className="list-disc list-inside space-y-1">
|
||||
{Object.keys(conversation.snapshot.trackedFileBackups).map(
|
||||
(filePath) => (
|
||||
<li
|
||||
key={filePath}
|
||||
className="font-mono text-xs break-all"
|
||||
>
|
||||
{filePath}
|
||||
</li>
|
||||
),
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
};
|
||||
@@ -11,12 +11,15 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import type { Conversation } from "@/lib/conversation-schema";
|
||||
import type {
|
||||
Conversation,
|
||||
SidechainConversation,
|
||||
} from "@/lib/conversation-schema";
|
||||
import type { ToolResultContent } from "@/lib/conversation-schema/content/ToolResultContentSchema";
|
||||
import { ConversationList } from "../conversationList/ConversationList";
|
||||
|
||||
type SidechainConversationModalProps = {
|
||||
conversation: Conversation;
|
||||
conversation: SidechainConversation;
|
||||
sidechainConversations: Conversation[];
|
||||
getToolResult: (toolUseId: string) => ToolResultContent | undefined;
|
||||
};
|
||||
@@ -25,7 +28,10 @@ const sidechainTitle = (conversations: Conversation[]): string => {
|
||||
const firstConversation = conversations.at(0);
|
||||
|
||||
const defaultTitle = `${conversations.length} conversations (${
|
||||
firstConversation?.type !== "summary" ? firstConversation?.uuid : ""
|
||||
firstConversation?.type !== "summary" &&
|
||||
firstConversation?.type !== "file-history-snapshot"
|
||||
? firstConversation?.uuid
|
||||
: ""
|
||||
})`;
|
||||
|
||||
if (!firstConversation) {
|
||||
@@ -57,8 +63,7 @@ export const SidechainConversationModal: FC<
|
||||
> = ({ conversation, sidechainConversations, getToolResult }) => {
|
||||
const title = sidechainTitle(sidechainConversations);
|
||||
|
||||
const rootUuid =
|
||||
conversation.type !== "summary" ? conversation.uuid : conversation.leafUuid;
|
||||
const rootUuid = conversation.uuid;
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import type { Conversation } from "@/lib/conversation-schema";
|
||||
import type {
|
||||
Conversation,
|
||||
SidechainConversation,
|
||||
} from "@/lib/conversation-schema";
|
||||
|
||||
export const useSidechain = (conversations: Conversation[]) => {
|
||||
const sidechainConversations = conversations.filter(
|
||||
(conv) => conv.type !== "summary" && conv.type !== "file-history-snapshot",
|
||||
);
|
||||
|
||||
const conversationMap = useMemo(() => {
|
||||
return new Map<string, Conversation>(
|
||||
conversations
|
||||
.filter((conv) => conv.type !== "summary")
|
||||
.map((conv) => [conv.uuid, conv] as const),
|
||||
return new Map<string, SidechainConversation>(
|
||||
sidechainConversations.map((conv) => [conv.uuid, conv] as const),
|
||||
);
|
||||
}, [conversations]);
|
||||
}, [sidechainConversations]);
|
||||
|
||||
const getRootConversationRecursive = useCallback(
|
||||
(conversation: Conversation): Conversation => {
|
||||
if (conversation.type === "summary") {
|
||||
return conversation;
|
||||
}
|
||||
|
||||
(conversation: SidechainConversation): SidechainConversation => {
|
||||
if (conversation.parentUuid === null) {
|
||||
return conversation;
|
||||
}
|
||||
@@ -31,20 +32,15 @@ export const useSidechain = (conversations: Conversation[]) => {
|
||||
);
|
||||
|
||||
const sidechainConversationGroups = useMemo(() => {
|
||||
const filtered = conversations
|
||||
.filter((conv) => conv.type !== "summary")
|
||||
.filter((conv) => conv.isSidechain === true);
|
||||
const filtered = sidechainConversations.filter(
|
||||
(conv) => conv.isSidechain === true,
|
||||
);
|
||||
|
||||
const groups = new Map<string, Conversation[]>();
|
||||
const groups = new Map<string, SidechainConversation[]>();
|
||||
|
||||
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 {
|
||||
@@ -53,11 +49,14 @@ export const useSidechain = (conversations: Conversation[]) => {
|
||||
}
|
||||
|
||||
return groups;
|
||||
}, [conversations, getRootConversationRecursive]);
|
||||
}, [sidechainConversations, getRootConversationRecursive]);
|
||||
|
||||
const isRootSidechain = useCallback(
|
||||
(conversation: Conversation) => {
|
||||
if (conversation.type === "summary") {
|
||||
if (
|
||||
conversation.type === "summary" ||
|
||||
conversation.type === "file-history-snapshot"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,3 +13,5 @@ export const AssistantEntrySchema = BaseEntrySchema.extend({
|
||||
requestId: z.string().optional(),
|
||||
isApiErrorMessage: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type AssistantEntry = z.infer<typeof AssistantEntrySchema>;
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const FileHistorySnapshotEntrySchema = z.object({
|
||||
// discriminator
|
||||
type: z.literal("file-history-snapshot"),
|
||||
|
||||
// required
|
||||
messageId: z.string(),
|
||||
snapshot: z.object({
|
||||
messageId: z.string(),
|
||||
trackedFileBackups: z.record(z.string(), z.unknown()),
|
||||
timestamp: z.string(),
|
||||
}),
|
||||
isSnapshotUpdate: z.boolean(),
|
||||
});
|
||||
|
||||
export type FileHistorySnapshotEntry = z.infer<
|
||||
typeof FileHistorySnapshotEntrySchema
|
||||
>;
|
||||
@@ -5,3 +5,5 @@ export const SummaryEntrySchema = z.object({
|
||||
summary: z.string(),
|
||||
leafUuid: z.string().uuid(),
|
||||
});
|
||||
|
||||
export type SummaryEntry = z.infer<typeof SummaryEntrySchema>;
|
||||
|
||||
@@ -10,3 +10,5 @@ export const SystemEntrySchema = BaseEntrySchema.extend({
|
||||
toolUseID: z.string(),
|
||||
level: z.enum(["info"]),
|
||||
});
|
||||
|
||||
export type SystemEntry = z.infer<typeof SystemEntrySchema>;
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import { z } from "zod";
|
||||
import { AssistantEntrySchema } from "./entry/AssistantEntrySchema";
|
||||
import {
|
||||
type AssistantEntry,
|
||||
AssistantEntrySchema,
|
||||
} from "./entry/AssistantEntrySchema";
|
||||
import { FileHistorySnapshotEntrySchema } from "./entry/FileHIstorySnapshotEntrySchema";
|
||||
import { SummaryEntrySchema } from "./entry/SummaryEntrySchema";
|
||||
import { SystemEntrySchema } from "./entry/SystemEntrySchema";
|
||||
import { UserEntrySchema } from "./entry/UserEntrySchema";
|
||||
import { type SystemEntry, SystemEntrySchema } from "./entry/SystemEntrySchema";
|
||||
import { type UserEntry, UserEntrySchema } from "./entry/UserEntrySchema";
|
||||
|
||||
export const ConversationSchema = z.union([
|
||||
UserEntrySchema,
|
||||
AssistantEntrySchema,
|
||||
SummaryEntrySchema,
|
||||
SystemEntrySchema,
|
||||
FileHistorySnapshotEntrySchema,
|
||||
]);
|
||||
|
||||
export type Conversation = z.infer<typeof ConversationSchema>;
|
||||
export type SidechainConversation = UserEntry | AssistantEntry | SystemEntry;
|
||||
|
||||
@@ -64,7 +64,7 @@ const LayerImpl = Effect.gen(function* () {
|
||||
response: {
|
||||
globalCommands: globalCommands,
|
||||
projectCommands: projectCommands,
|
||||
defaultCommands: ["init", "compact"],
|
||||
defaultCommands: ["init", "compact", "security-review", "review"],
|
||||
},
|
||||
status: 200,
|
||||
} as const satisfies ControllerResponse;
|
||||
|
||||
@@ -39,7 +39,8 @@ const LayerImpl = Effect.gen(function* () {
|
||||
if (
|
||||
conversation === undefined ||
|
||||
conversation.type === "summary" ||
|
||||
conversation.type === "x-error"
|
||||
conversation.type === "x-error" ||
|
||||
conversation.type === "file-history-snapshot"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user