From 056836b3d1e7462f69fcf0312d68498cbba129bf Mon Sep 17 00:00:00 2001 From: catlog22 Date: Wed, 18 Mar 2026 17:25:46 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20merge=20PR=20#139=20partial=20=E2=80=94?= =?UTF-8?q?=20hook=20path=20fixes,=20TaskDrawer=20multi-source=20files,=20?= =?UTF-8?q?lite-tasks=20URL=20params?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cherry-pick valid changes from PR #139 (fix/webuibugs-20260314). CodexLens build tools check dropped — target files deleted in 398601f8. Changes included: - Fix hook query key (rulesList → hooksList) and projectPath in cache ops - Pass projectPath to hook template install API calls - TaskDrawer: get files from nt.files, flowControl.target_files, or sourceRaw.file - LiteTasksPage: support URL params (:subType/:id) to auto-open task drawer - Add lite-tasks/:subType/:id route Closes #139 Co-Authored-By: Claude Opus 4.6 --- .../src/components/hook/HookWizard.tsx | 4 +++ .../src/components/shared/TaskDrawer.tsx | 11 +++--- ccw/frontend/src/hooks/useCli.ts | 27 +++++++++------ ccw/frontend/src/pages/HookManagerPage.tsx | 3 ++ ccw/frontend/src/pages/LiteTasksPage.tsx | 34 +++++++++++++++++-- ccw/frontend/src/router.tsx | 4 +++ 6 files changed, 66 insertions(+), 17 deletions(-) diff --git a/ccw/frontend/src/components/hook/HookWizard.tsx b/ccw/frontend/src/components/hook/HookWizard.tsx index bce2bcda..60a7def4 100644 --- a/ccw/frontend/src/components/hook/HookWizard.tsx +++ b/ccw/frontend/src/components/hook/HookWizard.tsx @@ -5,6 +5,7 @@ import { useState, useEffect } from 'react'; import { useIntl } from 'react-intl'; +import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore'; import { Dialog, DialogContent, @@ -139,6 +140,7 @@ export function HookWizard({ const [detectedPlatform, setDetectedPlatform] = useState('linux'); const [scope, setScope] = useState<'project' | 'global'>('project'); const [saving, setSaving] = useState(false); + const projectPath = useWorkflowStore(selectProjectPath); // Fetch available skills for skill-context wizard const { data: skillsData, isLoading: skillsLoading } = useQuery({ @@ -207,6 +209,7 @@ export function HookWizard({ body: JSON.stringify({ templateId: 'memory-auto-compress', scope, + projectPath, }), }); const result = await response.json(); @@ -229,6 +232,7 @@ export function HookWizard({ body: JSON.stringify({ templateId, scope, + projectPath, }), }); const result = await response.json(); diff --git a/ccw/frontend/src/components/shared/TaskDrawer.tsx b/ccw/frontend/src/components/shared/TaskDrawer.tsx index c01d0fe1..9c01a894 100644 --- a/ccw/frontend/src/components/shared/TaskDrawer.tsx +++ b/ccw/frontend/src/components/shared/TaskDrawer.tsx @@ -104,12 +104,15 @@ export function TaskDrawer({ task, isOpen, onClose }: TaskDrawerProps) { const dependsOn = nt.depends_on || []; const preAnalysis = nt.pre_analysis || flowControl?.pre_analysis || []; const implSteps = nt.implementation || flowControl?.implementation_approach || []; - const taskFiles = nt.files || flowControl?.target_files || []; + + // Get files from multiple sources: nt.files, flowControl.target_files, or sourceRaw.file + const rawData = nt._raw as Record | undefined; + const sourceRaw = (rawData?._raw as Record) || rawData; + const rawFile = sourceRaw?.file as string | undefined; + const taskFiles = nt.files || flowControl?.target_files || (rawFile ? [{ path: rawFile }] : []); const taskScope = nt.scope; // Detect if task supports status tracking (new format has explicit status/status_history) - const rawData = nt._raw as Record | undefined; - const sourceRaw = (rawData?._raw as Record) || rawData; const hasStatusTracking = sourceRaw ? (sourceRaw.status !== undefined || sourceRaw.status_history !== undefined) : false; @@ -391,7 +394,7 @@ export function TaskDrawer({ task, isOpen, onClose }: TaskDrawerProps) { {hasFiles ? (
- {flowControl?.target_files?.map((file, index) => { + {taskFiles.map((file, index) => { // Support multiple file formats: string, { path: string }, { name: string }, { path, name } let displayPath: string; if (typeof file === 'string') { diff --git a/ccw/frontend/src/hooks/useCli.ts b/ccw/frontend/src/hooks/useCli.ts index 062dc776..ddf8af03 100644 --- a/ccw/frontend/src/hooks/useCli.ts +++ b/ccw/frontend/src/hooks/useCli.ts @@ -445,7 +445,7 @@ export function useHooks(options: UseHooksOptions = {}): UseHooksReturn { const queryEnabled = enabled && !!projectPath; const query = useQuery({ - queryKey: workspaceQueryKeys.rulesList(projectPath), + queryKey: workspaceQueryKeys.hooksList(projectPath), queryFn: () => fetchHooks(projectPath), staleTime, enabled: queryEnabled, @@ -478,15 +478,17 @@ export function useHooks(options: UseHooksOptions = {}): UseHooksReturn { export function useToggleHook() { const queryClient = useQueryClient(); + const projectPath = useWorkflowStore(selectProjectPath); const mutation = useMutation({ mutationFn: ({ hookName, enabled }: { hookName: string; enabled: boolean }) => toggleHook(hookName, enabled), onMutate: async ({ hookName, enabled }) => { - await queryClient.cancelQueries({ queryKey: hooksKeys.all }); - const previousHooks = queryClient.getQueryData(hooksKeys.lists()); + const queryKey = workspaceQueryKeys.hooksList(projectPath || ''); + await queryClient.cancelQueries({ queryKey }); + const previousHooks = queryClient.getQueryData(queryKey); - queryClient.setQueryData(hooksKeys.lists(), (old) => { + queryClient.setQueryData(queryKey, (old) => { if (!old) return old; return { hooks: old.hooks.map((h) => (h.name === hookName ? { ...h, enabled } : h)), @@ -496,12 +498,13 @@ export function useToggleHook() { return { previousHooks }; }, onError: (_error, _vars, context) => { + const queryKey = workspaceQueryKeys.hooksList(projectPath || ''); if (context?.previousHooks) { - queryClient.setQueryData(hooksKeys.lists(), context.previousHooks); + queryClient.setQueryData(queryKey, context.previousHooks); } }, onSettled: () => { - queryClient.invalidateQueries({ queryKey: hooksKeys.all }); + queryClient.invalidateQueries({ queryKey: workspaceQueryKeys.hooksList(projectPath || '') }); }, }); @@ -528,10 +531,11 @@ export function useDeleteHook() { }); }, onMutate: async (hook) => { - await queryClient.cancelQueries({ queryKey: hooksKeys.all }); - const previousHooks = queryClient.getQueryData(hooksKeys.lists()); + const queryKey = workspaceQueryKeys.hooksList(projectPath || ''); + await queryClient.cancelQueries({ queryKey }); + const previousHooks = queryClient.getQueryData(queryKey); - queryClient.setQueryData(hooksKeys.lists(), (old) => { + queryClient.setQueryData(queryKey, (old) => { if (!old) return old; return { hooks: old.hooks.filter((h) => h.name !== hook.name), @@ -541,12 +545,13 @@ export function useDeleteHook() { return { previousHooks }; }, onError: (_error, _hook, context) => { + const queryKey = workspaceQueryKeys.hooksList(projectPath || ''); if (context?.previousHooks) { - queryClient.setQueryData(hooksKeys.lists(), context.previousHooks); + queryClient.setQueryData(queryKey, context.previousHooks); } }, onSettled: () => { - queryClient.invalidateQueries({ queryKey: hooksKeys.all }); + queryClient.invalidateQueries({ queryKey: workspaceQueryKeys.hooksList(projectPath || '') }); }, }); diff --git a/ccw/frontend/src/pages/HookManagerPage.tsx b/ccw/frontend/src/pages/HookManagerPage.tsx index 782b4482..3e8a2262 100644 --- a/ccw/frontend/src/pages/HookManagerPage.tsx +++ b/ccw/frontend/src/pages/HookManagerPage.tsx @@ -5,6 +5,7 @@ import { useState, useMemo } from 'react'; import { useIntl } from 'react-intl'; +import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore'; import { GitFork, Plus, @@ -157,6 +158,7 @@ export function HookManagerPage() { const { hooks, enabledCount, totalCount, isLoading, refetch } = useHooks(); const { toggleHook } = useToggleHook(); const { deleteHook } = useDeleteHook(); + const projectPath = useWorkflowStore(selectProjectPath); // Convert hooks to HookCardData and filter by search query and trigger type const filteredHooks = useMemo(() => { @@ -291,6 +293,7 @@ export function HookManagerPage() { body: JSON.stringify({ templateId, scope: 'project', + projectPath, }), }); diff --git a/ccw/frontend/src/pages/LiteTasksPage.tsx b/ccw/frontend/src/pages/LiteTasksPage.tsx index d22b23b0..32a54ae1 100644 --- a/ccw/frontend/src/pages/LiteTasksPage.tsx +++ b/ccw/frontend/src/pages/LiteTasksPage.tsx @@ -57,7 +57,7 @@ import { TabsNavigation } from '@/components/ui/TabsNavigation'; import { TaskDrawer } from '@/components/shared/TaskDrawer'; import { fetchLiteSessionContext, type LiteTask, type LiteTaskSession, type LiteSessionContext, type RoundSynthesis, type MultiCliContextPackage } from '@/lib/api'; import { LiteContextContent } from '@/components/lite-tasks/LiteContextContent'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { cn } from '@/lib/utils'; type LiteTaskTab = 'lite-plan' | 'lite-fix' | 'multi-cli-plan'; @@ -1205,11 +1205,13 @@ function ExpandedMultiCliPanel({ */ export function LiteTasksPage() { const navigate = useNavigate(); - const { formatMessage } = useIntl(); + const { subType, id } = useParams<{ subType?: string; id?: string }>(); const { litePlan, liteFix, multiCliPlan, isLoading, error, refetch } = useLiteTasks(); + const { formatMessage } = useIntl(); const [activeTab, setActiveTab] = React.useState('lite-plan'); const [expandedSessionId, setExpandedSessionId] = React.useState(null); const [selectedTask, setSelectedTask] = React.useState(null); + const paramsInitialized = React.useRef(false); const [searchQuery, setSearchQuery] = React.useState(''); const [sortField, setSortField] = React.useState('date'); const [sortOrder, setSortOrder] = React.useState('desc'); @@ -1259,6 +1261,34 @@ export function LiteTasksPage() { const filteredLiteFix = React.useMemo(() => filterAndSort(liteFix), [liteFix, filterAndSort]); const filteredMultiCliPlan = React.useMemo(() => filterAndSort(multiCliPlan), [multiCliPlan, filterAndSort]); + // Handle URL params to auto-open task drawer + React.useEffect(() => { + if (!id || paramsInitialized.current) return; + if (isLoading) return; + + // Find the session containing this task + const allSessions = [...litePlan, ...liteFix, ...multiCliPlan]; + const targetSession = allSessions.find(session => session.id === id); + + if (targetSession) { + paramsInitialized.current = true; + + // Set active tab based on subType + if (subType === 'lite-plan') setActiveTab('lite-plan'); + else if (subType === 'lite-fix') setActiveTab('lite-fix'); + else if (subType === 'multi-cli-plan') setActiveTab('multi-cli-plan'); + + // Expand the session + setExpandedSessionId(id); + + // Find and select the task + const task = targetSession.tasks?.find(t => t.task_id === id || t.id === id); + if (task) { + setSelectedTask(task); + } + } + }, [id, subType, isLoading, litePlan, liteFix, multiCliPlan]); + // Toggle sort const toggleSort = (field: SortField) => { if (sortField === field) { diff --git a/ccw/frontend/src/router.tsx b/ccw/frontend/src/router.tsx index d7ed669d..4fac127b 100644 --- a/ccw/frontend/src/router.tsx +++ b/ccw/frontend/src/router.tsx @@ -92,6 +92,10 @@ const routes: RouteObject[] = [ path: 'sessions/:sessionId/review', element: withErrorHandling(), }, + { + path: 'lite-tasks/:subType/:id', + element: withErrorHandling(), + }, { path: 'lite-tasks', element: withErrorHandling(),