feat: add CLI Viewer Page with multi-pane layout and state management

- Implemented the CliViewerPage component for displaying CLI outputs in a configurable multi-pane layout.
- Integrated Zustand for state management, allowing for dynamic layout changes and tab management.
- Added layout options: single, split horizontal, split vertical, and 2x2 grid.
- Created viewerStore for managing layout, panes, and tabs, including actions for adding/removing panes and tabs.
- Added CoordinatorPage barrel export for easier imports.
This commit is contained in:
catlog22
2026-02-03 17:28:26 +08:00
parent b63e254f36
commit 37ba849e75
101 changed files with 10422 additions and 1145 deletions

View File

@@ -3,10 +3,10 @@
// ========================================
// Convenient hook for locale management
import { useCallback } from 'react';
import { useCallback, useMemo } from 'react';
import { useAppStore, selectLocale } from '../stores/appStore';
import type { Locale } from '../types/store';
import { availableLocales } from '../lib/i18n';
import { availableLocales, formatMessage } from '../lib/i18n';
export interface UseLocaleReturn {
/** Current locale ('en' or 'zh') */
@@ -51,3 +51,25 @@ export function useLocale(): UseLocaleReturn {
availableLocales,
};
}
/**
* Hook to format i18n messages with the current locale
* @returns A formatMessage function for translating message IDs
*
* @example
* ```tsx
* const formatMessage = useFormatMessage();
* return <h1>{formatMessage('home.title')}</h1>;
* ```
*/
export function useFormatMessage(): (
id: string,
values?: Record<string, string | number | boolean | Date | null | undefined>
) => string {
// Use useMemo to avoid recreating the function on each render
return useMemo(() => {
return (id: string, values?: Record<string, string | number | boolean | Date | null | undefined>) => {
return formatMessage(id, values);
};
}, []);
}

View File

@@ -12,6 +12,9 @@ import { DEFAULT_DASHBOARD_LAYOUT } from '@/components/dashboard/defaultLayouts'
const DEBOUNCE_DELAY = 1000; // 1 second debounce for layout saves
const STORAGE_KEY = 'ccw-dashboard-layout';
// Version for layout schema - increment when widget IDs change
const LAYOUT_VERSION = 2; // v2: workflow-task + recent-sessions
export interface UseUserDashboardLayoutResult {
/** Current dashboard layouts */
layouts: DashboardLayouts;
@@ -59,8 +62,36 @@ export function useUserDashboardLayout(): UseUserDashboardLayoutResult {
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
const isSavingRef = useRef(false);
// Initialize layout if not set
// Initialize layout if not set or version mismatch
useEffect(() => {
// Check if stored version matches current version
const storedVersion = localStorage.getItem(`${STORAGE_KEY}-version`);
const versionMismatch = storedVersion !== String(LAYOUT_VERSION);
if (versionMismatch) {
// Version mismatch - reset to default and update version
console.log(`Dashboard layout version changed (${storedVersion} -> ${LAYOUT_VERSION}), resetting to default`);
localStorage.removeItem(STORAGE_KEY);
localStorage.setItem(`${STORAGE_KEY}-version`, String(LAYOUT_VERSION));
// Also clear dashboardLayout from Zustand persist storage
try {
const zustandStorage = localStorage.getItem('ccw-app-store');
if (zustandStorage) {
const parsed = JSON.parse(zustandStorage);
if (parsed.state?.dashboardLayout) {
delete parsed.state.dashboardLayout;
localStorage.setItem('ccw-app-store', JSON.stringify(parsed));
}
}
} catch (e) {
console.warn('Failed to clear Zustand dashboard layout:', e);
}
resetDashboardLayout();
return;
}
if (!dashboardLayout) {
// Try to load from localStorage first
try {
@@ -96,9 +127,10 @@ export function useUserDashboardLayout(): UseUserDashboardLayoutResult {
// Update Zustand store (which will persist to localStorage)
setDashboardLayouts(newLayouts);
// Also save to additional localStorage backup
// Also save to additional localStorage backup with version
const currentWidgets = dashboardLayout?.widgets || DEFAULT_DASHBOARD_LAYOUT.widgets;
setLocalStorageLayout({ layouts: newLayouts, widgets: currentWidgets });
localStorage.setItem(`${STORAGE_KEY}-version`, String(LAYOUT_VERSION));
// TODO: When backend API is ready, uncomment this:
// syncToBackend({ layouts: newLayouts, widgets: currentWidgets });
@@ -114,9 +146,10 @@ export function useUserDashboardLayout(): UseUserDashboardLayoutResult {
(newWidgets: WidgetConfig[]) => {
setDashboardWidgets(newWidgets);
// Also save to localStorage backup
// Also save to localStorage backup with version
const currentLayouts = dashboardLayout?.layouts || DEFAULT_DASHBOARD_LAYOUT.layouts;
setLocalStorageLayout({ layouts: currentLayouts, widgets: newWidgets });
localStorage.setItem(`${STORAGE_KEY}-version`, String(LAYOUT_VERSION));
// TODO: When backend API is ready, uncomment this:
// syncToBackend({ layouts: currentLayouts, widgets: newWidgets });
@@ -134,8 +167,9 @@ export function useUserDashboardLayout(): UseUserDashboardLayoutResult {
// Reset Zustand store
resetDashboardLayout();
// Reset localStorage backup
// Reset localStorage backup with version
setLocalStorageLayout(DEFAULT_DASHBOARD_LAYOUT);
localStorage.setItem(`${STORAGE_KEY}-version`, String(LAYOUT_VERSION));
// TODO: When backend API is ready, uncomment this:
// syncToBackend(DEFAULT_DASHBOARD_LAYOUT);