feat: add unifySameTitleSession option for unify resume messages

This commit is contained in:
d-kimsuon
2025-09-02 20:55:59 +09:00
parent 80bd51e592
commit 4c7219997f
7 changed files with 120 additions and 16 deletions

View File

@@ -36,7 +36,7 @@ export const ProjectPageContent = ({ projectId }: { projectId: string }) => {
void queryClient.invalidateQueries({
queryKey: projectQueryConfig(projectId).queryKey,
});
}, [config.hideNoUserMessageSession]);
}, [config.hideNoUserMessageSession, config.unifySameTitleSession]);
return (
<div className="container mx-auto px-4 py-8">
@@ -78,7 +78,7 @@ export const ProjectPageContent = ({ projectId }: { projectId: string }) => {
</h2>
{/* Filter Controls */}
<div className="mb-6 p-4 bg-muted/50 rounded-lg">
<div className="mb-6 p-4 bg-muted/50 rounded-lg space-y-4">
<div className="flex items-center space-x-2">
<Checkbox
id={checkboxId}
@@ -103,6 +103,32 @@ export const ProjectPageContent = ({ projectId }: { projectId: string }) => {
<p className="text-xs text-muted-foreground mt-1 ml-6">
Only show sessions that contain user commands or messages
</p>
<div className="flex items-center space-x-2">
<Checkbox
id={`${checkboxId}-unify`}
checked={config?.unifySameTitleSession}
onCheckedChange={async () => {
updateConfig({
...config,
unifySameTitleSession: !config?.unifySameTitleSession,
});
await queryClient.invalidateQueries({
queryKey: configQueryConfig.queryKey,
});
}}
/>
<label
htmlFor={`${checkboxId}-unify`}
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Unify sessions with same title
</label>
</div>
<p className="text-xs text-muted-foreground mt-1 ml-6">
Show only the latest session when multiple sessions have the same
title
</p>
</div>
{sessions.length === 0 ? (

View File

@@ -62,7 +62,10 @@ export const ConversationItem: FC<{
if (conversation.type === "user") {
const userConversationJsx =
typeof conversation.message.content === "string" ? (
<UserConversationContent content={conversation.message.content} />
<UserConversationContent
content={conversation.message.content}
id={`message-${conversation.uuid}`}
/>
) : (
<ul className="w-full" id={`message-${conversation.uuid}`}>
{conversation.message.content.map((content) => (

View File

@@ -14,19 +14,23 @@ import { UserTextContent } from "./UserTextContent";
export const UserConversationContent: FC<{
content: UserMessageContent;
}> = ({ content }) => {
id?: string;
}> = ({ content, id }) => {
if (typeof content === "string") {
return <UserTextContent text={content} />;
return <UserTextContent text={content} id={id} />;
}
if (content.type === "text") {
return <UserTextContent text={content.text} />;
return <UserTextContent text={content.text} id={id} />;
}
if (content.type === "image") {
if (content.source.type === "base64") {
return (
<Card className="border-purple-200 bg-purple-50/50 dark:border-purple-800 dark:bg-purple-950/20">
<Card
className="border-purple-200 bg-purple-50/50 dark:border-purple-800 dark:bg-purple-950/20"
id={id}
>
<CardHeader>
<div className="flex items-center gap-2">
<ImageIcon className="h-4 w-4 text-purple-600 dark:text-purple-400" />
@@ -56,7 +60,10 @@ export const UserConversationContent: FC<{
}
return (
<Card className="border-red-200 bg-red-50/50 dark:border-red-800 dark:bg-red-950/20">
<Card
className="border-red-200 bg-red-50/50 dark:border-red-800 dark:bg-red-950/20"
id={id}
>
<CardHeader>
<div className="flex items-center gap-2">
<AlertCircle className="h-4 w-4 text-red-600 dark:text-red-400" />

View File

@@ -6,12 +6,18 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { parseCommandXml } from "../../../../../../../server/service/parseCommandXml";
import { MarkdownContent } from "../../../../../../components/MarkdownContent";
export const UserTextContent: FC<{ text: string }> = ({ text }) => {
export const UserTextContent: FC<{ text: string; id?: string }> = ({
text,
id,
}) => {
const parsed = parseCommandXml(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 mb-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"
id={id}
>
<CardHeader className="py-0 px-4">
<div className="flex items-center gap-2">
<Terminal className="h-4 w-4 text-green-600 dark:text-green-400" />

View File

@@ -2,6 +2,7 @@ import z from "zod";
export const configSchema = z.object({
hideNoUserMessageSession: z.boolean().optional().default(true),
unifySameTitleSession: z.boolean().optional().default(true),
});
export type Config = z.infer<typeof configSchema>;

View File

@@ -20,6 +20,7 @@ export const configMiddleware = createMiddleware<HonoContext>(
"ccv-config",
JSON.stringify({
hideNoUserMessageSession: true,
unifySameTitleSession: true,
}),
);
}

View File

@@ -52,14 +52,74 @@ export const routes = (app: HonoAppType) => {
const [{ project }, { sessions }] = await Promise.all([
getProject(projectId),
getSessions(projectId).then(({ sessions }) => ({
sessions: sessions.filter((session) => {
if (c.get("config").hideNoUserMessageSession) {
getSessions(projectId).then(({ sessions }) => {
let filteredSessions = sessions;
// Filter sessions based on hideNoUserMessageSession setting
if (c.get("config").hideNoUserMessageSession) {
filteredSessions = filteredSessions.filter((session) => {
return session.meta.firstCommand !== null;
});
}
// Unify sessions with same title if unifySameTitleSession is enabled
if (c.get("config").unifySameTitleSession) {
const sessionMap = new Map<
string,
(typeof filteredSessions)[0]
>();
for (const session of filteredSessions) {
// Generate title for comparison
const title =
session.meta.firstCommand !== null
? (() => {
const cmd = session.meta.firstCommand;
switch (cmd.kind) {
case "command":
return cmd.commandArgs === undefined
? cmd.commandName
: `${cmd.commandName} ${cmd.commandArgs}`;
case "local-command":
return cmd.stdout;
case "text":
return cmd.content;
default:
return session.id;
}
})()
: session.id;
const existingSession = sessionMap.get(title);
if (existingSession) {
// Keep the session with the latest modification date
if (
session.meta.lastModifiedAt &&
existingSession.meta.lastModifiedAt
) {
if (
new Date(session.meta.lastModifiedAt) >
new Date(existingSession.meta.lastModifiedAt)
) {
sessionMap.set(title, session);
}
} else if (
session.meta.lastModifiedAt &&
!existingSession.meta.lastModifiedAt
) {
sessionMap.set(title, session);
}
// If no modification dates, keep the existing one
} else {
sessionMap.set(title, session);
}
}
return true;
}),
})),
filteredSessions = Array.from(sessionMap.values());
}
return { sessions: filteredSessions };
}),
] as const);
return c.json({ project, sessions });