Files
Netcatty/agents.md
T
LAPTOP-O016UC3M\Qi Chen f6d50c1795 Unifies aside panel UI, adds host tagging, improves selection
Migrates manual aside panels to a reusable design system for consistent slide-out UI across hosts, keys, port forwarding, and snippets.

Adds tag management to host forms and selection, enabling filtering and improved organization.

Improves host/group selection with sorting, tag filtering, and breadcrumb navigation.

Simplifies panel logic, centralizes host panel state, and enhances maintainability.
2025-12-10 23:29:11 +08:00

7.5 KiB

Agents Overview

This project is wired around three layers: domain (pure logic), application state (React hooks orchestrating the domain), and UI (components). Use this document as a quick guide for extending or reusing the codebase.

Current Agents (Roles)

  • Domain (domain/): Models and pure helpers. Examples:
    • models.ts defines Host/SSHKey/Snippet/Workspace entities.
    • host.ts handles distro normalization and host sanitization.
    • workspace.ts contains workspace tree operations (split/insert/prune/sizing).
  • Application State (application/state/): Hooks that own state and persistence boundaries.
    • useSettingsState handles theme, accent color, terminal themes, sync config (localStorage).
    • useVaultState owns hosts/keys/snippets/custom groups and import/export, persisting to storage.
    • useSessionState owns terminal sessions, workspace lifecycle, drag/split logic.
  • Infrastructure (infrastructure/): External edges and configuration.
    • config/ holds defaults, storage keys, terminal themes.
    • persistence/localStorageAdapter.ts abstracts localStorage read/write.
    • services/ contains networked services (Gemini AI, GitHub Gist sync).
  • UI (components/, App.tsx): Presentation; depends on hooks and domain helpers only.

How Things Talk

  • UI calls application hooks → hooks call domain helpers → persistence/config via infrastructure adapters.
  • App.tsx wires hooks to components; no business logic should live in components beyond view glue.
  • Local storage keys are centralized in infrastructure/config/storageKeys.ts; avoid ad-hoc localStorage calls elsewhere.

Extending the System

  1. New domain logic: Add pure functions/types under domain/; avoid side effects.
  2. New stateful behavior: Wrap it in a hook under application/state/; keep external I/O behind adapters.
  3. New integrations: Create adapters under infrastructure/services/ (or persistence/); expose typed functions.
  4. UI changes: Consume hook outputs/handlers; do not bypass state hooks for persistence or domain logic.

Data & Storage

  • Persisted keys: see storageKeys.ts. Use localStorageAdapter for all reads/writes.
  • Seed data: config/defaultData.ts; terminal themes: config/terminalThemes.ts.

Testing & Safety

  • Favor unit tests for domain helpers (e.g., workspace.ts, host.ts) and hook-level tests for application state.
  • When changing storage keys or schema, provide migration or backward-compat handling.
  • Keep components dumb: if a prop list grows large, consider deriving a smaller view model in the hook.

Coding Conventions

  • Keep logic pure in domain; side effects belong to application/infrastructure layers.
  • Prefer composition over deep prop drilling; lift shared state into hooks.
  • Avoid direct network/fetch in components; add a service/adaptor first.
  • Maintain ASCII-only unless required by existing file content.

Aside Panel Design System

VaultView subpages (Hosts, Keychain, Port Forwarding, Snippets, Known Hosts) share a unified aside panel design system via reusable components in components/ui/aside-panel.tsx.

Core Components

Import from ./ui/aside-panel:

import { 
  AsidePanel, 
  AsidePanelHeader, 
  AsidePanelContent, 
  AsidePanelFooter,
  AsideActionMenu,
  AsideActionMenuItem 
} from "./ui/aside-panel";

Basic Usage

