mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-06 16:31:12 +08:00
feat(hooks): add hook management and session timeline features
- Add hook quick templates component with configurable templates - Refactor NativeSessionPanel to use new SessionTimeline component - Add OpenCode session parser for parsing OpenCode CLI sessions - Enhance API with session-related endpoints - Add locale translations for hooks and native session features - Update hook commands and routes for better hook management
This commit is contained in:
@@ -6,25 +6,16 @@
|
||||
import * as React from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import {
|
||||
User,
|
||||
Bot,
|
||||
Brain,
|
||||
Wrench,
|
||||
Copy,
|
||||
Clock,
|
||||
Hash,
|
||||
FolderOpen,
|
||||
FileJson,
|
||||
Coins,
|
||||
ArrowDownUp,
|
||||
Archive,
|
||||
Loader2,
|
||||
AlertCircle,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -32,11 +23,7 @@ import {
|
||||
DialogTitle,
|
||||
} from '@/components/ui/Dialog';
|
||||
import { useNativeSession } from '@/hooks/useNativeSession';
|
||||
import type {
|
||||
NativeSessionTurn,
|
||||
NativeToolCall,
|
||||
NativeTokenInfo,
|
||||
} from '@/lib/api';
|
||||
import { SessionTimeline } from './SessionTimeline';
|
||||
|
||||
// ========== Types ==========
|
||||
|
||||
@@ -46,21 +33,6 @@ export interface NativeSessionPanelProps {
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
interface TurnCardProps {
|
||||
turn: NativeSessionTurn;
|
||||
isLatest: boolean;
|
||||
}
|
||||
|
||||
interface TokenDisplayProps {
|
||||
tokens: NativeTokenInfo;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface ToolCallItemProps {
|
||||
toolCall: NativeToolCall;
|
||||
index: number;
|
||||
}
|
||||
|
||||
// ========== Helpers ==========
|
||||
|
||||
/**
|
||||
@@ -76,16 +48,6 @@ function getToolVariant(tool: string): 'default' | 'secondary' | 'outline' | 'su
|
||||
return variants[tool?.toLowerCase()] || 'secondary';
|
||||
}
|
||||
|
||||
/**
|
||||
* Format token number with compact notation
|
||||
*/
|
||||
function formatTokenCount(count: number | undefined): string {
|
||||
if (count == null || count === 0) return '0';
|
||||
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
|
||||
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}K`;
|
||||
return count.toLocaleString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate a string to a max length with ellipsis
|
||||
*/
|
||||
@@ -106,173 +68,6 @@ async function copyToClipboard(text: string): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Sub-Components ==========
|
||||
|
||||
/**
|
||||
* TokenDisplay - Compact token info line
|
||||
*/
|
||||
function TokenDisplay({ tokens, className }: TokenDisplayProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center gap-3 text-xs text-muted-foreground', className)}>
|
||||
<span className="flex items-center gap-1" title={formatMessage({ id: 'nativeSession.tokens.total', defaultMessage: 'Total tokens' })}>
|
||||
<Coins className="h-3 w-3" />
|
||||
{formatTokenCount(tokens.total)}
|
||||
</span>
|
||||
{tokens.input != null && (
|
||||
<span className="flex items-center gap-1" title={formatMessage({ id: 'nativeSession.tokens.input', defaultMessage: 'Input tokens' })}>
|
||||
<ArrowDownUp className="h-3 w-3" />
|
||||
{formatTokenCount(tokens.input)}
|
||||
</span>
|
||||
)}
|
||||
{tokens.output != null && (
|
||||
<span title={formatMessage({ id: 'nativeSession.tokens.output', defaultMessage: 'Output tokens' })}>
|
||||
out: {formatTokenCount(tokens.output)}
|
||||
</span>
|
||||
)}
|
||||
{tokens.cached != null && tokens.cached > 0 && (
|
||||
<span className="flex items-center gap-1" title={formatMessage({ id: 'nativeSession.tokens.cached', defaultMessage: 'Cached tokens' })}>
|
||||
<Archive className="h-3 w-3" />
|
||||
{formatTokenCount(tokens.cached)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* ToolCallItem - Single tool call display with collapsible details
|
||||
*/
|
||||
function ToolCallItem({ toolCall, index }: ToolCallItemProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
<details className="group/tool border border-border/50 rounded-md overflow-hidden">
|
||||
<summary className="flex items-center gap-2 px-3 py-2 text-xs cursor-pointer hover:bg-muted/50 select-none">
|
||||
<Wrench className="h-3 w-3 text-muted-foreground flex-shrink-0" />
|
||||
<span className="font-mono font-medium">{toolCall.name}</span>
|
||||
<span className="text-muted-foreground">#{index + 1}</span>
|
||||
</summary>
|
||||
<div className="border-t border-border/50 divide-y divide-border/50">
|
||||
{toolCall.arguments && (
|
||||
<div className="p-3">
|
||||
<p className="text-xs font-medium text-muted-foreground mb-1">{formatMessage({ id: 'nativeSession.toolCall.input' })}</p>
|
||||
<pre className="p-2 bg-muted/30 rounded text-xs whitespace-pre-wrap overflow-x-auto font-mono leading-relaxed max-h-60 overflow-y-auto">
|
||||
{toolCall.arguments}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{toolCall.output && (
|
||||
<div className="p-3">
|
||||
<p className="text-xs font-medium text-muted-foreground mb-1">{formatMessage({ id: 'nativeSession.toolCall.output' })}</p>
|
||||
<pre className="p-2 bg-muted/30 rounded text-xs whitespace-pre-wrap overflow-x-auto font-mono leading-relaxed max-h-60 overflow-y-auto">
|
||||
{toolCall.output}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* TurnCard - Single conversation turn
|
||||
*/
|
||||
function TurnCard({ turn, isLatest }: TurnCardProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
const isUser = turn.role === 'user';
|
||||
const RoleIcon = isUser ? User : Bot;
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={cn(
|
||||
'overflow-hidden transition-all',
|
||||
isUser
|
||||
? 'bg-muted/30'
|
||||
: 'bg-blue-500/5 dark:bg-blue-500/10',
|
||||
isLatest && 'ring-2 ring-primary/50 shadow-md'
|
||||
)}
|
||||
>
|
||||
{/* Turn Header */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-border/50">
|
||||
<div className="flex items-center gap-2">
|
||||
<RoleIcon
|
||||
className={cn(
|
||||
'h-4 w-4',
|
||||
isUser ? 'text-primary' : 'text-blue-500'
|
||||
)}
|
||||
/>
|
||||
<span className="font-semibold text-sm capitalize">{turn.role}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
#{turn.turnNumber}
|
||||
</span>
|
||||
{isLatest && (
|
||||
<Badge variant="default" className="text-xs h-5 px-1.5">
|
||||
{formatMessage({ id: 'nativeSession.turn.latest', defaultMessage: 'Latest' })}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-xs text-muted-foreground">
|
||||
{turn.timestamp && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="h-3 w-3" />
|
||||
{new Date(turn.timestamp).toLocaleTimeString()}
|
||||
</span>
|
||||
)}
|
||||
{turn.tokens && (
|
||||
<TokenDisplay tokens={turn.tokens} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Turn Content */}
|
||||
<div className="p-4 space-y-3">
|
||||
{turn.content && (
|
||||
<pre className="p-3 bg-background/50 rounded-lg text-sm whitespace-pre-wrap overflow-x-auto font-mono leading-relaxed max-h-96 overflow-y-auto">
|
||||
{turn.content}
|
||||
</pre>
|
||||
)}
|
||||
|
||||
{/* Thoughts Section */}
|
||||
{turn.thoughts && turn.thoughts.length > 0 && (
|
||||
<details className="group/thoughts">
|
||||
<summary className="flex items-center gap-2 text-sm cursor-pointer hover:text-foreground text-muted-foreground select-none py-1">
|
||||
<Brain className="h-4 w-4" />
|
||||
<span className="font-medium">
|
||||
{formatMessage({ id: 'nativeSession.turn.thoughts', defaultMessage: 'Thoughts' })}
|
||||
</span>
|
||||
<span className="text-xs">({turn.thoughts.length})</span>
|
||||
</summary>
|
||||
<ul className="mt-2 space-y-1 pl-6 text-sm text-muted-foreground list-disc">
|
||||
{turn.thoughts.map((thought, i) => (
|
||||
<li key={i} className="leading-relaxed">{thought}</li>
|
||||
))}
|
||||
</ul>
|
||||
</details>
|
||||
)}
|
||||
|
||||
{/* Tool Calls Section */}
|
||||
{turn.toolCalls && turn.toolCalls.length > 0 && (
|
||||
<details className="group/calls">
|
||||
<summary className="flex items-center gap-2 text-sm cursor-pointer hover:text-foreground text-muted-foreground select-none py-1">
|
||||
<Wrench className="h-4 w-4" />
|
||||
<span className="font-medium">
|
||||
{formatMessage({ id: 'nativeSession.turn.toolCalls', defaultMessage: 'Tool Calls' })}
|
||||
</span>
|
||||
<span className="text-xs">({turn.toolCalls.length})</span>
|
||||
</summary>
|
||||
<div className="mt-2 space-y-2">
|
||||
{turn.toolCalls.map((tc, i) => (
|
||||
<ToolCallItem key={i} toolCall={tc} index={i} />
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// ========== Main Component ==========
|
||||
|
||||
/**
|
||||
@@ -350,17 +145,7 @@ export function NativeSessionPanel({
|
||||
)}
|
||||
</DialogHeader>
|
||||
|
||||
{/* Token Summary Bar */}
|
||||
{session?.totalTokens && (
|
||||
<div className="flex items-center gap-4 px-6 py-2.5 border-b bg-muted/30 shrink-0">
|
||||
<span className="text-xs font-medium text-foreground">
|
||||
{formatMessage({ id: 'nativeSession.tokenSummary', defaultMessage: 'Total Tokens' })}
|
||||
</span>
|
||||
<TokenDisplay tokens={session.totalTokens} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content Area */}
|
||||
{/* Content Area with SessionTimeline */}
|
||||
{isLoading ? (
|
||||
<div className="flex-1 flex items-center justify-center py-16">
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
@@ -375,24 +160,9 @@ export function NativeSessionPanel({
|
||||
<span>{formatMessage({ id: 'nativeSession.error', defaultMessage: 'Failed to load session' })}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : session && session.turns.length > 0 ? (
|
||||
) : session ? (
|
||||
<div className="flex-1 overflow-y-auto px-6 py-4">
|
||||
<div className="space-y-4">
|
||||
{session.turns.map((turn, idx) => (
|
||||
<React.Fragment key={turn.turnNumber}>
|
||||
<TurnCard
|
||||
turn={turn}
|
||||
isLatest={idx === session.turns.length - 1}
|
||||
/>
|
||||
{/* Connector line between turns */}
|
||||
{idx < session.turns.length - 1 && (
|
||||
<div className="flex justify-center" aria-hidden="true">
|
||||
<div className="w-px h-4 bg-border" />
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
<SessionTimeline session={session} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 flex items-center justify-center py-16 text-muted-foreground">
|
||||
|
||||
Reference in New Issue
Block a user