mirror of
https://github.com/ValueCell-ai/ClawX.git
synced 2026-08-16 01:41:58 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0116f6e968 |
@@ -1,96 +0,0 @@
|
||||
/**
|
||||
* Window Management Utilities
|
||||
* Handles window state persistence and multi-window management
|
||||
*/
|
||||
import { BrowserWindow, screen } from 'electron';
|
||||
|
||||
interface WindowState {
|
||||
x?: number;
|
||||
y?: number;
|
||||
width: number;
|
||||
height: number;
|
||||
isMaximized: boolean;
|
||||
}
|
||||
|
||||
// Lazy-load electron-store (ESM module)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let windowStateStore: any = null;
|
||||
|
||||
async function getStore() {
|
||||
if (!windowStateStore) {
|
||||
const Store = (await import('electron-store')).default;
|
||||
windowStateStore = new Store<{ windowState: WindowState }>({
|
||||
name: 'window-state',
|
||||
defaults: {
|
||||
windowState: {
|
||||
width: 1280,
|
||||
height: 800,
|
||||
isMaximized: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
return windowStateStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get saved window state with bounds validation
|
||||
*/
|
||||
export async function getWindowState(): Promise<WindowState> {
|
||||
const store = await getStore();
|
||||
const state = store.get('windowState');
|
||||
|
||||
// Validate that the window is visible on a screen
|
||||
if (state.x !== undefined && state.y !== undefined) {
|
||||
const displays = screen.getAllDisplays();
|
||||
const isVisible = displays.some((display) => {
|
||||
const { x, y, width, height } = display.bounds;
|
||||
return (
|
||||
state.x! >= x &&
|
||||
state.x! < x + width &&
|
||||
state.y! >= y &&
|
||||
state.y! < y + height
|
||||
);
|
||||
});
|
||||
|
||||
if (!isVisible) {
|
||||
// Reset position if not visible
|
||||
delete state.x;
|
||||
delete state.y;
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save window state
|
||||
*/
|
||||
export async function saveWindowState(win: BrowserWindow): Promise<void> {
|
||||
const store = await getStore();
|
||||
const isMaximized = win.isMaximized();
|
||||
|
||||
if (!isMaximized) {
|
||||
const bounds = win.getBounds();
|
||||
store.set('windowState', {
|
||||
x: bounds.x,
|
||||
y: bounds.y,
|
||||
width: bounds.width,
|
||||
height: bounds.height,
|
||||
isMaximized,
|
||||
});
|
||||
} else {
|
||||
store.set('windowState.isMaximized', true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Track window state changes
|
||||
*/
|
||||
export function trackWindowState(win: BrowserWindow): void {
|
||||
// Save state on window events
|
||||
['resize', 'move', 'close'].forEach((event) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
win.on(event as any, () => saveWindowState(win));
|
||||
});
|
||||
}
|
||||
@@ -95,6 +95,7 @@
|
||||
"electron-updater": "^6.8.3",
|
||||
"json5": "2.2.3",
|
||||
"node-machine-id": "^1.1.12",
|
||||
"pino": "^9.14.0",
|
||||
"posthog-node": "^5.28.0",
|
||||
"tar": "^6.2.1",
|
||||
"timeago.js": "^4.0.2",
|
||||
|
||||
Generated
+7
-4
@@ -31,6 +31,9 @@ importers:
|
||||
node-machine-id:
|
||||
specifier: ^1.1.12
|
||||
version: 1.1.12
|
||||
pino:
|
||||
specifier: ^9.14.0
|
||||
version: 9.14.0
|
||||
posthog-node:
|
||||
specifier: ^5.28.0
|
||||
version: 5.28.5
|
||||
@@ -4677,8 +4680,8 @@ packages:
|
||||
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
libsignal@git+https://git@github.com:whiskeysockets/libsignal-node.git#bcea72df9ec34d9d9140ab30619cf479c7c144c7:
|
||||
resolution: {commit: bcea72df9ec34d9d9140ab30619cf479c7c144c7, repo: git@github.com:whiskeysockets/libsignal-node.git, type: git}
|
||||
libsignal@https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/bcea72df9ec34d9d9140ab30619cf479c7c144c7:
|
||||
resolution: {gitHosted: true, tarball: https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/bcea72df9ec34d9d9140ab30619cf479c7c144c7}
|
||||
version: 6.0.0
|
||||
|
||||
lie@3.3.0:
|
||||
@@ -9571,7 +9574,7 @@ snapshots:
|
||||
'@cacheable/node-cache': 1.7.6
|
||||
'@hapi/boom': 9.1.4
|
||||
async-mutex: 0.5.0
|
||||
libsignal: git+https://git@github.com:whiskeysockets/libsignal-node.git#bcea72df9ec34d9d9140ab30619cf479c7c144c7
|
||||
libsignal: https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/bcea72df9ec34d9d9140ab30619cf479c7c144c7
|
||||
lru-cache: 11.2.7
|
||||
music-metadata: 11.12.3
|
||||
p-queue: 9.1.0
|
||||
@@ -11829,7 +11832,7 @@ snapshots:
|
||||
prelude-ls: 1.2.1
|
||||
type-check: 0.4.0
|
||||
|
||||
libsignal@git+https://git@github.com:whiskeysockets/libsignal-node.git#bcea72df9ec34d9d9140ab30619cf479c7c144c7:
|
||||
libsignal@https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/bcea72df9ec34d9d9140ab30619cf479c7c144c7:
|
||||
dependencies:
|
||||
curve25519-js: 0.0.4
|
||||
protobufjs: 7.5.8
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
/**
|
||||
* Error Boundary Component
|
||||
* Catches and displays errors in the component tree
|
||||
*/
|
||||
import { Component, ReactNode } from 'react';
|
||||
import { AlertTriangle, RefreshCw } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
fallback?: ReactNode;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
export class ErrorBoundary extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { hasError: false, error: null };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||
console.error('Error caught by boundary:', error, errorInfo);
|
||||
}
|
||||
|
||||
handleReset = () => {
|
||||
this.setState({ hasError: false, error: null });
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
if (this.props.fallback) {
|
||||
return this.props.fallback;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center p-6">
|
||||
<Card className="max-w-md">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertTriangle className="h-6 w-6 text-destructive" />
|
||||
<CardTitle>Something went wrong</CardTitle>
|
||||
</div>
|
||||
<CardDescription>
|
||||
An unexpected error occurred. Please try again.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{this.state.error && (
|
||||
<pre className="rounded-lg bg-surface-input p-4 text-sm overflow-auto max-h-40">
|
||||
{this.state.error.message}
|
||||
</pre>
|
||||
)}
|
||||
<Button onClick={this.handleReset} className="w-full">
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
Try Again
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
/**
|
||||
* Status Badge Component
|
||||
* Displays connection/state status with color coding
|
||||
*/
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
export type Status = 'connected' | 'disconnected' | 'connecting' | 'error' | 'running' | 'stopped' | 'starting' | 'reconnecting';
|
||||
|
||||
interface StatusBadgeProps {
|
||||
status: Status;
|
||||
label?: string;
|
||||
showDot?: boolean;
|
||||
}
|
||||
|
||||
const statusConfig: Record<Status, { label: string; variant: 'success' | 'secondary' | 'warning' | 'destructive' }> = {
|
||||
connected: { label: 'Connected', variant: 'success' },
|
||||
running: { label: 'Running', variant: 'success' },
|
||||
disconnected: { label: 'Disconnected', variant: 'secondary' },
|
||||
stopped: { label: 'Stopped', variant: 'secondary' },
|
||||
connecting: { label: 'Connecting', variant: 'warning' },
|
||||
starting: { label: 'Starting', variant: 'warning' },
|
||||
reconnecting: { label: 'Reconnecting', variant: 'warning' },
|
||||
error: { label: 'Error', variant: 'destructive' },
|
||||
};
|
||||
|
||||
export function StatusBadge({ status, label, showDot = true }: StatusBadgeProps) {
|
||||
const config = statusConfig[status];
|
||||
const displayLabel = label || config.label;
|
||||
|
||||
return (
|
||||
<Badge variant={config.variant} className="gap-1.5">
|
||||
{showDot && (
|
||||
<span
|
||||
className={cn(
|
||||
'h-1.5 w-1.5 rounded-full',
|
||||
config.variant === 'success' && 'bg-green-600',
|
||||
config.variant === 'secondary' && 'bg-muted-foreground',
|
||||
config.variant === 'warning' && 'bg-yellow-600 animate-pulse',
|
||||
config.variant === 'destructive' && 'bg-red-600'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{displayLabel}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ComponentRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 min-w-44 overflow-hidden rounded-md border border-border bg-surface-modal p-1 text-popover-foreground shadow-md',
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
));
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ComponentRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors',
|
||||
'focus:bg-black/5 focus:text-foreground dark:focus:bg-white/10',
|
||||
'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
export { rendererExtensionRegistry } from './registry';
|
||||
export { registerRendererExtensionModule, loadRendererExtensions } from './loader';
|
||||
export type {
|
||||
RendererExtension,
|
||||
NavItemDef,
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { rendererExtensionRegistry } from './registry';
|
||||
import type { RendererExtension } from './types';
|
||||
|
||||
interface RendererExtensionManifest {
|
||||
extensions?: {
|
||||
renderer?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
const registeredModules = new Map<string, () => RendererExtension>();
|
||||
|
||||
export function registerRendererExtensionModule(id: string, factory: () => RendererExtension): void {
|
||||
registeredModules.set(id, factory);
|
||||
}
|
||||
|
||||
export function loadRendererExtensions(manifest?: RendererExtensionManifest): void {
|
||||
const extensionIds = manifest?.extensions?.renderer;
|
||||
|
||||
if (!extensionIds || extensionIds.length === 0) {
|
||||
for (const [, factory] of registeredModules) {
|
||||
rendererExtensionRegistry.register(factory());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (const id of extensionIds) {
|
||||
const factory = registeredModules.get(id);
|
||||
if (factory) {
|
||||
rendererExtensionRegistry.register(factory());
|
||||
} else {
|
||||
console.warn(`[extensions] Renderer extension "${id}" not found in registered modules`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* A hook that ensures a loading state remains true for at least a minimum duration (e.g., 1000ms),
|
||||
* preventing flickering for very fast loading times.
|
||||
*
|
||||
* @param isLoading - The actual loading state from the data source
|
||||
* @param minDurationMs - Minimum duration to show loading (default: 1000)
|
||||
*/
|
||||
export function useMinLoading(isLoading: boolean, minDurationMs: number = 500) {
|
||||
const [showLoading, setShowLoading] = useState(isLoading);
|
||||
const startTime = useRef<number>(0);
|
||||
|
||||
// Guarantee that the loading UI activates immediately without any asynchronous delay
|
||||
if (isLoading && !showLoading) {
|
||||
setShowLoading(true);
|
||||
}
|
||||
|
||||
// Record the actual timestamp in an effect to respect React purity rules
|
||||
useEffect(() => {
|
||||
if (isLoading && startTime.current === 0) {
|
||||
startTime.current = Date.now();
|
||||
}
|
||||
}, [isLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
let timeout: NodeJS.Timeout;
|
||||
|
||||
if (!isLoading && showLoading) {
|
||||
const elapsed = startTime.current > 0 ? Date.now() - startTime.current : 0;
|
||||
const remaining = Math.max(0, minDurationMs - elapsed);
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
setShowLoading(false);
|
||||
startTime.current = 0;
|
||||
}, remaining);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timeout) clearTimeout(timeout);
|
||||
};
|
||||
}, [isLoading, showLoading, minDurationMs]);
|
||||
|
||||
return isLoading || showLoading;
|
||||
}
|
||||
@@ -115,10 +115,6 @@ vi.mock('@/hooks/use-stick-to-bottom-instant', () => ({
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/use-min-loading', () => ({
|
||||
useMinLoading: () => false,
|
||||
}));
|
||||
|
||||
vi.mock('@/pages/Chat/ChatToolbar', () => ({
|
||||
ChatToolbar: () => null,
|
||||
}));
|
||||
|
||||
@@ -137,10 +137,6 @@ vi.mock('@/hooks/use-stick-to-bottom-instant', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/use-min-loading', () => ({
|
||||
useMinLoading: () => false,
|
||||
}));
|
||||
|
||||
vi.mock('@/pages/Chat/ChatToolbar', () => ({
|
||||
ChatToolbar: () => <div data-testid="mock-chat-toolbar" />,
|
||||
}));
|
||||
|
||||
@@ -71,10 +71,6 @@ vi.mock('@/hooks/use-stick-to-bottom-instant', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/use-min-loading', () => ({
|
||||
useMinLoading: (value: boolean) => value,
|
||||
}));
|
||||
|
||||
vi.mock('@/pages/Chat/ChatInput', () => ({
|
||||
ChatInput: () => <div data-testid="chat-input" />,
|
||||
}));
|
||||
|
||||
@@ -97,10 +97,6 @@ vi.mock('@/hooks/use-stick-to-bottom-instant', () => ({
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/use-min-loading', () => ({
|
||||
useMinLoading: () => false,
|
||||
}));
|
||||
|
||||
vi.mock('@/pages/Chat/ChatToolbar', () => ({ ChatToolbar: () => null }));
|
||||
vi.mock('@/pages/Chat/ChatInput', () => ({ ChatInput: () => null }));
|
||||
|
||||
|
||||
@@ -102,10 +102,6 @@ vi.mock('@/hooks/use-stick-to-bottom-instant', () => ({
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/use-min-loading', () => ({
|
||||
useMinLoading: () => false,
|
||||
}));
|
||||
|
||||
vi.mock('@/pages/Chat/ChatToolbar', () => ({ ChatToolbar: () => null }));
|
||||
vi.mock('@/pages/Chat/ChatInput', () => ({ ChatInput: () => null }));
|
||||
|
||||
|
||||
@@ -102,10 +102,6 @@ vi.mock('@/hooks/use-stick-to-bottom-instant', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/use-min-loading', () => ({
|
||||
useMinLoading: () => false,
|
||||
}));
|
||||
|
||||
vi.mock('@/pages/Chat/ChatInput', () => ({
|
||||
ChatInput: () => null,
|
||||
}));
|
||||
|
||||
@@ -102,10 +102,6 @@ vi.mock('@/hooks/use-stick-to-bottom-instant', () => ({
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/use-min-loading', () => ({
|
||||
useMinLoading: () => false,
|
||||
}));
|
||||
|
||||
vi.mock('@/pages/Chat/ChatToolbar', () => ({
|
||||
ChatToolbar: () => null,
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user