Skip to content

Commit e260a3f

Browse files
author
catlog22
committed
feat: enhance theme customization and UI components
- Implemented a new color generation module to create CSS variables based on a single hue value, supporting both light and dark modes. - Added unit tests for the color generation logic to ensure accuracy and robustness. - Replaced dropdown location filter with tab navigation in RulesManagerPage and SkillsManagerPage for improved UX. - Updated app store to manage custom theme hues and states, allowing for dynamic theme adjustments. - Sanitized notification content before persisting to localStorage to prevent sensitive data exposure. - Refactored memory retrieval logic to handle archived status more flexibly. - Improved Tailwind CSS configuration with new gradient utilities and animations. - Minor adjustments to SettingsPage layout for better visual consistency.
1 parent 8861622 commit e260a3f

30 files changed

Lines changed: 1375 additions & 386 deletions

.claude/commands/workflow/review-cycle-fix.md

Lines changed: 58 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -532,12 +532,18 @@ Use fix_strategy.test_pattern to run affected tests:
532532

533533
### Error Handling
534534

535-
**Planning Failures**:
536-
- Invalid template → Abort with error message
537-
- Insufficient findings data → Request complete export
538-
- Planning timeout → Retry once, then fail gracefully
539-
540-
**Execution Failures**:
535+
**Batching Failures (Phase 1.5)**:
536+
- Invalid findings data → Abort with error message
537+
- Empty batches after grouping → Warn and skip empty batches
538+
539+
**Planning Failures (Phase 2)**:
540+
- Planning agent timeout → Mark batch as failed, continue with other batches
541+
- Partial plan missing → Skip batch, warn user
542+
- Agent crash → Collect available partial plans, proceed with aggregation
543+
- All agents fail → Abort entire fix session with error
544+
- Aggregation conflicts → Apply conflict resolution (serialize conflicting groups)
545+
546+
**Execution Failures (Phase 3)**:
541547
- Agent crash → Mark group as failed, continue with other groups
542548
- Test command not found → Skip test verification, warn user
543549
- Git operations fail → Abort with error, preserve state
@@ -549,14 +555,34 @@ Use fix_strategy.test_pattern to run affected tests:
549555

550556
### TodoWrite Structure
551557