<AsidePanel 
  open={isOpen} 
  onClose={handleClose}
  title="Panel Title"
  subtitle="Optional subtitle"
  // For sub-panels with back navigation:
  showBackButton={true}
  onBack={handleBack}
  // Optional action menu:
  actions={
    <AsideActionMenu>
      <AsideActionMenuItem onClick={handleDuplicate}>
        <Copy size={14} className="mr-2" /> Duplicate
      </AsideActionMenuItem>
      <AsideActionMenuItem variant="destructive" onClick={handleDelete}>
        <Trash2 size={14} className="mr-2" /> Delete
      </AsideActionMenuItem>
    </AsideActionMenu>
  }
>
  <AsidePanelContent>
    {/* Your scrollable content here */}
  </AsidePanelContent>
  <AsidePanelFooter>
    <Button className="w-full">Save</Button>
  </AsidePanelFooter>
</AsidePanel>

Note: When title prop is provided, AsidePanel automatically renders the header. Do NOT use AsidePanelHeader directly inside AsidePanel - this would cause duplicate headers.

Component Props

AsidePanel

  • open: boolean - Controls panel visibility
  • onClose: () => void - Close button handler
  • title?: string - Header title (header only renders if title is provided)
  • subtitle?: string - Secondary text below title
  • showBackButton?: boolean - Show back arrow (for sub-panels)
  • onBack?: () => void - Back button handler
  • actions?: ReactNode - Right-side actions (buttons or AsideActionMenu)
  • width?: string - Panel width (default: "w-[380px]")
  • children: ReactNode - Panel content

AsidePanelContent

  • children: ReactNode - Content wrapped in ScrollArea with space-y-4 gap
  • className?: string - Additional CSS classes

AsidePanelFooter

  • children: ReactNode - Footer content (usually buttons)
  • className?: string - Additional CSS classes

AsideActionMenu / AsideActionMenuItem

  • Popover-based dropdown menu for header actions
  • variant="destructive" for delete actions (red text)

Design Specifications

  • Position: absolute right-0 top-0 bottom-0 (relative to parent container with relative positioning)
  • Width: w-[380px] (configurable via width prop)
  • Background: bg-background (solid, no backdrop-blur)
  • Border: border-l border-border/60
  • Z-index: z-30
  • Header: shrink-0 to prevent scrolling, close button uses X icon
  • Content: flex-1 overflow-hidden with internal ScrollArea and space-y-4 gap
  • Important: Parent container must have relative positioning for the panel to position correctly

Panel Navigation Patterns

  • Main panels: Close with X icon, no back button
  • Sub-panels (stacked): ArrowLeft (←) back button + X close button
  • Use panel stack state for nested navigation: panelStack: PanelMode[]
  • popPanel() returns to previous panel, closePanel() closes all panels

SelectHostPanel Integration

For host selection, use SelectHostPanel component with:

  • Breadcrumb navigation in content area (not header)
  • multiSelect prop for multiple host selection
  • selectedHostIds array for controlled selection
  • Sort dropdown and tag filter for large host lists
  • Uses absolute positioning (not fixed) - parent needs relative

Migration from Manual Implementation

Replace manual panel structure:

// OLD: Manual implementation
<div className="fixed right-0 top-0 bottom-0 w-[380px] border-l border-border/60 bg-background z-50 flex flex-col">
  <div className="px-4 py-3 flex items-center justify-between border-b border-border/60 app-no-drag shrink-0">
    {/* header content */}
  </div>
  <ScrollArea className="flex-1">
    <div className="p-4 space-y-4">{/* content */}</div>
  </ScrollArea>
</div>

// NEW: Using AsidePanel components (header via props)
<AsidePanel open={open} onClose={onClose} title="Title">
  <AsidePanelContent>{/* content */}</AsidePanelContent>
</AsidePanel>

Important Positioning Notes

  • AsidePanel uses absolute positioning with top-0 bottom-0 right-0
  • The panel positions relative to its nearest positioned ancestor
  • For correct alignment with the top of the page:
    • Render AsidePanel at the root level of your section (e.g., VaultView root div)
    • Do NOT render AsidePanel inside a scrollable content area or nested containers
    • The parent container should be absolute inset-0 or have relative positioning