feat: Implement collapsible sidebar and refined status bar UI (#45)

* chore: mark peer dependencies in package-lock.json

* docs: add comprehensive developer guide for antigravity-manager

* build: add @radix-ui/react-tooltip dependency

The tooltip component is needed for future UI enhancements to provide better user guidance and information display.

* feat(StatusBar): add collapsed view with toggle button

- Introduce `isCollapsed` prop to support both expanded and compact layouts
- Collapsed view shows a single icon button with status tooltip
- Enhanced visual styling with shadows, borders, and improved spacing
- Use Tooltip component to display status in collapsed mode

* feat(ui): add Radix UI tooltip component wrapper

Create a reusable tooltip component using Radix UI primitives.
The wrapper provides styled content with animations and integrates
with the project's class name utility for consistent theming.

* feat(layout): add collapsible sidebar with persistent state

- Add toggle button to collapse/expand sidebar with smooth animations
- Persist sidebar state in localStorage to maintain user preference
- Show tooltips for navigation items when sidebar is collapsed
- Adjust StatusBar component to support collapsed state

* chore: remove "peer": true flags from package-lock.json

* build: mark peer dependencies in package-lock.json

* build: add yaml v2.8.2 dependency and update package-lock

* docs(project_overview): update skill documentation formatting

* docs: remove extra blank line in AGENTS.md

* docs: update changelog formatting to use hyphens

* docs: improve readability of OpenSpec workflow files

* chore: standardize YAML string quotes in GitHub configs

* style(StatusBar): reformat JSX for consistency and readability

- Simplify className expres

* style(ui): standardize quotes and class order in tooltip component

- Use single quotes consistently across imports and strings
- Reorder Tailwind CSS classes to follow recommended conventions

* style(ipc): remove unnecessary parentheses in debug log statement

* style(MainLayout): improve code formatting and consistency

- Reformat multi-line import for better readability
- Normalize Tailwind CSS class ordering
- Remove unnecessary whitespace and ensure consistent quotes
- Maintain all functionality while improving code style

* style(i18n): format long translation strings for better readability

* style: remove unnecessary line break in logger.info call

The logger.info call had an unnecessary line break that made the code less readable. The line has been consolidated to improve code style consistency.

* chore: disable several ESLint rules for development flexibility

Disable TypeScript ESLint rules and other linting rules that are too restrictive during active development. This allows faster iteration without constant linting errors for common patterns like `any` types, unused variables, and inline comments.

* refactor: use const for immutable variables in request mapper

- Change `let` to `const` for variables that are not reassigned
- Improves code clarity and prevents accidental reassignment

* refactor: replace direct hasOwnProperty calls with safe variant

Use Object.prototype.hasOwnProperty.call to avoid issues when objects have overridden hasOwnProperty method. This ensures consistent behavior across all object types.

* fix: disable eslint for intentional assignment in useEffect

The eslint warning was triggered by directly using setProxyConfig with config.proxy inside useEffect without including it in the dependency array. This assignment is intentional and only needs to run when config changes, not when setProxyConfig changes. Adding the eslint-disable comment prevents the warning while maintaining correct behavior.

* chore: disable react-hooks/exhaustive-deps rule

This rule was already disabled; the change only adds a trailing comma to the ESLint configuration for consistency.

* fix(eslint): re-enable lint rules for hooks and case-declarations

---------

Co-authored-by: Draculabo <[email protected]>
This commit is contained in:
Sujit BiswasandDraculabo authored and GitHub committed 2026-01-28 18:55:40 +08:00
1 parent 9ca0ad0396
commit 1265d044f6
21 files changed
+531 -96

No files matched your search

+79 -18
View File
@@ -3,10 +3,15 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { isProcessRunning, startAntigravity, closeAntigravity } from '@/actions/process';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { Play, Square, Loader2 } from 'lucide-react';
import { Play, Square, Loader2, Power } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
export const StatusBar: React.FC = () => {
interface StatusBarProps {
isCollapsed?: boolean;
}
export const StatusBar: React.FC<StatusBarProps> = ({ isCollapsed = false }) => {
const { t } = useTranslation();
const queryClient = useQueryClient();
@@ -40,26 +45,78 @@ export const StatusBar: React.FC = () => {
const isPending = startMutation.isPending || stopMutation.isPending;
// Collapsed View
if (isCollapsed) {
return (
<div className="flex justify-center py-2">
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={handleToggle}
disabled={isLoading || isPending}
className={cn(
'h-10 w-10 rounded-lg transition-all duration-300',
isRunning
? 'bg-green-100/50 text-green-700 hover:bg-green-100 hover:text-green-800 dark:bg-green-900/30 dark:text-green-400 dark:hover:bg-green-900/50'
: 'bg-red-100/50 text-red-700 hover:bg-red-100 hover:text-red-800 dark:bg-red-900/30 dark:text-red-400 dark:hover:bg-red-900/50',
)}
>
{isPending ? (
<Loader2 className="h-5 w-5 animate-spin" />
) : (
<Power className="h-5 w-5" />
)}
</Button>
</TooltipTrigger>
<TooltipContent side="right" className="flex items-center gap-2">
<div
className={cn('h-2 w-2 rounded-full', isRunning ? 'bg-green-500' : 'bg-red-500')}
/>
<p>
{isLoading
? t('status.checking')
: isRunning
? t('status.running')
: t('status.stopped')}
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
);
}
// Expanded View
return (
<div
className={cn(
'flex items-center justify-between px-4 py-2 text-sm font-medium transition-colors',
'flex items-center justify-between overflow-hidden rounded-lg px-3 py-2.5 text-sm font-medium ring-1 transition-colors ring-inset',
isRunning
? 'bg-green-100 text-green-900 dark:bg-green-900/30 dark:text-green-100'
: 'bg-red-100 text-red-900 dark:bg-red-900/30 dark:text-red-100',
? 'bg-green-100 text-green-900 ring-green-200 dark:bg-green-900/20 dark:text-green-100 dark:ring-green-900/50'
: 'bg-red-100 text-red-900 ring-red-200 dark:bg-red-900/20 dark:text-red-100 dark:ring-red-900/50',
)}
>
<div className="flex items-center gap-2">
<div className="flex flex-1 items-start gap-3">
{isLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />
<Loader2 className="mt-0.5 h-3.5 w-3.5 shrink-0 animate-spin" />
) : isRunning ? (
<div className="h-3 w-3 rounded-full bg-green-500" />
<div className="mt-1.5 h-2.5 w-2.5 shrink-0 rounded-full bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.5)]" />
) : (
<div className="h-3 w-3 rounded-full bg-red-500" />
<div className="mt-1.5 h-2.5 w-2.5 shrink-0 rounded-full bg-red-500 shadow-[0_0_8px_rgba(239,68,68,0.5)]" />
)}
<span>
{isLoading ? t('status.checking') : isRunning ? t('status.running') : t('status.stopped')}
</span>
<div className="flex flex-col gap-1 leading-none">
<span className="text-xs font-semibold tracking-wider uppercase opacity-80">Status</span>
<span className="text-sm leading-tight font-medium">
{isLoading
? t('status.checking')
: isRunning
? t('status.running')
: t('status.stopped')}
</span>
</div>
</div>
<Button
variant="ghost"
@@ -67,18 +124,22 @@ export const StatusBar: React.FC = () => {
onClick={handleToggle}
disabled={isLoading || isPending}
className={cn(
'h-8 px-3 hover:bg-white/20',
isRunning ? 'hover:text-green-900' : 'hover:text-red-900',
'ml-2 h-8 shrink-0 rounded-md border px-3 transition-all',
isRunning
? 'border-green-200 bg-green-200/50 text-green-900 hover:bg-green-200 dark:border-green-800 dark:bg-green-800/30 dark:text-green-100 dark:hover:bg-green-800/50'
: 'border-red-200 bg-red-200/50 text-red-900 hover:bg-red-200 dark:border-red-800 dark:bg-red-800/30 dark:text-red-100 dark:hover:bg-red-800/50',
)}
>
{isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : isRunning ? (
<Square className="mr-2 h-4 w-4 fill-current" />
<Square className="h-3.5 w-3.5 fill-current" />
) : (
<Play className="mr-2 h-4 w-4 fill-current" />
<Play className="h-3.5 w-3.5 fill-current" />
)}
{isRunning ? t('action.stop') : t('action.start')}
<span className="ml-2 font-semibold">
{isRunning ? t('action.stop') : t('action.start')}
</span>
</Button>
</div>
);
+28
View File
@@ -0,0 +1,28 @@
import * as React from 'react';
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
import { cn } from '@/lib/utils';
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
'bg-popover text-popover-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 origin-[--radix-tooltip-content-transform-origin] overflow-hidden rounded-md border px-3 py-1.5 text-sm shadow-md',
className,
)}
{...props}
/>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
+1 -3
View File
@@ -100,9 +100,7 @@ export async function isProcessRunning(): Promise<boolean> {
logger.debug('No Antigravity process found (pgrep returned 1)');
}
logger.debug(
`Found ${processes.length} processes matching 'Antigravity/antigravity'`,
);
logger.debug(`Found ${processes.length} processes matching 'Antigravity/antigravity'`);
for (const proc of processes) {
// Skip self
+109 -27
View File
@@ -1,14 +1,34 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import { Link, Outlet, useLocation } from '@tanstack/react-router';
import { cn } from '@/lib/utils';
import { StatusBar } from '@/components/StatusBar';
import { LayoutDashboard, Settings, Network, Rocket } from 'lucide-react';
import {
LayoutDashboard,
Settings,
Network,
Rocket,
ChevronLeft,
ChevronRight,
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
export const MainLayout: React.FC = () => {
const location = useLocation();
const { t } = useTranslation();
// Initialize state from localStorage if available, default to false (expanded)
const [isCollapsed, setIsCollapsed] = useState(() => {
const saved = localStorage.getItem('sidebar-collapsed');
return saved ? JSON.parse(saved) : false;
});
// Persist state changes
useEffect(() => {
localStorage.setItem('sidebar-collapsed', JSON.stringify(isCollapsed));
}, [isCollapsed]);
const navItems = [
{
to: '/',
@@ -31,42 +51,104 @@ export const MainLayout: React.FC = () => {
<div className="bg-background text-foreground flex h-screen flex-col overflow-hidden">
<div className="flex flex-1 overflow-hidden">
{/* Sidebar */}
<aside className="bg-muted/10 flex w-64 flex-col border-r">
<div className="p-6">
<h1 className="flex items-center gap-2 text-xl font-bold tracking-tight">
<div className="bg-primary text-primary-foreground flex h-6 w-6 items-center justify-center rounded">
<aside
className={cn(
'bg-muted/10 group relative flex flex-col border-r transition-all duration-300 ease-in-out',
isCollapsed ? 'w-[70px]' : 'w-64',
)}
>
{/* Toggle Button - Visible on hover or always if preferred.
Positioned absolute on the border line */}
<Button
variant="ghost"
size="icon"
className="bg-background hover:bg-accent hover:text-accent-foreground absolute top-6 -right-3 z-10 h-6 w-6 rounded-full border opacity-0 shadow-md transition-opacity group-hover:opacity-100"
onClick={() => setIsCollapsed(!isCollapsed)}
>
{isCollapsed ? (
<ChevronRight className="h-3 w-3" />
) : (
<ChevronLeft className="h-3 w-3" />
)}
</Button>
<div className={cn('flex flex-col', isCollapsed ? 'items-center p-4' : 'p-6')}>
<div className="flex items-center gap-2 overflow-hidden whitespace-nowrap">
<div className="bg-primary text-primary-foreground flex h-6 w-6 shrink-0 items-center justify-center rounded">
<Rocket className="h-4 w-4" />
</div>
Antigravity
</h1>
<p className="text-muted-foreground mt-1 text-xs">Manager</p>
</div>
<nav className="flex-1 space-y-2 px-4">
{navItems.map((item) => (
<Link
key={item.to}
to={item.to}
<div
className={cn(
'flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors',
location.pathname === item.to
? 'bg-primary text-primary-foreground'
: 'hover:bg-muted text-muted-foreground hover:text-foreground',
'overflow-hidden transition-all duration-300',
isCollapsed ? 'w-0 opacity-0' : 'w-auto opacity-100',
)}
>
<item.icon className="h-4 w-4" />
{item.label}
</Link>
))}
<h1 className="text-xl font-bold tracking-tight">Antigravity</h1>
</div>
</div>
<div
className={cn(
'text-muted-foreground mt-1 overflow-hidden text-xs whitespace-nowrap transition-all duration-300',
isCollapsed ? 'h-0 opacity-0' : 'h-auto opacity-100',
)}
>
Manager
</div>
</div>
<nav className="flex-1 space-y-2 px-2">
<TooltipProvider>
{navItems.map((item) => {
const isActive = location.pathname === item.to;
if (isCollapsed) {
return (
<Tooltip key={item.to} delayDuration={0}>
<TooltipTrigger asChild>
<Link
to={item.to}
className={cn(
'mx-auto flex h-10 w-10 items-center justify-center rounded-md transition-colors',
isActive
? 'bg-primary text-primary-foreground'
: 'hover:bg-muted text-muted-foreground hover:text-foreground',
)}
>
<item.icon className="h-5 w-5" />
<span className="sr-only">{item.label}</span>
</Link>
</TooltipTrigger>
<TooltipContent side="right">{item.label}</TooltipContent>
</Tooltip>
);
}
return (
<Link
key={item.to}
to={item.to}
className={cn(
'flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors',
isActive
? 'bg-primary text-primary-foreground'
: 'hover:bg-muted text-muted-foreground hover:text-foreground',
)}
>
<item.icon className="h-4 w-4" />
{item.label}
</Link>
);
})}
</TooltipProvider>
</nav>
<div className="border-t p-4">
<StatusBar />
<div className="border-t p-2">
<StatusBar isCollapsed={isCollapsed} />
</div>
</aside>
{/* Content Area */}
<main className="flex-1 overflow-auto">
<main className="flex-1 overflow-auto transition-all duration-300">
<Outlet />
</main>
</div>
+3 -3
View File
@@ -51,7 +51,7 @@ export function transformClaudeRequestIn(
const toolIdToName = new Map<string, string>();
// 1. System Instruction
let systemInstruction = buildSystemInstruction(claudeReq.system, claudeReq.model);
const systemInstruction = buildSystemInstruction(claudeReq.system, claudeReq.model);
// Map model name
const mappedModel = hasWebSearchTool
@@ -100,7 +100,7 @@ export function transformClaudeRequestIn(
);
// 3. Tools
let tools = buildTools(claudeReq.tools, hasWebSearchTool);
const tools = buildTools(claudeReq.tools, hasWebSearchTool);
// 5. Safety Settings
const safetySettings = [
@@ -152,7 +152,7 @@ export function transformClaudeRequestIn(
delete innerRequest.systemInstruction;
// 3. Clean generationConfig
let genConfig = innerRequest.generationConfig || {};
const genConfig = innerRequest.generationConfig || {};
delete genConfig.thinkingConfig;
delete genConfig.responseMimeType;
delete genConfig.responseModalities;
+2 -2
View File
@@ -67,7 +67,7 @@ function flattenRefs(map: any, defs: Record<string, any>) {
// Recursively process all children
for (const k in map) {
if (map.hasOwnProperty(k)) {
if (Object.prototype.hasOwnProperty.call(map, k)) {
const v = map[k];
if (typeof v === 'object' && v !== null) {
flattenRefs(v, defs);
@@ -91,7 +91,7 @@ function cleanJsonSchemaRecursive(value: any) {
// 1. Recursively process all children first to ensure nested structures are cleaned
for (const k in map) {
if (map.hasOwnProperty(k)) {
if (Object.prototype.hasOwnProperty.call(map, k)) {
cleanJsonSchemaRecursive(map[k]);
}
}
+4 -2
View File
@@ -337,8 +337,10 @@ i18n
generic: '发生未知错误。',
keychainUnavailable: 'Keychain 不可用。',
keychainHint: {
translocation: '检测到 macOS App Translocation。请将应用移动到 /Applications 后重试。',
keychainDenied: 'Keychain 访问被拒绝。应用可能未签名,可参考 README 的“macOS 自签”说明。',
translocation:
'检测到 macOS App Translocation。请将应用移动到 /Applications 后重试。',
keychainDenied:
'Keychain 访问被拒绝。应用可能未签名,可参考 README 的“macOS 自签”说明。',
signNotarize: '请在可用时使用已签名并公证的版本。',
},
dataMigrationFailed: '旧版账号数据无法解密。',
+1
View File
@@ -33,6 +33,7 @@ function SettingsPage() {
// Sync config to local state when loaded
useEffect(() => {
if (config) {
// eslint-disable-next-line
setProxyConfig(config.proxy);
}
}, [config]);
+3 -1
View File
@@ -40,9 +40,11 @@ export class ProtobufUtils {
return this.readVarint(data, offset).nextOffset;
case 1: // 64-bit
return offset + 8;
case 2: // Length-delimited
case 2: {
// Length-delimited
const { value: length, nextOffset } = this.readVarint(data, offset);
return nextOffset + Number(length);
}
case 5: // 32-bit
return offset + 4;
default:
+1 -3
View File
@@ -446,9 +446,7 @@ export async function decryptWithMigration(
);
}
if (reencrypted) {
logger.info(
`Security: Re-encrypted data from ${fallback.source} to ${primary.source}`,
);
logger.info(`Security: Re-encrypted data from ${fallback.source} to ${primary.source}`);
}
return { value, reencrypted, usedFallback: fallback.source };
} catch {