552-
**Initialization**:
558+
**Initialization (after Phase 1.5 batching)**:
559+
```javascript
560+
TodoWrite({
561+
todos: [
562+
{content: "Phase 1: Discovery & Initialization", status: "completed", activeForm: "Discovering"},
563+
{content: "Phase 1.5: Intelligent Batching", status: "completed", activeForm: "Batching"},
564+
{content: "Phase 2: Parallel Planning", status: "in_progress", activeForm: "Planning"},
565+
{content: " → Batch 1: 4 findings (auth.ts:security)", status: "pending", activeForm: "Planning batch 1"},
566+
{content: " → Batch 2: 3 findings (query.ts:security)", status: "pending", activeForm: "Planning batch 2"},
567+
{content: " → Batch 3: 2 findings (config.ts:quality)", status: "pending", activeForm: "Planning batch 3"},
568+
{content: "Phase 3: Execution", status: "pending", activeForm: "Executing"},
569+
{content: "Phase 4: Completion", status: "pending", activeForm: "Completing"}
570+
]
571+
});
572+
```
573+
574+
**During Planning (parallel agents running)**:
553575
```javascript
554576
TodoWrite({
555577
todos: [
556-
{content: "Phase 1: Discovery & Initialization", status: "completed"},
557-
{content: "Phase 2: Planning", status: "in_progress"},
558-
{content: "Phase 3: Execution", status: "pending"},
559-
{content: "Phase 4: Completion", status: "pending"}
578+
{content: "Phase 1: Discovery & Initialization", status: "completed", activeForm: "Discovering"},
579+
{content: "Phase 1.5: Intelligent Batching", status: "completed", activeForm: "Batching"},
580+
{content: "Phase 2: Parallel Planning", status: "in_progress", activeForm: "Planning"},
581+
{content: " → Batch 1: 4 findings (auth.ts:security)", status: "completed", activeForm: "Planning batch 1"},
582+
{content: " → Batch 2: 3 findings (query.ts:security)", status: "in_progress", activeForm: "Planning batch 2"},
583+
{content: " → Batch 3: 2 findings (config.ts:quality)", status: "in_progress", activeForm: "Planning batch 3"},
584+
{content: "Phase 3: Execution", status: "pending", activeForm: "Executing"},
585+
{content: "Phase 4: Completion", status: "pending", activeForm: "Completing"}
560586
]
561587
});
562588
```
@@ -565,23 +591,25 @@ TodoWrite({
565591
```javascript
566592
TodoWrite({
567593
todos: [
568-
{content: "Phase 1: Discovery & Initialization", status: "completed"},
569-
{content: "Phase 2: Planning", status: "completed"},
570-
{content: "Phase 3: Execution", status: "in_progress"},
571-
{content: " → Stage 1: Parallel execution (3 groups)", status: "completed"},
572-
{content: " • Group G1: Auth validation (2 findings)", status: "completed"},
573-
{content: " • Group G2: Query security (3 findings)", status: "completed"},
574-
{content: " • Group G3: Config quality (1 finding)", status: "completed"},
575-
{content: " → Stage 2: Serial execution (1 group)", status: "in_progress"},
576-
{content: " • Group G4: Dependent fixes (2 findings)", status: "in_progress"},
577-
{content: "Phase 4: Completion", status: "pending"}
594+
{content: "Phase 1: Discovery & Initialization", status: "completed", activeForm: "Discovering"},
595+
{content: "Phase 1.5: Intelligent Batching", status: "completed", activeForm: "Batching"},
596+
{content: "Phase 2: Parallel Planning (3 batches → 5 groups)", status: "completed", activeForm: "Planning"},
597+
{content: "Phase 3: Execution", status: "in_progress", activeForm: "Executing"},
598+
{content: " → Stage 1: Parallel execution (3 groups)", status: "completed", activeForm: "Executing stage 1"},
599+
{content: " • Group G1: Auth validation (2 findings)", status: "completed", activeForm: "Fixing G1"},
600+
{content: " • Group G2: Query security (3 findings)", status: "completed", activeForm: "Fixing G2"},
601+
{content: " • Group G3: Config quality (1 finding)", status: "completed", activeForm: "Fixing G3"},
602+
{content: " → Stage 2: Serial execution (1 group)", status: "in_progress", activeForm: "Executing stage 2"},
603+
{content: " • Group G4: Dependent fixes (2 findings)", status: "in_progress", activeForm: "Fixing G4"},
604+
{content: "Phase 4: Completion", status: "pending", activeForm: "Completing"}
578605
]
579606
});
580607
```
581608

582609
**Update Rules**:
583-
- Add stage items dynamically based on fix-plan.json timeline
584-
- Add group items per stage
610+
- Add batch items dynamically during Phase 1.5
611+
- Mark batch items completed as parallel agents return results
612+
- Add stage/group items dynamically after Phase 2 plan aggregation
585613
- Mark completed immediately after each group finishes
586614
- Update parent phase status when all child items complete
587615

@@ -591,12 +619,13 @@ TodoWrite({
591619

592620
## Best Practices
593621

594-
1. **Trust AI Planning**: Planning agent's grouping and execution strategy are based on dependency analysis
595-
2. **Conservative Approach**: Test verification is mandatory - no fixes kept without passing tests
596-
3. **Parallel Efficiency**: Default 3 concurrent agents balances speed and resource usage
597-
4. **Resume Support**: Fix sessions can resume from checkpoints after interruption
598-
5. **Manual Review**: Always review failed fixes manually - may require architectural changes
599-
6. **Incremental Fixing**: Start with small batches (5-10 findings) before large-scale fixes
622+
1. **Leverage Parallel Planning**: For 10+ findings, parallel batching significantly reduces planning time
623+
2. **Tune Batch Size**: Use `--batch-size` to control granularity (smaller batches = more parallelism, larger = better grouping context)
624+
3. **Conservative Approach**: Test verification is mandatory - no fixes kept without passing tests
625+
4. **Parallel Efficiency**: MAX_PARALLEL=10 for planning agents, 3 concurrent execution agents per stage
626+
5. **Resume Support**: Fix sessions can resume from checkpoints after interruption
627+
6. **Manual Review**: Always review failed fixes manually - may require architectural changes
628+
7. **Incremental Fixing**: Start with small batches (5-10 findings) before large-scale fixes
600629

601630
## Related Commands
602631

ccw/frontend/src/components/cli-viewer/TabBar.tsx

Lines changed: 149 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
// ========================================
22
// TabBar Component
33
// ========================================
4-
// Tab management for CLI viewer panes
4+
// Tab management for CLI viewer panes with drag-and-drop support
55

6-
import { useCallback, useMemo } from 'react';
6+
import { useCallback, useMemo, useState } from 'react';
77
import { useIntl } from 'react-intl';
88
import { X, Pin, PinOff, MoreHorizontal, SplitSquareHorizontal, SplitSquareVertical } from 'lucide-react';
99
import { cn } from '@/lib/utils';
@@ -14,7 +14,7 @@ import {
1414
DropdownMenuItem,
1515
DropdownMenuSeparator,
1616
DropdownMenuTrigger,
17-
} from '@/components/ui/DropdownMenu';
17+
} from '@/components/ui/Dropdown';
1818
import {
1919
useViewerStore,
2020
useViewerPanes,
@@ -32,6 +32,7 @@ export interface TabBarProps {
3232

3333
interface TabItemProps {
3434
tab: TabState;
35+
paneId: PaneId;
3536
isActive: boolean;
3637
onSelect: () => void;
3738
onClose: (e: React.MouseEvent) => void;
@@ -49,28 +50,117 @@ const STATUS_COLORS = {
4950

5051
// ========== Helper Components ==========
5152

53+
// Data transfer key for tab drag-and-drop
54+
const TAB_DRAG_DATA_TYPE = 'application/x-cli-viewer-tab';
55+
56+
interface TabDragData {
57+
tabId: string;
58+
sourcePaneId: string;
59+
}
60+
5261
/**
53-
* Individual tab item
62+
* Individual tab item with drag-and-drop support
5463
*/
55-
function TabItem({ tab, isActive, onSelect, onClose, onTogglePin }: TabItemProps) {
64+
function TabItem({ tab, paneId, isActive, onSelect, onClose, onTogglePin }: TabItemProps) {
65+
const [isDragging, setIsDragging] = useState(false);
66+
const [isDragOver, setIsDragOver] = useState(false);
67+
const moveTab = useViewerStore((state) => state.moveTab);
68+
const panes = useViewerPanes();
69+
5670
// Simplify title for display
5771
const displayTitle = useMemo(() => {
5872
// If title contains tool name pattern, extract it
5973
const parts = tab.title.split('-');
6074
return parts[0] || tab.title;
6175
}, [tab.title]);
6276

77+
// Drag start handler
78+
const handleDragStart = useCallback((e: React.DragEvent) => {
79+
const dragData: TabDragData = {
80+
tabId: tab.id,
81+
sourcePaneId: paneId,
82+
};
83+
e.dataTransfer.setData(TAB_DRAG_DATA_TYPE, JSON.stringify(dragData));
84+
e.dataTransfer.effectAllowed = 'move';
85+
setIsDragging(true);
86+
}, [tab.id, paneId]);
87+
88+
// Drag end handler
89+
const handleDragEnd = useCallback(() => {
90+
setIsDragging(false);
91+
}, []);
92+
93+
// Drag over handler
94+
const handleDragOver = useCallback((e: React.DragEvent) => {
95+
if (e.dataTransfer.types.includes(TAB_DRAG_DATA_TYPE)) {
96+
e.preventDefault();
97+
e.dataTransfer.dropEffect = 'move';
98+
setIsDragOver(true);
99+
}
100+
}, []);
101+
102+
// Drag leave handler
103+
const handleDragLeave = useCallback(() => {
104+
setIsDragOver(false);
105+
}, []);
106+
107+
// Drop handler
108+
const handleDrop = useCallback((e: React.DragEvent) => {
109+
e.preventDefault();
110+
setIsDragOver(false);
111+
112+
const rawData = e.dataTransfer.getData(TAB_DRAG_DATA_TYPE);
113+
if (!rawData) return;
114+
115+
try {
116+
const dragData: TabDragData = JSON.parse(rawData);
117+
const { tabId: sourceTabId, sourcePaneId } = dragData;
118+
119+
// Don't do anything if dropping on the same tab
120+
if (sourceTabId === tab.id) return;
121+
122+
// Find the target index
123+
const targetPane = panes[paneId];
124+
if (!targetPane) return;
125+
126+
const targetIndex = targetPane.tabs.findIndex((t) => t.id === tab.id);
127+
if (targetIndex === -1) return;
128+
129+
// Move the tab
130+
moveTab(sourcePaneId, sourceTabId, paneId, targetIndex);
131+
} catch (err) {
132+
console.error('[TabBar] Failed to parse drag data:', err);
133+
}
134+
}, [tab.id, paneId, panes, moveTab]);
135+
63136
return (
64-
<button
137+
<div
138+
role="tab"
139+
tabIndex={0}
140+
draggable={!tab.isPinned}
141+
onDragStart={handleDragStart}
142+
onDragEnd={handleDragEnd}
143+
onDragOver={handleDragOver}
144+
onDragLeave={handleDragLeave}
145+
onDrop={handleDrop}
65146
onClick={onSelect}
147+
onKeyDown={(e) => {
148+
if (e.key === 'Enter' || e.key === ' ') {
149+
e.preventDefault();
150+
onSelect();
151+
}
152+
}}
66153
className={cn(
67154
'group relative flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-xs',
68155
'border border-border/50 shrink-0 min-w-0 max-w-[160px]',
69-
'transition-all duration-150',
156+
'transition-all duration-150 select-none',
70157
isActive
71158
? 'bg-slate-100 dark:bg-slate-800 border-slate-300 dark:border-slate-600 shadow-sm'
72159
: 'bg-muted/30 hover:bg-muted/50 border-border/30',
73-
tab.isPinned && 'border-amber-500/50'
160+
tab.isPinned && 'border-amber-500/50',
161+
isDragging && 'opacity-50 cursor-grabbing',
162+
isDragOver && 'border-primary border-dashed bg-primary/10',
163+
!tab.isPinned && 'cursor-grab'
74164
)}
75165
title={tab.title}
76166
>
@@ -111,7 +201,7 @@ function TabItem({ tab, isActive, onSelect, onClose, onTogglePin }: TabItemProps
111201
</button>
112202
)}
113203
</div>
114-
</button>
204+
</div>
115205
);
116206
}
117207

@@ -125,17 +215,20 @@ function TabItem({ tab, isActive, onSelect, onClose, onTogglePin }: TabItemProps
125215
* - Active tab highlighting
126216
* - Close button on hover
127217
* - Pin/unpin functionality
218+
* - Drag-and-drop tab reordering and moving between panes
128219
* - Pane actions dropdown
129220
*/
130221
export function TabBar({ paneId, className }: TabBarProps) {
131222
const { formatMessage } = useIntl();
223+
const [isDragOver, setIsDragOver] = useState(false);
132224
const panes = useViewerPanes();
133225
const pane = panes[paneId];
134226
const setActiveTab = useViewerStore((state) => state.setActiveTab);
135227
const removeTab = useViewerStore((state) => state.removeTab);
136228
const togglePinTab = useViewerStore((state) => state.togglePinTab);
137229
const addPane = useViewerStore((state) => state.addPane);
138230
const removePane = useViewerStore((state) => state.removePane);
231+
const moveTab = useViewerStore((state) => state.moveTab);
139232

140233
const handleTabSelect = useCallback(
141234
(tabId: string) => {
@@ -172,6 +265,43 @@ export function TabBar({ paneId, className }: TabBarProps) {
172265
removePane(paneId);
173266
}, [paneId, removePane]);
174267

268+
// Drag over handler for tab bar container (allows dropping to end of list)
269+
const handleContainerDragOver = useCallback((e: React.DragEvent) => {
270+
if (e.dataTransfer.types.includes(TAB_DRAG_DATA_TYPE)) {
271+
e.preventDefault();
272+
e.dataTransfer.dropEffect = 'move';
273+
setIsDragOver(true);
274+
}
275+
}, []);
276+
277+
// Drag leave handler for container
278+
const handleContainerDragLeave = useCallback((e: React.DragEvent) => {
279+
// Only set false if leaving the container entirely, not just moving to a child
280+
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
281+
setIsDragOver(false);
282+
}
283+
}, []);
284+
285+
// Drop handler for tab bar container (drops to end of list)
286+
const handleContainerDrop = useCallback((e: React.DragEvent) => {
287+
e.preventDefault();
288+
setIsDragOver(false);
289+
290+
const rawData = e.dataTransfer.getData(TAB_DRAG_DATA_TYPE);
291+
if (!rawData) return;
292+
293+
try {
294+
const dragData: TabDragData = JSON.parse(rawData);
295+
const { tabId: sourceTabId, sourcePaneId } = dragData;
296+
297+
// Move the tab to the end of this pane
298+
const targetIndex = pane?.tabs.length || 0;
299+
moveTab(sourcePaneId, sourceTabId, paneId, targetIndex);
300+
} catch (err) {
301+
console.error('[TabBar] Failed to parse drag data:', err);
302+
}
303+
}, [paneId, pane, moveTab]);
304+
175305
// Sort tabs: pinned first, then by order
176306
const sortedTabs = useMemo(() => {
177307
if (!pane) return [];
@@ -197,7 +327,15 @@ export function TabBar({ paneId, className }: TabBarProps) {
197327
)}
198328
>
199329
{/* Tabs */}
200-
<div className="flex items-center gap-1 flex-1 min-w-0 overflow-x-auto">
330+
<div
331+
onDragOver={handleContainerDragOver}
332+
onDragLeave={handleContainerDragLeave}
333+
onDrop={handleContainerDrop}
334+
className={cn(
335+
'flex items-center gap-1 flex-1 min-w-0 overflow-x-auto',
336+
isDragOver && 'bg-primary/5 border border-primary border-dashed rounded'
337+
)}
338+
>
201339
{sortedTabs.length === 0 ? (
202340
<span className="text-xs text-muted-foreground px-2">
203341
{formatMessage({ id: 'cliViewer.tabs.noTabs', defaultMessage: 'No tabs open' })}
@@ -207,6 +345,7 @@ export function TabBar({ paneId, className }: TabBarProps) {
207345
<TabItem
208346
key={tab.id}
209347
tab={tab}
348+
paneId={paneId}
210349
isActive={pane.activeTabId === tab.id}
211350
onSelect={() => handleTabSelect(tab.id)}
212351
onClose={(e) => handleTabClose(e, tab.id)}

0 commit comments

Comments
 (0)