fix: merge PR #139 partial — hook path fixes, TaskDrawer multi-source files, lite-tasks URL params

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: will <yangxiangnan@wabestway.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
catlog22
2026-03-18 17:25:46 +08:00
parent 6ff0467e02
commit 683b85228f
6 changed files with 66 additions and 17 deletions

View File

@@ -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<Platform>('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<SkillsResponse>({
@@ -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();

View File

@@ -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<string, unknown> | undefined;
const sourceRaw = (rawData?._raw as Record<string, unknown>) || 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<string, unknown> | undefined;
const sourceRaw = (rawData?._raw as Record<string, unknown>) || rawData;
const hasStatusTracking = sourceRaw
? (sourceRaw.status !== undefined || sourceRaw.status_history !== undefined)
: false;
@@ -391,7 +394,7 @@ export function TaskDrawer({ task, isOpen, onClose }: TaskDrawerProps) {
<TabsContent value="files" className="mt-4 pb-6">
{hasFiles ? (
<div className="space-y-3">
{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') {

View File

@@ -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<HooksResponse>(hooksKeys.lists());
const queryKey = workspaceQueryKeys.hooksList(projectPath || '');
await queryClient.cancelQueries({ queryKey });
const previousHooks = queryClient.getQueryData<HooksResponse>(queryKey);
queryClient.setQueryData<HooksResponse>(hooksKeys.lists(), (old) => {
queryClient.setQueryData<HooksResponse>(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<HooksResponse>(hooksKeys.lists());
const queryKey = workspaceQueryKeys.hooksList(projectPath || '');
await queryClient.cancelQueries({ queryKey });
const previousHooks = queryClient.getQueryData<HooksResponse>(queryKey);
queryClient.setQueryData<HooksResponse>(hooksKeys.lists(), (old) => {
queryClient.setQueryData<HooksResponse>(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 || '') });
},
});

View File

@@ -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,
}),
});

View File

@@ -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<LiteTaskTab>('lite-plan');
const [expandedSessionId, setExpandedSessionId] = React.useState<string | null>(null);
const [selectedTask, setSelectedTask] = React.useState<LiteTask | null>(null);
const paramsInitialized = React.useRef(false);
const [searchQuery, setSearchQuery] = React.useState('');
const [sortField, setSortField] = React.useState<SortField>('date');
const [sortOrder, setSortOrder] = React.useState<SortOrder>('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) {

View File

@@ -92,6 +92,10 @@ const routes: RouteObject[] = [
path: 'sessions/:sessionId/review',
element: withErrorHandling(<ReviewSessionPage />),
},
{
path: 'lite-tasks/:subType/:id',
element: withErrorHandling(<LiteTasksPage />),
},
{
path: 'lite-tasks',
element: withErrorHandling(<LiteTasksPage />),