mirror of
https://github.com/aljazceru/claude-code-viewer.git
synced 2026-01-03 13:44:23 +01:00
feat: add filter option only having user messages
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
"@hono/auth-js": "^1.1.0",
|
||||
"@hono/zod-validator": "^0.7.2",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
@@ -31,6 +32,7 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"hono": "^4.9.5",
|
||||
"jotai": "^2.13.1",
|
||||
"lucide-react": "^0.542.0",
|
||||
"next": "15.5.2",
|
||||
"react": "^19.1.1",
|
||||
|
||||
73
pnpm-lock.yaml
generated
73
pnpm-lock.yaml
generated
@@ -20,6 +20,9 @@ importers:
|
||||
'@radix-ui/react-avatar':
|
||||
specifier: ^1.1.10
|
||||
version: 1.1.10(@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-checkbox':
|
||||
specifier: ^1.3.3
|
||||
version: 1.3.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-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)
|
||||
@@ -44,6 +47,9 @@ importers:
|
||||
hono:
|
||||
specifier: ^4.9.5
|
||||
version: 4.9.5
|
||||
jotai:
|
||||
specifier: ^2.13.1
|
||||
version: 2.13.1(@types/react@19.1.12)(react@19.1.1)
|
||||
lucide-react:
|
||||
specifier: ^0.542.0
|
||||
version: 0.542.0(react@19.1.1)
|
||||
@@ -611,6 +617,19 @@ packages:
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-checkbox@1.3.3':
|
||||
resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==}
|
||||
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-collapsible@1.1.12':
|
||||
resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==}
|
||||
peerDependencies:
|
||||
@@ -827,6 +846,15 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-previous@1.1.1':
|
||||
resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-rect@1.1.1':
|
||||
resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==}
|
||||
peerDependencies:
|
||||
@@ -1403,6 +1431,24 @@ packages:
|
||||
jose@6.1.0:
|
||||
resolution: {integrity: sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==}
|
||||
|
||||
jotai@2.13.1:
|
||||
resolution: {integrity: sha512-cRsw6kFeGC9Z/D3egVKrTXRweycZ4z/k7i2MrfCzPYsL9SIWcPXTyqv258/+Ay8VUEcihNiE/coBLE6Kic6b8A==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
peerDependencies:
|
||||
'@babel/core': '>=7.0.0'
|
||||
'@babel/template': '>=7.0.0'
|
||||
'@types/react': '>=17.0.0'
|
||||
react: '>=17.0.0'
|
||||
peerDependenciesMeta:
|
||||
'@babel/core':
|
||||
optional: true
|
||||
'@babel/template':
|
||||
optional: true
|
||||
'@types/react':
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
|
||||
js-tokens@9.0.1:
|
||||
resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
|
||||
|
||||
@@ -2456,6 +2502,22 @@ snapshots:
|
||||
'@types/react': 19.1.12
|
||||
'@types/react-dom': 19.1.9(@types/react@19.1.12)
|
||||
|
||||
'@radix-ui/react-checkbox@1.3.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)':
|
||||
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-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-use-controllable-state': 1.2.2(@types/react@19.1.12)(react@19.1.1)
|
||||
'@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.12)(react@19.1.1)
|
||||
'@radix-ui/react-use-size': 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-collapsible@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)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.3
|
||||
@@ -2655,6 +2717,12 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.1.12
|
||||
|
||||
'@radix-ui/react-use-previous@1.1.1(@types/react@19.1.12)(react@19.1.1)':
|
||||
dependencies:
|
||||
react: 19.1.1
|
||||
optionalDependencies:
|
||||
'@types/react': 19.1.12
|
||||
|
||||
'@radix-ui/react-use-rect@1.1.1(@types/react@19.1.12)(react@19.1.1)':
|
||||
dependencies:
|
||||
'@radix-ui/rect': 1.1.1
|
||||
@@ -3171,6 +3239,11 @@ snapshots:
|
||||
|
||||
jose@6.1.0: {}
|
||||
|
||||
jotai@2.13.1(@types/react@19.1.12)(react@19.1.1):
|
||||
optionalDependencies:
|
||||
'@types/react': 19.1.12
|
||||
react: 19.1.1
|
||||
|
||||
js-tokens@9.0.1: {}
|
||||
|
||||
json-parse-even-better-errors@4.0.0: {}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useAtom } from "jotai";
|
||||
import { ArrowLeftIcon, FolderIcon, MessageSquareIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useId } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@@ -10,14 +12,24 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { hideSessionsWithoutUserMessagesAtom } from "../store/filterAtoms";
|
||||
import { pagesPath } from "../../../../lib/$path";
|
||||
import { useProject } from "../hooks/useProject";
|
||||
import { firstCommandToTitle } from "../services/firstCommandToTitle";
|
||||
|
||||
export const ProjectPageContent = ({ projectId }: { projectId: string }) => {
|
||||
const checkboxId = useId();
|
||||
const {
|
||||
data: { project, sessions },
|
||||
} = useProject(projectId);
|
||||
const [hideSessionsWithoutUserMessages, setHideSessionsWithoutUserMessages] =
|
||||
useAtom(hideSessionsWithoutUserMessagesAtom);
|
||||
|
||||
// Apply filtering
|
||||
const filteredSessions = hideSessionsWithoutUserMessages
|
||||
? sessions.filter((session) => session.meta.firstCommand !== null)
|
||||
: sessions;
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
@@ -47,10 +59,36 @@ export const ProjectPageContent = ({ projectId }: { projectId: string }) => {
|
||||
<section>
|
||||
<h2 className="text-xl font-semibold mb-4">
|
||||
Conversation Sessions{" "}
|
||||
{project.meta.sessionCount ? `(${project.meta.sessionCount})` : ""}
|
||||
{filteredSessions.length > 0 ? `(${filteredSessions.length})` : ""}
|
||||
{hideSessionsWithoutUserMessages &&
|
||||
filteredSessions.length !== sessions.length && (
|
||||
<span className="text-sm text-muted-foreground ml-2">
|
||||
of {sessions.length} total
|
||||
</span>
|
||||
)}
|
||||
</h2>
|
||||
|
||||
{sessions.length === 0 ? (
|
||||
{/* Filter Controls */}
|
||||
<div className="mb-6 p-4 bg-muted/50 rounded-lg">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={checkboxId}
|
||||
checked={hideSessionsWithoutUserMessages}
|
||||
onCheckedChange={setHideSessionsWithoutUserMessages}
|
||||
/>
|
||||
<label
|
||||
htmlFor={checkboxId}
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
Hide sessions without user messages
|
||||
</label>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1 ml-6">
|
||||
Only show sessions that contain user commands or messages
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{filteredSessions.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col items-center justify-center py-12">
|
||||
<MessageSquareIcon className="w-12 h-12 text-muted-foreground mb-4" />
|
||||
@@ -64,7 +102,7 @@ export const ProjectPageContent = ({ projectId }: { projectId: string }) => {
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid gap-4 md:grid-cols-1 lg:grid-cols-1">
|
||||
{sessions.map((session) => (
|
||||
{filteredSessions.map((session) => (
|
||||
<Card
|
||||
key={session.id}
|
||||
className="hover:shadow-md transition-shadow"
|
||||
@@ -89,7 +127,7 @@ export const ProjectPageContent = ({ projectId }: { projectId: string }) => {
|
||||
Last modified:{" "}
|
||||
{session.meta.lastModifiedAt
|
||||
? new Date(
|
||||
session.meta.lastModifiedAt,
|
||||
session.meta.lastModifiedAt
|
||||
).toLocaleDateString()
|
||||
: ""}
|
||||
</p>
|
||||
@@ -101,7 +139,7 @@ export const ProjectPageContent = ({ projectId }: { projectId: string }) => {
|
||||
<Button asChild className="w-full">
|
||||
<Link
|
||||
href={`/projects/${projectId}/sessions/${encodeURIComponent(
|
||||
session.id,
|
||||
session.id
|
||||
)}`}
|
||||
>
|
||||
View Session
|
||||
|
||||
@@ -35,9 +35,9 @@ export const ConversationItem: FC<{
|
||||
);
|
||||
}
|
||||
|
||||
// sidechain = サブタスクのこと
|
||||
if (conversation.isSidechain) {
|
||||
// sidechain = サブタスクのこと
|
||||
// 別途ツール呼び出しの方で描画可能にするのでここでは表示しない
|
||||
// Root 以外はモーダルで中身を表示するのでここでは描画しない
|
||||
if (!isRootSidechain(conversation)) {
|
||||
return null;
|
||||
}
|
||||
@@ -46,7 +46,7 @@ export const ConversationItem: FC<{
|
||||
<SidechainConversationModal
|
||||
conversation={conversation}
|
||||
sidechainConversations={getSidechainConversations(
|
||||
conversation.uuid,
|
||||
conversation.uuid
|
||||
).map((original) => {
|
||||
if (original.type === "summary") return original;
|
||||
return {
|
||||
|
||||
24
src/app/projects/[projectId]/store/filterAtoms.ts
Normal file
24
src/app/projects/[projectId]/store/filterAtoms.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { atom } from "jotai";
|
||||
import { atomWithStorage } from "jotai/utils";
|
||||
|
||||
export type SessionFilterOptions = {
|
||||
hideSessionsWithoutUserMessages: boolean;
|
||||
};
|
||||
|
||||
export const sessionFilterAtom = atomWithStorage<SessionFilterOptions>(
|
||||
"session-filters",
|
||||
{
|
||||
hideSessionsWithoutUserMessages: true,
|
||||
}
|
||||
);
|
||||
|
||||
export const hideSessionsWithoutUserMessagesAtom = atom(
|
||||
(get) => get(sessionFilterAtom).hideSessionsWithoutUserMessages,
|
||||
(get, set, newValue: boolean) => {
|
||||
const currentFilters = get(sessionFilterAtom);
|
||||
set(sessionFilterAtom, {
|
||||
...currentFilters,
|
||||
hideSessionsWithoutUserMessages: newValue,
|
||||
});
|
||||
}
|
||||
);
|
||||
32
src/components/ui/checkbox.tsx
Normal file
32
src/components/ui/checkbox.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import type * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Checkbox({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="flex items-center justify-center text-current transition-none"
|
||||
>
|
||||
<CheckIcon className="size-3.5" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export { Checkbox };
|
||||
@@ -6,11 +6,18 @@ import type { SessionMeta } from "../types";
|
||||
|
||||
const firstCommandCache = new Map<string, ParsedCommand | null>();
|
||||
|
||||
const ignoreCommands = ["/clear", "/login", "/logout", "/exit"];
|
||||
const ignoreCommands = [
|
||||
"/clear",
|
||||
"/login",
|
||||
"/logout",
|
||||
"/exit",
|
||||
"/mcp",
|
||||
"/memory",
|
||||
];
|
||||
|
||||
const getFirstCommand = (
|
||||
jsonlFilePath: string,
|
||||
lines: string[],
|
||||
lines: string[]
|
||||
): ParsedCommand | null => {
|
||||
const cached = firstCommandCache.get(jsonlFilePath);
|
||||
if (cached !== undefined) {
|
||||
@@ -30,14 +37,14 @@ const getFirstCommand = (
|
||||
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;
|
||||
})();
|
||||
? 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;
|
||||
@@ -77,7 +84,7 @@ const getFirstCommand = (
|
||||
};
|
||||
|
||||
export const getSessionMeta = async (
|
||||
jsonlFilePath: string,
|
||||
jsonlFilePath: string
|
||||
): Promise<SessionMeta> => {
|
||||
const stats = statSync(jsonlFilePath);
|
||||
const lastModifiedUnixTime = stats.ctime.getTime();
|
||||
|
||||
Reference in New Issue
Block a user