* Add Skills + CLI external agent workflow * feat: add Skills + CLI transport for ACP agents * chore: remove branch-local compatibility shims
8.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.tsdefines Host/SSHKey/Snippet/Workspace entities.host.tshandles distro normalization and host sanitization.workspace.tscontains workspace tree operations (split/insert/prune/sizing).
- Application State (
application/state/): Hooks that own state and persistence boundaries.useSettingsStatehandles theme, accent color, terminal themes, sync config (localStorage).useVaultStateowns hosts/keys/snippets/custom groups and import/export, persisting to storage.useSessionStateowns terminal sessions, workspace lifecycle, drag/split logic.
- Infrastructure (
infrastructure/): External edges and configuration.config/holds defaults, storage keys, terminal themes.persistence/localStorageAdapter.tsabstracts 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.tsxwires 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-hoclocalStoragecalls elsewhere.
Extending the System
- New domain logic: Add pure functions/types under
domain/; avoid side effects. - New stateful behavior: Wrap it in a hook under
application/state/; keep external I/O behind adapters. - New integrations: Create adapters under
infrastructure/services/(orpersistence/); expose typed functions. - UI changes: Consume hook outputs/handlers; do not bypass state hooks for persistence or domain logic.
Data & Storage
- Persisted keys: see
storageKeys.ts. UselocalStorageAdapterfor all reads/writes. - Seed data:
config/defaultData.ts; terminal themes:config/terminalThemes.ts. - Temporary files: All temporary files (e.g., SFTP downloaded files for external editing) must be written to Netcatty's dedicated temp directory via
tempDirBridge.getTempFilePath(fileName). Do not write directly toos.tmpdir(). This ensures proper cleanup and user visibility in Settings > System.
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.
Review Boundaries
- Treat
electron/cli/*,netcatty-tool-cli, the CLI discovery file, and the local TCP bridge as internal Netcatty integration surfaces unless a task explicitly says otherwise. - Do not review those surfaces as public APIs by default, and do not assume they must support third-party callers, manual launches, or non-Netcatty agents.
- On supported first-party paths, assume Netcatty's own launcher provides required integration environment such as
NETCATTY_TOOL_CLI_DISCOVERY_FILE. - If a review concern depends on external exposure, third-party compatibility, or public API stability, call it out as out of scope unless the task explicitly includes that contract.
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 visibilityonClose: () => void- Close button handlertitle?: string- Header title (header only renders if title is provided)subtitle?: string- Secondary text below titleshowBackButton?: boolean- Show back arrow (for sub-panels)onBack?: () => void- Back button handleractions?: 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 withspace-y-4gapclassName?: 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 withrelativepositioning) - Width:
w-[380px](configurable viawidthprop) - Background:
bg-background(solid, no backdrop-blur) - Border:
border-l border-border/60 - Z-index:
z-30 - Header:
shrink-0to prevent scrolling, close button uses X icon - Content:
flex-1 overflow-hiddenwith internal ScrollArea andspace-y-4gap - Important: Parent container must have
relativepositioning 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)
multiSelectprop for multiple host selectionselectedHostIdsarray for controlled selection- Sort dropdown and tag filter for large host lists
- Uses
absolutepositioning (notfixed) - parent needsrelative
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
absolutepositioning withtop-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-0or haverelativepositioning