mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
Refactor and optimize various components and files
- Removed deprecated `ccw-contentPattern-optimization-summary.md` and related files. - Updated `A2UIPopupCard.tsx` to clarify comments on interaction handling. - Enhanced `QueueListColumn.tsx` and `QueuePanel.tsx` to handle potential undefined values for `config`. - Added `useEffect` in `QueuePanel.tsx` to load scheduler state on mount. - Improved `SchedulerPanel.tsx` to handle potential undefined values for `sessionPool`. - Introduced auto-initialization logic in `queueSchedulerStore.ts` to prevent multiple initialization calls. - Updated `A2UIWebSocketHandler.ts` to refine selection handling logic. - Enhanced `hooks-routes.ts` to support multi-question surfaces. - Added submit and cancel buttons in `ask-question.ts` for better user interaction. - Deleted `codex_prompt.md` and `contentPattern-library-options.md` as part of cleanup. - Removed `status-reference.md` to streamline documentation.
This commit is contained in:
@@ -329,11 +329,11 @@ function SinglePagePopup({ surface, onClose }: A2UIPopupCardProps) {
|
||||
'data-[state=open]:duration-300 data-[state=closed]:duration-200'
|
||||
)}
|
||||
onInteractOutside={(e) => {
|
||||
// Prevent closing when clicking outside
|
||||
// Prevent closing when clicking outside - only cancel button can close
|
||||
e.preventDefault();
|
||||
}}
|
||||
onEscapeKeyDown={(e) => {
|
||||
// Prevent closing with ESC key
|
||||
// Prevent closing with ESC key - only cancel button can close
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
@@ -574,8 +574,14 @@ function MultiPagePopup({ surface, onClose }: A2UIPopupCardProps) {
|
||||
'data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95',
|
||||
'data-[state=open]:duration-300 data-[state=closed]:duration-200'
|
||||
)}
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
onEscapeKeyDown={(e) => e.preventDefault()}
|
||||
onInteractOutside={(e) => {
|
||||
// Prevent closing when clicking outside - only cancel button can close
|
||||
e.preventDefault();
|
||||
}}
|
||||
onEscapeKeyDown={(e) => {
|
||||
// Prevent closing with ESC key - only cancel button can close
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
{/* Header with current page title */}
|
||||
<DialogHeader className="space-y-2 pb-4">
|
||||
|
||||
@@ -199,7 +199,7 @@ function SchedulerBar() {
|
||||
{/* Progress + Concurrency */}
|
||||
{isActive && (
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{progress}% | {concurrency}/{config.maxConcurrentSessions}
|
||||
{progress}% | {concurrency}/{config?.maxConcurrentSessions ?? 2}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -285,6 +285,9 @@ export function QueueListColumn() {
|
||||
const associationChain = useIssueQueueIntegrationStore(selectAssociationChain);
|
||||
const buildAssociationChain = useIssueQueueIntegrationStore((s) => s.buildAssociationChain);
|
||||
|
||||
// NOTE: loadInitialState is called from a parent component or app initialization
|
||||
// to avoid infinite loop issues with Zustand selectors
|
||||
|
||||
const sortedItems = useMemo(
|
||||
() => [...items].sort((a, b) => a.execution_order - b.execution_order),
|
||||
[items]
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
// Integrates with issueQueueIntegrationStore for association chain.
|
||||
// Note: Scheduler controls are in the standalone SchedulerPanel.
|
||||
|
||||
import { useState, useMemo, useCallback, memo } from 'react';
|
||||
import { useState, useMemo, useCallback, memo, useEffect } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import {
|
||||
ListChecks,
|
||||
@@ -193,6 +193,12 @@ function QueueTabContent(_props: { embedded?: boolean }) {
|
||||
// Scheduler store data
|
||||
const schedulerItems = useQueueSchedulerStore(selectQueueItems);
|
||||
const schedulerStatus = useQueueSchedulerStore(selectQueueSchedulerStatus);
|
||||
const loadInitialState = useQueueSchedulerStore((s) => s.loadInitialState);
|
||||
|
||||
// Load scheduler state on mount
|
||||
useEffect(() => {
|
||||
loadInitialState();
|
||||
}, [loadInitialState]);
|
||||
|
||||
// Legacy API data (fallback)
|
||||
const queueQuery = useIssueQueue();
|
||||
|
||||
@@ -82,7 +82,7 @@ export function SchedulerPanel() {
|
||||
[updateConfig]
|
||||
);
|
||||
|
||||
const sessionEntries = Object.entries(sessionPool);
|
||||
const sessionEntries = Object.entries(sessionPool ?? {});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
@@ -102,7 +102,7 @@ export function SchedulerPanel() {
|
||||
})}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground ml-auto tabular-nums">
|
||||
{concurrency}/{config.maxConcurrentSessions}
|
||||
{concurrency}/{config?.maxConcurrentSessions ?? 2}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -188,7 +188,7 @@ export function SchedulerPanel() {
|
||||
type="number"
|
||||
min={1}
|
||||
max={10}
|
||||
value={config.maxConcurrentSessions}
|
||||
value={config?.maxConcurrentSessions ?? 2}
|
||||
onChange={handleConcurrencyChange}
|
||||
className="w-14 h-6 text-xs text-center rounded border border-border bg-background px-1"
|
||||
/>
|
||||
|
||||
@@ -28,6 +28,7 @@ import { FileSidebarPanel } from '@/components/terminal-dashboard/FileSidebarPan
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
|
||||
import { useConfigStore } from '@/stores/configStore';
|
||||
import { useQueueSchedulerStore } from '@/stores/queueSchedulerStore';
|
||||
|
||||
// ========== Main Page Component ==========
|
||||
|
||||
|
||||
@@ -305,11 +305,11 @@ const EMPTY_ITEMS: QueueItem[] = [];
|
||||
|
||||
/** Select current scheduler status */
|
||||
export const selectQueueSchedulerStatus = (state: QueueSchedulerStore): QueueSchedulerStatus =>
|
||||
state.status;
|
||||
state?.status ?? 'idle';
|
||||
|
||||
/** Select all queue items */
|
||||
export const selectQueueItems = (state: QueueSchedulerStore): QueueItem[] =>
|
||||
state.items;
|
||||
state?.items ?? [];
|
||||
|
||||
/**
|
||||
* Select items that are ready to execute (status 'queued' or 'pending').
|
||||
@@ -346,9 +346,11 @@ export const selectExecutingItems = (state: QueueSchedulerStore): QueueItem[] =>
|
||||
* Returns 0 when there are no items.
|
||||
*/
|
||||
export const selectSchedulerProgress = (state: QueueSchedulerStore): number => {
|
||||
const total = state.items.length;
|
||||
if (!state) return 0;
|
||||
const items = state.items ?? [];
|
||||
const total = items.length;
|
||||
if (total === 0) return 0;
|
||||
const terminal = state.items.filter(
|
||||
const terminal = items.filter(
|
||||
(item) => item.status === 'completed' || item.status === 'failed'
|
||||
).length;
|
||||
return Math.round((terminal / total) * 100);
|
||||
@@ -369,3 +371,35 @@ export const selectCurrentConcurrency = (state: QueueSchedulerStore): number =>
|
||||
/** Select scheduler error */
|
||||
export const selectSchedulerError = (state: QueueSchedulerStore): string | null =>
|
||||
state.error;
|
||||
|
||||
// ========== Auto-initialization ==========
|
||||
|
||||
/**
|
||||
* Flag to prevent multiple initialization calls.
|
||||
* This is set outside the store to avoid triggering re-renders.
|
||||
*/
|
||||
let schedulerInitialized = false;
|
||||
|
||||
/**
|
||||
* Initialize the queue scheduler state once.
|
||||
* Safe to call multiple times - will only initialize once.
|
||||
*/
|
||||
export function initializeScheduler(): void {
|
||||
if (!schedulerInitialized) {
|
||||
schedulerInitialized = true;
|
||||
useQueueSchedulerStore.getState().loadInitialState().catch((error) => {
|
||||
console.error('[QueueScheduler] Failed to initialize:', error);
|
||||
// Reset flag on error to allow retry
|
||||
schedulerInitialized = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-initialize when this module is imported (deferred to next tick)
|
||||
if (typeof window !== 'undefined') {
|
||||
// Defer initialization to avoid blocking initial render
|
||||
// and to ensure all store subscriptions are set up first
|
||||
setTimeout(() => {
|
||||
initializeScheduler();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
@@ -405,7 +405,7 @@ export class A2UIWebSocketHandler {
|
||||
case 'submit': {
|
||||
const otherText = this.inputValues.get(`__other__:${questionId}`);
|
||||
|
||||
// Check if this is a single-select or multi-select
|
||||
// Check if this is a single-select
|
||||
const singleSelection = this.singleSelectSelections.get(questionId);
|
||||
if (singleSelection !== undefined) {
|
||||
// Resolve __other__ to actual text input
|
||||
@@ -413,14 +413,27 @@ export class A2UIWebSocketHandler {
|
||||
this.inputValues.delete(`__other__:${questionId}`);
|
||||
return resolveAndCleanup({ questionId, value, cancelled: false });
|
||||
}
|
||||
// Multi-select submit
|
||||
const multiSelected = this.multiSelectSelections.get(questionId) ?? new Set<string>();
|
||||
// Resolve __other__ in multi-select: replace with actual text
|
||||
const values = Array.from(multiSelected).map(v =>
|
||||
v === '__other__' && otherText ? otherText : v
|
||||
);
|
||||
this.inputValues.delete(`__other__:${questionId}`);
|
||||
return resolveAndCleanup({ questionId, value: values, cancelled: false });
|
||||
|
||||
// Check if this is a multi-select
|
||||
const multiSelected = this.multiSelectSelections.get(questionId);
|
||||
if (multiSelected !== undefined && multiSelected.size > 0) {
|
||||
// Resolve __other__ in multi-select: replace with actual text
|
||||
const values = Array.from(multiSelected).map(v =>
|
||||
v === '__other__' && otherText ? otherText : v
|
||||
);
|
||||
this.inputValues.delete(`__other__:${questionId}`);
|
||||
return resolveAndCleanup({ questionId, value: values, cancelled: false });
|
||||
}
|
||||
|
||||
// Check if this is a text input (no selections, but has input value)
|
||||
const inputValue = this.inputValues.get(questionId);
|
||||
if (inputValue !== undefined) {
|
||||
this.inputValues.delete(questionId);
|
||||
return resolveAndCleanup({ questionId, value: inputValue, cancelled: false });
|
||||
}
|
||||
|
||||
// No value found - submit empty string
|
||||
return resolveAndCleanup({ questionId, value: '', cancelled: false });
|
||||
}
|
||||
|
||||
case 'input-change': {
|
||||
|
||||
@@ -343,7 +343,19 @@ export async function handleHooksRoutes(ctx: HooksRouteContext): Promise<boolean
|
||||
const initState = extraData.initialState as Record<string, unknown>;
|
||||
const questionId = initState.questionId as string | undefined;
|
||||
const questionType = initState.questionType as string | undefined;
|
||||
if (questionId && questionType === 'select') {
|
||||
|
||||
// Handle multi-question surfaces (multi-page): initialize tracking for each page
|
||||
if (questionType === 'multi-question' && Array.isArray(initState.pages)) {
|
||||
const pages = initState.pages as Array<{ questionId: string; type: string }>;
|
||||
for (const page of pages) {
|
||||
if (page.type === 'multi-select') {
|
||||
a2uiWebSocketHandler.initMultiSelect(page.questionId);
|
||||
} else if (page.type === 'select') {
|
||||
a2uiWebSocketHandler.initSingleSelect(page.questionId);
|
||||
}
|
||||
}
|
||||
} else if (questionId && questionType === 'select') {
|
||||
// Single-question surface: initialize based on question type
|
||||
a2uiWebSocketHandler.initSingleSelect(questionId);
|
||||
} else if (questionId && questionType === 'multi-select') {
|
||||
a2uiWebSocketHandler.initMultiSelect(questionId);
|
||||
|
||||
@@ -409,12 +409,37 @@ function generateQuestionSurface(question: Question, surfaceId: string, timeoutM
|
||||
component: {
|
||||
TextField: {
|
||||
value: question.defaultValue ? { literalString: String(question.defaultValue) } : undefined,
|
||||
onChange: { actionId: 'answer', parameters: { questionId: question.id } },
|
||||
onChange: { actionId: 'input-change', parameters: { questionId: question.id } },
|
||||
placeholder: question.placeholder || 'Enter your answer',
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
});
|
||||
// Add Submit/Cancel buttons for input type
|
||||
components.push({
|
||||
id: 'submit-btn',
|
||||
component: {
|
||||
Button: {
|
||||
onClick: { actionId: 'submit', parameters: { questionId: question.id } },
|
||||
content: {
|
||||
Text: { text: { literalString: 'Submit' } },
|
||||
},
|
||||
variant: 'primary',
|
||||
},
|
||||
},
|
||||
});
|
||||
components.push({
|
||||
id: 'cancel-btn',
|
||||
component: {
|
||||
Button: {
|
||||
onClick: { actionId: 'cancel', parameters: { questionId: question.id } },
|
||||
content: {
|
||||
Text: { text: { literalString: 'Cancel' } },
|
||||
},
|
||||
variant: 'secondary',
|
||||
},
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user