mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
feat: upgrade to v7.0.0 with major new features including Team Architecture v2 and Queue Scheduler
- Updated version in README and package.json to v7.0.0 - Added new features in WORKFLOW_GUIDE and WORKFLOW_GUIDE_CN - Introduced session lifecycle commands for managing workflow sessions - Enhanced NativeSessionPanel to support loading sessions by path or execution ID - Created useNativeSessionByPath hook for fetching session content by file path - Improved session metadata structure in API definitions - Increased stale and garbage collection times for session hooks - Refactored HistoryPage to utilize new session handling logic
This commit is contained in:
@@ -23,13 +23,18 @@ import {
|
||||
DialogTitle,
|
||||
} from '@/components/ui/Dialog';
|
||||
import { useNativeSession } from '@/hooks/useNativeSession';
|
||||
import { useNativeSessionByPath } from '@/hooks/useNativeSessionByPath';
|
||||
import { SessionTimeline } from './SessionTimeline';
|
||||
import { getToolVariant } from '@/lib/cli-tool-theme';
|
||||
import type { NativeSessionListItem } from '@/lib/api';
|
||||
|
||||
// ========== Types ==========
|
||||
|
||||
export interface NativeSessionPanelProps {
|
||||
executionId: string;
|
||||
/** Legacy: CCW execution ID for lookup */
|
||||
executionId?: string;
|
||||
/** New: Session metadata with path for direct file loading */
|
||||
session?: NativeSessionListItem | null;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
@@ -63,14 +68,34 @@ async function copyToClipboard(text: string): Promise<boolean> {
|
||||
*
|
||||
* Shows session metadata, token summary, and all conversation turns
|
||||
* with thoughts and tool calls for Gemini/Codex/Qwen native sessions.
|
||||
*
|
||||
* Supports two modes:
|
||||
* - executionId: Look up session via CCW database
|
||||
* - session: Load session directly from file path
|
||||
*/
|
||||
export function NativeSessionPanel({
|
||||
executionId,
|
||||
session,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: NativeSessionPanelProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
const { data: session, isLoading, error } = useNativeSession(open ? executionId : null);
|
||||
|
||||
// Use appropriate hook based on what's provided
|
||||
// Priority: session (path-based) > executionId (lookup-based)
|
||||
const pathBasedResult = useNativeSessionByPath(
|
||||
open && session ? session.filePath : null,
|
||||
session?.tool
|
||||
);
|
||||
|
||||
const idBasedResult = useNativeSession(
|
||||
open && !session && executionId ? executionId : null
|
||||
);
|
||||
|
||||
// Determine which result to use
|
||||
const { data, isLoading, error } = session
|
||||
? pathBasedResult
|
||||
: idBasedResult;
|
||||
|
||||
const [copiedField, setCopiedField] = React.useState<string | null>(null);
|
||||
|
||||
@@ -91,44 +116,46 @@ export function NativeSessionPanel({
|
||||
<FileJson className="h-5 w-5" />
|
||||
{formatMessage({ id: 'nativeSession.title', defaultMessage: 'Native Session' })}
|
||||
</DialogTitle>
|
||||
{session && (
|
||||
{(data || session) && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={getToolVariant(session.tool)}>
|
||||
{session.tool.toUpperCase()}
|
||||
<Badge variant={getToolVariant(data?.tool || session?.tool || 'claude')}>
|
||||
{(data?.tool || session?.tool || 'unknown').toUpperCase()}
|
||||
</Badge>
|
||||
{session.model && (
|
||||
{data?.model && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{session.model}
|
||||
{data.model}
|
||||
</Badge>
|
||||
)}
|
||||
<span
|
||||
className="text-xs text-muted-foreground font-mono"
|
||||
title={session.sessionId}
|
||||
>
|
||||
{truncate(session.sessionId, 16)}
|
||||
</span>
|
||||
{(data?.sessionId || session?.sessionId) && (
|
||||
<span
|
||||
className="text-xs text-muted-foreground font-mono"
|
||||
title={data?.sessionId || session?.sessionId}
|
||||
>
|
||||
{truncate(data?.sessionId || session?.sessionId || '', 16)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{session && (
|
||||
{(data || session) && (
|
||||
<div className="flex flex-wrap items-center gap-4 text-xs text-muted-foreground mt-2">
|
||||
<span className="flex items-center gap-1" title={formatMessage({ id: 'nativeSession.meta.startTime', defaultMessage: 'Start time' })}>
|
||||
<Clock className="h-3 w-3" />
|
||||
{new Date(session.startTime).toLocaleString()}
|
||||
{new Date(data?.startTime || session?.createdAt || '').toLocaleString()}
|
||||
</span>
|
||||
{session.workingDir && (
|
||||
{(data?.workingDir || session?.projectHash) && (
|
||||
<span className="flex items-center gap-1" title={formatMessage({ id: 'nativeSession.meta.workingDir', defaultMessage: 'Working directory' })}>
|
||||
<FolderOpen className="h-3 w-3" />
|
||||
<span className="font-mono max-w-48 truncate">{session.workingDir}</span>
|
||||
<span className="font-mono max-w-48 truncate">{data?.workingDir || session?.projectHash}</span>
|
||||
</span>
|
||||
)}
|
||||
{session.projectHash && (
|
||||
{data?.projectHash && (
|
||||
<span className="flex items-center gap-1" title={formatMessage({ id: 'nativeSession.meta.projectHash', defaultMessage: 'Project hash' })}>
|
||||
<Hash className="h-3 w-3" />
|
||||
<span className="font-mono">{truncate(session.projectHash, 12)}</span>
|
||||
<span className="font-mono">{truncate(data.projectHash, 12)}</span>
|
||||
</span>
|
||||
)}
|
||||
<span>{session.turns.length} {formatMessage({ id: 'nativeSession.meta.turns', defaultMessage: 'turns' })}</span>
|
||||
{data && <span>{data.turns.length} {formatMessage({ id: 'nativeSession.meta.turns', defaultMessage: 'turns' })}</span>}
|
||||
</div>
|
||||
)}
|
||||
</DialogHeader>
|
||||
@@ -142,15 +169,18 @@ export function NativeSessionPanel({
|
||||
</div>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="flex-1 flex items-center justify-center py-16">
|
||||
<div className="flex-1 flex flex-col items-center justify-center py-16 gap-3">
|
||||
<div className="flex items-center gap-2 text-destructive">
|
||||
<AlertCircle className="h-5 w-5" />
|
||||
<span>{formatMessage({ id: 'nativeSession.error', defaultMessage: 'Failed to load session' })}</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'nativeSession.errorHint', defaultMessage: 'The session file may have been moved or deleted.' })}
|
||||
</p>
|
||||
</div>
|
||||
) : session ? (
|
||||
) : data ? (
|
||||
<div className="flex-1 overflow-y-auto px-6 py-4">
|
||||
<SessionTimeline session={session} />
|
||||
<SessionTimeline session={data} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 flex items-center justify-center py-16 text-muted-foreground">
|
||||
@@ -159,12 +189,12 @@ export function NativeSessionPanel({
|
||||
)}
|
||||
|
||||
{/* Footer Actions */}
|
||||
{session && (
|
||||
{data && (
|
||||
<div className="flex items-center gap-2 px-6 py-4 border-t bg-muted/30 shrink-0">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleCopy(session.sessionId, 'sessionId')}
|
||||
onClick={() => handleCopy(data.sessionId, 'sessionId')}
|
||||
className="h-8"
|
||||
>
|
||||
<Copy className="h-4 w-4 mr-2" />
|
||||
@@ -176,7 +206,7 @@ export function NativeSessionPanel({
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
const json = JSON.stringify(session, null, 2);
|
||||
const json = JSON.stringify(data, null, 2);
|
||||
handleCopy(json, 'json');
|
||||
}}
|
||||
className="h-8"
|
||||
|
||||
87
ccw/frontend/src/hooks/useNativeSessionByPath.ts
Normal file
87
ccw/frontend/src/hooks/useNativeSessionByPath.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
// ========================================
|
||||
// useNativeSessionByPath Hook
|
||||
// ========================================
|
||||
// TanStack Query hook for native CLI session content by file path
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
fetchNativeSessionWithOptions,
|
||||
type NativeSession,
|
||||
} from '../lib/api';
|
||||
|
||||
// ========== Query Keys ==========
|
||||
|
||||
export const nativeSessionPathKeys = {
|
||||
all: ['nativeSessionPath'] as const,
|
||||
details: () => [...nativeSessionPathKeys.all, 'detail'] as const,
|
||||
detail: (filePath: string | null) => [...nativeSessionPathKeys.details(), filePath] as const,
|
||||
};
|
||||
|
||||
// ========== Constants ==========
|
||||
|
||||
const STALE_TIME = 5 * 60 * 1000; // 5 minutes
|
||||
const GC_TIME = 30 * 60 * 1000; // 30 minutes
|
||||
|
||||
// ========== Types ==========
|
||||
|
||||
export interface UseNativeSessionByPathOptions {
|
||||
staleTime?: number;
|
||||
gcTime?: number;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface UseNativeSessionByPathReturn {
|
||||
data: NativeSession | undefined;
|
||||
isLoading: boolean;
|
||||
isFetching: boolean;
|
||||
error: Error | null;
|
||||
refetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
// ========== Hook ==========
|
||||
|
||||
/**
|
||||
* Hook for fetching native CLI session content by file path
|
||||
*
|
||||
* @param filePath - The file path to the session file
|
||||
* @param tool - The tool type (gemini, qwen, codex, claude, opencode)
|
||||
* @param options - Query options
|
||||
*/
|
||||
export function useNativeSessionByPath(
|
||||
filePath: string | null,
|
||||
tool?: string,
|
||||
options: UseNativeSessionByPathOptions = {}
|
||||
): UseNativeSessionByPathReturn {
|
||||
const { staleTime = STALE_TIME, gcTime = GC_TIME, enabled = true } = options;
|
||||
|
||||
const query = useQuery<NativeSession>({
|
||||
queryKey: nativeSessionPathKeys.detail(filePath),
|
||||
queryFn: () => {
|
||||
if (!filePath) throw new Error('filePath is required');
|
||||
return fetchNativeSessionWithOptions({
|
||||
filePath,
|
||||
tool: tool as 'gemini' | 'qwen' | 'codex' | 'claude' | 'opencode' | undefined,
|
||||
}) as Promise<NativeSession>;
|
||||
},
|
||||
enabled: !!filePath && enabled,
|
||||
staleTime,
|
||||
gcTime,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
retry: 2,
|
||||
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
|
||||
});
|
||||
|
||||
const refetch = async () => {
|
||||
await query.refetch();
|
||||
};
|
||||
|
||||
return {
|
||||
data: query.data,
|
||||
isLoading: query.isLoading,
|
||||
isFetching: query.isFetching,
|
||||
error: query.error,
|
||||
refetch,
|
||||
};
|
||||
}
|
||||
@@ -15,8 +15,8 @@ import { workspaceQueryKeys } from '@/lib/queryKeys';
|
||||
|
||||
// ========== Constants ==========
|
||||
|
||||
const STALE_TIME = 30 * 1000;
|
||||
const GC_TIME = 5 * 60 * 1000;
|
||||
const STALE_TIME = 2 * 60 * 1000; // 2 minutes (increased from 30s)
|
||||
const GC_TIME = 10 * 60 * 1000; // 10 minutes (increased from 5min)
|
||||
|
||||
// ========== Types ==========
|
||||
|
||||
|
||||
@@ -2372,15 +2372,15 @@ export async function fetchNativeSessionWithOptions(
|
||||
|
||||
/**
|
||||
* Native session metadata for list endpoint
|
||||
* Matches backend NativeSession interface
|
||||
*/
|
||||
export interface NativeSessionListItem {
|
||||
id: string;
|
||||
tool: string;
|
||||
path: string;
|
||||
title?: string;
|
||||
startTime: string;
|
||||
updatedAt: string;
|
||||
projectHash?: string;
|
||||
sessionId: string; // Native UUID
|
||||
tool: string; // gemini | qwen | codex | claude | opencode
|
||||
filePath: string; // Full path to session file
|
||||
projectHash?: string; // Project directory hash
|
||||
createdAt: string; // ISO date string
|
||||
updatedAt: string; // ISO date string
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
ChevronRight,
|
||||
FileJson,
|
||||
Clock,
|
||||
Calendar,
|
||||
} from 'lucide-react';
|
||||
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -54,50 +53,6 @@ import { getToolVariant } from '@/lib/cli-tool-theme';
|
||||
|
||||
type HistoryTab = 'executions' | 'observability' | 'native-sessions';
|
||||
|
||||
// ========== Date Grouping Helpers ==========
|
||||
|
||||
type DateGroup = 'today' | 'yesterday' | 'thisWeek' | 'older';
|
||||
|
||||
function getDateGroup(date: Date): DateGroup {
|
||||
const now = new Date();
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const yesterday = new Date(today);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
const weekAgo = new Date(today);
|
||||
weekAgo.setDate(weekAgo.getDate() - 7);
|
||||
|
||||
if (date >= today) return 'today';
|
||||
if (date >= yesterday) return 'yesterday';
|
||||
if (date >= weekAgo) return 'thisWeek';
|
||||
return 'older';
|
||||
}
|
||||
|
||||
function groupSessionsByDate(sessions: NativeSessionListItem[]): Map<DateGroup, NativeSessionListItem[]> {
|
||||
const groups = new Map<DateGroup, NativeSessionListItem[]>([
|
||||
['today', []],
|
||||
['yesterday', []],
|
||||
['thisWeek', []],
|
||||
['older', []],
|
||||
]);
|
||||
|
||||
sessions.forEach((session) => {
|
||||
const date = new Date(session.updatedAt);
|
||||
const group = getDateGroup(date);
|
||||
groups.get(group)?.push(session);
|
||||
});
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
const dateGroupOrder: DateGroup[] = ['today', 'yesterday', 'thisWeek', 'older'];
|
||||
|
||||
const dateGroupLabels: Record<DateGroup, string> = {
|
||||
today: '今天',
|
||||
yesterday: '昨天',
|
||||
thisWeek: '本周',
|
||||
older: '更早',
|
||||
};
|
||||
|
||||
/**
|
||||
* HistoryPage component - Display CLI execution history
|
||||
*/
|
||||
@@ -111,6 +66,7 @@ export function HistoryPage() {
|
||||
const [deleteTarget, setDeleteTarget] = React.useState<string | null>(null);
|
||||
const [selectedExecution, setSelectedExecution] = React.useState<string | null>(null);
|
||||
const [isPanelOpen, setIsPanelOpen] = React.useState(false);
|
||||
const [selectedNativeSession, setSelectedNativeSession] = React.useState<NativeSessionListItem | null>(null);
|
||||
const [nativeExecutionId, setNativeExecutionId] = React.useState<string | null>(null);
|
||||
const [isNativePanelOpen, setIsNativePanelOpen] = React.useState(false);
|
||||
const isImmersiveMode = useAppStore(selectIsImmersiveMode);
|
||||
@@ -157,7 +113,7 @@ export function HistoryPage() {
|
||||
|
||||
// Native session click handler - opens NativeSessionPanel
|
||||
const handleNativeSessionClick = (session: NativeSessionListItem) => {
|
||||
setNativeExecutionId(session.id);
|
||||
setSelectedNativeSession(session);
|
||||
setIsNativePanelOpen(true);
|
||||
};
|
||||
|
||||
@@ -540,19 +496,14 @@ export function HistoryPage() {
|
||||
<div className="border-t divide-y">
|
||||
{sessions.map((session) => (
|
||||
<button
|
||||
key={session.id}
|
||||
key={session.sessionId}
|
||||
className="w-full flex items-center justify-between px-4 py-3 hover:bg-muted/30 transition-colors text-left"
|
||||
onClick={() => handleNativeSessionClick(session)}
|
||||
>
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<span className="font-mono text-sm truncate max-w-48" title={session.id}>
|
||||
{session.id.length > 24 ? session.id.slice(0, 24) + '...' : session.id}
|
||||
<span className="font-mono text-sm truncate max-w-48" title={session.sessionId}>
|
||||
{session.sessionId.length > 24 ? session.sessionId.slice(0, 24) + '...' : session.sessionId}
|
||||
</span>
|
||||
{session.title && (
|
||||
<span className="text-sm text-muted-foreground truncate max-w-64">
|
||||
{session.title}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-xs text-muted-foreground shrink-0">
|
||||
<span className="flex items-center gap-1">
|
||||
@@ -598,19 +549,14 @@ export function HistoryPage() {
|
||||
<div className="border-t divide-y">
|
||||
{sessions.map((session) => (
|
||||
<button
|
||||
key={session.id}
|
||||
key={session.sessionId}
|
||||
className="w-full flex items-center justify-between px-4 py-3 hover:bg-muted/30 transition-colors text-left"
|
||||
onClick={() => handleNativeSessionClick(session)}
|
||||
>
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<span className="font-mono text-sm truncate max-w-48" title={session.id}>
|
||||
{session.id.length > 24 ? session.id.slice(0, 24) + '...' : session.id}
|
||||
<span className="font-mono text-sm truncate max-w-48" title={session.sessionId}>
|
||||
{session.sessionId.length > 24 ? session.sessionId.slice(0, 24) + '...' : session.sessionId}
|
||||
</span>
|
||||
{session.title && (
|
||||
<span className="text-sm text-muted-foreground truncate max-w-64">
|
||||
{session.title}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-xs text-muted-foreground shrink-0">
|
||||
<span className="flex items-center gap-1">
|
||||
@@ -639,7 +585,8 @@ export function HistoryPage() {
|
||||
|
||||
{/* Native Session Panel */}
|
||||
<NativeSessionPanel
|
||||
executionId={nativeExecutionId || ''}
|
||||
session={selectedNativeSession}
|
||||
executionId={nativeExecutionId || undefined}
|
||||
open={isNativePanelOpen}
|
||||
onOpenChange={setIsNativePanelOpen}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user